@lumx/react 2.2.25 → 2.2.26-alpha-a11y-slideshow.2
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/esm/_internal/ClickAwayProvider.js +9 -5
- package/esm/_internal/ClickAwayProvider.js.map +1 -1
- package/esm/_internal/List2.js.map +1 -1
- package/esm/_internal/ProgressTrackerStepPanel.js +2 -1
- package/esm/_internal/ProgressTrackerStepPanel.js.map +1 -1
- package/esm/_internal/Slides.js +206 -76
- package/esm/_internal/Slides.js.map +1 -1
- package/esm/_internal/TabPanel.js +2 -1
- package/esm/_internal/TabPanel.js.map +1 -1
- package/esm/_internal/progress-tracker.js +2 -1
- package/esm/_internal/progress-tracker.js.map +1 -1
- package/esm/_internal/slideshow.js +2 -0
- package/esm/_internal/slideshow.js.map +1 -1
- package/esm/_internal/state.js +145 -0
- package/esm/_internal/state.js.map +1 -0
- package/esm/_internal/tabs.js +1 -0
- package/esm/_internal/tabs.js.map +1 -1
- package/esm/_internal/useRovingTabIndex.js +9 -144
- package/esm/_internal/useRovingTabIndex.js.map +1 -1
- package/esm/index.js +3 -1
- package/esm/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/slideshow/Slides.tsx +33 -3
- package/src/components/slideshow/Slideshow.stories.tsx +90 -2
- package/src/components/slideshow/Slideshow.tsx +15 -3
- package/src/components/slideshow/SlideshowControls.stories.tsx +1 -1
- package/src/components/slideshow/SlideshowControls.tsx +43 -6
- package/src/components/slideshow/SlideshowItem.tsx +0 -5
- package/src/components/slideshow/SlideshowItemGroup.tsx +56 -0
- package/src/components/slideshow/__snapshots__/Slideshow.test.tsx.snap +10 -1
- package/src/components/slideshow/useSlideFocusManagement.tsx +67 -0
- package/src/hooks/useRovingTabIndex.tsx +9 -0
- package/src/utils/focus/constants.ts +5 -0
- package/src/utils/focus/getFirstAndLastFocusable.ts +4 -10
- package/src/utils/focus/getFocusableElements.test.ts +174 -0
- package/src/utils/focus/getFocusableElements.ts +7 -0
- package/types.d.ts +19 -5
|
@@ -8,9 +8,9 @@ import { Emphasis, IconButton, IconButtonProps, Theme } from '@lumx/react';
|
|
|
8
8
|
import { Comp, GenericProps, getRootClassName, handleBasicClasses, HasTheme } from '@lumx/react/utils';
|
|
9
9
|
import { WINDOW } from '@lumx/react/constants';
|
|
10
10
|
import { useSlideshowControls, DEFAULT_OPTIONS } from '@lumx/react/hooks/useSlideshowControls';
|
|
11
|
+
import { useRovingTabIndex } from '@lumx/react/hooks/useRovingTabIndex';
|
|
11
12
|
|
|
12
13
|
import { useSwipeNavigate } from './useSwipeNavigate';
|
|
13
|
-
import { useKeyNavigate } from './useKeyNavigate';
|
|
14
14
|
import { PAGINATION_ITEM_SIZE, PAGINATION_ITEMS_MAX } from './constants';
|
|
15
15
|
import { usePaginationVisibleRange } from './usePaginationVisibleRange';
|
|
16
16
|
|
|
@@ -28,6 +28,8 @@ export interface SlideshowControlsProps extends GenericProps, HasTheme {
|
|
|
28
28
|
/** Props to pass to the previous button (minus those already set by the SlideshowControls props). */
|
|
29
29
|
previousButtonProps: Pick<IconButtonProps, 'label'> &
|
|
30
30
|
Omit<IconButtonProps, 'label' | 'onClick' | 'icon' | 'emphasis' | 'color'>;
|
|
31
|
+
/** Props to pass to the pagination wrapper */
|
|
32
|
+
paginationProps?: Omit<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style' | 'role'>;
|
|
31
33
|
/** Number of slides. */
|
|
32
34
|
slidesCount: number;
|
|
33
35
|
/** On next button click callback. */
|
|
@@ -38,8 +40,15 @@ export interface SlideshowControlsProps extends GenericProps, HasTheme {
|
|
|
38
40
|
onPreviousClick?(loopback?: boolean): void;
|
|
39
41
|
/** whether the slideshow is currently playing */
|
|
40
42
|
isAutoPlaying?: boolean;
|
|
41
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* function to be executed in order to retrieve the label for the pagination item
|
|
45
|
+
* @deprecated Use paginationItemProps instead.
|
|
46
|
+
* */
|
|
42
47
|
paginationItemLabel?: (index: number) => string;
|
|
48
|
+
/**
|
|
49
|
+
* function to be executed in order to retrieve the props for a pagination item.
|
|
50
|
+
*/
|
|
51
|
+
paginationItemProps?: (itemIndex: number) => React.HTMLAttributes<HTMLButtonElement>;
|
|
43
52
|
/** Props to pass to the lay button (minus those already set by the SlideshowControls props). */
|
|
44
53
|
playButtonProps?: Pick<IconButtonProps, 'label'> &
|
|
45
54
|
Omit<IconButtonProps, 'label' | 'onClick' | 'icon' | 'emphasis' | 'color'>;
|
|
@@ -80,11 +89,13 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
80
89
|
onPreviousClick,
|
|
81
90
|
parentRef,
|
|
82
91
|
previousButtonProps,
|
|
92
|
+
paginationProps,
|
|
83
93
|
slidesCount,
|
|
84
94
|
theme,
|
|
85
95
|
isAutoPlaying = false,
|
|
86
96
|
playButtonProps,
|
|
87
97
|
paginationItemLabel,
|
|
98
|
+
paginationItemProps,
|
|
88
99
|
...forwardedProps
|
|
89
100
|
} = props;
|
|
90
101
|
|
|
@@ -93,8 +104,7 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
93
104
|
// Checking window object to avoid errors in SSR.
|
|
94
105
|
parent = parentRef instanceof HTMLElement ? parentRef : parentRef?.current;
|
|
95
106
|
}
|
|
96
|
-
|
|
97
|
-
useKeyNavigate(parent, onNextClick, onPreviousClick);
|
|
107
|
+
const paginationRef = React.useRef(null);
|
|
98
108
|
// Listen to touch swipe navigate left & right.
|
|
99
109
|
useSwipeNavigate(
|
|
100
110
|
parent,
|
|
@@ -104,6 +114,18 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
104
114
|
useCallback(() => onPreviousClick?.(false), [onPreviousClick]),
|
|
105
115
|
);
|
|
106
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Add roving tab index pattern to pagination items and activate slide on focus.
|
|
119
|
+
*/
|
|
120
|
+
useRovingTabIndex({
|
|
121
|
+
parentRef: paginationRef,
|
|
122
|
+
elementSelector: 'button',
|
|
123
|
+
keepTabIndex: true,
|
|
124
|
+
onElementFocus: (element) => {
|
|
125
|
+
element.click();
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
107
129
|
// Pagination "bullet" range.
|
|
108
130
|
const visibleRange = usePaginationVisibleRange(activeIndex as number, slidesCount);
|
|
109
131
|
|
|
@@ -126,8 +148,13 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
126
148
|
emphasis={Emphasis.low}
|
|
127
149
|
onClick={onPreviousClick}
|
|
128
150
|
/>
|
|
129
|
-
<div className={`${CLASSNAME}__pagination`}>
|
|
130
|
-
<div
|
|
151
|
+
<div ref={paginationRef} className={`${CLASSNAME}__pagination`}>
|
|
152
|
+
<div
|
|
153
|
+
className={`${CLASSNAME}__pagination-items`}
|
|
154
|
+
style={wrapperStyle}
|
|
155
|
+
role="tablist"
|
|
156
|
+
{...paginationProps}
|
|
157
|
+
>
|
|
131
158
|
{useMemo(
|
|
132
159
|
() =>
|
|
133
160
|
range(slidesCount).map((index) => {
|
|
@@ -137,6 +164,10 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
137
164
|
(index === visibleRange.min || index === visibleRange.max);
|
|
138
165
|
const isActive = activeIndex === index;
|
|
139
166
|
const isOutRange = index < visibleRange.min || index > visibleRange.max;
|
|
167
|
+
const { className: itemClassName = undefined, ...itemProps } = paginationItemProps
|
|
168
|
+
? paginationItemProps(index)
|
|
169
|
+
: {};
|
|
170
|
+
|
|
140
171
|
return (
|
|
141
172
|
<button
|
|
142
173
|
className={classNames(
|
|
@@ -146,15 +177,20 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
146
177
|
isOnEdge,
|
|
147
178
|
isOutRange,
|
|
148
179
|
}),
|
|
180
|
+
itemClassName,
|
|
149
181
|
)}
|
|
150
182
|
key={index}
|
|
151
183
|
type="button"
|
|
184
|
+
tabIndex={isActive ? undefined : -1}
|
|
185
|
+
role="tab"
|
|
186
|
+
aria-selected={isActive}
|
|
152
187
|
onClick={() => onPaginationClick?.(index)}
|
|
153
188
|
{...{
|
|
154
189
|
'aria-label': paginationItemLabel
|
|
155
190
|
? paginationItemLabel(index)
|
|
156
191
|
: `${index + 1} / ${slidesCount}`,
|
|
157
192
|
}}
|
|
193
|
+
{...itemProps}
|
|
158
194
|
/>
|
|
159
195
|
);
|
|
160
196
|
}),
|
|
@@ -163,6 +199,7 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
163
199
|
visibleRange.min,
|
|
164
200
|
visibleRange.max,
|
|
165
201
|
activeIndex,
|
|
202
|
+
paginationItemProps,
|
|
166
203
|
paginationItemLabel,
|
|
167
204
|
onPaginationClick,
|
|
168
205
|
],
|
|
@@ -8,8 +8,6 @@ import { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/
|
|
|
8
8
|
* Defines the props of the component.
|
|
9
9
|
*/
|
|
10
10
|
export interface SlideshowItemProps extends GenericProps {
|
|
11
|
-
/** whether the slideshow item is currently visible */
|
|
12
|
-
isCurrentlyVisible?: boolean;
|
|
13
11
|
/** interval in which slides are automatically shown */
|
|
14
12
|
interval?: number;
|
|
15
13
|
}
|
|
@@ -33,7 +31,6 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
|
33
31
|
*/
|
|
34
32
|
export const SlideshowItem: Comp<SlideshowItemProps, HTMLDivElement> = forwardRef((props, ref) => {
|
|
35
33
|
const { className, children, ...forwardedProps } = props;
|
|
36
|
-
|
|
37
34
|
return (
|
|
38
35
|
<div
|
|
39
36
|
ref={ref}
|
|
@@ -43,8 +40,6 @@ export const SlideshowItem: Comp<SlideshowItemProps, HTMLDivElement> = forwardRe
|
|
|
43
40
|
prefix: CLASSNAME,
|
|
44
41
|
}),
|
|
45
42
|
)}
|
|
46
|
-
aria-roledescription="slide"
|
|
47
|
-
role="group"
|
|
48
43
|
{...forwardedProps}
|
|
49
44
|
>
|
|
50
45
|
{children}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
|
|
5
|
+
import { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/react/utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Defines the props of the component.
|
|
9
|
+
*/
|
|
10
|
+
export interface SlideshowItemGroupProps extends GenericProps {
|
|
11
|
+
role?: 'tabpanel' | 'group';
|
|
12
|
+
label?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Component display name.
|
|
17
|
+
*/
|
|
18
|
+
const COMPONENT_NAME = 'SlideshowItemGroup';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Component default class name and class prefix.
|
|
22
|
+
*/
|
|
23
|
+
export const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
24
|
+
|
|
25
|
+
export const buildSlideShowGroupId = (slidesId: string, index: number) => `${slidesId}-slide-${index}`;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SlideshowItemGroup component.
|
|
29
|
+
*
|
|
30
|
+
* @param props Component props.
|
|
31
|
+
* @param ref Component ref.
|
|
32
|
+
* @return React element.
|
|
33
|
+
*/
|
|
34
|
+
export const SlideshowItemGroup: Comp<SlideshowItemGroupProps, HTMLDivElement> = forwardRef((props, ref) => {
|
|
35
|
+
const { className, children, role = 'group', label, ...forwardedProps } = props;
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
ref={ref}
|
|
39
|
+
role={role}
|
|
40
|
+
className={classNames(
|
|
41
|
+
className,
|
|
42
|
+
handleBasicClasses({
|
|
43
|
+
prefix: CLASSNAME,
|
|
44
|
+
}),
|
|
45
|
+
)}
|
|
46
|
+
aria-roledescription="slide"
|
|
47
|
+
aria-label={label}
|
|
48
|
+
{...forwardedProps}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
SlideshowItemGroup.displayName = COMPONENT_NAME;
|
|
56
|
+
SlideshowItemGroup.className = CLASSNAME;
|
|
@@ -20,7 +20,14 @@ Array [
|
|
|
20
20
|
onNextClick={[Function]}
|
|
21
21
|
onPaginationClick={[Function]}
|
|
22
22
|
onPreviousClick={[Function]}
|
|
23
|
+
paginationItemProps={[Function]}
|
|
23
24
|
parentRef={null}
|
|
25
|
+
playButtonProps={
|
|
26
|
+
Object {
|
|
27
|
+
"aria-controls": "slideshow-slides2",
|
|
28
|
+
"onClick": undefined,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
24
31
|
previousButtonProps={
|
|
25
32
|
Object {
|
|
26
33
|
"aria-controls": "slideshow-slides2",
|
|
@@ -32,11 +39,13 @@ Array [
|
|
|
32
39
|
/>
|
|
33
40
|
</div>
|
|
34
41
|
}
|
|
42
|
+
aria-label="Simple carousel example"
|
|
35
43
|
autoPlay={false}
|
|
36
44
|
groupBy={1}
|
|
45
|
+
hasControls={true}
|
|
37
46
|
id="slideshow1"
|
|
38
|
-
interval={1000}
|
|
39
47
|
isAutoPlaying={false}
|
|
48
|
+
slideGroupLabel={[Function]}
|
|
40
49
|
slidesId="slideshow-slides2"
|
|
41
50
|
style={
|
|
42
51
|
Object {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { getFocusableElements } from '@lumx/react/utils/focus/getFocusableElements';
|
|
3
|
+
|
|
4
|
+
import { CLASSNAME as ITEM_GROUP_CLASSNAME } from './SlideshowItemGroup';
|
|
5
|
+
|
|
6
|
+
export interface UseSlideFocusManagementProps {
|
|
7
|
+
activeIndex: number;
|
|
8
|
+
groupBy?: number;
|
|
9
|
+
wrapperRef: React.RefObject<HTMLDivElement>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manage how slides must behave when visible or not.
|
|
14
|
+
* When not visible, they should be hidden from screen readers and not focusable.
|
|
15
|
+
*/
|
|
16
|
+
export const useSlideFocusManagement = ({ activeIndex, groupBy = 1, wrapperRef }: UseSlideFocusManagementProps) => {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const element = wrapperRef?.current;
|
|
19
|
+
const startIndexVisible = activeIndex;
|
|
20
|
+
const endIndexVisible = startIndexVisible + 1;
|
|
21
|
+
|
|
22
|
+
const slideshowChildren = element?.querySelectorAll<HTMLElement>(`.${ITEM_GROUP_CLASSNAME}`);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Classname set on elements whose focus was blocked.
|
|
26
|
+
* This is to easily find elements that have been tempered with,
|
|
27
|
+
* and not elements whose focus was already initially blocked.
|
|
28
|
+
* */
|
|
29
|
+
const elementWithBlockedFocusClass = `${ITEM_GROUP_CLASSNAME}__no-focus`;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Display given slide to screen readers and, if focus was blocked, restore focus on elements..
|
|
33
|
+
*/
|
|
34
|
+
const enableSlide = (slide: HTMLElement) => {
|
|
35
|
+
// Hide from screen readers
|
|
36
|
+
slide.setAttribute('aria-hidden', 'false');
|
|
37
|
+
// Find elements we have blocked focus on
|
|
38
|
+
slide.querySelectorAll(`.${elementWithBlockedFocusClass}`).forEach((focusableElement) => {
|
|
39
|
+
focusableElement.removeAttribute('tabindex');
|
|
40
|
+
focusableElement.classList.remove(elementWithBlockedFocusClass);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Hide given slide from screen readers and block focus on all focusable elements within.
|
|
46
|
+
*/
|
|
47
|
+
const blockSlide = (slide: HTMLElement) => {
|
|
48
|
+
slide.setAttribute('aria-hidden', 'true');
|
|
49
|
+
getFocusableElements(slide).forEach((focusableElement) => {
|
|
50
|
+
focusableElement.setAttribute('tabindex', '-1');
|
|
51
|
+
focusableElement.classList.add(elementWithBlockedFocusClass);
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (slideshowChildren && slideshowChildren?.length > 0) {
|
|
56
|
+
Array.from(slideshowChildren).forEach((slide, slideIndex) => {
|
|
57
|
+
const slideIsVisible = slideIndex >= startIndexVisible && slideIndex < endIndexVisible;
|
|
58
|
+
|
|
59
|
+
if (slideIsVisible) {
|
|
60
|
+
enableSlide(slide);
|
|
61
|
+
} else {
|
|
62
|
+
blockSlide(slide);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}, [activeIndex, groupBy, wrapperRef]);
|
|
67
|
+
};
|
|
@@ -4,6 +4,8 @@ interface UseRovingTabIndexOptions {
|
|
|
4
4
|
parentRef: RefObject<HTMLElement>;
|
|
5
5
|
elementSelector: string;
|
|
6
6
|
keepTabIndex?: boolean;
|
|
7
|
+
/** Action to trigger when an element is focused using roving tab index */
|
|
8
|
+
onElementFocus?: (element: HTMLElement) => void;
|
|
7
9
|
/** List of values to be used as extra dependencies of the useEffect */
|
|
8
10
|
extraDependencies?: any[];
|
|
9
11
|
}
|
|
@@ -12,6 +14,7 @@ export const useRovingTabIndex = ({
|
|
|
12
14
|
parentRef,
|
|
13
15
|
elementSelector,
|
|
14
16
|
keepTabIndex,
|
|
17
|
+
onElementFocus,
|
|
15
18
|
extraDependencies = [],
|
|
16
19
|
}: UseRovingTabIndexOptions): void => {
|
|
17
20
|
useEffect(
|
|
@@ -47,6 +50,12 @@ export const useRovingTabIndex = ({
|
|
|
47
50
|
}
|
|
48
51
|
const newElement = elements[newTabFocus];
|
|
49
52
|
newElement?.focus();
|
|
53
|
+
|
|
54
|
+
// When an element is focused using roving tab index, trigger the onElementFocus callback
|
|
55
|
+
if (newElement && onElementFocus) {
|
|
56
|
+
onElementFocus(newElement);
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
if (keepTabIndex) {
|
|
51
60
|
(evt.currentTarget as HTMLElement).setAttribute('tabindex', '-1');
|
|
52
61
|
newElement?.setAttribute('tabindex', '0');
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** CSS selector listing all tabbable elements. */
|
|
2
|
+
export const TABBABLE_ELEMENTS_SELECTOR = `a[href], button, textarea, input:not([type="hidden"]):not([hidden]), [tabindex]`;
|
|
3
|
+
|
|
4
|
+
/** CSS selector matching element that are disabled (should not receive focus). */
|
|
5
|
+
export const DISABLED_SELECTOR = `[hidden], [tabindex="-1"], [disabled]:not([disabled="false"]), [aria-disabled]:not([aria-disabled="false"])`;
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const TABBABLE_ELEMENTS_SELECTOR = `a[href], button, textarea, input:not([type="hidden"]):not([hidden]), [tabindex]`;
|
|
3
|
-
|
|
4
|
-
/** CSS selector matching element that are disabled (should not receive focus). */
|
|
5
|
-
const DISABLED_SELECTOR = `[hidden], [tabindex="-1"], [disabled]:not([disabled="false"]), [aria-disabled]:not([aria-disabled="false"])`;
|
|
6
|
-
|
|
7
|
-
const isNotDisabled = (element: HTMLElement) => !element.matches(DISABLED_SELECTOR);
|
|
1
|
+
import { getFocusableElements } from './getFocusableElements';
|
|
8
2
|
|
|
9
3
|
/**
|
|
10
4
|
* Get first and last elements focusable in an element.
|
|
@@ -13,12 +7,12 @@ const isNotDisabled = (element: HTMLElement) => !element.matches(DISABLED_SELECT
|
|
|
13
7
|
* @return first and last focusable elements
|
|
14
8
|
*/
|
|
15
9
|
export function getFirstAndLastFocusable(parentElement: HTMLElement) {
|
|
16
|
-
const focusableElements =
|
|
10
|
+
const focusableElements = getFocusableElements(parentElement);
|
|
17
11
|
|
|
18
12
|
// First non disabled element.
|
|
19
|
-
const first = focusableElements
|
|
13
|
+
const first = focusableElements[0];
|
|
20
14
|
// Last non disabled element.
|
|
21
|
-
const last = focusableElements.
|
|
15
|
+
const last = focusableElements[focusableElements.length - 1];
|
|
22
16
|
|
|
23
17
|
if (last && first) {
|
|
24
18
|
return { first, last };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { getFocusableElements } from '@lumx/react/utils/focus/getFocusableElements';
|
|
2
|
+
|
|
3
|
+
function htmlToElement(html: string): any {
|
|
4
|
+
const template = document.createElement('template');
|
|
5
|
+
template.innerHTML = html.trim();
|
|
6
|
+
return template.content.firstChild;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe(getFocusableElements.name, () => {
|
|
10
|
+
it('should get empty', () => {
|
|
11
|
+
const element = htmlToElement(`<div></div>`);
|
|
12
|
+
const focusable = getFocusableElements(element);
|
|
13
|
+
expect(focusable).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should get single item', () => {
|
|
17
|
+
const element = htmlToElement(`<div><button /></div>`);
|
|
18
|
+
const focusable = getFocusableElements(element);
|
|
19
|
+
expect(focusable).toMatchInlineSnapshot(`
|
|
20
|
+
Array [
|
|
21
|
+
<button />,
|
|
22
|
+
]
|
|
23
|
+
`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// it('should get first and last', () => {
|
|
27
|
+
// const element = htmlToElement(`
|
|
28
|
+
// <div>
|
|
29
|
+
// <div>Non focusable div</div>
|
|
30
|
+
// <button>Simple button</button>
|
|
31
|
+
// <div>Non focusable div</div>
|
|
32
|
+
// <input />
|
|
33
|
+
// <div>Non focusable div</div>
|
|
34
|
+
// </div>
|
|
35
|
+
// `);
|
|
36
|
+
// const focusable = getFocusableElements(element);
|
|
37
|
+
// expect(focusable.first).toMatchInlineSnapshot(`
|
|
38
|
+
// <button>
|
|
39
|
+
// Simple button
|
|
40
|
+
// </button>
|
|
41
|
+
// `);
|
|
42
|
+
// expect(focusable.first).toMatchInlineSnapshot(`
|
|
43
|
+
// <button>
|
|
44
|
+
// Simple button
|
|
45
|
+
// </button>
|
|
46
|
+
// `);
|
|
47
|
+
// });
|
|
48
|
+
|
|
49
|
+
describe('match focusable elements', () => {
|
|
50
|
+
it('should match input element', () => {
|
|
51
|
+
const element = htmlToElement(`<div><input /></div>`);
|
|
52
|
+
const focusable = getFocusableElements(element);
|
|
53
|
+
expect(focusable).toMatchInlineSnapshot(`
|
|
54
|
+
Array [
|
|
55
|
+
<input />,
|
|
56
|
+
]
|
|
57
|
+
`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should match link element', () => {
|
|
61
|
+
const element = htmlToElement(`<div><a href="#" /></div>`);
|
|
62
|
+
const focusable = getFocusableElements(element);
|
|
63
|
+
expect(focusable).toMatchInlineSnapshot(`
|
|
64
|
+
Array [
|
|
65
|
+
<a
|
|
66
|
+
href="#"
|
|
67
|
+
/>,
|
|
68
|
+
]
|
|
69
|
+
`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should match textarea element', () => {
|
|
73
|
+
const element = htmlToElement(`<div><textarea /></div>`);
|
|
74
|
+
const focusable = getFocusableElements(element);
|
|
75
|
+
expect(focusable).toMatchInlineSnapshot(`
|
|
76
|
+
Array [
|
|
77
|
+
<textarea>
|
|
78
|
+
</div>
|
|
79
|
+
</textarea>,
|
|
80
|
+
]
|
|
81
|
+
`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should match element with tabindex', () => {
|
|
85
|
+
const element = htmlToElement(`<div><span tabindex="0" /></div>`);
|
|
86
|
+
const focusable = getFocusableElements(element);
|
|
87
|
+
expect(focusable).toMatchInlineSnapshot(`
|
|
88
|
+
Array [
|
|
89
|
+
<span
|
|
90
|
+
tabindex="0"
|
|
91
|
+
/>,
|
|
92
|
+
]
|
|
93
|
+
`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should keep disabled=false', () => {
|
|
97
|
+
const element = htmlToElement(`<div><button disabled="false" /><button /></div>`);
|
|
98
|
+
const focusable = getFocusableElements(element);
|
|
99
|
+
expect(focusable).toMatchInlineSnapshot(`
|
|
100
|
+
Array [
|
|
101
|
+
<button
|
|
102
|
+
disabled="false"
|
|
103
|
+
/>,
|
|
104
|
+
<button />,
|
|
105
|
+
]
|
|
106
|
+
`);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should keep aria-disabled=false', () => {
|
|
110
|
+
const element = htmlToElement(`<div><button aria-disabled="false" /><button /></div>`);
|
|
111
|
+
const focusable = getFocusableElements(element);
|
|
112
|
+
expect(focusable).toMatchInlineSnapshot(`
|
|
113
|
+
Array [
|
|
114
|
+
<button
|
|
115
|
+
aria-disabled="false"
|
|
116
|
+
/>,
|
|
117
|
+
<button />,
|
|
118
|
+
]
|
|
119
|
+
`);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('skip disabled elements', () => {
|
|
124
|
+
it('should skip disabled', () => {
|
|
125
|
+
const element = htmlToElement(`<div><button disabled /><button /></div>`);
|
|
126
|
+
const focusable = getFocusableElements(element);
|
|
127
|
+
expect(focusable).toMatchInlineSnapshot(`
|
|
128
|
+
Array [
|
|
129
|
+
<button />,
|
|
130
|
+
]
|
|
131
|
+
`);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should skip aria-disabled', () => {
|
|
135
|
+
const element = htmlToElement(`<div><button aria-disabled /><button /></div>`);
|
|
136
|
+
const focusable = getFocusableElements(element);
|
|
137
|
+
expect(focusable).toMatchInlineSnapshot(`
|
|
138
|
+
Array [
|
|
139
|
+
<button />,
|
|
140
|
+
]
|
|
141
|
+
`);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should skip tabindex=-1', () => {
|
|
145
|
+
const element = htmlToElement(`<div><button tabindex="-1" /><button /></div>`);
|
|
146
|
+
const focusable = getFocusableElements(element);
|
|
147
|
+
expect(focusable).toMatchInlineSnapshot(`
|
|
148
|
+
Array [
|
|
149
|
+
<button />,
|
|
150
|
+
]
|
|
151
|
+
`);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should skip input type hidden', () => {
|
|
155
|
+
const element = htmlToElement(`<div><input type="hidden" /><button /></div>`);
|
|
156
|
+
const focusable = getFocusableElements(element);
|
|
157
|
+
expect(focusable).toMatchInlineSnapshot(`
|
|
158
|
+
Array [
|
|
159
|
+
<button />,
|
|
160
|
+
]
|
|
161
|
+
`);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should skip hidden input', () => {
|
|
165
|
+
const element = htmlToElement(`<div><input hidden /><button /></div>`);
|
|
166
|
+
const focusable = getFocusableElements(element);
|
|
167
|
+
expect(focusable).toMatchInlineSnapshot(`
|
|
168
|
+
Array [
|
|
169
|
+
<button />,
|
|
170
|
+
]
|
|
171
|
+
`);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DISABLED_SELECTOR, TABBABLE_ELEMENTS_SELECTOR } from './constants';
|
|
2
|
+
|
|
3
|
+
const isNotDisabled = (element: HTMLElement) => !element.matches(DISABLED_SELECTOR);
|
|
4
|
+
|
|
5
|
+
export function getFocusableElements(element: HTMLElement): HTMLElement[] {
|
|
6
|
+
return Array.from(element.querySelectorAll<HTMLElement>(TABBABLE_ELEMENTS_SELECTOR)).filter(isNotDisabled);
|
|
7
|
+
}
|
package/types.d.ts
CHANGED
|
@@ -2135,13 +2135,13 @@ export declare const clamp: (value: number, min: number, max: number) => number;
|
|
|
2135
2135
|
/**
|
|
2136
2136
|
* Defines the props of the component.
|
|
2137
2137
|
*/
|
|
2138
|
-
export interface SlideshowProps extends GenericProps, Pick<SlidesProps, "autoPlay" | "slidesId" | "id" | "theme" | "fillHeight" | "groupBy"> {
|
|
2138
|
+
export interface SlideshowProps extends GenericProps, Pick<SlidesProps, "autoPlay" | "slidesId" | "id" | "theme" | "fillHeight" | "groupBy" | "slideGroupLabel"> {
|
|
2139
2139
|
/** current slide active */
|
|
2140
2140
|
activeIndex?: SlidesProps["activeIndex"];
|
|
2141
2141
|
/** Interval between each slide when automatic rotation is enabled. */
|
|
2142
2142
|
interval?: number;
|
|
2143
2143
|
/** Props to pass to the slideshow controls (minus those already set by the Slideshow props). */
|
|
2144
|
-
slideshowControlsProps?: Pick<SlideshowControlsProps, "nextButtonProps" | "previousButtonProps"> & Omit<SlideshowControlsProps, "activeIndex" | "onPaginationClick" | "onNextClick" | "onPreviousClick" | "slidesCount" | "parentRef" | "theme">;
|
|
2144
|
+
slideshowControlsProps?: Pick<SlideshowControlsProps, "nextButtonProps" | "previousButtonProps" | "paginationItemProps"> & Omit<SlideshowControlsProps, "activeIndex" | "onPaginationClick" | "onNextClick" | "onPreviousClick" | "slidesCount" | "parentRef" | "theme">;
|
|
2145
2145
|
/** Callback when slide changes */
|
|
2146
2146
|
onChange?(index: number): void;
|
|
2147
2147
|
}
|
|
@@ -2157,8 +2157,6 @@ export declare const Slideshow: Comp<SlideshowProps, HTMLDivElement>;
|
|
|
2157
2157
|
* Defines the props of the component.
|
|
2158
2158
|
*/
|
|
2159
2159
|
export interface SlideshowItemProps extends GenericProps {
|
|
2160
|
-
/** whether the slideshow item is currently visible */
|
|
2161
|
-
isCurrentlyVisible?: boolean;
|
|
2162
2160
|
/** interval in which slides are automatically shown */
|
|
2163
2161
|
interval?: number;
|
|
2164
2162
|
}
|
|
@@ -2242,6 +2240,8 @@ export interface SlideshowControlsProps extends GenericProps, HasTheme {
|
|
|
2242
2240
|
parentRef?: RefObject<HTMLDivElement> | HTMLDivElement | null;
|
|
2243
2241
|
/** Props to pass to the previous button (minus those already set by the SlideshowControls props). */
|
|
2244
2242
|
previousButtonProps: Pick<IconButtonProps, "label"> & Omit<IconButtonProps, "label" | "onClick" | "icon" | "emphasis" | "color">;
|
|
2243
|
+
/** Props to pass to the pagination wrapper */
|
|
2244
|
+
paginationProps?: Omit<React.HTMLAttributes<HTMLDivElement>, "className" | "style" | "role">;
|
|
2245
2245
|
/** Number of slides. */
|
|
2246
2246
|
slidesCount: number;
|
|
2247
2247
|
/** On next button click callback. */
|
|
@@ -2252,8 +2252,15 @@ export interface SlideshowControlsProps extends GenericProps, HasTheme {
|
|
|
2252
2252
|
onPreviousClick?(loopback?: boolean): void;
|
|
2253
2253
|
/** whether the slideshow is currently playing */
|
|
2254
2254
|
isAutoPlaying?: boolean;
|
|
2255
|
-
/**
|
|
2255
|
+
/**
|
|
2256
|
+
* function to be executed in order to retrieve the label for the pagination item
|
|
2257
|
+
* @deprecated Use paginationItemProps instead.
|
|
2258
|
+
* */
|
|
2256
2259
|
paginationItemLabel?: (index: number) => string;
|
|
2260
|
+
/**
|
|
2261
|
+
* function to be executed in order to retrieve the props for a pagination item.
|
|
2262
|
+
*/
|
|
2263
|
+
paginationItemProps?: (itemIndex: number) => React.HTMLAttributes<HTMLButtonElement>;
|
|
2257
2264
|
/** Props to pass to the lay button (minus those already set by the SlideshowControls props). */
|
|
2258
2265
|
playButtonProps?: Pick<IconButtonProps, "label"> & Omit<IconButtonProps, "label" | "onClick" | "icon" | "emphasis" | "color">;
|
|
2259
2266
|
}
|
|
@@ -2280,6 +2287,13 @@ export interface SlidesProps extends GenericProps, HasTheme {
|
|
|
2280
2287
|
toggleAutoPlay: () => void;
|
|
2281
2288
|
/** component to be rendered after the slides */
|
|
2282
2289
|
afterSlides?: React.ReactNode;
|
|
2290
|
+
/** Whether the slides have controls linked */
|
|
2291
|
+
hasControls?: boolean;
|
|
2292
|
+
/**
|
|
2293
|
+
* Accessible label to set on a slide group.
|
|
2294
|
+
* Receives the group position starting from 1 and the total number of groups.
|
|
2295
|
+
* */
|
|
2296
|
+
slideGroupLabel?: (groupPosition: number, groupTotal: number) => string;
|
|
2283
2297
|
}
|
|
2284
2298
|
/**
|
|
2285
2299
|
* Slides component.
|