Skip to content

Commit f52f47c

Browse files
authored
FocusTrapZone: port most tests to cypress (#21741)
1 parent d4c3346 commit f52f47c

14 files changed

+787
-1110
lines changed

azure-pipelines.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,12 @@ jobs:
102102

103103
# only run e2e tests when the appropriate storybook is published by scoping to relevant packages
104104
- script: |
105-
yarn e2e $(sinceArg) --scope @fluentui/react-components --scope @fluentui/react
106-
displayName: Cypress E2E tests
105+
yarn e2e $(sinceArg) --scope @fluentui/react-components
106+
displayName: v9 Cypress E2E tests
107+
108+
- script: |
109+
yarn e2e $(sinceArg) --scope @fluentui/react
110+
displayName: v8 Cypress E2E tests
107111
108112
- template: .devops/templates/cleanup.yml
109113

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as React from 'react';
2+
3+
/**
4+
* Define a global function on `window` and clean it up after the test finishes.
5+
* NOTE: This only runs once (updates to the function are not respected).
6+
*/
7+
export function useGlobal<Globals>(name: keyof Globals, func: Required<Globals>[typeof name]) {
8+
React.useEffect(() => {
9+
((window as unknown) as Globals)[name] = func;
10+
return () => {
11+
// Clean up the global to avoid timing issues where a test tries to call the version of a
12+
// global defined by a previous test (this can happen in headless mode)
13+
delete ((window as unknown) as Globals)[name];
14+
};
15+
// eslint-disable-next-line react-hooks/exhaustive-deps -- should only run on mount
16+
}, []);
17+
}
18+
19+
export type UsePropsGlobals<TProps> = {
20+
/** Callback allowing a test to update the value returned by `useProps` inside a story. */
21+
setProps?: (props: TProps) => void;
22+
};
23+
24+
/**
25+
* Define a global `window.setProps` to set props for the story, and clean it up
26+
* after the test finishes.
27+
* @returns the latest props
28+
*/
29+
export function useProps<TProps>() {
30+
const [props, setProps] = React.useState<TProps | undefined>();
31+
useGlobal<UsePropsGlobals<TProps>>('setProps' as const, setProps);
32+
return props;
33+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as React from 'react';
2+
import { FocusTrapZone } from '@fluentui/react';
3+
import { useGlobal } from '../../../e2e/utils';
4+
import { FTZTestGlobals } from './types';
5+
import { rootClass } from './shared';
6+
7+
/**
8+
* It maintains a proper stack of FocusTrapZones as more are mounted/unmounted
9+
*/
10+
export const FocusStack = () => {
11+
// Whether to render each FocusTrapZone
12+
const [shouldRender, setShouldRender] = React.useState([true, false, false, false, false]);
13+
14+
const updateFTZ = (num: 1 | 2 | 3 | 4, newValue: boolean) => {
15+
setShouldRender(prevValues => {
16+
const newValues = [...prevValues];
17+
newValues[num] = newValue;
18+
return newValues;
19+
});
20+
};
21+
22+
useGlobal<FTZTestGlobals>('getFocusStack', () => FocusTrapZone.focusStack);
23+
24+
return (
25+
<div className={rootClass}>
26+
<FocusTrapZone id="ftz0">
27+
ftz0
28+
<button onClick={() => updateFTZ(1, true)}>add ftz1</button>
29+
<button onClick={() => updateFTZ(3, true)}>add ftz3</button>
30+
<button onClick={() => updateFTZ(4, true)}>add ftz4</button>
31+
</FocusTrapZone>
32+
33+
{shouldRender[1] && (
34+
<FocusTrapZone id="ftz1">
35+
ftz1
36+
<button onClick={() => updateFTZ(2, true)}>add ftz2</button>
37+
</FocusTrapZone>
38+
)}
39+
{shouldRender[2] && (
40+
<FocusTrapZone id="ftz2">
41+
ftz2
42+
<button onClick={() => updateFTZ(1, false)}>remove ftz1</button>
43+
<button onClick={() => updateFTZ(2, false)}>remove ftz2</button>
44+
</FocusTrapZone>
45+
)}
46+
{shouldRender[3] && (
47+
<FocusTrapZone id="ftz3" forceFocusInsideTrap={false}>
48+
ftz3
49+
<button onClick={() => updateFTZ(3, false)}>remove ftz3</button>
50+
</FocusTrapZone>
51+
)}
52+
{shouldRender[4] && (
53+
<FocusTrapZone id="ftz4" disabled>
54+
ftz4
55+
</FocusTrapZone>
56+
)}
57+
</div>
58+
);
59+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from 'react';
2+
import { FocusZone, FocusTrapZone } from '@fluentui/react';
3+
import type { IFocusTrapZone, IFocusTrapZoneProps } from '@fluentui/react';
4+
import { useGlobal, useProps } from '../../../e2e/utils';
5+
import type { FTZTestGlobals } from './types';
6+
import { rootClass } from './shared';
7+
8+
/** Imperatively focusing the FTZ */
9+
export const ImperativeFocus = () => {
10+
const props = useProps<IFocusTrapZoneProps>();
11+
const focusTrapZoneRef = React.useRef<IFocusTrapZone>(null);
12+
13+
useGlobal<FTZTestGlobals>('imperativeFocus', () => focusTrapZoneRef.current?.focus());
14+
15+
return (
16+
// don't render until props have been set
17+
props && (
18+
<div className={rootClass}>
19+
<FocusTrapZone disableFirstFocus componentRef={focusTrapZoneRef} {...props}>
20+
<button>first</button>
21+
<FocusZone>
22+
<button>mid</button>
23+
<button>last</button>
24+
</FocusZone>
25+
</FocusTrapZone>
26+
<button>after</button>
27+
</div>
28+
)
29+
);
30+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as React from 'react';
2+
import { FocusTrapZone } from '@fluentui/react';
3+
import type { IFocusTrapZoneProps } from '@fluentui/react';
4+
import { useProps } from '../../../e2e/utils';
5+
import { rootClass } from './shared';
6+
7+
/**
8+
* Tab and shift-tab when the FTZ contains 0 tabbable items
9+
*/
10+
export const NoTabbableItems = () => {
11+
const props = useProps<IFocusTrapZoneProps>();
12+
13+
return (
14+
// don't render until props have been set
15+
props && (
16+
<div className={rootClass}>
17+
<button>before</button>
18+
<FocusTrapZone forceFocusInsideTrap {...props}>
19+
<button tabIndex={-1}>first</button>
20+
<button tabIndex={-1}>mid</button>
21+
<button tabIndex={-1}>last</button>
22+
</FocusTrapZone>
23+
<button>after</button>
24+
</div>
25+
)
26+
);
27+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as React from 'react';
2+
import { FocusTrapZone } from '@fluentui/react';
3+
import type { IFocusTrapZoneProps } from '@fluentui/react';
4+
import { useProps } from '../../../e2e/utils';
5+
import { rootClass } from './shared';
6+
7+
/** Respects default and explicit prop values */
8+
export const PropValues = () => {
9+
const [buttonClicked, setButtonClicked] = React.useState('');
10+
const props = useProps<IFocusTrapZoneProps>();
11+
12+
return (
13+
// don't render until props have been set
14+
props && (
15+
<div className={rootClass} onClick={ev => setButtonClicked((ev.target as HTMLButtonElement).textContent || '')}>
16+
<span id="buttonClicked" style={{ display: 'block' /* avoid inherited div styling */ }}>
17+
clicked {buttonClicked}
18+
</span>
19+
<button>before</button>
20+
<FocusTrapZone {...props}>
21+
<button>first</button>
22+
<button>mid</button>
23+
<button className="last-class" id="last">
24+
last
25+
</button>
26+
</FocusTrapZone>
27+
<button>after</button>
28+
</div>
29+
)
30+
);
31+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as React from 'react';
2+
import { FocusZone, FocusTrapZone, FocusZoneDirection } from '@fluentui/react';
3+
import { rootClass } from './shared';
4+
5+
/**
6+
* Tab and shift-tab wrap at extreme ends of the FTZ:
7+
*
8+
* can tab between a button and a FocusZone
9+
*/
10+
export const TabWrappingButtonFocusZone = () => {
11+
return (
12+
<div className={rootClass}>
13+
<FocusTrapZone forceFocusInsideTrap={false}>
14+
<div>
15+
<button>first</button>
16+
</div>
17+
<FocusZone direction={FocusZoneDirection.horizontal}>
18+
<div>
19+
<button>fzFirst</button>
20+
</div>
21+
<div>
22+
<div>
23+
<button>fzMid1</button>
24+
<button>fzMid2</button>
25+
<button>fzLast</button>
26+
</div>
27+
</div>
28+
</FocusZone>
29+
</FocusTrapZone>
30+
</div>
31+
);
32+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as React from 'react';
2+
import { FocusZone, FocusTrapZone, FocusZoneDirection } from '@fluentui/react';
3+
import { rootClass } from './shared';
4+
5+
/**
6+
* Tab and shift-tab wrap at extreme ends of the FTZ:
7+
*
8+
* can trap focus when FTZ bookmark elements are FocusZones,
9+
* and those elements have inner elements focused that are not the first inner element
10+
*/
11+
export const TabWrappingFocusZoneBumpers = () => {
12+
return (
13+
<div className={rootClass}>
14+
<button>before</button>
15+
<FocusTrapZone forceFocusInsideTrap={false}>
16+
<FocusZone direction={FocusZoneDirection.horizontal}>
17+
<button>fz1First</button>
18+
<button>fz1Mid</button>
19+
<button>fz1Last</button>
20+
</FocusZone>
21+
<button>mid</button>
22+
<FocusZone direction={FocusZoneDirection.horizontal}>
23+
<button>fz2First</button>
24+
<button>fz2Mid</button>
25+
<button>fz2Last</button>
26+
</FocusZone>
27+
</FocusTrapZone>
28+
<button>after</button>
29+
</div>
30+
);
31+
};

packages/react-examples/src/react/FocusTrapZone/e2e/FocusTrapZone.TabWrappingMultiFocusZone.stories.tsx

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,33 @@
11
import * as React from 'react';
2-
import { FocusZone, FocusTrapZone, FocusZoneDirection, mergeStyles } from '@fluentui/react';
3-
4-
const rootClass = mergeStyles({
5-
position: 'relative',
6-
button: { position: 'absolute', height: 30, width: 30 },
7-
'#a': { top: 0, left: 0 },
8-
'#b': { top: 0, left: 30 },
9-
'#c': { top: 0, left: 60 },
10-
'#d': { top: 30, left: 0 },
11-
'#e': { top: 30, left: 30 },
12-
'#f': { top: 30, left: 60 },
13-
});
2+
import { FocusZone, FocusTrapZone, FocusZoneDirection } from '@fluentui/react';
3+
import { rootClass } from './shared';
144

155
/**
166
* Tab and shift-tab wrap at extreme ends of the FTZ:
177
*
18-
* can tab across FocusZones with different button structures
8+
* can tab between multiple FocusZones with different button structures
199
*/
2010
export const TabWrappingMultiFocusZone = () => {
2111
return (
2212
<div className={rootClass}>
2313
<FocusTrapZone forceFocusInsideTrap={false}>
2414
<FocusZone direction={FocusZoneDirection.horizontal}>
2515
<div>
26-
<button id="a">a</button>
16+
<button>fz1First</button>
2717
</div>
2818
<div>
29-
<button id="b">b</button>
19+
<button>fz1Mid</button>
3020
</div>
3121
<div>
32-
<button id="c">c</button>
22+
<button>fz1Last</button>
3323
</div>
3424
</FocusZone>
3525
<FocusZone direction={FocusZoneDirection.horizontal}>
3626
<div>
3727
<div>
38-
<button id="d">d</button>
39-
<button id="e">e</button>
40-
<button id="f">f</button>
28+
<button>fz2First</button>
29+
<button>fz2Mid</button>
30+
<button>fz2Last</button>
4131
</div>
4232
</div>
4333
</FocusZone>

0 commit comments

Comments
 (0)