Skip to content

Commit

Permalink
Simplify Dialog usage using useDialog
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-leamon committed Apr 29, 2024
1 parent 1037356 commit c2578ae
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 210 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"publishConfig": {
"registry": "https://registry.npmjs.org"
},
"version": "1.0.111",
"version": "1.0.112",
"description": "Formation is a comprehensive component library powered by React, Styled Components, and CSS variables for creating apps and websites that demand responsive, unified cross-platform experiences.",
"resolutions": {
"string-width": "^4",
Expand Down
76 changes: 18 additions & 58 deletions src/components/Dialog/Dialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,94 +1,54 @@
import React, { useState } from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import { Button, Gap, Dialog, DialogProvider, Break, dialogController } from '../../internal'
import { Button, Gap, Dialog, DialogProvider, useDialog } from '../../internal'

export default {
title: 'Input/Dialog',
component: Dialog,
decorators: [(Story) => (
<DialogProvider>
<Story />
<Dialog />
</DialogProvider>
)],
} as ComponentMeta<typeof Dialog>

const Template: ComponentStory<typeof Dialog> = (args: any) => {
const [response, setResponse] = useState<string | boolean | null>('')

return (
<Gap>
<Button onClick={() => dialogController.openDialog({
mode: args.mode,
message: args.message,
callback: setResponse,
placeholder: args.placeholder
})}>
{args.label}
</Button>
<p>{response !== null ? response.toString() : ''}</p>
<Dialog />
</Gap>
)
}
export const Alert = Template.bind({})
Alert.args = {
label: 'Open Alert Dialog',
message: 'To create visuals with cameras, please give AVsync.LIVE permission to access your camera. Go to chrome://settings/content/camera',
mode: 'alert'
}

export const Confirm = Template.bind({})
Confirm.args = {
label: 'Open Confirm Dialog',
message: 'Are you sure you want to delete this Scene?',
mode: 'confirm'
}

export const Prompt = Template.bind({})
Prompt.args = {
label: 'Open Prompt Dialog',
message: 'Choose a title for your blog post',
placeholder: 'Title', // Add a placeholder for the prompt input
mode: 'prompt'
}

const AllDialogsTemplate: ComponentStory<typeof Dialog> = () => {
const [confirmResponse, setConfirmResponse] = useState<string | boolean | null>('')
const [promptResponse, setPromptResponse] = useState<string | boolean | null>('')
const UseDialogTemplate: ComponentStory<typeof Dialog> = () => {
const { openDialog } = useDialog()
const [response, setResponse] = useState<string | boolean | null>('')

return (
<Gap>
<Button onClick={() => dialogController.openDialog({
<Button onClick={() => openDialog({
mode: 'alert',
message: 'This is an alert!',
callback: (result) => setResponse(result)
})}>
Open Alert Dialog
</Button>
<Break />
<p>{response !== null ? `Alert response: ${response}` : ''}</p>

<Button onClick={() => dialogController.openDialog({
<Button onClick={() => openDialog({
mode: 'confirm',
message: 'Are you sure you want to delete this Scene?',
callback: setConfirmResponse
message: 'Are you sure you want to proceed?',
callback: (result) => setResponse(result)
})}>
Open Confirm Dialog
</Button>
<p>{confirmResponse !== null ? confirmResponse.toString() : ''}</p>
<p>{response !== null ? `Confirm response: ${response}` : ''}</p>

<Break />

<Button onClick={() => dialogController.openDialog({
<Button onClick={() => openDialog({
mode: 'prompt',
message: 'Enter your name:',
callback: setPromptResponse,
placeholder: 'Name'
message: 'Please enter your name:',
placeholder: 'Name',
callback: (result) => setResponse(result)
})}>
Open Prompt Dialog
</Button>
<p>{promptResponse !== null ? promptResponse.toString() : ''}</p>
<Dialog />
<p>{response !== null ? `Prompt response: ${response}` : ''}</p>
</Gap>
)
}

export const AllDialogs = AllDialogsTemplate.bind({})
export const UseDialog = UseDialogTemplate.bind({})
150 changes: 29 additions & 121 deletions src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, { useState, useEffect, useRef, createContext, useContext } from 'react'
import React, { useState, createContext, useContext, useRef, useEffect } from 'react'
import styled, { css, keyframes } from 'styled-components'
import { Gap, Button, TextInput, Break, Fit } from '../../internal'

import { dialogController } from './DialogController'

interface DialogConfig {
mode: 'alert' | 'confirm' | 'prompt'
message: string
Expand All @@ -26,62 +24,22 @@ const DialogContext = createContext<DialogContextType>({

export const useDialog = () => useContext(DialogContext)

interface DialogProviderProps {
children: React.ReactNode
}
export const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false)
const [config, setConfig] = useState<DialogConfig | undefined>()

/**
* `Dialog` is a customizable dialog component for creating alert, confirm, and prompt dialogs.
* It provides an interactive way to display information and collect user input in a web application.
* This component supports three modes: 'alert' for simple messages, 'confirm' for yes/no decisions,
* and 'prompt' for user input. The dialog appears with an animation and can be configured with a
* message, placeholder text for prompts, and a callback function to handle user responses.
*
* The dialog also features an outside click detection to provide a shaking effect, enhancing the
* user experience by drawing attention to the dialog when users click outside of it.
*
* @component
* @param {function} useDialog - Custom hook to manage dialog state and configuration.
* @param {function} useState - React useState hook for managing internal state.
* @param {function} useEffect - React useEffect hook for handling side effects.
* @param {function} useRef - React useRef hook for referencing DOM elements.
*
* @example
* // To create and use a confirm dialog with a custom message and callback function
* const { showDialog, hideDialog } = useDialog();
* showDialog({
* mode: 'confirm',
* message: 'Are you sure?',
* callback: (response) => console.log(`User response: ${response}`)
* });
*
* @example
* // To create and use a prompt dialog for user input
* const { showDialog } = useDialog();
* showDialog({
* mode: 'prompt',
* message: 'Enter your name:',
* placeholder: 'Name',
* callback: (input) => console.log(`User input: ${input}`)
* });
*/
export const DialogProvider: React.FC<DialogProviderProps> = ({ children }) => {
const [dialogState, setDialogState] = useState<DialogContextType>({
isOpen: false,
openDialog: (config) => {
setDialogState(prevState => ({ ...prevState, isOpen: true, config }))
},
closeDialog: () => {
setDialogState(prevState => ({ ...prevState, isOpen: false, config: undefined }))
}
})
const openDialog = (config: DialogConfig) => {
setConfig(config)
setIsOpen(true)
}

// Set the actual functions in the DialogController
dialogController.setOpenDialogFunction(dialogState.openDialog)
dialogController.setCloseDialogFunction(dialogState.closeDialog)
const closeDialog = () => {
setIsOpen(false)
setConfig(undefined)
}

return (
<DialogContext.Provider value={dialogState}>
<DialogContext.Provider value={{ isOpen, config, openDialog, closeDialog }}>
{children}
</DialogContext.Provider>
)
Expand Down Expand Up @@ -112,71 +70,7 @@ export const Dialog = () => {

const handleClose = (value: boolean | string | null) => {
closeDialog()
if (config && config.callback) {
config.callback(value)
}
}

useEffect(() => {
const blurFocusableElements = () => {
const focusableElements = document.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
focusableElements.forEach((element: any) => {
if (typeof element.blur === 'function') {
element.blur()
}
})
}

if (isOpen && config?.mode !== 'prompt') {
blurFocusableElements()
}
}, [isOpen, config])

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && isOpen && config?.mode !== 'prompt') {
event.preventDefault()
switch (config?.mode) {
case 'confirm':
handleClose(true)
break
case 'alert':
handleClose(null)
break
default:
break
}
}
}

document.addEventListener('keydown', handleKeyDown)

return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}, [isOpen, config, handleClose])

const renderButtons = () => {
switch (config?.mode) {
case 'alert':
return <Button onClick={() => handleClose(null)} primary expand>OK</Button>
case 'confirm':
return (
<>
<Button onClick={() => handleClose(true)} primary>Yes</Button>
<Button onClick={() => handleClose(false)}>No</Button>
</>
)
case 'prompt':
return (
<>
<Button onClick={() => handleClose(inputValue)} primary expand>OK</Button>
<Button onClick={() => handleClose(null)} expand>Cancel</Button>
</>
)
default:
return null
}
config?.callback?.(value)
}

useEffect(() => {
Expand Down Expand Up @@ -205,7 +99,21 @@ export const Dialog = () => {
)
}
<Fit gap={0.5}>
{renderButtons()}
{config && (
config.mode === 'alert' ? <Button onClick={() => handleClose(null)} primary expand>OK</Button> :
config.mode === 'confirm' ? (
<>
<Button onClick={() => handleClose(true)} primary>Yes</Button>
<Button onClick={() => handleClose(false)}>No</Button>
</>
) :
config.mode === 'prompt' ? (
<>
<Button onClick={() => handleClose(inputValue)} primary expand>OK</Button>
<Button onClick={() => handleClose(null)} expand>Cancel</Button>
</>
) : null
)}
</Fit>
</Gap>
</S.DialogContent>
Expand Down
27 changes: 0 additions & 27 deletions src/components/Dialog/DialogController.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/NavSpaces/NavSpaces.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ const Template: ComponentStory<typeof NavSpaces> = args => {
export const Positions = Template.bind({})
Positions.args = {
label: 'Position title',
sidebarWidth: '320px',
sidebarWidth: '380px',
secondaryTopNav: [
{
title: 'People',
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ export { QRCode } from './components/QRCode/QRCode'
export { Small } from './components/Small/Small'
export { TextArea } from './components/TextArea/TextArea'
export { Dialog, DialogProvider, useDialog } from './components/Dialog/Dialog'
export { dialogController } from './components/Dialog/DialogController'
export { DropCorners } from './components/DragAndDrop/DropCorners'

import { Link } from './components/Link/Link'
Expand Down
1 change: 0 additions & 1 deletion src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ export { ProgressTimer } from './components/ProgressTimer/ProgressTimer'
export { Control } from './components/Control/Control'
export { MultiSelect } from './components/MultiSelect/MultiSelect'
export { Dialog, DialogProvider, useDialog } from './components/Dialog/Dialog'
export { dialogController } from './components/Dialog/DialogController'

// Tissue (2)
export { Navigation } from './components/Navigation/Navigation'
Expand Down

0 comments on commit c2578ae

Please sign in to comment.