From d77db9cbed015243c83bf33d51319d732d5056a8 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:06:58 -0700 Subject: [PATCH 1/3] fix: add dev warning when dialog has no accessible title When a dialog is rendered without an aria-label, aria-labelledby, or a visible title element, emit a console.warn in development mode to help developers catch this common accessibility mistake early. The warning fires once per dialog instance and is tree-shaken in production builds. Co-Authored-By: Claude Opus 4.6 --- packages/react-aria/src/dialog/useDialog.ts | 19 +++++++ .../react-aria/test/dialog/useDialog.test.js | 52 ++++++++++++++++--- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/packages/react-aria/src/dialog/useDialog.ts b/packages/react-aria/src/dialog/useDialog.ts index f44cf365089..20e2b2981a4 100644 --- a/packages/react-aria/src/dialog/useDialog.ts +++ b/packages/react-aria/src/dialog/useDialog.ts @@ -75,6 +75,25 @@ export function useDialog(props: AriaDialogProps, ref: RefObject { + if (process.env.NODE_ENV !== 'production' && !hasWarned.current) { + let hasAriaLabel = props['aria-label'] != null; + let hasAriaLabelledby = props['aria-labelledby'] != null; + if (!hasAriaLabel && !hasAriaLabelledby && titleId == null) { + console.warn( + 'A dialog must have a visible title for accessibility. ' + + 'Either provide an aria-label or aria-labelledby prop, or render a heading element inside the dialog with the titleProps from useDialog.' + ); + hasWarned.current = true; + } + } + }); + // We do not use aria-modal due to a Safari bug which forces the first focusable element to be focused // on mount when inside an iframe, no matter which element we programmatically focus. // See https://bugs.webkit.org/show_bug.cgi?id=211934. diff --git a/packages/react-aria/test/dialog/useDialog.test.js b/packages/react-aria/test/dialog/useDialog.test.js index 3d824a65a68..48b24d3fafa 100644 --- a/packages/react-aria/test/dialog/useDialog.test.js +++ b/packages/react-aria/test/dialog/useDialog.test.js @@ -16,25 +16,30 @@ import {useDialog} from '../../src/dialog/useDialog'; function Example(props) { let ref = useRef(); - let {dialogProps} = useDialog(props, ref); - return
{props.children}
; + let {dialogProps, titleProps} = useDialog(props, ref); + return ( +
+ {props.showTitle &&

Title

} + {props.children} +
+ ); } describe('useDialog', function () { it('should have role="dialog" by default', function () { - let res = render(); + let res = render(); let el = res.getByTestId('test'); expect(el).toHaveAttribute('role', 'dialog'); }); it('should accept role="alertdialog"', function () { - let res = render(); + let res = render(); let el = res.getByTestId('test'); expect(el).toHaveAttribute('role', 'alertdialog'); }); it('should focus the overlay on mount', function () { - let res = render(); + let res = render(); let el = res.getByTestId('test'); expect(el).toHaveAttribute('tabIndex', '-1'); expect(document.activeElement).toBe(el); @@ -42,11 +47,46 @@ describe('useDialog', function () { it('should not focus the overlay if something inside is auto focused', function () { let res = render( - + ); let input = res.getByTestId('input'); expect(document.activeElement).toBe(input); }); + + describe('dev warnings', function () { + let originalWarn; + + beforeEach(function () { + originalWarn = console.warn; + console.warn = jest.fn(); + }); + + afterEach(function () { + console.warn = originalWarn; + }); + + it('should warn when dialog has no accessible title', function () { + render(); + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining('A dialog must have a visible title for accessibility') + ); + }); + + it('should not warn when aria-label is provided', function () { + render(); + expect(console.warn).not.toHaveBeenCalled(); + }); + + it('should not warn when aria-labelledby is provided', function () { + render(); + expect(console.warn).not.toHaveBeenCalled(); + }); + + it('should not warn when a title element is rendered', function () { + render(); + expect(console.warn).not.toHaveBeenCalled(); + }); + }); }); From 25d36d68e3a5be965fa9ae1b1ff100d494ecc163 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:58:10 -0700 Subject: [PATCH 2/3] fix(dialog): resolve test failures in dialog title dev warning Check the DOM element directly for aria-label/aria-labelledby attributes instead of relying on hook props, since wrapper components (e.g. RAC Dialog) may add aria-labelledby from context after useDialog runs. Add the warning pattern to the allowed warnings list in setupTests.js so existing tests that render dialogs without accessible labels are not broken. --- packages/react-aria/src/dialog/useDialog.ts | 11 +++++++---- scripts/setupTests.js | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/react-aria/src/dialog/useDialog.ts b/packages/react-aria/src/dialog/useDialog.ts index 20e2b2981a4..04e67dccdd2 100644 --- a/packages/react-aria/src/dialog/useDialog.ts +++ b/packages/react-aria/src/dialog/useDialog.ts @@ -79,12 +79,15 @@ export function useDialog(props: AriaDialogProps, ref: RefObject { - if (process.env.NODE_ENV !== 'production' && !hasWarned.current) { - let hasAriaLabel = props['aria-label'] != null; - let hasAriaLabelledby = props['aria-labelledby'] != null; - if (!hasAriaLabel && !hasAriaLabelledby && titleId == null) { + if (process.env.NODE_ENV !== 'production' && !hasWarned.current && ref.current) { + let el = ref.current; + let hasAriaLabel = el.hasAttribute('aria-label'); + let hasAriaLabelledby = el.hasAttribute('aria-labelledby'); + if (!hasAriaLabel && !hasAriaLabelledby) { console.warn( 'A dialog must have a visible title for accessibility. ' + 'Either provide an aria-label or aria-labelledby prop, or render a heading element inside the dialog with the titleProps from useDialog.' diff --git a/scripts/setupTests.js b/scripts/setupTests.js index fedbf8c0bd3..891aa7cd563 100644 --- a/scripts/setupTests.js +++ b/scripts/setupTests.js @@ -39,7 +39,8 @@ const ERROR_PATTERNS_WE_SHOULD_FIX_BUT_ALLOW = [ ]; const WARNING_PATTERNS_WE_SHOULD_FIX_BUT_ALLOW = [ - 'Browserslist: caniuse-lite is outdated' + 'Browserslist: caniuse-lite is outdated', + 'A dialog must have a visible title for accessibility' ]; function failTestOnConsoleError() { From 7a62c3b4372cdd332cc5ed5b48d667d18d4215de Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Mon, 30 Mar 2026 06:39:24 -0700 Subject: [PATCH 3/3] fix: address review feedback - remove setupTests allowlist entry, drop 'visible' from warning Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/react-aria/src/dialog/useDialog.ts | 2 +- packages/react-aria/test/dialog/useDialog.test.js | 2 +- scripts/setupTests.js | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/react-aria/src/dialog/useDialog.ts b/packages/react-aria/src/dialog/useDialog.ts index 04e67dccdd2..8d6f503252f 100644 --- a/packages/react-aria/src/dialog/useDialog.ts +++ b/packages/react-aria/src/dialog/useDialog.ts @@ -89,7 +89,7 @@ export function useDialog(props: AriaDialogProps, ref: RefObject); expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining('A dialog must have a visible title for accessibility') + expect.stringContaining('A dialog must have a title for accessibility') ); }); diff --git a/scripts/setupTests.js b/scripts/setupTests.js index 891aa7cd563..fedbf8c0bd3 100644 --- a/scripts/setupTests.js +++ b/scripts/setupTests.js @@ -39,8 +39,7 @@ const ERROR_PATTERNS_WE_SHOULD_FIX_BUT_ALLOW = [ ]; const WARNING_PATTERNS_WE_SHOULD_FIX_BUT_ALLOW = [ - 'Browserslist: caniuse-lite is outdated', - 'A dialog must have a visible title for accessibility' + 'Browserslist: caniuse-lite is outdated' ]; function failTestOnConsoleError() {