| 46 | * A dialog is an overlay shown above other content in an application. |
| 47 | */ |
| 48 | export function useDialog( |
| 49 | props: AriaDialogProps, |
| 50 | ref: RefObject<FocusableElement | null> |
| 51 | ): DialogAria { |
| 52 | let {role = 'dialog'} = props; |
| 53 | let titleId: string | undefined = useSlotId(); |
| 54 | titleId = props['aria-label'] ? undefined : titleId; |
| 55 | |
| 56 | let isRefocusing = useRef(false); |
| 57 | |
| 58 | // Focus the dialog itself on mount, unless a child element is already focused. |
| 59 | useEffect(() => { |
| 60 | if (ref.current && !isFocusWithin(ref.current)) { |
| 61 | focusSafely(ref.current); |
| 62 | |
| 63 | // Safari on iOS does not move the VoiceOver cursor to the dialog |
| 64 | // or announce that it has opened until it has rendered. A workaround |
| 65 | // is to wait for half a second, then blur and re-focus the dialog. |
| 66 | let timeout = setTimeout(() => { |
| 67 | // Check that the dialog is still focused, or focused was lost to the body. |
| 68 | if (getActiveElement() === ref.current || getActiveElement() === document.body) { |
| 69 | isRefocusing.current = true; |
| 70 | if (ref.current) { |
| 71 | ref.current.blur(); |
| 72 | focusSafely(ref.current); |
| 73 | } |
| 74 | isRefocusing.current = false; |
| 75 | } |
| 76 | }, 500); |
| 77 | |
| 78 | return () => { |
| 79 | clearTimeout(timeout); |
| 80 | }; |
| 81 | } |
| 82 | }, [ref]); |
| 83 | |
| 84 | useOverlayFocusContain(); |
| 85 | |
| 86 | // Warn in dev mode if the dialog has no accessible title. |
| 87 | // This catches a common mistake where useDialog and useOverlayTriggerState |
| 88 | // are used in the same component, causing the title element to not be |
| 89 | // in the DOM when useSlotId queries for it. |
| 90 | // Check the DOM element directly since aria-labelledby may be added by |
| 91 | // wrapper components (e.g. RAC Dialog uses trigger ID as a fallback). |
| 92 | let hasWarned = useRef(false); |
| 93 | useEffect(() => { |
| 94 | if (process.env.NODE_ENV !== 'production' && !hasWarned.current && ref.current) { |
| 95 | let el = ref.current; |
| 96 | let hasAriaLabel = el.hasAttribute('aria-label'); |
| 97 | let hasAriaLabelledby = el.hasAttribute('aria-labelledby'); |
| 98 | if (!hasAriaLabel && !hasAriaLabelledby) { |
| 99 | console.warn( |
| 100 | 'A dialog must have a title for accessibility. ' + |
| 101 | 'Either provide an aria-label or aria-labelledby prop, or render a heading element inside the dialog.' |
| 102 | ); |
| 103 | hasWarned.current = true; |
| 104 | } |
| 105 | } |