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