Skip to content

Commit e378d9a

Browse files
authored
[Breaking Change][Fix]: remove default slotted title actions from dialog body (#34469)
1 parent c5c83ae commit e378d9a

14 files changed

Lines changed: 248 additions & 179 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "fixes accesibility issues with dialog close button",
4+
"packageName": "@fluentui/web-components",
5+
"email": "jes@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

packages/web-components/docs/web-components.api.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2311,7 +2311,8 @@ export class Dialog extends FASTElement {
23112311

23122312
// @public
23132313
export class DialogBody extends FASTElement {
2314-
noTitleAction: boolean;
2314+
// @internal
2315+
clickHandler(event: MouseEvent): boolean | void;
23152316
}
23162317

23172318
// @public
@@ -2784,6 +2785,9 @@ export const ImageStyles: ElementStyles;
27842785
// @public
27852786
export const ImageTemplate: ElementViewTemplate<Image_2>;
27862787

2788+
// @public
2789+
export function isDialog(element?: Node | null, tagName?: string): element is Dialog;
2790+
27872791
// @public
27882792
export function isDropdown(element?: Node | null, tagName?: string): element is BaseDropdown;
27892793

packages/web-components/src/dialog-body/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
| Name | Description |
44
| -------------- | ---------------------------------------------------------- |
55
| `title` | slot for title content |
6-
| `title-action` | slot for close button |
6+
| `title-action` | slot for additional title actions |
7+
| `close` | slot for close button |
78
| | default slot for content rendered between title and footer |
89
| `action` | slot for actions content |

packages/web-components/src/dialog-body/dialog-body.spec.ts

Lines changed: 0 additions & 54 deletions
This file was deleted.

packages/web-components/src/dialog-body/dialog-body.stories.ts

Lines changed: 51 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,48 @@ const dismissCircle20Regular = html`<svg
3434
></path>
3535
</svg>`;
3636

37+
const info20Regular = html`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
38+
<path
39+
fill="currentColor"
40+
d="M10.492 8.91A.5.5 0 0 0 9.5 9v4.502l.008.09a.5.5 0 0 0 .992-.09V9zm.307-2.16a.75.75 0 1 0-1.5 0a.75.75 0 0 0 1.5 0M18 10a8 8 0 1 0-16 0a8 8 0 0 0 16 0M3 10a7 7 0 1 1 14 0a7 7 0 0 1-14 0"
41+
/>
42+
</svg>`;
43+
44+
const dismissed16Regular = html.partial(`
45+
<svg
46+
fill="currentColor"
47+
aria-hidden="true"
48+
width="20"
49+
height="20"
50+
viewBox="0 0 20 20"
51+
xmlns="http://www.w3.org/2000/svg"
52+
>
53+
<path
54+
d="m4.09 4.22.06-.07a.5.5 0 0 1 .63-.06l.07.06L10 9.29l5.15-5.14a.5.5 0 0 1 .63-.06l.07.06c.18.17.2.44.06.63l-.06.07L10.71 10l5.14 5.15c.18.17.2.44.06.63l-.06.07a.5.5 0 0 1-.63.06l-.07-.06L10 10.71l-5.15 5.14a.5.5 0 0 1-.63.06l-.07-.06a.5.5 0 0 1-.06-.63l.06-.07L9.29 10 4.15 4.85a.5.5 0 0 1-.06-.63l.06-.07-.06.07Z"
55+
fill="currentColor"
56+
></path>
57+
</svg>`);
58+
59+
const titleActionTemplate = html`
60+
<fluent-button tabindex="0" slot="close" appearance="transparent" icon-only aria-label="close">
61+
${dismissed16Regular}
62+
</fluent-button>
63+
`;
64+
3765
const storyTemplate = html<StoryArgs<FluentDialogBody>>`
3866
<fluent-dialog-body>
39-
${x => x.titleSlottedContent?.()}
40-
${x => x.titleActionSlottedContent?.()}
41-
${x => x.slottedContent?.()}
42-
${x => x.actionSlottedContent?.()}
43-
</fluent-dialog>
67+
${x => x.titleSlottedContent?.()} ${x => x.titleActionSlottedContent?.()} ${x => x.closeSlottedContent?.()}
68+
${x => x.slottedContent?.()} ${x => x.actionSlottedContent?.()}
69+
</fluent-dialog-body>
4470
`;
4571

4672
export default {
4773
render: renderComponent(storyTemplate),
4874
title: 'Components/Dialog/Dialog Body',
75+
args: {
76+
closeSlottedContent: () => titleActionTemplate,
77+
},
4978
argTypes: {
50-
noTitleAction: {
51-
control: 'boolean',
52-
description:
53-
'Used to opt out of rendering the default title action that is rendered when the dialog `type` is set to `non-modal`.',
54-
table: {
55-
category: 'attributes',
56-
defaultValue: { summary: 'false' },
57-
type: { summary: 'boolean' },
58-
},
59-
},
6079
slottedContent: {
6180
control: false,
6281
name: '',
@@ -71,11 +90,16 @@ export default {
7190
},
7291
titleActionSlottedContent: {
7392
control: false,
74-
description:
75-
'Slot for the title action elements (e.g. Close button). When the dialog `type` is set to `non-modal` and no title action is provided, a default title action button is rendered.',
93+
description: 'Slot for the title action elements.',
7694
name: 'title-action',
7795
table: { category: 'slots', type: {} },
7896
},
97+
closeSlottedContent: {
98+
control: false,
99+
description: 'Slot for the close element.',
100+
name: 'close',
101+
table: { category: 'slots', type: {} },
102+
},
79103
titleSlottedContent: {
80104
control: false,
81105
description: 'Slot for the title element.',
@@ -136,47 +160,33 @@ export const Actions: Story = {
136160
</p>
137161
`,
138162
titleActionSlottedContent: () => html`
139-
<fluent-button appearance="transparent" icon-only slot="title-action"> ${dismissed20Regular} </fluent-button>
163+
<fluent-button appearance="transparent" icon-only slot="title-action"> ${info20Regular} </fluent-button>
140164
`,
141165
titleSlottedContent: () => html` <div slot="title">Actions</div> `,
142166
},
143167
};
144168

145-
export const NoTitleAction: Story = {
169+
export const NoClose: Story = {
146170
args: {
147-
noTitleAction: true,
148-
titleSlottedContent: () => html` <div slot="title">No Title Action</div> `,
171+
closeSlottedContent: () => html``,
172+
titleSlottedContent: () => html` <div slot="title">No Close Slot</div> `,
149173
slottedContent: () => html`
150-
<p>Omitting the title action will prevent the default close button from being rendered in a non-modal dialog.</p>
174+
<p>Omitting the close slot will prevent the default close button from being rendered in a non-modal dialog.</p>
151175
`,
152176
},
153177
};
154178

155-
export const CustomTitleAction: Story = {
179+
export const CustomClose: Story = {
156180
args: {
157181
slottedContent: () => html`
158-
<p>This dialog has a custom title action that is rendered in place of the default close button.</p>
182+
<p>This dialog has a custom <code>close</code> slot that is rendered in place of the default close button.</p>
159183
`,
160184
titleSlottedContent: () => html` <div slot="title">Custom Title Action</div> `,
161185

162-
titleActionSlottedContent: () => html`
163-
<fluent-button
164-
slot="title-action"
165-
appearance="transparent"
166-
icon-only
167-
@click="${() => alert('This is a custom action')}"
168-
>
169-
${dismissCircle20Regular}
186+
closeSlottedContent: () => html`
187+
<fluent-button slot="close" appearance="transparent" icon-only @click="${() => alert('This is a custom action')}">
188+
${info20Regular}
170189
</fluent-button>
171190
`,
172191
},
173192
};
174-
175-
export const NoTitleAndNoAction: Story = {
176-
args: {
177-
noTitleAction: true,
178-
slottedContent: () => html`
179-
<p>Omitting the title action will prevent the default close button from being rendered in a non-modal dialog.</p>
180-
`,
181-
},
182-
};

packages/web-components/src/dialog-body/dialog-body.styles.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,13 @@ export const styles = css`
7575
z-index: 2;
7676
}
7777
78-
/* Hide slots if nothing is slotted to remove grid gap */
79-
:not(:has(:is([slot='title'], [slot='title-action']))) .title:not(:has(.title-action)),
80-
:not(:has([slot='action'])) .actions {
81-
display: none;
78+
::slotted([slot='title-action']) {
79+
margin-inline-start: auto;
80+
}
81+
82+
/* align title content to the end when there is no title*/
83+
:not(:has(:is([slot='title'], [slot='title-action']))) .title {
84+
justify-content: end;
8285
}
8386
8487
@container (min-width: 480px) {
Lines changed: 9 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,17 @@
11
import { type ElementViewTemplate, html, ref } from '@microsoft/fast-element';
2-
import { DialogType } from '../dialog/dialog.options.js';
3-
4-
const dismissed16Regular = html.partial(`
5-
<svg
6-
fill="currentColor"
7-
aria-hidden="true"
8-
width="20"
9-
height="20"
10-
viewBox="0 0 20 20"
11-
xmlns="http://www.w3.org/2000/svg"
12-
>
13-
<path
14-
d="m4.09 4.22.06-.07a.5.5 0 0 1 .63-.06l.07.06L10 9.29l5.15-5.14a.5.5 0 0 1 .63-.06l.07.06c.18.17.2.44.06.63l-.06.07L10.71 10l5.14 5.15c.18.17.2.44.06.63l-.06.07a.5.5 0 0 1-.63.06l-.07-.06L10 10.71l-5.15 5.14a.5.5 0 0 1-.63.06l-.07-.06a.5.5 0 0 1-.06-.63l.06-.07L9.29 10 4.15 4.85a.5.5 0 0 1-.06-.63l.06-.07-.06.07Z"
15-
fill="currentColor"
16-
></path>
17-
</svg>`);
182

193
/**
204
* Template for the dialog form
215
* @public
226
*/
237
export const template: ElementViewTemplate = html`
24-
<div class="title" part="title">
25-
<slot name="title"></slot>
26-
<slot name="title-action">
27-
<fluent-button
28-
?hidden=${x => x.noTitleAction || x.parentNode?.type === DialogType.alert}
29-
tabindex="0"
30-
part="title-action"
31-
class="title-action"
32-
appearance="transparent"
33-
icon-only
34-
@click=${x => x.parentNode?.hide()}
35-
${ref('defaultTitleAction')}
36-
>
37-
${dismissed16Regular}
38-
</fluent-button>
39-
</slot>
40-
</div>
41-
<div class="content" part="content"><slot></slot></div>
42-
<div class="actions" part="actions"><slot name="action"></slot></div>
8+
<template>
9+
<div class="title" part="title">
10+
<slot name="title"></slot>
11+
<slot name="title-action"></slot>
12+
<slot name="close" @click="${(x, c) => x.clickHandler(c.event as PointerEvent)}"></slot>
13+
</div>
14+
<div class="content" part="content"><slot></slot></div>
15+
<div class="actions" part="actions"><slot name="action"></slot></div>
16+
</template>
4317
`;
Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { attr, FASTElement } from '@microsoft/fast-element';
1+
import { FASTElement } from '@microsoft/fast-element';
2+
import { isDialog } from '../dialog/dialog.options';
23
/**
34
* Dialog Body component that extends the FASTElement class.
45
*
@@ -9,9 +10,20 @@ import { attr, FASTElement } from '@microsoft/fast-element';
910
*/
1011
export class DialogBody extends FASTElement {
1112
/**
12-
* @public
13-
* Indicates whether the dialog has a title action
13+
* Handles click event for the close slot
14+
*
15+
* @param e - the click event
16+
* @internal
1417
*/
15-
@attr({ mode: 'boolean', attribute: 'no-title-action' })
16-
public noTitleAction: boolean = false;
18+
public clickHandler(event: MouseEvent): boolean | void {
19+
if (!event.defaultPrevented) {
20+
const dialog = this.parentElement;
21+
22+
if (isDialog(dialog)) {
23+
dialog.hide();
24+
}
25+
}
26+
27+
return true;
28+
}
1729
}

packages/web-components/src/dialog/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Fluent WC3 Dialog has feature parity with the Fluent UI React 9 Dialog implement
2828
<!-- Header -->
2929
<fluent-text slot="title">Dialog</fluent-text>
3030
<fluent-button slot="title-action"><svg></svg></fluent-button>
31+
<fluent-button slot="close"><svg></svg></fluent-button>
3132

3233
<!-- Default Content -->
3334
<fluent-text>Default Content</fluent-text>
@@ -41,13 +42,12 @@ Fluent WC3 Dialog has feature parity with the Fluent UI React 9 Dialog implement
4142

4243
### **Attributes**
4344

44-
| Name | Privacy | Type | Default | Description |
45-
| ------------------ | ------- | ------------ | ------------------ | --------------------------------------------------------- |
46-
| `type` | public | `DialogType` | `DialogType.modal` | Indicates that the type of modal to render. |
47-
| `no-title-action` | public | `boolean` | `false` | Used to set whether the default title action is rendered. |
48-
| `aria-labelledby` | public | `string` | `undefined` | optional based on implementation\*\* |
49-
| `aria-describedby` | public | `string` | `undefined` | optional based on implementation\*\* |
50-
| `aria-label ` | public | `string` | `undefined` | optional based on implementation\*\* |
45+
| Name | Privacy | Type | Default | Description |
46+
| ------------------ | ------- | ------------ | ------------------ | ------------------------------------------- |
47+
| `type` | public | `DialogType` | `DialogType.modal` | Indicates that the type of modal to render. |
48+
| `aria-labelledby` | public | `string` | `undefined` | optional based on implementation\*\* |
49+
| `aria-describedby` | public | `string` | `undefined` | optional based on implementation\*\* |
50+
| `aria-label ` | public | `string` | `undefined` | optional based on implementation\*\* |
5151

5252
\*\* See the [W3C Specification](https://w3c.github.io/aria-practices/#dialog_roles_states_props) for requirements and details.
5353

packages/web-components/src/dialog/dialog.options.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ValuesOf } from '../utils/index.js';
2+
import { Dialog } from './dialog.js';
23

34
/**
45
* Dialog modal type
@@ -11,3 +12,19 @@ export const DialogType = {
1112
} as const;
1213

1314
export type DialogType = ValuesOf<typeof DialogType>;
15+
16+
/**
17+
* Predicate function that determines if the element should be considered a dialog.
18+
*
19+
* @param element - The element to check.
20+
* @param tagName - The tag name to check.
21+
* @returns true if the element is a dialog.
22+
* @public
23+
*/
24+
export function isDialog(element?: Node | null, tagName: string = '-dialog'): element is Dialog {
25+
if (element?.nodeType !== Node.ELEMENT_NODE) {
26+
return false;
27+
}
28+
29+
return (element as Element).tagName.toLowerCase().endsWith(tagName);
30+
}

0 commit comments

Comments
 (0)