Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react): expose modal state in useModal #622

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
})
})