@lumx/react 3.6.7 → 3.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_internal/types.d.ts +29 -29
- package/index.d.ts +45 -45
- package/index.js +28 -16
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/alert-dialog/AlertDialog.tsx +2 -12
- package/src/components/autocomplete/Autocomplete.test.tsx +1 -1
- package/src/components/autocomplete/AutocompleteMultiple.test.tsx +1 -1
- package/src/components/badge/Badge.test.tsx +1 -1
- package/src/components/button/Button.test.tsx +2 -2
- package/src/components/button/IconButton.test.tsx +1 -1
- package/src/components/chip/Chip.test.tsx +22 -5
- package/src/components/chip/Chip.tsx +9 -5
- package/src/components/date-picker/DatePickerControlled.tsx +5 -4
- package/src/components/generic-block/GenericBlock.test.tsx +9 -9
- package/src/components/grid-column/GridColumn.tsx +32 -34
- package/src/components/icon/Icon.test.tsx +1 -1
- package/src/components/input-helper/InputHelper.test.tsx +1 -1
- package/src/components/link/Link.test.tsx +3 -3
- package/src/components/link-preview/LinkPreview.test.tsx +1 -2
- package/src/components/message/Message.test.tsx +1 -1
- package/src/components/mosaic/Mosaic.tsx +5 -4
- package/src/components/popover/Popover.tsx +13 -20
- package/src/components/select/Select.test.tsx +3 -3
- package/src/components/select/SelectMultiple.stories.tsx +56 -58
- package/src/components/select/SelectMultiple.test.tsx +4 -4
- package/src/components/side-navigation/SideNavigation.test.tsx +2 -2
- package/src/components/slideshow/SlideshowControls.stories.tsx +4 -9
- package/src/components/slideshow/useSlideFocusManagement.tsx +1 -1
- package/src/components/tabs/TabProvider.test.tsx +1 -1
- package/src/components/thumbnail/Thumbnail.stories.tsx +5 -5
- package/src/constants.ts +2 -2
- package/src/stories/decorators/withCombinations.tsx +76 -74
- package/src/utils/date/getFirstDayOfWeek.ts +2 -1
- package/src/utils/date/getMonthCalendar.test.ts +4 -0
- package/src/utils/date/getMonthCalendar.ts +10 -2
- package/src/utils/focus/constants.ts +4 -2
- package/src/utils/focus/getFirstAndLastFocusable.test.ts +19 -19
- package/src/utils/focus/getFocusableElements.test.ts +13 -13
- package/src/utils/type.ts +17 -13
- package/src/utils/utils.test.ts +5 -5
|
@@ -265,66 +265,64 @@ export const SelectWithinADialog = ({ theme }: any) => {
|
|
|
265
265
|
searchText && searchText.length > 0 ? CHOICES.filter((choice) => choice.includes(searchText)) : CHOICES;
|
|
266
266
|
|
|
267
267
|
return (
|
|
268
|
-
|
|
269
|
-
<
|
|
270
|
-
<header>
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
<input type="hidden" />
|
|
268
|
+
<Dialog isOpen>
|
|
269
|
+
<header>
|
|
270
|
+
<Toolbar label={<span className="lumx-typography-title">Dialog header</span>} />
|
|
271
|
+
</header>
|
|
272
|
+
<div className="lumx-spacing-padding-horizontal-huge lumx-spacing-padding-bottom-huge">
|
|
273
|
+
{/* Testing hidden input do not count in th focus trap*/}
|
|
274
|
+
<input hidden type="file" />
|
|
275
|
+
<input type="hidden" />
|
|
277
276
|
|
|
278
|
-
|
|
277
|
+
<div className="lumx-spacing-margin-bottom-huge">The select should capture the focus on open.</div>
|
|
279
278
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
279
|
+
<SelectMultiple
|
|
280
|
+
isOpen={isOpen}
|
|
281
|
+
value={values}
|
|
282
|
+
onClear={clearSelected}
|
|
283
|
+
clearButtonProps={{ label: 'Clear' }}
|
|
284
|
+
label={LABEL}
|
|
285
|
+
placeholder={PLACEHOLDER}
|
|
286
|
+
theme={theme}
|
|
287
|
+
onInputClick={toggleSelect}
|
|
288
|
+
onDropdownClose={closeSelect}
|
|
289
|
+
icon={mdiTram}
|
|
290
|
+
focusElement={searchFieldRef}
|
|
291
|
+
>
|
|
292
|
+
<List isClickable>
|
|
293
|
+
<>
|
|
294
|
+
<ListSubheader>
|
|
295
|
+
<TextField
|
|
296
|
+
clearButtonProps={{ label: 'Clear' }}
|
|
297
|
+
placeholder="Search"
|
|
298
|
+
role="searchbox"
|
|
299
|
+
inputRef={searchFieldRef}
|
|
300
|
+
onChange={setSearchText}
|
|
301
|
+
value={searchText}
|
|
302
|
+
/>
|
|
303
|
+
</ListSubheader>
|
|
304
|
+
<ListDivider role="presentation" />
|
|
305
|
+
</>
|
|
307
306
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
</>
|
|
307
|
+
{filteredChoices.length > 0
|
|
308
|
+
? filteredChoices.map((choice) => (
|
|
309
|
+
<ListItem
|
|
310
|
+
isSelected={values.includes(choice)}
|
|
311
|
+
key={choice}
|
|
312
|
+
onItemSelected={selectItem(choice)}
|
|
313
|
+
size={Size.tiny}
|
|
314
|
+
>
|
|
315
|
+
{choice}
|
|
316
|
+
</ListItem>
|
|
317
|
+
))
|
|
318
|
+
: [
|
|
319
|
+
<ListItem key={0} size={Size.tiny}>
|
|
320
|
+
No data
|
|
321
|
+
</ListItem>,
|
|
322
|
+
]}
|
|
323
|
+
</List>
|
|
324
|
+
</SelectMultiple>
|
|
325
|
+
</div>
|
|
326
|
+
</Dialog>
|
|
329
327
|
);
|
|
330
328
|
};
|
|
@@ -39,7 +39,7 @@ const setup = (props: Partial<SelectMultipleProps> = {}) => {
|
|
|
39
39
|
return { props, select, getDropdown, helpers, inputWrapper, chip, valueChips };
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
describe(
|
|
42
|
+
describe('<SelectMultiple>', () => {
|
|
43
43
|
describe('Props', () => {
|
|
44
44
|
it('should have default classNames', () => {
|
|
45
45
|
const { select, inputWrapper, valueChips, chip } = setup();
|
|
@@ -48,7 +48,7 @@ describe(`<SelectMultiple>`, () => {
|
|
|
48
48
|
expect(valueChips).toBeEmptyDOMElement();
|
|
49
49
|
expect(chip).not.toBeInTheDocument();
|
|
50
50
|
expect(select.className).toMatchInlineSnapshot(
|
|
51
|
-
|
|
51
|
+
'"lumx-select lumx-select--has-multiple lumx-select lumx-select--is-empty lumx-select--theme-light"',
|
|
52
52
|
);
|
|
53
53
|
});
|
|
54
54
|
|
|
@@ -119,7 +119,7 @@ describe(`<SelectMultiple>`, () => {
|
|
|
119
119
|
expect(inputWrapper).not.toBeInTheDocument();
|
|
120
120
|
expect(chip).toBeInTheDocument();
|
|
121
121
|
expect(select.className).toMatchInlineSnapshot(
|
|
122
|
-
|
|
122
|
+
'"lumx-select lumx-select--has-multiple lumx-select lumx-select--is-empty lumx-select--theme-light"',
|
|
123
123
|
);
|
|
124
124
|
});
|
|
125
125
|
|
|
@@ -127,7 +127,7 @@ describe(`<SelectMultiple>`, () => {
|
|
|
127
127
|
const { select, chip } = setup({ variant: SelectVariant.chip, value: ['val1', 'val2'] });
|
|
128
128
|
expect(chip).toHaveTextContent('val1 +1');
|
|
129
129
|
expect(select.className).toMatchInlineSnapshot(
|
|
130
|
-
|
|
130
|
+
'"lumx-select lumx-select--has-multiple lumx-select lumx-select--has-value lumx-select--theme-light"',
|
|
131
131
|
);
|
|
132
132
|
});
|
|
133
133
|
|
|
@@ -21,12 +21,12 @@ const setup = (props: Partial<SideNavigationProps> = {}) => {
|
|
|
21
21
|
describe(`<${SideNavigation.displayName}>`, () => {
|
|
22
22
|
it('should render default', () => {
|
|
23
23
|
const { sideNavigation } = setup();
|
|
24
|
-
expect(sideNavigation.className).toMatchInlineSnapshot(
|
|
24
|
+
expect(sideNavigation.className).toMatchInlineSnapshot('"lumx-side-navigation"');
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
it('should render dark theme', () => {
|
|
28
28
|
const { sideNavigation } = setup({ theme: Theme.dark });
|
|
29
|
-
expect(sideNavigation.className).toMatchInlineSnapshot(
|
|
29
|
+
expect(sideNavigation.className).toMatchInlineSnapshot('"lumx-color-font-light-N lumx-side-navigation"');
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
// Common tests suite.
|
|
@@ -7,15 +7,10 @@ import { LANDSCAPE_IMAGES } from '@lumx/react/stories/controls/image';
|
|
|
7
7
|
export default { title: 'LumX components/slideshow/Slideshow controls' };
|
|
8
8
|
|
|
9
9
|
export const Simple = () => {
|
|
10
|
-
const {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
onPreviousClick,
|
|
15
|
-
onPaginationClick,
|
|
16
|
-
} = SlideshowControls.useSlideshowControls({
|
|
17
|
-
itemsCount: 9,
|
|
18
|
-
});
|
|
10
|
+
const { activeIndex, slidesCount, onNextClick, onPreviousClick, onPaginationClick } =
|
|
11
|
+
SlideshowControls.useSlideshowControls({
|
|
12
|
+
itemsCount: 9,
|
|
13
|
+
});
|
|
19
14
|
|
|
20
15
|
return (
|
|
21
16
|
<SlideshowControls
|
|
@@ -11,7 +11,7 @@ export interface UseSlideFocusManagementProps {
|
|
|
11
11
|
* This is to easily find elements that have been tempered with,
|
|
12
12
|
* and not elements whose focus was already initially blocked.
|
|
13
13
|
* */
|
|
14
|
-
const BLOCKED_FOCUS_CLASSNAME =
|
|
14
|
+
const BLOCKED_FOCUS_CLASSNAME = 'focus-blocked';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Manage how slides must behave when visible or not.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import { mdiAbTesting
|
|
3
|
+
import { mdiAbTesting } from '@lumx/icons';
|
|
4
4
|
import { Alignment, AspectRatio, Badge, FlexBox, Icon, Size, Thumbnail, ThumbnailVariant } from '@lumx/react';
|
|
5
5
|
import { CustomLink } from '@lumx/react/stories/utils/CustomLink';
|
|
6
6
|
import { IMAGE_SIZES, imageArgType, IMAGES } from '@lumx/react/stories/controls/image';
|
|
@@ -159,16 +159,16 @@ export const Original = () => (
|
|
|
159
159
|
</tr>
|
|
160
160
|
<tr>
|
|
161
161
|
<td>
|
|
162
|
-
<Thumbnail alt="" image={IMAGES.landscape1} />
|
|
162
|
+
<Thumbnail alt="landscape image" image={IMAGES.landscape1} />
|
|
163
163
|
</td>
|
|
164
164
|
<td>
|
|
165
|
-
<Thumbnail alt="" image={IMAGES.landscape1} imgProps={IMAGE_SIZES.landscape1} />
|
|
165
|
+
<Thumbnail alt="landscape image" image={IMAGES.landscape1} imgProps={IMAGE_SIZES.landscape1} />
|
|
166
166
|
</td>
|
|
167
167
|
<td>
|
|
168
|
-
<Thumbnail alt="" image={IMAGES.portrait1} />
|
|
168
|
+
<Thumbnail alt="portrait image" image={IMAGES.portrait1} />
|
|
169
169
|
</td>
|
|
170
170
|
<td>
|
|
171
|
-
<Thumbnail alt="" image={IMAGES.portrait1} imgProps={IMAGE_SIZES.portrait1} />
|
|
171
|
+
<Thumbnail alt="portrait image" image={IMAGES.portrait1} imgProps={IMAGE_SIZES.portrait1} />
|
|
172
172
|
</td>
|
|
173
173
|
</tr>
|
|
174
174
|
</table>
|
package/src/constants.ts
CHANGED
|
@@ -9,9 +9,9 @@ export {
|
|
|
9
9
|
/**
|
|
10
10
|
* Optional global `window` instance (not defined when running SSR).
|
|
11
11
|
*/
|
|
12
|
-
export const WINDOW = typeof window !==
|
|
12
|
+
export const WINDOW = typeof window !== 'undefined' ? window : undefined;
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Optional global `document` instance (not defined when running SSR).
|
|
16
16
|
*/
|
|
17
|
-
export const DOCUMENT = typeof document !==
|
|
17
|
+
export const DOCUMENT = typeof document !== 'undefined' ? document : undefined;
|
|
@@ -20,80 +20,82 @@ function formatProps(config?: Combination): PropCombination {
|
|
|
20
20
|
/**
|
|
21
21
|
* SB decorator generating a tables of combination of props (max 3 levels of props)
|
|
22
22
|
*/
|
|
23
|
-
export const withCombinations =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
23
|
+
export const withCombinations =
|
|
24
|
+
({
|
|
25
|
+
combinations,
|
|
26
|
+
tableStyle,
|
|
27
|
+
firstColStyle,
|
|
28
|
+
cellStyle,
|
|
29
|
+
combinator = Object.assign,
|
|
30
|
+
}: {
|
|
31
|
+
/** Props combinations */
|
|
32
|
+
combinations: {
|
|
33
|
+
rows?: Combination;
|
|
34
|
+
cols?: Combination;
|
|
35
|
+
sections?: Combination;
|
|
36
|
+
};
|
|
37
|
+
/** Inject style on table */
|
|
38
|
+
tableStyle?: React.CSSProperties;
|
|
39
|
+
/** Inject style on first col */
|
|
40
|
+
firstColStyle?: React.CSSProperties;
|
|
41
|
+
/** Inject style on cells */
|
|
42
|
+
cellStyle?: React.CSSProperties;
|
|
43
|
+
/** Combinator function */
|
|
44
|
+
combinator?: (a: any, b: any) => any;
|
|
45
|
+
}) =>
|
|
46
|
+
(Story: any, ctx: any) => {
|
|
47
|
+
const rows = formatProps(combinations.rows);
|
|
48
|
+
const cols = formatProps(combinations.cols);
|
|
49
|
+
const sections = formatProps(combinations.sections);
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
{Object.entries(sections).map(([level2Key, level2Value]) => (
|
|
54
|
+
<div key={level2Key}>
|
|
55
|
+
{level2Key && <h2>{level2Key}</h2>}
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
<table style={{ ...tableStyle, borderCollapse: 'separate', borderSpacing: 8 }}>
|
|
58
|
+
{combinations.cols && (
|
|
59
|
+
<thead>
|
|
60
|
+
<tr>
|
|
61
|
+
<th />
|
|
62
|
+
{Object.keys(cols).map((key) => (
|
|
63
|
+
<th key={key}>
|
|
64
|
+
<small>{key}</small>
|
|
65
|
+
</th>
|
|
66
|
+
))}
|
|
67
|
+
</tr>
|
|
68
|
+
</thead>
|
|
69
|
+
)}
|
|
70
|
+
<tbody>
|
|
71
|
+
{Object.entries(rows).map(([level0Key, level0Value], i1) => (
|
|
72
|
+
<tr key={i1}>
|
|
73
|
+
<th style={{ ...firstColStyle, textAlign: 'left' }}>
|
|
74
|
+
<small>{level0Key}</small>
|
|
63
75
|
</th>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
})}
|
|
91
|
-
</tr>
|
|
92
|
-
))}
|
|
93
|
-
</tbody>
|
|
94
|
-
</table>
|
|
95
|
-
</div>
|
|
96
|
-
))}
|
|
97
|
-
</>
|
|
98
|
-
);
|
|
99
|
-
};
|
|
76
|
+
{Object.entries(cols).map(([level1Key, level1Value]) => {
|
|
77
|
+
const args = [level0Value, level1Value, level2Value].reduce(
|
|
78
|
+
(acc, value) => combinator(acc, value),
|
|
79
|
+
{ ...ctx.args },
|
|
80
|
+
);
|
|
81
|
+
return (
|
|
82
|
+
<td
|
|
83
|
+
key={level1Key}
|
|
84
|
+
style={{
|
|
85
|
+
textAlign: combinations.cols ? 'center' : undefined,
|
|
86
|
+
...cellStyle,
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<Story args={args} />
|
|
90
|
+
</td>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
</tr>
|
|
94
|
+
))}
|
|
95
|
+
</tbody>
|
|
96
|
+
</table>
|
|
97
|
+
</div>
|
|
98
|
+
))}
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
@@ -22,7 +22,8 @@ const FIRST_DAY_FOR_LOCALES = [
|
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
// Locales with Monday as the first day of the week
|
|
25
|
-
localeRX:
|
|
25
|
+
localeRX:
|
|
26
|
+
/^(ar-(ma|tn)|az|be|bg|bs|ca|cs|da|de|el|en-(au|gb|ie|in|nz)|eo|es|et|eu|fi|fr|fy|gl|gu|hr|ht|hu|hy|id|is|it|ka|kk|kn|lb|lt|lv|mk|mn|ms|mt|nb|nl|nn|oc|pl|pt|ro|ru|sk|sl|sq|sr|sv|ta|tr|uk|uz|vi|zh-(cn|tw))$/i,
|
|
26
27
|
firstDay: 1,
|
|
27
28
|
},
|
|
28
29
|
{
|
|
@@ -57,6 +57,8 @@ describe(getMonthCalendar.name, () => {
|
|
|
57
57
|
'1': { date: new Date('2017-02-27') },
|
|
58
58
|
'2': { date: new Date('2017-02-28') },
|
|
59
59
|
},
|
|
60
|
+
// Empty row (used for padding to avoid layout shift)
|
|
61
|
+
{},
|
|
60
62
|
],
|
|
61
63
|
});
|
|
62
64
|
});
|
|
@@ -117,6 +119,8 @@ describe(getMonthCalendar.name, () => {
|
|
|
117
119
|
'1': { date: new Date('2017-02-27'), isOutOfRange: true },
|
|
118
120
|
'2': { date: new Date('2017-02-28'), isOutOfRange: true },
|
|
119
121
|
},
|
|
122
|
+
// Empty row (used for padding to avoid layout shift)
|
|
123
|
+
{},
|
|
120
124
|
],
|
|
121
125
|
});
|
|
122
126
|
});
|
|
@@ -11,6 +11,9 @@ interface MonthCalendar {
|
|
|
11
11
|
weeks: Array<AnnotatedWeek>;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** Up to 6 rows can appear in a month calendar => 4 weeks + 1 start of month partial week + 1 send of month partial week */
|
|
15
|
+
const MONTH_ROW_COUNT = 6;
|
|
16
|
+
|
|
14
17
|
/**
|
|
15
18
|
* Get month calendar.
|
|
16
19
|
* A list of weeks with days indexed by week day number
|
|
@@ -30,7 +33,8 @@ export const getMonthCalendar = (
|
|
|
30
33
|
|
|
31
34
|
const weeks: Array<AnnotatedWeek> = [];
|
|
32
35
|
let week: AnnotatedWeek = {};
|
|
33
|
-
|
|
36
|
+
let rowCount = 0;
|
|
37
|
+
while (rowCount < MONTH_ROW_COUNT) {
|
|
34
38
|
const weekDayNumber = iterDate.getDay();
|
|
35
39
|
const day: AnnotatedDay = { date: new Date(iterDate.getTime()) };
|
|
36
40
|
|
|
@@ -39,9 +43,13 @@ export const getMonthCalendar = (
|
|
|
39
43
|
day.isOutOfRange = true;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
if (iterDate.getMonth() === month) {
|
|
47
|
+
week[weekDayNumber] = day;
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
if (weekDayNumber === lastDayOfWeek.number) {
|
|
44
51
|
weeks.push(week);
|
|
52
|
+
rowCount += 1;
|
|
45
53
|
week = {};
|
|
46
54
|
}
|
|
47
55
|
iterDate.setDate(iterDate.getDate() + 1);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** CSS selector listing all tabbable elements. */
|
|
2
|
-
export const TABBABLE_ELEMENTS_SELECTOR =
|
|
2
|
+
export const TABBABLE_ELEMENTS_SELECTOR =
|
|
3
|
+
'a[href], button, textarea, input:not([type="hidden"]):not([hidden]), [tabindex]';
|
|
3
4
|
|
|
4
5
|
/** CSS selector matching element that are disabled (should not receive focus). */
|
|
5
|
-
export const DISABLED_SELECTOR =
|
|
6
|
+
export const DISABLED_SELECTOR =
|
|
7
|
+
'[hidden], [tabindex="-1"], [disabled]:not([disabled="false"]), [aria-disabled]:not([aria-disabled="false"])';
|
|
@@ -8,13 +8,13 @@ function htmlToElement(html: string): any {
|
|
|
8
8
|
|
|
9
9
|
describe(getFirstAndLastFocusable.name, () => {
|
|
10
10
|
it('should get empty', () => {
|
|
11
|
-
const element = htmlToElement(
|
|
11
|
+
const element = htmlToElement('<div></div>');
|
|
12
12
|
const focusable = getFirstAndLastFocusable(element);
|
|
13
13
|
expect(focusable).toEqual({});
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
it('should get single item', () => {
|
|
17
|
-
const element = htmlToElement(
|
|
17
|
+
const element = htmlToElement('<div><button /></div>');
|
|
18
18
|
const focusable = getFirstAndLastFocusable(element);
|
|
19
19
|
expect(focusable.last).toBe(focusable.first);
|
|
20
20
|
});
|
|
@@ -44,13 +44,13 @@ describe(getFirstAndLastFocusable.name, () => {
|
|
|
44
44
|
|
|
45
45
|
describe('match focusable elements', () => {
|
|
46
46
|
it('should match input element', () => {
|
|
47
|
-
const element = htmlToElement(
|
|
47
|
+
const element = htmlToElement('<div><input /></div>');
|
|
48
48
|
const focusable = getFirstAndLastFocusable(element);
|
|
49
|
-
expect(focusable.first).toMatchInlineSnapshot(
|
|
49
|
+
expect(focusable.first).toMatchInlineSnapshot('<input />');
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
it('should match link element', () => {
|
|
53
|
-
const element = htmlToElement(
|
|
53
|
+
const element = htmlToElement('<div><a href="#" /></div>');
|
|
54
54
|
const focusable = getFirstAndLastFocusable(element);
|
|
55
55
|
expect(focusable.first).toMatchInlineSnapshot(`
|
|
56
56
|
<a
|
|
@@ -60,7 +60,7 @@ describe(getFirstAndLastFocusable.name, () => {
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
it('should match textarea element', () => {
|
|
63
|
-
const element = htmlToElement(
|
|
63
|
+
const element = htmlToElement('<div><textarea /></div>');
|
|
64
64
|
const focusable = getFirstAndLastFocusable(element);
|
|
65
65
|
expect(focusable.first).toMatchInlineSnapshot(`
|
|
66
66
|
<textarea>
|
|
@@ -70,7 +70,7 @@ describe(getFirstAndLastFocusable.name, () => {
|
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
it('should match element with tabindex', () => {
|
|
73
|
-
const element = htmlToElement(
|
|
73
|
+
const element = htmlToElement('<div><span tabindex="0" /></div>');
|
|
74
74
|
const focusable = getFirstAndLastFocusable(element);
|
|
75
75
|
expect(focusable.first).toMatchInlineSnapshot(`
|
|
76
76
|
<span
|
|
@@ -80,7 +80,7 @@ describe(getFirstAndLastFocusable.name, () => {
|
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
it('should keep disabled=false', () => {
|
|
83
|
-
const element = htmlToElement(
|
|
83
|
+
const element = htmlToElement('<div><button disabled="false" /><button /></div>');
|
|
84
84
|
const focusable = getFirstAndLastFocusable(element);
|
|
85
85
|
expect(focusable.first).toMatchInlineSnapshot(`
|
|
86
86
|
<button
|
|
@@ -90,7 +90,7 @@ describe(getFirstAndLastFocusable.name, () => {
|
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
it('should keep aria-disabled=false', () => {
|
|
93
|
-
const element = htmlToElement(
|
|
93
|
+
const element = htmlToElement('<div><button aria-disabled="false" /><button /></div>');
|
|
94
94
|
const focusable = getFirstAndLastFocusable(element);
|
|
95
95
|
expect(focusable.first).toMatchInlineSnapshot(`
|
|
96
96
|
<button
|
|
@@ -102,33 +102,33 @@ describe(getFirstAndLastFocusable.name, () => {
|
|
|
102
102
|
|
|
103
103
|
describe('skip disabled elements', () => {
|
|
104
104
|
it('should skip disabled', () => {
|
|
105
|
-
const element = htmlToElement(
|
|
105
|
+
const element = htmlToElement('<div><button disabled /><button /></div>');
|
|
106
106
|
const focusable = getFirstAndLastFocusable(element);
|
|
107
|
-
expect(focusable.first).toMatchInlineSnapshot(
|
|
107
|
+
expect(focusable.first).toMatchInlineSnapshot('<button />');
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
it('should skip aria-disabled', () => {
|
|
111
|
-
const element = htmlToElement(
|
|
111
|
+
const element = htmlToElement('<div><button aria-disabled /><button /></div>');
|
|
112
112
|
const focusable = getFirstAndLastFocusable(element);
|
|
113
|
-
expect(focusable.first).toMatchInlineSnapshot(
|
|
113
|
+
expect(focusable.first).toMatchInlineSnapshot('<button />');
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
it('should skip tabindex=-1', () => {
|
|
117
|
-
const element = htmlToElement(
|
|
117
|
+
const element = htmlToElement('<div><button tabindex="-1" /><button /></div>');
|
|
118
118
|
const focusable = getFirstAndLastFocusable(element);
|
|
119
|
-
expect(focusable.first).toMatchInlineSnapshot(
|
|
119
|
+
expect(focusable.first).toMatchInlineSnapshot('<button />');
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
it('should skip input type hidden', () => {
|
|
123
|
-
const element = htmlToElement(
|
|
123
|
+
const element = htmlToElement('<div><input type="hidden" /><button /></div>');
|
|
124
124
|
const focusable = getFirstAndLastFocusable(element);
|
|
125
|
-
expect(focusable.first).toMatchInlineSnapshot(
|
|
125
|
+
expect(focusable.first).toMatchInlineSnapshot('<button />');
|
|
126
126
|
});
|
|
127
127
|
|
|
128
128
|
it('should skip hidden input', () => {
|
|
129
|
-
const element = htmlToElement(
|
|
129
|
+
const element = htmlToElement('<div><input hidden /><button /></div>');
|
|
130
130
|
const focusable = getFirstAndLastFocusable(element);
|
|
131
|
-
expect(focusable.first).toMatchInlineSnapshot(
|
|
131
|
+
expect(focusable.first).toMatchInlineSnapshot('<button />');
|
|
132
132
|
});
|
|
133
133
|
});
|
|
134
134
|
});
|