Skip to content

Commit

Permalink
Merge pull request #622 from omnifed/587-feat-return-state-of-modal-w…
Browse files Browse the repository at this point in the history
…ithin-usemodal-hook

feat(react): expose modal state in useModal
  • Loading branch information
caseybaggz authored Oct 23, 2024
2 parents f23e31c + 052e404 commit 10ee67c
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 6 deletions.
53 changes: 52 additions & 1 deletion docs/app/react/use-modal/doc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The `useModal` hook allows you to manage displaying a Modal.

<WhenToUseAdmonition description="This hook is ideal for when you are using a custom Modal." />

## Usage
## Basic Usage

```tsx title="custom-modal.tsx" {9}
import {
Expand Down Expand Up @@ -43,13 +43,53 @@ function SomePage() {
}
```

## Advanced Usage

When you need to dynamically load a Modal, you can use the `useModal` hook to manage the Modal's state.

```tsx title="dynamic-modal.tsx" {9}
import {
Modal,
Button,
trapFocus,
useModal
} from '@cerberus/react'
import { lazy, Suspense } from 'react'

const SomeDynamicComponent = lazy(() => import('./SomeDynamicComponent'))

function SomePage() {
const modal = useModal()
const handleKeyDown = trapFocus(modalRef)

return (
<div>
<Button onClick={modal.show}>Show Modal</Button>

<Modal onKeyDown={handleKeyDown} ref={modal.modalRef}>
<Suspense>
<Show when={modal.isOpen}>
<SomeDynamicComponent />
</Show>
</Suspense>

<Button onClick={modal.close}>
Close
</Button>
</Modal>
</div>
)
}
```

## API

```ts showLineNumbers=false
interface UseModalReturnValue {
modalRef: RefObject<HTMLDialogElement>
show: () => void
close: () => void
isOpen: boolean
}

define function useModal(): UseModalReturnValue
Expand All @@ -58,3 +98,14 @@ define function useModal(): UseModalReturnValue
### Arguments

The `useModal` hook does not take any arguments.

### Return Value

The `useModal` hook returns an object with the following properties:

| Name | Default | Description |
| -------- | ------- | -------------------------------------- |
| modalRef | | The ref that attaches to the Modal component. |
| show | | Triggers the Modal to open. |
| close | | Closes the Modal. |
| isOpen | `false` | Helper value to know the state of the dialog. |
23 changes: 19 additions & 4 deletions packages/react/src/hooks/useModal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useCallback, useMemo, useRef, type RefObject } from 'react'
import { useCallback, useMemo, useRef, useState, type RefObject } from 'react'

/**
* This module provides a hook for using a custom modal.
Expand All @@ -20,29 +20,44 @@ interface UseModalReturnValue {
* Closes the modal.
*/
close: () => void
/**
* Whether the modal is open based on the show and close methods.
*/
isOpen: boolean
}

/**
* Provides a hook for using a custom modal.
* Provides a hook for using a custom modal via the native dialog element
* methods.
*
* Cerberus modals use the native dialog element. This hook
* does not control the modal via React state but rather by calling the
* native dialog element's `showModal` and `close` methods.
*
* @memberof module:Modal
* @returns The modal hook.
* @see https://cerberus.digitalu.design/react/modal
* @description [Moz Dev Dialog Docs](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal)
*/
export function useModal(): UseModalReturnValue {
const modalRef = useRef<HTMLDialogElement | null>(null)
const [isOpen, setIsOpen] = useState<boolean>(false)

const show = useCallback(() => {
modalRef.current?.showModal()
setIsOpen(true)
}, [])

const close = useCallback(() => {
modalRef.current?.close()
setIsOpen(false)
}, [])

return useMemo(() => {
return {
modalRef,
show,
close,
isOpen,
}
}, [modalRef, show, close])
}, [modalRef, show, close, isOpen])
}
8 changes: 7 additions & 1 deletion tests/react/hooks/useModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ describe('useModal', () => {
setupStrictMode()

function Test() {
const { modalRef, show, close } = useModal()
const { modalRef, show, close, isOpen } = useModal()
return (
<div>
<p>Modal state: {String(isOpen)}</p>
<button onClick={show}>Show</button>
<Modal ref={modalRef}>
<p>Modal content</p>
Expand All @@ -28,16 +29,21 @@ describe('useModal', () => {

test('should show modal', async () => {
render(<Test />)
expect(screen.getByText(/modal state: false/i)).toBeTruthy()
expect(screen.queryByRole('dialog')).toBeFalsy()
await userEvent.click(screen.getByText(/show/i))
expect(screen.getByRole('dialog')).toBeTruthy()
expect(screen.getByText(/modal state: true/i)).toBeTruthy
})

test('should close modal', async () => {
render(<Test />)
expect(screen.getByText(/modal state: false/i)).toBeTruthy()
await userEvent.click(screen.getByText(/show/i))
expect(screen.getByRole('dialog')).toBeTruthy()
expect(screen.getByText(/modal state: true/i)).toBeTruthy
await userEvent.click(screen.getByText(/close/i))
expect(screen.queryByRole('dialog')).toBeFalsy()
expect(screen.getByText(/modal state: false/i)).toBeTruthy()
})
})

0 comments on commit 10ee67c

Please sign in to comment.