Skip to content

Commit 7988f34

Browse files
maradwan26adamalstonheloiselui
authored
feat(cds-dialog): new preview component (#21853)
* feat(dialog): new wc preview component * fix: footer elements selector * fix: align stories * fix(react): update scrolling content handling * chore: export as preview * fix: formatting * fix: remove legacy test * fix: deprecate ariaLabel * docs: sync react and wc stories * chore: update snapshots * chore: cleanup and formatting * chore: cleanup * fix: esc key handling and selector override * test: update tests * fix: ts * fix: a11y * refactor: remove uneccesary aria-label prop * chore: cleanup * chore: cleanup aria-labelledby * docs: dialog markdown cdn * refactor: extend class * fix: prevent close on esc for non-modal dialog * test: label handling * Update packages/react/src/components/Dialog/Dialog.tsx Co-authored-by: Adam Alston <aalston9@gmail.com> * fix: derive body label from custom id * refactor: types, set labels on render * fix(a11y): apply proper labelling and role on scroll container * refactor: aria-labelledby title fallback * chore: cleanup * fix(esc): add !open guard * fix(body): scope userDefinedTabindex guard to tabindex only * docs: add see preview reference * chore: remove preview prefix from export --------- Co-authored-by: Adam Alston <aalston9@gmail.com> Co-authored-by: Heloise Lui <71858203+heloiselui@users.noreply.github.com>
1 parent 13f2327 commit 7988f34

25 files changed

Lines changed: 1399 additions & 109 deletions

e2e/components/Dialog/Dialog-test.avt.e2e.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corp. 2016, 2025
2+
* Copyright IBM Corp. 2016, 2026
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
@@ -36,6 +36,7 @@ test.describe('@avt Dialog', () => {
3636
await page.keyboard.press('Tab');
3737
await page.keyboard.press('Tab');
3838
await page.keyboard.press('Tab');
39+
await page.keyboard.press('Tab');
3940
await expect(page.getByRole('button', { name: 'Cancel' })).toBeFocused();
4041

4142
await page.keyboard.press('Tab');

packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12051,15 +12051,18 @@ Map {
1205112051
"Dialog": {
1205212052
"$$typeof": Symbol(react.forward_ref),
1205312053
"propTypes": {
12054-
"aria-label": {
12054+
"aria-describedby": {
1205512055
"type": "string",
1205612056
},
12057-
"aria-labelledby": {
12057+
"aria-label": {
1205812058
"type": "string",
1205912059
},
12060-
"ariaDescribedBy": {
12060+
"aria-labelledby": {
1206112061
"type": "string",
1206212062
},
12063+
"ariaDescribedBy": [Function],
12064+
"ariaLabel": [Function],
12065+
"ariaLabelledBy": [Function],
1206312066
"children": {
1206412067
"type": "node",
1206512068
},

packages/react/src/components/Dialog/Dialog-test.js

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -172,39 +172,39 @@ describe('Dialog', () => {
172172
expect(body).toHaveAttribute('tabindex', '0');
173173
});
174174

175-
it('should support `DialogBody` resize based scroll detection and function refs', () => {
176-
jest.useFakeTimers();
177-
175+
it('should support `DialogBody` resize observer based scroll detection and function refs', async () => {
178176
const bodyRef = jest.fn();
179177

180-
render(
178+
const { rerender } = render(
181179
<Dialog open>
182180
<DialogBody ref={bodyRef} data-testid="body">
183-
Body
181+
<div style={{ height: '100px' }}>Content</div>
184182
</DialogBody>
185183
</Dialog>
186184
);
187-
188185
const body = screen.getByTestId('body');
189186

187+
expect(body).not.toHaveClass(`${prefix}--dialog-scroll-content`);
188+
190189
Object.defineProperty(body, 'clientHeight', {
191190
configurable: true,
192-
value: 10,
191+
value: 300,
193192
});
194193
Object.defineProperty(body, 'scrollHeight', {
195194
configurable: true,
196-
value: 20,
195+
value: 600,
197196
});
198197

199-
act(() => {
200-
window.dispatchEvent(new Event('resize'));
201-
jest.advanceTimersByTime(250);
202-
});
198+
rerender(
199+
<Dialog open>
200+
<DialogBody ref={bodyRef} data-testid="body">
201+
<div style={{ height: '800px' }}>Content</div>
202+
</DialogBody>
203+
</Dialog>
204+
);
203205

204206
expect(body).toHaveClass(`${prefix}--dialog-scroll-content`);
205207
expect(bodyRef).toHaveBeenCalledWith(body);
206-
207-
jest.useRealTimers();
208208
});
209209

210210
it('should support `DialogBody` object refs', () => {
@@ -333,6 +333,52 @@ describe('Dialog', () => {
333333
raf.mockRestore();
334334
caf.mockRestore();
335335
});
336+
337+
it('prefers aria-label prop over deprecated ariaLabel prop', () => {
338+
render(<Dialog open aria-label="label" ariaLabel="deprecated label" />);
339+
340+
const dialog = screen.getByRole('dialog');
341+
342+
expect(dialog).toHaveAttribute('aria-label', 'label');
343+
});
344+
345+
it('prefers aria-labelledby and aria-describedby props over deprecated camelCase props', () => {
346+
render(
347+
<Dialog
348+
open
349+
aria-labelledby="title"
350+
aria-describedby="description"
351+
ariaLabelledBy="deprecated-title"
352+
ariaDescribedBy="deprecated-description"
353+
/>
354+
);
355+
356+
const dialog = screen.getByRole('dialog');
357+
358+
expect(dialog).toHaveAttribute('aria-labelledby', 'title');
359+
expect(dialog).toHaveAttribute('aria-describedby', 'description');
360+
});
361+
362+
it.each([
363+
['aria-label', { 'aria-label': 'label' }],
364+
['deprecated ariaLabel', { ariaLabel: 'deprecated label' }],
365+
['aria-labelledby', { 'aria-labelledby': 'label' }],
366+
['deprecated ariaLabelledBy', { ariaLabelledBy: 'deprecated label' }],
367+
])(
368+
'does not apply aria-labelledby=title.id fallback when %s is provided',
369+
(_, props) => {
370+
render(
371+
<Dialog open {...props}>
372+
<DialogTitle>Title</DialogTitle>
373+
</Dialog>
374+
);
375+
376+
const dialog = screen.getByRole('dialog');
377+
const title = screen.getByText('Title');
378+
379+
expect(dialog.getAttribute('aria-labelledby')).not.toBe(title.id);
380+
}
381+
);
336382
});
337383

338384
it('should bring focusAfterCloseRef element into focus on close when the ref is defined', async () => {

packages/react/src/components/Dialog/Dialog.stories.js

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corp. 2016, 2025
2+
* Copyright IBM Corp. 2016, 2026
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
@@ -34,7 +34,14 @@ export default {
3434
page: mdx,
3535
},
3636
controls: {
37-
exclude: ['hasScrollingContent', 'modal', 'open', 'focusAfterCloseRef'],
37+
exclude: [
38+
'hasScrollingContent',
39+
'modal',
40+
'open',
41+
'focusAfterCloseRef',
42+
'ariaDescribedBy',
43+
'ariaLabelledBy',
44+
],
3845
},
3946
},
4047
};
@@ -64,11 +71,7 @@ export const Modal = ({ open: _open, ...args }) => {
6471
<Button type="button" onClick={toggleDialog}>
6572
Toggle open
6673
</Button>
67-
<Dialog
68-
{...args}
69-
open={open}
70-
onRequestClose={handleRequestClose}
71-
aria-labelledby="title">
74+
<Dialog {...args} open={open} onRequestClose={handleRequestClose}>
7275
<DialogHeader>
7376
<DialogSubtitle>Configure dialog settings</DialogSubtitle>
7477
<DialogTitle id="title">Modal Dialog Example</DialogTitle>
@@ -182,11 +185,7 @@ export const NonModal = ({ open: _open, ...args }) => {
182185
<Button type="button" onClick={toggleDialog}>
183186
Toggle open
184187
</Button>
185-
<Dialog
186-
{...args}
187-
open={open}
188-
onRequestClose={handleRequestClose}
189-
aria-label="Dialog Title">
188+
<Dialog {...args} open={open} onRequestClose={handleRequestClose}>
190189
<DialogHeader>
191190
<DialogSubtitle>Non-modal dialog example Subtitle</DialogSubtitle>
192191
<DialogTitle>Non-Modal Dialog</DialogTitle>
@@ -259,11 +258,7 @@ export const WithScrollingContent = ({ open: _open, ...args }) => {
259258
<Button type="button" onClick={toggleDialog}>
260259
Toggle open
261260
</Button>
262-
<Dialog
263-
{...args}
264-
open={open}
265-
onRequestClose={handleRequestClose}
266-
aria-label="Dialog Title">
261+
<Dialog {...args} open={open} onRequestClose={handleRequestClose}>
267262
<DialogHeader>
268263
<DialogSubtitle>Configure dialog settings</DialogSubtitle>
269264
<DialogTitle>Modal Dialog Example</DialogTitle>
@@ -373,12 +368,7 @@ export const PassiveDialog = ({ open: _open, ...args }) => {
373368
<Button type="button" onClick={toggleDialog}>
374369
Toggle open
375370
</Button>
376-
<Dialog
377-
{...args}
378-
open={open}
379-
modal
380-
onRequestClose={handleRequestClose}
381-
aria-label="Dialog Title">
371+
<Dialog {...args} open={open} modal onRequestClose={handleRequestClose}>
382372
<DialogHeader>
383373
<DialogTitle>Information Message</DialogTitle>
384374
<DialogControls>

0 commit comments

Comments
 (0)