@lumx/react 3.9.4-alpha.0 → 3.9.4-alpha.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/index.js +86 -72
- package/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/slideshow/Slides.tsx +8 -2
- package/src/components/slideshow/constants.ts +4 -0
- package/src/components/slideshow/useSlideFocusManagement.tsx +64 -67
- package/src/components/slideshow/useSlideshowControls.ts +13 -1
package/package.json
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"url": "https://github.com/lumapps/design-system/issues"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@lumx/core": "^3.9.4-alpha.
|
|
10
|
-
"@lumx/icons": "^3.9.4-alpha.
|
|
9
|
+
"@lumx/core": "^3.9.4-alpha.2",
|
|
10
|
+
"@lumx/icons": "^3.9.4-alpha.2",
|
|
11
11
|
"@popperjs/core": "^2.5.4",
|
|
12
12
|
"body-scroll-lock": "^3.1.5",
|
|
13
13
|
"classnames": "^2.3.2",
|
|
@@ -111,5 +111,5 @@
|
|
|
111
111
|
"build:storybook": "storybook build"
|
|
112
112
|
},
|
|
113
113
|
"sideEffects": false,
|
|
114
|
-
"version": "3.9.4-alpha.
|
|
114
|
+
"version": "3.9.4-alpha.2"
|
|
115
115
|
}
|
|
@@ -3,10 +3,11 @@ import chunk from 'lodash/chunk';
|
|
|
3
3
|
|
|
4
4
|
import classNames from 'classnames';
|
|
5
5
|
|
|
6
|
-
import { FULL_WIDTH_PERCENT } from '@lumx/react/components/slideshow/constants';
|
|
6
|
+
import { FULL_WIDTH_PERCENT, NEXT_SLIDE_EVENT } from '@lumx/react/components/slideshow/constants';
|
|
7
7
|
import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
|
|
8
8
|
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
9
9
|
import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
10
|
+
import { useKeyNavigate } from '@lumx/react/components/slideshow/useKeyNavigate';
|
|
10
11
|
import { buildSlideShowGroupId, SlideshowItemGroup } from './SlideshowItemGroup';
|
|
11
12
|
|
|
12
13
|
export interface SlidesProps extends GenericProps, HasTheme {
|
|
@@ -84,6 +85,11 @@ export const Slides: Comp<SlidesProps, HTMLDivElement> = forwardRef((props, ref)
|
|
|
84
85
|
|
|
85
86
|
const slidesRef = React.useRef<HTMLDivElement>(null);
|
|
86
87
|
|
|
88
|
+
const slide = slidesRef.current;
|
|
89
|
+
const onNextSlide = React.useCallback(() => slide?.dispatchEvent(new CustomEvent(NEXT_SLIDE_EVENT)), [slide]);
|
|
90
|
+
const onPrevSlide = React.useCallback(() => slide?.dispatchEvent(new CustomEvent(NEXT_SLIDE_EVENT)), [slide]);
|
|
91
|
+
useKeyNavigate(slide, onNextSlide, onPrevSlide);
|
|
92
|
+
|
|
87
93
|
return (
|
|
88
94
|
<section
|
|
89
95
|
id={id}
|
|
@@ -107,7 +113,7 @@ export const Slides: Comp<SlidesProps, HTMLDivElement> = forwardRef((props, ref)
|
|
|
107
113
|
<SlideshowItemGroup
|
|
108
114
|
key={index}
|
|
109
115
|
id={slidesId && buildSlideShowGroupId(slidesId, index)}
|
|
110
|
-
label={slideGroupLabel
|
|
116
|
+
label={slideGroupLabel?.(index + 1, groups.length)}
|
|
111
117
|
isDisplayed={index >= startIndexVisible && index < endIndexVisible}
|
|
112
118
|
slidesRef={slidesRef}
|
|
113
119
|
>
|
|
@@ -22,3 +22,7 @@ export const PAGINATION_ITEMS_MAX = 5;
|
|
|
22
22
|
* Size of a pagination item. Used to translate wrapper.
|
|
23
23
|
*/
|
|
24
24
|
export const PAGINATION_ITEM_SIZE = 12;
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export const NEXT_SLIDE_EVENT = 'lumx-next-slide-event';
|
|
28
|
+
export const PREV_SLIDE_EVENT = 'lumx-prev-slide-event';
|
|
@@ -7,11 +7,11 @@ export interface UseSlideFocusManagementProps {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Data attribute set on elements whose focus was blocked.
|
|
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
|
-
|
|
14
|
-
const
|
|
13
|
+
*/
|
|
14
|
+
const BLOCKED_FOCUS = 'data-focus-blocked';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Manage how slides must behave when visible or not.
|
|
@@ -19,89 +19,86 @@ const BLOCKED_FOCUS_CLASSNAME = 'focus-blocked';
|
|
|
19
19
|
*/
|
|
20
20
|
export const useSlideFocusManagement = ({ isSlideDisplayed, slidesRef }: UseSlideFocusManagementProps) => {
|
|
21
21
|
const [slide, setSlide] = React.useState<HTMLDivElement | null>(null);
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
const [focusableElementSet, setFocusableElementSet] = React.useState<Set<HTMLElement>>();
|
|
24
|
+
React.useEffect(() => {
|
|
23
25
|
if (!slide) {
|
|
24
26
|
return undefined;
|
|
25
27
|
}
|
|
28
|
+
// Update the slide's focusable element list (including the blocked elements)
|
|
29
|
+
const updateFocusableElements = () =>
|
|
30
|
+
setFocusableElementSet((set = new Set()) => {
|
|
31
|
+
// TODO: remove when `inert` gets sufficient browser support
|
|
32
|
+
const focusedBlocked = Array.from(slide.querySelectorAll(`[${BLOCKED_FOCUS}]`)) as HTMLElement[];
|
|
33
|
+
for (const element of focusedBlocked) {
|
|
34
|
+
set.add(element);
|
|
35
|
+
}
|
|
36
|
+
for (const element of getFocusableElements(slide)) {
|
|
37
|
+
set.add(element);
|
|
38
|
+
}
|
|
39
|
+
return set;
|
|
40
|
+
});
|
|
26
41
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* Display given slide to screen readers and, if focus was blocked, restore focus on elements.
|
|
32
|
-
*/
|
|
33
|
-
const enableSlide = () => {
|
|
34
|
-
slide.removeAttribute('inert');
|
|
35
|
-
slide.setAttribute('aria-hidden', 'false');
|
|
36
|
-
// Find elements we have blocked focus on
|
|
37
|
-
// (won't be necessary once "inert" gets sufficient browser support)
|
|
38
|
-
focusableElements = Array.from(slide.querySelectorAll(`.${BLOCKED_FOCUS_CLASSNAME}`));
|
|
39
|
-
for (const focusableElement of focusableElements) {
|
|
40
|
-
focusableElement.removeAttribute('tabindex');
|
|
41
|
-
focusableElement.classList.remove(BLOCKED_FOCUS_CLASSNAME);
|
|
42
|
+
// Observe changes in the content of the slide
|
|
43
|
+
const observer = new MutationObserver((mutationsList) => {
|
|
44
|
+
if (mutationsList.some((mutation) => mutation.type === 'childList')) {
|
|
45
|
+
updateFocusableElements();
|
|
42
46
|
}
|
|
43
|
-
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
updateFocusableElements();
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
observer.observe(slide, { attributes: true, childList: true, subtree: true });
|
|
52
|
+
return observer.disconnect();
|
|
53
|
+
}, [slide]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!slide || !focusableElementSet) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const focusableElements = Array.from(focusableElementSet);
|
|
60
|
+
|
|
61
|
+
if (!isSlideDisplayed) {
|
|
62
|
+
/* Block slide */
|
|
49
63
|
slide.setAttribute('inert', '');
|
|
50
64
|
slide.setAttribute('aria-hidden', 'true');
|
|
51
|
-
|
|
65
|
+
|
|
66
|
+
// TODO: remove when `inert` gets sufficient browser support
|
|
52
67
|
for (const focusableElement of focusableElements) {
|
|
53
68
|
focusableElement.setAttribute('tabindex', '-1');
|
|
54
|
-
focusableElement.
|
|
69
|
+
focusableElement.setAttribute(BLOCKED_FOCUS, '');
|
|
55
70
|
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
enableSlide();
|
|
61
|
-
} else {
|
|
62
|
-
blockSlide();
|
|
63
|
-
}
|
|
64
|
-
};
|
|
71
|
+
} else {
|
|
72
|
+
/* Un-block slide */
|
|
73
|
+
slide.removeAttribute('inert');
|
|
74
|
+
slide.removeAttribute('aria-hidden');
|
|
65
75
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (mutation.type === 'childList') {
|
|
71
|
-
handleDisplay();
|
|
72
|
-
}
|
|
76
|
+
// TODO: remove when `inert` gets sufficient browser support
|
|
77
|
+
for (const focusableElement of focusableElements) {
|
|
78
|
+
focusableElement.removeAttribute('tabindex');
|
|
79
|
+
focusableElement.removeAttribute(BLOCKED_FOCUS);
|
|
73
80
|
}
|
|
74
|
-
});
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
// Change focus on slide displayed
|
|
83
|
+
const isUserActivated = slidesRef?.current?.dataset.lumxUserActivated === 'true';
|
|
84
|
+
if (isUserActivated) {
|
|
85
|
+
let elementToFocus: HTMLElement | undefined = slide;
|
|
77
86
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
// We have exactly one focusable element => focus it
|
|
88
|
+
if (focusableElementSet.size === 1) {
|
|
89
|
+
// eslint-disable-next-line prefer-destructuring
|
|
90
|
+
elementToFocus = focusableElements[0];
|
|
91
|
+
}
|
|
81
92
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
93
|
+
// We have not focusable element => focus the pagination item
|
|
94
|
+
if (focusableElementSet.size === 0) {
|
|
95
|
+
elementToFocus = document.querySelector(`[aria-controls="${slide?.id}"]`) as HTMLElement;
|
|
96
|
+
}
|
|
87
97
|
|
|
88
|
-
|
|
89
|
-
if (focusableElements.length === 0) {
|
|
90
|
-
elementToFocus = document.querySelector(`[aria-controls="${slide?.id}"]`) as HTMLElement;
|
|
98
|
+
elementToFocus?.focus({ preventScroll: true });
|
|
91
99
|
}
|
|
92
|
-
|
|
93
|
-
elementToFocus?.focus({ preventScroll: true });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** If slide is hidden, start observing for elements to block focus */
|
|
97
|
-
if (!isSlideDisplayed) {
|
|
98
|
-
observer.observe(slide, { attributes: true, childList: true, subtree: true });
|
|
99
|
-
|
|
100
|
-
return () => observer.disconnect();
|
|
101
100
|
}
|
|
102
|
-
|
|
103
|
-
return undefined;
|
|
104
|
-
}, [isSlideDisplayed, slide, slidesRef]);
|
|
101
|
+
}, [focusableElementSet, isSlideDisplayed, slide, slidesRef]);
|
|
105
102
|
|
|
106
103
|
return setSlide;
|
|
107
104
|
};
|
|
@@ -3,7 +3,7 @@ import { clamp } from '@lumx/react';
|
|
|
3
3
|
import { useInterval } from '@lumx/react/hooks/useInterval';
|
|
4
4
|
import { useId } from '@lumx/react/hooks/useId';
|
|
5
5
|
|
|
6
|
-
import { AUTOPLAY_DEFAULT_INTERVAL } from './constants';
|
|
6
|
+
import { AUTOPLAY_DEFAULT_INTERVAL, NEXT_SLIDE_EVENT, PREV_SLIDE_EVENT } from './constants';
|
|
7
7
|
|
|
8
8
|
export interface UseSlideshowControlsOptions {
|
|
9
9
|
/** default active index to be displayed */
|
|
@@ -179,6 +179,18 @@ export const useSlideshowControls = ({
|
|
|
179
179
|
[goTo, stopAutoPlay],
|
|
180
180
|
);
|
|
181
181
|
|
|
182
|
+
// Listen to custom next/prev slide events
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (!element) return undefined;
|
|
185
|
+
|
|
186
|
+
element.addEventListener(NEXT_SLIDE_EVENT, onNextClick);
|
|
187
|
+
element.addEventListener(PREV_SLIDE_EVENT, onPreviousClick);
|
|
188
|
+
return () => {
|
|
189
|
+
element.removeEventListener(NEXT_SLIDE_EVENT, onNextClick);
|
|
190
|
+
element.removeEventListener(PREV_SLIDE_EVENT, onPreviousClick);
|
|
191
|
+
};
|
|
192
|
+
}, [element, onNextClick, onPreviousClick]);
|
|
193
|
+
|
|
182
194
|
// If the activeIndex props changes, update the current slide
|
|
183
195
|
useEffect(() => {
|
|
184
196
|
setActiveIndex(activeIndex as number);
|