@lumx/react 3.9.3 → 3.9.4-alpha.0
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.d.ts +11 -9
- package/index.js +426 -415
- package/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/image-lightbox/internal/ImageSlideshow.tsx +2 -0
- package/src/components/slideshow/Slides.tsx +7 -5
- package/src/components/slideshow/Slideshow.stories.tsx +12 -1
- package/src/components/slideshow/Slideshow.tsx +2 -16
- package/src/components/slideshow/SlideshowControls.stories.tsx +2 -3
- package/src/components/slideshow/SlideshowControls.tsx +63 -73
- package/src/components/slideshow/SlideshowItem.tsx +1 -10
- package/src/components/slideshow/SlideshowItemGroup.tsx +16 -33
- package/src/components/slideshow/useSlideFocusManagement.tsx +51 -36
- package/src/{hooks → components/slideshow}/useSlideshowControls.ts +57 -60
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.
|
|
10
|
-
"@lumx/icons": "^3.9.
|
|
9
|
+
"@lumx/core": "^3.9.4-alpha.0",
|
|
10
|
+
"@lumx/icons": "^3.9.4-alpha.0",
|
|
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.
|
|
114
|
+
"version": "3.9.4-alpha.0"
|
|
115
115
|
}
|
|
@@ -31,6 +31,7 @@ export const ImageSlideshow: React.FC<ImageSlideshowProps> = ({
|
|
|
31
31
|
activeIndex,
|
|
32
32
|
slideshowId,
|
|
33
33
|
setSlideshow,
|
|
34
|
+
slideshow,
|
|
34
35
|
slideshowSlidesId,
|
|
35
36
|
slidesCount,
|
|
36
37
|
onNextClick,
|
|
@@ -61,6 +62,7 @@ export const ImageSlideshow: React.FC<ImageSlideshowProps> = ({
|
|
|
61
62
|
onNextClick={onNextClick}
|
|
62
63
|
onPreviousClick={onPreviousClick}
|
|
63
64
|
onPaginationClick={onPaginationClick}
|
|
65
|
+
parentRef={slideshow}
|
|
64
66
|
{...slideshowControlsProps}
|
|
65
67
|
paginationItemProps={(index: number) => {
|
|
66
68
|
const props = slideshowControlsProps?.paginationItemProps?.(index) || {};
|
|
@@ -6,6 +6,7 @@ import classNames from 'classnames';
|
|
|
6
6
|
import { FULL_WIDTH_PERCENT } 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
|
+
import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
9
10
|
import { buildSlideShowGroupId, SlideshowItemGroup } from './SlideshowItemGroup';
|
|
10
11
|
|
|
11
12
|
export interface SlidesProps extends GenericProps, HasTheme {
|
|
@@ -32,7 +33,7 @@ export interface SlidesProps extends GenericProps, HasTheme {
|
|
|
32
33
|
/**
|
|
33
34
|
* Accessible label to set on a slide group.
|
|
34
35
|
* Receives the group position starting from 1 and the total number of groups.
|
|
35
|
-
|
|
36
|
+
*/
|
|
36
37
|
slideGroupLabel?: (groupPosition: number, groupTotal: number) => string;
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -70,7 +71,6 @@ export const Slides: Comp<SlidesProps, HTMLDivElement> = forwardRef((props, ref)
|
|
|
70
71
|
slideGroupLabel,
|
|
71
72
|
...forwardedProps
|
|
72
73
|
} = props;
|
|
73
|
-
const wrapperRef = React.useRef<HTMLDivElement>(null);
|
|
74
74
|
const startIndexVisible = activeIndex;
|
|
75
75
|
const endIndexVisible = startIndexVisible + 1;
|
|
76
76
|
|
|
@@ -82,10 +82,12 @@ export const Slides: Comp<SlidesProps, HTMLDivElement> = forwardRef((props, ref)
|
|
|
82
82
|
return groupBy && groupBy > 1 ? chunk(childrenArray, groupBy) : childrenArray;
|
|
83
83
|
}, [children, groupBy]);
|
|
84
84
|
|
|
85
|
+
const slidesRef = React.useRef<HTMLDivElement>(null);
|
|
86
|
+
|
|
85
87
|
return (
|
|
86
88
|
<section
|
|
87
89
|
id={id}
|
|
88
|
-
ref={ref}
|
|
90
|
+
ref={useMergeRefs(slidesRef, ref)}
|
|
89
91
|
{...forwardedProps}
|
|
90
92
|
className={classNames(className, handleBasicClasses({ prefix: CLASSNAME, theme }), {
|
|
91
93
|
[`${CLASSNAME}--fill-height`]: fillHeight,
|
|
@@ -100,14 +102,14 @@ export const Slides: Comp<SlidesProps, HTMLDivElement> = forwardRef((props, ref)
|
|
|
100
102
|
onMouseLeave={toggleAutoPlay}
|
|
101
103
|
aria-live={isAutoPlaying ? 'off' : 'polite'}
|
|
102
104
|
>
|
|
103
|
-
<div
|
|
105
|
+
<div className={`${CLASSNAME}__wrapper`} style={wrapperStyle}>
|
|
104
106
|
{groups.map((group, index) => (
|
|
105
107
|
<SlideshowItemGroup
|
|
106
108
|
key={index}
|
|
107
109
|
id={slidesId && buildSlideShowGroupId(slidesId, index)}
|
|
108
|
-
role={hasControls ? 'tabpanel' : 'group'}
|
|
109
110
|
label={slideGroupLabel ? slideGroupLabel(index + 1, groups.length) : undefined}
|
|
110
111
|
isDisplayed={index >= startIndexVisible && index < endIndexVisible}
|
|
112
|
+
slidesRef={slidesRef}
|
|
111
113
|
>
|
|
112
114
|
{group}
|
|
113
115
|
</SlideshowItemGroup>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
/* eslint-disable jsx-a11y/anchor-is-valid */
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import range from 'lodash/range';
|
|
3
|
-
import { AspectRatio, Button, FlexBox, ImageBlock, Slideshow, SlideshowItem, Orientation } from '@lumx/react';
|
|
4
|
+
import { AspectRatio, Button, FlexBox, ImageBlock, Slideshow, SlideshowItem, Orientation, Link } from '@lumx/react';
|
|
4
5
|
import { IMAGES, LANDSCAPE_IMAGES } from '@lumx/react/stories/controls/image';
|
|
5
6
|
|
|
6
7
|
export default {
|
|
@@ -65,6 +66,16 @@ export const ResponsiveSlideShowSwipe = () => {
|
|
|
65
66
|
}}
|
|
66
67
|
slideGroupLabel={(currentGroup, totalGroup) => `${currentGroup} of ${totalGroup}`}
|
|
67
68
|
>
|
|
69
|
+
<SlideshowItem>
|
|
70
|
+
<FlexBox
|
|
71
|
+
style={{ border: '1px solid grey', maxWidth: 300, height: 300 }}
|
|
72
|
+
hAlign="center"
|
|
73
|
+
vAlign="center"
|
|
74
|
+
>
|
|
75
|
+
<Link href="#">A link</Link>
|
|
76
|
+
<Button>A button</Button>
|
|
77
|
+
</FlexBox>
|
|
78
|
+
</SlideshowItem>
|
|
68
79
|
{slides.map((slide) => (
|
|
69
80
|
<SlideshowItem key={`${slide}`}>
|
|
70
81
|
<FlexBox
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import React, { forwardRef } from 'react';
|
|
2
2
|
|
|
3
3
|
import { SlideshowControls, SlideshowControlsProps, Theme, Slides, SlidesProps } from '@lumx/react';
|
|
4
|
-
import { DEFAULT_OPTIONS } from '@lumx/react/hooks/useSlideshowControls';
|
|
5
4
|
import { Comp, GenericProps } from '@lumx/react/utils/type';
|
|
6
5
|
import { useFocusWithin } from '@lumx/react/hooks/useFocusWithin';
|
|
7
6
|
import { mergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
8
|
-
import {
|
|
7
|
+
import { DEFAULT_OPTIONS } from './useSlideshowControls';
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Defines the props of the component.
|
|
@@ -14,7 +13,7 @@ export interface SlideshowProps
|
|
|
14
13
|
extends GenericProps,
|
|
15
14
|
Pick<SlidesProps, 'autoPlay' | 'slidesId' | 'id' | 'theme' | 'fillHeight' | 'groupBy' | 'slideGroupLabel'> {
|
|
16
15
|
/** current slide active */
|
|
17
|
-
activeIndex?:
|
|
16
|
+
activeIndex?: number;
|
|
18
17
|
/** Interval between each slide when automatic rotation is enabled. */
|
|
19
18
|
interval?: number;
|
|
20
19
|
/** Props to pass to the slideshow controls (minus those already set by the Slideshow props). */
|
|
@@ -134,27 +133,14 @@ export const Slideshow: Comp<SlideshowProps, HTMLDivElement> = forwardRef((props
|
|
|
134
133
|
parentRef={slideshow}
|
|
135
134
|
theme={theme}
|
|
136
135
|
isAutoPlaying={isAutoPlaying}
|
|
137
|
-
nextButtonProps={{
|
|
138
|
-
'aria-controls': slideshowSlidesId,
|
|
139
|
-
...slideshowControlsProps.nextButtonProps,
|
|
140
|
-
}}
|
|
141
|
-
previousButtonProps={{
|
|
142
|
-
'aria-controls': slideshowSlidesId,
|
|
143
|
-
...slideshowControlsProps.previousButtonProps,
|
|
144
|
-
}}
|
|
145
136
|
playButtonProps={
|
|
146
137
|
autoPlay
|
|
147
138
|
? {
|
|
148
|
-
'aria-controls': slideshowSlidesId,
|
|
149
139
|
onClick: toggleForcePause,
|
|
150
140
|
...slideshowControlsProps.playButtonProps,
|
|
151
141
|
}
|
|
152
142
|
: undefined
|
|
153
143
|
}
|
|
154
|
-
paginationItemProps={(index) => ({
|
|
155
|
-
'aria-controls': buildSlideShowGroupId(slideshowSlidesId, index),
|
|
156
|
-
...slideshowControlsProps.paginationItemProps?.(index),
|
|
157
|
-
})}
|
|
158
144
|
/>
|
|
159
145
|
</div>
|
|
160
146
|
) : undefined
|
|
@@ -78,11 +78,10 @@ export const ControllingSlideshow = ({ images = Object.values(LANDSCAPE_IMAGES),
|
|
|
78
78
|
parentRef={slideshow}
|
|
79
79
|
theme={theme}
|
|
80
80
|
isAutoPlaying={isAutoPlaying}
|
|
81
|
-
nextButtonProps={{ label: 'Next'
|
|
82
|
-
previousButtonProps={{ label: 'Previous'
|
|
81
|
+
nextButtonProps={{ label: 'Next' }}
|
|
82
|
+
previousButtonProps={{ label: 'Previous' }}
|
|
83
83
|
playButtonProps={{
|
|
84
84
|
label: 'Play/Pause',
|
|
85
|
-
'aria-controls': slideshowSlidesId,
|
|
86
85
|
onClick: toggleForcePause,
|
|
87
86
|
}}
|
|
88
87
|
paginationItemLabel={(index) => `Slide ${index}`}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import React, { forwardRef, RefObject, useCallback,
|
|
1
|
+
import React, { forwardRef, RefObject, useCallback, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
import range from 'lodash/range';
|
|
5
5
|
|
|
6
6
|
import { mdiChevronLeft, mdiChevronRight, mdiPlayCircleOutline, mdiPauseCircleOutline } from '@lumx/icons';
|
|
7
|
-
import { Emphasis, IconButton, IconButtonProps, Theme } from '@lumx/react';
|
|
7
|
+
import { Emphasis, IconButton, IconButtonProps, Slides, Theme } from '@lumx/react';
|
|
8
8
|
import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
|
|
9
9
|
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
10
10
|
import { WINDOW } from '@lumx/react/constants';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { useKeyNavigate } from '@lumx/react/components/slideshow/useKeyNavigate';
|
|
12
|
+
import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
13
13
|
|
|
14
|
+
import { buildSlideShowGroupId } from '@lumx/react/components/slideshow/SlideshowItemGroup';
|
|
15
|
+
import { DEFAULT_OPTIONS, useSlideshowControls } from './useSlideshowControls';
|
|
14
16
|
import { useSwipeNavigate } from './useSwipeNavigate';
|
|
15
17
|
import { PAGINATION_ITEM_SIZE, PAGINATION_ITEMS_MAX } from './constants';
|
|
16
18
|
import { usePaginationVisibleRange } from './usePaginationVisibleRange';
|
|
@@ -34,11 +36,11 @@ export interface SlideshowControlsProps extends GenericProps, HasTheme {
|
|
|
34
36
|
/** Number of slides. */
|
|
35
37
|
slidesCount: number;
|
|
36
38
|
/** On next button click callback. */
|
|
37
|
-
onNextClick?(
|
|
39
|
+
onNextClick?(loopBack?: boolean): void;
|
|
38
40
|
/** On pagination change callback. */
|
|
39
41
|
onPaginationClick?(index: number): void;
|
|
40
42
|
/** On previous button click callback. */
|
|
41
|
-
onPreviousClick?(
|
|
43
|
+
onPreviousClick?(loopBack?: boolean): void;
|
|
42
44
|
/** whether the slideshow is currently playing */
|
|
43
45
|
isAutoPlaying?: boolean;
|
|
44
46
|
/**
|
|
@@ -100,7 +102,7 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
100
102
|
...forwardedProps
|
|
101
103
|
} = props;
|
|
102
104
|
|
|
103
|
-
let parent;
|
|
105
|
+
let parent: HTMLElement | null | undefined;
|
|
104
106
|
if (WINDOW) {
|
|
105
107
|
// Checking window object to avoid errors in SSR.
|
|
106
108
|
parent = parentRef instanceof HTMLElement ? parentRef : parentRef?.current;
|
|
@@ -109,33 +111,30 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
109
111
|
// Listen to touch swipe navigate left & right.
|
|
110
112
|
useSwipeNavigate(
|
|
111
113
|
parent,
|
|
112
|
-
// Go next without
|
|
114
|
+
// Go next without loop back.
|
|
113
115
|
useCallback(() => onNextClick?.(false), [onNextClick]),
|
|
114
|
-
// Go previous without
|
|
116
|
+
// Go previous without loop back.
|
|
115
117
|
useCallback(() => onPreviousClick?.(false), [onPreviousClick]),
|
|
116
118
|
);
|
|
117
119
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
useRovingTabIndex({
|
|
122
|
-
parentRef: paginationRef,
|
|
123
|
-
elementSelector: 'button',
|
|
124
|
-
keepTabIndex: true,
|
|
125
|
-
onElementFocus: (element) => {
|
|
126
|
-
element.click();
|
|
127
|
-
},
|
|
128
|
-
});
|
|
120
|
+
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
|
121
|
+
const onButtonFocus = useCallback((index: number) => () => setFocusedIndex(index), [setFocusedIndex]);
|
|
122
|
+
const onFocusOut = useCallback(() => setFocusedIndex(null), [setFocusedIndex]);
|
|
129
123
|
|
|
130
124
|
// Pagination "bullet" range.
|
|
131
|
-
const visibleRange = usePaginationVisibleRange(activeIndex as number, slidesCount);
|
|
125
|
+
const visibleRange = usePaginationVisibleRange(focusedIndex ?? (activeIndex as number), slidesCount);
|
|
132
126
|
|
|
133
127
|
// Inline style of wrapper element.
|
|
134
128
|
const wrapperStyle = { transform: `translateX(-${PAGINATION_ITEM_SIZE * visibleRange.min}px)` };
|
|
135
129
|
|
|
130
|
+
const controlsRef = React.useRef<HTMLDivElement>(null);
|
|
131
|
+
useKeyNavigate(controlsRef.current, onNextClick, onPreviousClick);
|
|
132
|
+
|
|
133
|
+
const slideshowSlidesId = React.useMemo(() => parent?.querySelector(`.${Slides.className}__slides`)?.id, [parent]);
|
|
134
|
+
|
|
136
135
|
return (
|
|
137
136
|
<div
|
|
138
|
-
ref={ref}
|
|
137
|
+
ref={useMergeRefs(ref, controlsRef)}
|
|
139
138
|
{...forwardedProps}
|
|
140
139
|
className={classNames(className, handleBasicClasses({ prefix: CLASSNAME, theme }), {
|
|
141
140
|
[`${CLASSNAME}--has-infinite-pagination`]: slidesCount > PAGINATION_ITEMS_MAX,
|
|
@@ -148,64 +147,53 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
148
147
|
color={theme === Theme.dark ? 'light' : 'dark'}
|
|
149
148
|
emphasis={Emphasis.low}
|
|
150
149
|
onClick={onPreviousClick}
|
|
150
|
+
aria-controls={slideshowSlidesId}
|
|
151
151
|
/>
|
|
152
|
+
|
|
152
153
|
<div ref={paginationRef} className={`${CLASSNAME}__pagination`}>
|
|
153
154
|
<div
|
|
154
155
|
className={`${CLASSNAME}__pagination-items`}
|
|
155
156
|
style={wrapperStyle}
|
|
156
|
-
role="tablist"
|
|
157
157
|
{...paginationProps}
|
|
158
|
+
onBlur={onFocusOut}
|
|
158
159
|
>
|
|
159
|
-
{
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
/>
|
|
197
|
-
);
|
|
198
|
-
}),
|
|
199
|
-
[
|
|
200
|
-
slidesCount,
|
|
201
|
-
visibleRange.min,
|
|
202
|
-
visibleRange.max,
|
|
203
|
-
activeIndex,
|
|
204
|
-
paginationItemProps,
|
|
205
|
-
paginationItemLabel,
|
|
206
|
-
onPaginationClick,
|
|
207
|
-
],
|
|
208
|
-
)}
|
|
160
|
+
{range(slidesCount).map((index) => {
|
|
161
|
+
const isOnEdge =
|
|
162
|
+
index !== 0 &&
|
|
163
|
+
index !== slidesCount - 1 &&
|
|
164
|
+
(index === visibleRange.min || index === visibleRange.max);
|
|
165
|
+
const isActive = activeIndex === index;
|
|
166
|
+
const isOutRange = index < visibleRange.min || index > visibleRange.max;
|
|
167
|
+
const {
|
|
168
|
+
className: itemClassName = undefined,
|
|
169
|
+
label = undefined,
|
|
170
|
+
...itemProps
|
|
171
|
+
} = paginationItemProps ? paginationItemProps(index) : {};
|
|
172
|
+
|
|
173
|
+
const ariaLabel = label || paginationItemLabel?.(index) || `${index + 1} / ${slidesCount}`;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<button
|
|
177
|
+
className={classNames(
|
|
178
|
+
handleBasicClasses({
|
|
179
|
+
prefix: `${CLASSNAME}__pagination-item`,
|
|
180
|
+
isActive,
|
|
181
|
+
isOnEdge,
|
|
182
|
+
isOutRange,
|
|
183
|
+
}),
|
|
184
|
+
itemClassName,
|
|
185
|
+
)}
|
|
186
|
+
key={index}
|
|
187
|
+
type="button"
|
|
188
|
+
aria-current={isActive || undefined}
|
|
189
|
+
aria-controls={buildSlideShowGroupId(slideshowSlidesId, index)}
|
|
190
|
+
onClick={() => onPaginationClick?.(index)}
|
|
191
|
+
onFocus={onButtonFocus(index)}
|
|
192
|
+
aria-label={ariaLabel}
|
|
193
|
+
{...itemProps}
|
|
194
|
+
/>
|
|
195
|
+
);
|
|
196
|
+
})}
|
|
209
197
|
</div>
|
|
210
198
|
</div>
|
|
211
199
|
|
|
@@ -216,6 +204,7 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
216
204
|
className={`${CLASSNAME}__play`}
|
|
217
205
|
color={theme === Theme.dark ? 'light' : 'dark'}
|
|
218
206
|
emphasis={Emphasis.low}
|
|
207
|
+
aria-controls={slideshowSlidesId}
|
|
219
208
|
/>
|
|
220
209
|
) : null}
|
|
221
210
|
|
|
@@ -226,6 +215,7 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
226
215
|
color={theme === Theme.dark ? 'light' : 'dark'}
|
|
227
216
|
emphasis={Emphasis.low}
|
|
228
217
|
onClick={onNextClick}
|
|
218
|
+
aria-controls={slideshowSlidesId}
|
|
229
219
|
/>
|
|
230
220
|
</div>
|
|
231
221
|
);
|
|
@@ -33,16 +33,7 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
|
33
33
|
export const SlideshowItem: Comp<SlideshowItemProps, HTMLDivElement> = forwardRef((props, ref) => {
|
|
34
34
|
const { className, children, ...forwardedProps } = props;
|
|
35
35
|
return (
|
|
36
|
-
<div
|
|
37
|
-
ref={ref}
|
|
38
|
-
className={classNames(
|
|
39
|
-
className,
|
|
40
|
-
handleBasicClasses({
|
|
41
|
-
prefix: CLASSNAME,
|
|
42
|
-
}),
|
|
43
|
-
)}
|
|
44
|
-
{...forwardedProps}
|
|
45
|
-
>
|
|
36
|
+
<div ref={ref} className={classNames(className, CLASSNAME)} {...forwardedProps}>
|
|
46
37
|
{children}
|
|
47
38
|
</div>
|
|
48
39
|
);
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { mergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
3
|
+
import { getRootClassName } from '@lumx/react/utils/className';
|
|
5
4
|
|
|
6
|
-
import { Comp, GenericProps } from '@lumx/react/utils/type';
|
|
7
|
-
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
8
5
|
import { useSlideFocusManagement } from './useSlideFocusManagement';
|
|
9
6
|
|
|
10
7
|
/**
|
|
11
8
|
* Defines the props of the component.
|
|
12
9
|
*/
|
|
13
|
-
export interface SlideshowItemGroupProps
|
|
14
|
-
|
|
10
|
+
export interface SlideshowItemGroupProps {
|
|
11
|
+
id?: string;
|
|
15
12
|
label?: string;
|
|
16
13
|
isDisplayed?: boolean;
|
|
14
|
+
slidesRef?: React.RefObject<HTMLDivElement>;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
/**
|
|
@@ -26,39 +24,24 @@ const COMPONENT_NAME = 'SlideshowItemGroup';
|
|
|
26
24
|
*/
|
|
27
25
|
export const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
28
26
|
|
|
29
|
-
export const buildSlideShowGroupId = (slidesId: string, index: number) =>
|
|
27
|
+
export const buildSlideShowGroupId = (slidesId: string | undefined, index: number) =>
|
|
28
|
+
slidesId && `${slidesId}-slide-${index}`;
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* @param props Component props.
|
|
35
|
-
* @param ref Component ref.
|
|
36
|
-
* @return React element.
|
|
31
|
+
* Internal slideshow item group component.
|
|
37
32
|
*/
|
|
38
|
-
export const SlideshowItemGroup:
|
|
39
|
-
const {
|
|
40
|
-
const groupRef = React.useRef<HTMLDivElement>(null);
|
|
33
|
+
export const SlideshowItemGroup: React.FC<SlideshowItemGroupProps> = (props) => {
|
|
34
|
+
const { id, children, label, isDisplayed, slidesRef } = props;
|
|
41
35
|
|
|
42
|
-
useSlideFocusManagement({
|
|
36
|
+
const groupRef = useSlideFocusManagement({
|
|
37
|
+
isSlideDisplayed: isDisplayed,
|
|
38
|
+
slidesRef,
|
|
39
|
+
});
|
|
43
40
|
|
|
44
41
|
return (
|
|
45
|
-
<div
|
|
46
|
-
ref={mergeRefs(groupRef, ref)}
|
|
47
|
-
role={role}
|
|
48
|
-
className={classNames(
|
|
49
|
-
className,
|
|
50
|
-
handleBasicClasses({
|
|
51
|
-
prefix: CLASSNAME,
|
|
52
|
-
}),
|
|
53
|
-
)}
|
|
54
|
-
aria-roledescription="slide"
|
|
55
|
-
aria-label={label}
|
|
56
|
-
{...forwardedProps}
|
|
57
|
-
>
|
|
42
|
+
<div id={id} ref={groupRef} role="group" className={CLASSNAME} aria-label={label} tabIndex={-1}>
|
|
58
43
|
{children}
|
|
59
44
|
</div>
|
|
60
45
|
);
|
|
61
|
-
}
|
|
62
|
-
|
|
46
|
+
};
|
|
63
47
|
SlideshowItemGroup.displayName = COMPONENT_NAME;
|
|
64
|
-
SlideshowItemGroup.className = CLASSNAME;
|
|
@@ -3,7 +3,7 @@ import { getFocusableElements } from '@lumx/react/utils/focus/getFocusableElemen
|
|
|
3
3
|
|
|
4
4
|
export interface UseSlideFocusManagementProps {
|
|
5
5
|
isSlideDisplayed?: boolean;
|
|
6
|
-
|
|
6
|
+
slidesRef?: React.RefObject<HTMLDivElement>;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -17,42 +17,45 @@ const BLOCKED_FOCUS_CLASSNAME = 'focus-blocked';
|
|
|
17
17
|
* Manage how slides must behave when visible or not.
|
|
18
18
|
* When not visible, they should be hidden from screen readers and not focusable.
|
|
19
19
|
*/
|
|
20
|
-
export const useSlideFocusManagement = ({ isSlideDisplayed,
|
|
20
|
+
export const useSlideFocusManagement = ({ isSlideDisplayed, slidesRef }: UseSlideFocusManagementProps) => {
|
|
21
|
+
const [slide, setSlide] = React.useState<HTMLDivElement | null>(null);
|
|
21
22
|
useEffect(() => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (!element) {
|
|
23
|
+
if (!slide) {
|
|
25
24
|
return undefined;
|
|
26
25
|
}
|
|
27
26
|
|
|
27
|
+
const isUserActivated = slidesRef?.current?.dataset.lumxUserActivated === 'true';
|
|
28
|
+
let focusableElements: Array<HTMLElement> = [];
|
|
29
|
+
|
|
28
30
|
/**
|
|
29
31
|
* Display given slide to screen readers and, if focus was blocked, restore focus on elements.
|
|
30
32
|
*/
|
|
31
33
|
const enableSlide = () => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
slide.removeAttribute('inert');
|
|
35
|
+
slide.setAttribute('aria-hidden', 'false');
|
|
34
36
|
// Find elements we have blocked focus on
|
|
35
|
-
|
|
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) {
|
|
36
40
|
focusableElement.removeAttribute('tabindex');
|
|
37
41
|
focusableElement.classList.remove(BLOCKED_FOCUS_CLASSNAME);
|
|
38
|
-
}
|
|
42
|
+
}
|
|
39
43
|
};
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* Hide given slide from screen readers and block focus on all focusable elements within.
|
|
43
47
|
*/
|
|
44
48
|
const blockSlide = () => {
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
slide.setAttribute('inert', '');
|
|
50
|
+
slide.setAttribute('aria-hidden', 'true');
|
|
51
|
+
focusableElements = getFocusableElements(slide);
|
|
52
|
+
for (const focusableElement of focusableElements) {
|
|
47
53
|
focusableElement.setAttribute('tabindex', '-1');
|
|
48
54
|
focusableElement.classList.add(BLOCKED_FOCUS_CLASSNAME);
|
|
49
|
-
}
|
|
55
|
+
}
|
|
50
56
|
};
|
|
51
57
|
|
|
52
58
|
const handleDisplay = () => {
|
|
53
|
-
if (!element) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
59
|
if (isSlideDisplayed) {
|
|
57
60
|
enableSlide();
|
|
58
61
|
} else {
|
|
@@ -60,33 +63,45 @@ export const useSlideFocusManagement = ({ isSlideDisplayed, slideRef }: UseSlide
|
|
|
60
63
|
}
|
|
61
64
|
};
|
|
62
65
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
66
|
+
// Create an observer instance linked to the callback function
|
|
67
|
+
// (won't be necessary once "inert" gets sufficient browser support)
|
|
68
|
+
const observer = new MutationObserver((mutationsList) => {
|
|
69
|
+
for (const mutation of mutationsList) {
|
|
70
|
+
if (mutation.type === 'childList') {
|
|
71
|
+
handleDisplay();
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
|
-
};
|
|
74
|
+
});
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
const observer = new MutationObserver(callback);
|
|
76
|
+
handleDisplay();
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
// Change focus on slide displayed
|
|
79
|
+
if (isSlideDisplayed && isUserActivated) {
|
|
80
|
+
let elementToFocus: HTMLElement | undefined = slide;
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
|
|
82
|
+
// We have exactly one focusable element => focus it
|
|
83
|
+
if (focusableElements.length === 1) {
|
|
84
|
+
// eslint-disable-next-line prefer-destructuring
|
|
85
|
+
elementToFocus = focusableElements[0];
|
|
83
86
|
}
|
|
84
|
-
}
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
|
|
88
|
+
// We have not focusable element => focus the pagination item
|
|
89
|
+
if (focusableElements.length === 0) {
|
|
90
|
+
elementToFocus = document.querySelector(`[aria-controls="${slide?.id}"]`) as HTMLElement;
|
|
89
91
|
}
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
}
|
|
102
|
+
|
|
103
|
+
return undefined;
|
|
104
|
+
}, [isSlideDisplayed, slide, slidesRef]);
|
|
105
|
+
|
|
106
|
+
return setSlide;
|
|
92
107
|
};
|