Skip to content

Commit 6d3a9a0

Browse files
tay1orjones2nikhiltomadamalston
committed
test(dialog): initial unit tests, mock HTMLDialogElement in jest config, revert feature flag unification (#19134)
* test(dialog): initial unit tests, mock HTMLDialogElement in jest config * fix(composed-modal): separate dialog element and focuswrap w/out sentinel flags * fix(modal): separate dialog element and focuswrap w/out sentinel flags * Update packages/react/src/components/Modal/Modal.tsx Co-authored-by: Adam Alston <aalston9@gmail.com> --------- Co-authored-by: Nikhil Tomar <63502271+2nikhiltom@users.noreply.github.com> Co-authored-by: Adam Alston <aalston9@gmail.com>
1 parent cdcd629 commit 6d3a9a0

File tree

10 files changed

+567
-163
lines changed

10 files changed

+567
-163
lines changed

config/jest-config-carbon/setup/setup.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,25 @@ if (global.window && global.AnimationEvent === undefined) {
7474
}
7575
global.AnimationEvent = AnimationEvent;
7676
}
77+
78+
// jsdom doesn't implement HTMLDialogElement
79+
// https://github.com/jsdom/jsdom/issues/3294
80+
if (global.window) {
81+
if (!window.HTMLDialogElement.prototype.show) {
82+
HTMLDialogElement.prototype.show = jest.fn(function () {
83+
this.open = true;
84+
});
85+
}
86+
87+
if (!window.HTMLDialogElement.prototype.showModal) {
88+
HTMLDialogElement.prototype.showModal = jest.fn(function () {
89+
this.open = true;
90+
});
91+
}
92+
93+
if (!window.HTMLDialogElement.prototype.close) {
94+
HTMLDialogElement.prototype.close = jest.fn(function () {
95+
this.open = false;
96+
});
97+
}
98+
}

packages/react/.storybook/templates/WithFeatureFlags/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function WithFeatureFlags({ children }) {
2626
text={
2727
<span>
2828
This story is rendered with{' '}
29-
<LinkTo title="Experimental/Feature Flags" name="Overview">
29+
<LinkTo title="Getting Started/Feature Flags" name="Overview">
3030
all available feature flags
3131
</LinkTo>{' '}
3232
enabled

packages/react/src/components/ComposedModal/ComposedModal.featureflag.mdx

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,53 @@
11
import { Meta } from '@storybook/blocks';
22

3-
<Meta title="Components/ComposedModal/Feature Flag" name="Flag details" />
3+
<Meta title="Components/ComposedModal/Feature Flags" name="Flag details" />
44

5-
# ComposedModal
5+
# Feature Flags for ComposedModal
66

77
[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/react/src/components/ComposedModal)
88
&nbsp;|&nbsp;
99
[Usage guidelines](https://www.carbondesignsystem.com/components/modal/usage)
1010
&nbsp;|&nbsp;
1111
[Accessibility](https://www.carbondesignsystem.com/components/modal/accessibility)
1212

13+
## `enable-experimental-focus-wrap-without-sentinels`
14+
15+
`ComposedModal` supports the `enable-experimental-focus-wrap-without-sentinels`
16+
feature flag. When enabled, the hidden "sentinel nodes" used for focus wrap are
17+
no longer emitted to the DOM. These were previously used to mark the beginning
18+
and end of the area where focus should wrap within.
19+
20+
The new behavior looks at all interactive child nodes and wraps focus based on
21+
tabbable order of those nodes. The focus direction is determined whether `tab`
22+
is being pressed (forward) or `shift`+`tab` is being pressed (backwards).
23+
24+
Note: The native dialog element handles focus, so `enable-dialog-element` must
25+
be off for `enable-experimental-focus-wrap-without-sentinels` to have any
26+
effect.
27+
28+
### Enabling the flag
29+
30+
To enable the flag, use the `FeatureFlags` component and set the prop:
31+
32+
```js
33+
<FeatureFlags enableExperimentalFocusWrapWithoutSentinels>
34+
<ComposedModal ... />
35+
</FeatureFlags>
36+
```
37+
1338
## `enable-dialog-element`
1439

15-
`ComposedModal` supports the `enable-dialog-element` feature flag. This enables
16-
a new approach internal to the ComposedModal that uses the native `<dialog>`
17-
element. With this, the browser natively controls the focus wrap behavior. This
18-
means that the DOM no longer includes the "sentinel nodes" previously needed for
19-
the ComposedModal to manage focus wrap.
40+
`ComposedModal` supports the `enable-dialog-element` feature flag. When enabled,
41+
ComposedModal will use the native `<dialog>` element instead of `role="dialog"`.
42+
With the `<dialog>` element, the browser natively controls the focus wrap
43+
behavior. This means that:
2044

21-
`ComposedModal` also supports the
22-
`enable-experimental-focus-wrap-without-sentinels` feature flag that was
23-
implemented previous to the dialog element refactor. In ComposedModal, this flag
24-
is an alias for the `enable-dialog-element` flag. They do the same thing and you
25-
only need to use one, preferably `enable-dialog-element`.
45+
- The DOM no longer includes the "sentinel nodes" previously needed for the
46+
ComposedModal to manage focus wrap.
47+
- `enable-experimental-focus-wrap-without-sentinels` has no effect and can be
48+
removed when using `enable-dialog-element`
2649

27-
## Enabling the flag
50+
### Enabling the flag
2851

2952
To enable the flag, use the `FeatureFlags` component and set the prop:
3053

packages/react/src/components/ComposedModal/ComposedModal.featureflag.stories.js

Lines changed: 134 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -16,74 +16,158 @@ import Select from '../Select';
1616
import SelectItem from '../SelectItem';
1717
import TextInput from '../TextInput';
1818
import Button from '../Button';
19-
import {
20-
StructuredListWrapper,
21-
StructuredListHead,
22-
StructuredListBody,
23-
StructuredListRow,
24-
StructuredListCell,
25-
} from '../StructuredList';
26-
import mdx from './ComposedModal.featureflag.mdx';
27-
import { WithFeatureFlags } from '../../../.storybook/templates/WithFeatureFlags';
28-
import { title } from 'process';
19+
import { FeatureFlags } from '../FeatureFlags';
20+
import { Annotation } from '../../../.storybook/templates/Annotation';
21+
import LinkTo from '@storybook/addon-links/react';
2922

3023
export default {
31-
title: 'Components/ComposedModal/Feature Flag',
24+
title: 'Components/ComposedModal/Feature Flags',
3225
component: ComposedModal,
3326
subcomponents: {
3427
ModalHeader,
3528
ModalBody,
3629
ModalFooter,
3730
},
3831
tags: ['!autodocs'],
39-
decorators: [
40-
(Story) => (
41-
<WithFeatureFlags>
42-
<Story />
43-
</WithFeatureFlags>
44-
),
45-
],
4632
};
4733

4834
export const EnableDialogElement = (args) => {
4935
const [open, setOpen] = useState(true);
5036
return (
51-
<>
52-
<Button onClick={() => setOpen(true)}>Launch composed modal</Button>
53-
<ComposedModal {...args} open={open} onClose={() => setOpen(false)}>
54-
<ModalHeader
55-
label="Account resources"
56-
title="Add a custom domain"
57-
{...args}
58-
/>
59-
<ModalBody>
60-
<p style={{ marginBottom: '1rem' }}>
61-
Custom domains direct requests for your apps in this Cloud Foundry
62-
organization to a URL that you own. A custom domain can be a shared
63-
domain, a shared subdomain, or a shared domain and host.
64-
</p>
65-
<TextInput
66-
data-modal-primary-focus
67-
id="text-input-1"
68-
labelText="Domain name"
69-
placeholder="e.g. github.com"
70-
style={{ marginBottom: '1rem' }}
37+
<FeatureFlags enableDialogElement>
38+
<Annotation
39+
type="feature-flags"
40+
text={
41+
<span>
42+
This story is rendered with{' '}
43+
<LinkTo title="Getting Started/Feature Flags" name="Overview">
44+
enable-dialog-element
45+
</LinkTo>{' '}
46+
enabled
47+
</span>
48+
}>
49+
<Button onClick={() => setOpen(true)}>Launch composed modal</Button>
50+
<ComposedModal {...args} open={open} onClose={() => setOpen(false)}>
51+
<ModalHeader
52+
label="Account resources"
53+
title="Add a custom domain"
54+
{...args}
7155
/>
72-
<Select id="select-1" defaultValue="us-south" labelText="Region">
73-
<SelectItem value="us-south" text="US South" />
74-
<SelectItem value="us-east" text="US East" />
75-
</Select>
76-
</ModalBody>
77-
<ModalFooter
78-
primaryButtonText="Add"
79-
secondaryButtonText="Cancel"
80-
{...args}
81-
/>
82-
</ComposedModal>
83-
</>
56+
<ModalBody>
57+
<p style={{ marginBottom: '1rem' }}>
58+
Custom domains direct requests for your apps in this Cloud Foundry
59+
organization to a URL that you own. A custom domain can be a
60+
shared domain, a shared subdomain, or a shared domain and host.
61+
</p>
62+
<TextInput
63+
data-modal-primary-focus
64+
id="text-input-1"
65+
labelText="Domain name"
66+
placeholder="e.g. github.com"
67+
style={{ marginBottom: '1rem' }}
68+
/>
69+
<Select id="select-1" defaultValue="us-south" labelText="Region">
70+
<SelectItem value="us-south" text="US South" />
71+
<SelectItem value="us-east" text="US East" />
72+
</Select>
73+
</ModalBody>
74+
<ModalFooter
75+
primaryButtonText="Add"
76+
secondaryButtonText="Cancel"
77+
{...args}
78+
/>
79+
</ComposedModal>
80+
</Annotation>
81+
</FeatureFlags>
8482
);
8583
};
84+
EnableDialogElement.storyName = 'enable-dialog-element';
85+
EnableDialogElement.argTypes = {
86+
children: {
87+
table: {
88+
disable: true,
89+
},
90+
},
91+
className: {
92+
table: {
93+
disable: true,
94+
},
95+
},
96+
containerClassName: {
97+
table: {
98+
disable: true,
99+
},
100+
},
101+
onClose: {
102+
action: 'onClose',
103+
},
104+
onKeyDown: {
105+
action: 'onKeyDown',
106+
},
107+
selectorPrimaryFocus: {
108+
table: {
109+
disable: true,
110+
},
111+
},
112+
selectorsFloatingMenus: {
113+
table: {
114+
disable: true,
115+
},
116+
},
117+
};
86118

119+
export const EnableExperimentalFocusWrapWithoutSentinels = (args) => {
120+
const [open, setOpen] = useState(true);
121+
return (
122+
<FeatureFlags enableExperimentalFocusWrapWithoutSentinels>
123+
<Annotation
124+
type="feature-flags"
125+
text={
126+
<span>
127+
This story is rendered with{' '}
128+
<LinkTo title="Getting Started/Feature Flags" name="Overview">
129+
enable-experimental-focus-wrap-without-sentinels
130+
</LinkTo>{' '}
131+
enabled
132+
</span>
133+
}>
134+
<Button onClick={() => setOpen(true)}>Launch composed modal</Button>
135+
<ComposedModal {...args} open={open} onClose={() => setOpen(false)}>
136+
<ModalHeader
137+
label="Account resources"
138+
title="Add a custom domain"
139+
{...args}
140+
/>
141+
<ModalBody>
142+
<p style={{ marginBottom: '1rem' }}>
143+
Custom domains direct requests for your apps in this Cloud Foundry
144+
organization to a URL that you own. A custom domain can be a
145+
shared domain, a shared subdomain, or a shared domain and host.
146+
</p>
147+
<TextInput
148+
data-modal-primary-focus
149+
id="text-input-1"
150+
labelText="Domain name"
151+
placeholder="e.g. github.com"
152+
style={{ marginBottom: '1rem' }}
153+
/>
154+
<Select id="select-1" defaultValue="us-south" labelText="Region">
155+
<SelectItem value="us-south" text="US South" />
156+
<SelectItem value="us-east" text="US East" />
157+
</Select>
158+
</ModalBody>
159+
<ModalFooter
160+
primaryButtonText="Add"
161+
secondaryButtonText="Cancel"
162+
{...args}
163+
/>
164+
</ComposedModal>
165+
</Annotation>
166+
</FeatureFlags>
167+
);
168+
};
169+
EnableExperimentalFocusWrapWithoutSentinels.storyName =
170+
'enable-experimental-focus-wrap-without-sentinels';
87171
EnableDialogElement.argTypes = {
88172
children: {
89173
table: {

0 commit comments

Comments
 (0)