Skip to content

Instantly share code, notes, and snippets.

@statico
Created December 19, 2021 18:26
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save statico/c385705ce14106cd013d413560d98622 to your computer and use it in GitHub Desktop.
Save statico/c385705ce14106cd013d413560d98622 to your computer and use it in GitHub Desktop.
Chakra UI await-able alert, confirm, and prompt modal dialogs
/*
* Usage:
* const { alert, confirm, prompt } = useModals()
* alert("Hey!") // awaitable too
* if (await confirm("Are you sure?")) ...
* const result = await prompt("Enter a URL", "http://")
*/
import React, {
createContext,
ReactNode,
useCallback,
useContext,
useRef,
useState,
} from "react"
import {
Button,
Input,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalOverlay,
Stack,
Text,
} from "@chakra-ui/react"
// TODO: Select field contents when a prompt() loads
// TODO: Fix Promise<> return types instead of using any
enum ModalType {
Alert,
Confirm,
Prompt,
}
export interface Modals {
alert: (message: string) => Promise<any>
confirm: (message: string) => Promise<any>
prompt: (message: string, defaultValue?: string) => Promise<any>
}
const defaultContext: Modals = {
alert() {
throw new Error("<ModalProvider> is missing")
},
confirm() {
throw new Error("<ModalProvider> is missing")
},
prompt() {
throw new Error("<ModalProvider> is missing")
},
}
const Context = createContext<Modals>(defaultContext)
interface AnyEvent {
preventDefault(): void
}
export const ModalProvider = ({ children }: { children: ReactNode }) => {
const [modal, setModal] = useState<ReactNode | null>(null)
const input = useRef<HTMLInputElement>(null)
const ok = useRef<HTMLButtonElement>(null)
const createOpener = useCallback(
(type: ModalType) =>
(message: string, defaultValue = "") =>
new Promise((resolve) => {
const handleClose = (e?: AnyEvent) => {
e?.preventDefault()
setModal(null)
resolve(null)
}
const handleCancel = (e?: AnyEvent) => {
e?.preventDefault()
setModal(null)
if (type === ModalType.Prompt) resolve(null)
else resolve(false)
}
const handleOK = (e?: AnyEvent) => {
e?.preventDefault()
setModal(null)
if (type === ModalType.Prompt) resolve(input.current?.value)
else resolve(true)
}
setModal(
<Modal
isOpen={true}
onClose={handleClose}
initialFocusRef={type === ModalType.Prompt ? input : ok}
>
<ModalOverlay />
<ModalContent>
<ModalBody mt={5}>
<Stack spacing={5}>
<Text> {message}</Text>
{type === ModalType.Prompt && (
<Input ref={input} defaultValue={defaultValue} />
)}
</Stack>
</ModalBody>
<ModalFooter>
{type !== ModalType.Alert && (
<Button mr={3} variant="ghost" onClick={handleCancel}>
Cancel
</Button>
)}
<Button onClick={handleOK} ref={ok}>
OK
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}),
[children]
)
return (
<Context.Provider
value={{
alert: createOpener(ModalType.Alert),
confirm: createOpener(ModalType.Confirm),
prompt: createOpener(ModalType.Prompt),
}}
>
{children}
{modal}
</Context.Provider>
)
}
const useModals = () => useContext(Context)
export default useModals
@alfonmga
Copy link

Thanks for sharing ❤️

@kimmobrunfeldt
Copy link

thanks! a few type and prop adjustments:

import {
  Button,
  Flex,
  Input,
  Modal,
  ModalBody,
  ModalContent,
  ModalFooter,
  ModalOverlay,
  Stack,
  Text,
} from '@chakra-ui/react'
import {
  createContext,
  ReactNode,
  useCallback,
  useContext,
  useRef,
  useState,
} from 'react'

enum ModalType {
  Alert,
  Confirm,
  Prompt,
}

export type Modals = {
  alert: (
    message: string | ReactNode,
    opts?: ModalOpenerProps
  ) => Promise<boolean | null>
  confirm: (
    message: string | ReactNode,
    opts?: ModalOpenerProps
  ) => Promise<boolean | null>
  prompt: (
    message: string | ReactNode,
    opts?: ModalOpenerProps & {
      defaultValue?: string
    }
  ) => Promise<string | null>
}

export type ModalOpenerProps = {
  okText?: string
  cancelText?: string
  icon?: ReactNode
  modalProps?: Partial<React.ComponentProps<typeof Modal>>
  okButtonProps?: Partial<React.ComponentProps<typeof Button>>
  cancelButtonProps?: Partial<React.ComponentProps<typeof Button>>
}

const defaultContext: Modals = {
  alert() {
    throw new Error('<ModalProvider> is missing')
  },
  confirm() {
    throw new Error('<ModalProvider> is missing')
  },
  prompt() {
    throw new Error('<ModalProvider> is missing')
  },
}

const Context = createContext<Modals>(defaultContext)

interface AnyEvent {
  preventDefault(): void
}

export const ModalProvider = ({ children }: { children: ReactNode }) => {
  const [modal, setModal] = useState<ReactNode | null>(null)
  const input = useRef<HTMLInputElement>(null)
  const ok = useRef<HTMLButtonElement>(null)

  const createOpener = useCallback(
    (type: ModalType) =>
      (
        message: string,
        opts: ModalOpenerProps & { defaultValue?: string } = {}
      ) =>
        new Promise<boolean | string | undefined>((resolve) => {
          const handleClose = (e?: AnyEvent) => {
            e?.preventDefault()
            setModal(null)
            resolve(undefined)
          }

          const handleCancel = (e?: AnyEvent) => {
            e?.preventDefault()
            setModal(null)
            if (type === ModalType.Prompt) {
              resolve(undefined)
            } else {
              resolve(false)
            }
          }

          const handleOK = (e?: AnyEvent) => {
            e?.preventDefault()
            setModal(null)
            if (type === ModalType.Prompt) {
              resolve(input.current?.value)
            } else {
              resolve(true)
            }
          }

          setModal(
            <Modal
              isOpen={true}
              onClose={handleClose}
              initialFocusRef={type === ModalType.Prompt ? input : ok}
              {...opts.modalProps}
            >
              <ModalOverlay />
              <ModalContent>
                <ModalBody mt={5}>
                  <Flex gap={4} alignItems="center">
                    {opts.icon}
                    <Stack spacing={5}>
                      <Text>{message}</Text>
                      {type === ModalType.Prompt && (
                        <Input ref={input} defaultValue={opts.defaultValue} />
                      )}
                    </Stack>
                  </Flex>
                </ModalBody>
                <ModalFooter>
                  {type !== ModalType.Alert && (
                    <Button
                      mr={3}
                      variant="ghost"
                      onClick={handleCancel}
                      {...opts.cancelButtonProps}
                    >
                      {opts.cancelText ?? 'Cancel'}
                    </Button>
                  )}
                  <Button onClick={handleOK} ref={ok} {...opts.okButtonProps}>
                    {opts.okText ?? 'OK'}
                  </Button>
                </ModalFooter>
              </ModalContent>
            </Modal>
          )
        }),
    [children]
  )

  return (
    <Context.Provider
      value={
        {
          alert: createOpener(ModalType.Alert),
          confirm: createOpener(ModalType.Confirm),
          prompt: createOpener(ModalType.Prompt),
        } as Modals
      }
    >
      {children}
      {modal}
    </Context.Provider>
  )
}

const useModals = () => useContext(Context)

export default useModals

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment