@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/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.3",
10
- "@lumx/icons": "^3.9.3",
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.3"
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 ref={wrapperRef} className={`${CLASSNAME}__wrapper`} style={wrapperStyle}>
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 { buildSlideShowGroupId } from './SlideshowItemGroup';
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?: SlidesProps['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', 'aria-controls': slideshowSlidesId }}
82
- previousButtonProps={{ label: 'Previous', 'aria-controls': slideshowSlidesId }}
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, useMemo } from 'react';
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 { useSlideshowControls, DEFAULT_OPTIONS } from '@lumx/react/hooks/useSlideshowControls';
12
- import { useRovingTabIndex } from '@lumx/react/hooks/useRovingTabIndex';
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?(loopback?: boolean): void;
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?(loopback?: boolean): void;
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 loopback.
114
+ // Go next without loop back.
113
115
  useCallback(() => onNextClick?.(false), [onNextClick]),
114
- // Go previous without loopback.
116
+ // Go previous without loop back.
115
117
  useCallback(() => onPreviousClick?.(false), [onPreviousClick]),
116
118
  );
117
119
 
118
- /**
119
- * Add roving tab index pattern to pagination items and activate slide on focus.
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
- {useMemo(
160
- () =>
161
- range(slidesCount).map((index) => {
162
- const isOnEdge =
163
- index !== 0 &&
164
- index !== slidesCount - 1 &&
165
- (index === visibleRange.min || index === visibleRange.max);
166
- const isActive = activeIndex === index;
167
- const isOutRange = index < visibleRange.min || index > visibleRange.max;
168
- const {
169
- className: itemClassName = undefined,
170
- label = undefined,
171
- ...itemProps
172
- } = paginationItemProps ? paginationItemProps(index) : {};
173
-
174
- const ariaLabel =
175
- label || paginationItemLabel?.(index) || `${index + 1} / ${slidesCount}`;
176
-
177
- return (
178
- <button
179
- className={classNames(
180
- handleBasicClasses({
181
- prefix: `${CLASSNAME}__pagination-item`,
182
- isActive,
183
- isOnEdge,
184
- isOutRange,
185
- }),
186
- itemClassName,
187
- )}
188
- key={index}
189
- type="button"
190
- tabIndex={isActive ? undefined : -1}
191
- role="tab"
192
- aria-selected={isActive}
193
- onClick={() => onPaginationClick?.(index)}
194
- aria-label={ariaLabel}
195
- {...itemProps}
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, { forwardRef } from 'react';
1
+ import React from 'react';
2
2
 
3
- import classNames from 'classnames';
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 extends GenericProps {
14
- role?: 'tabpanel' | 'group';
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) => `${slidesId}-slide-${index}`;
27
+ export const buildSlideShowGroupId = (slidesId: string | undefined, index: number) =>
28
+ slidesId && `${slidesId}-slide-${index}`;
30
29
 
31
30
  /**
32
- * SlideshowItemGroup component.
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: Comp<SlideshowItemGroupProps, HTMLDivElement> = forwardRef((props, ref) => {
39
- const { className, children, role = 'group', label, isDisplayed, ...forwardedProps } = props;
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({ isSlideDisplayed: isDisplayed, slideRef: groupRef });
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
- slideRef: React.RefObject<HTMLDivElement>;
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, slideRef }: UseSlideFocusManagementProps) => {
20
+ export const useSlideFocusManagement = ({ isSlideDisplayed, slidesRef }: UseSlideFocusManagementProps) => {
21
+ const [slide, setSlide] = React.useState<HTMLDivElement | null>(null);
21
22
  useEffect(() => {
22
- const element = slideRef?.current;
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
- // Hide from screen readers
33
- element.setAttribute('aria-hidden', 'false');
34
+ slide.removeAttribute('inert');
35
+ slide.setAttribute('aria-hidden', 'false');
34
36
  // Find elements we have blocked focus on
35
- element.querySelectorAll(`.${BLOCKED_FOCUS_CLASSNAME}`).forEach((focusableElement) => {
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
- element.setAttribute('aria-hidden', 'true');
46
- getFocusableElements(element).forEach((focusableElement) => {
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
- // Callback function to execute when mutations are observed
64
- const callback: MutationCallback = (mutationsList) => {
65
- if (element) {
66
- for (const mutation of mutationsList) {
67
- if (mutation.type === 'childList') {
68
- handleDisplay();
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
- // Create an observer instance linked to the callback function
75
- const observer = new MutationObserver(callback);
76
+ handleDisplay();
76
77
 
77
- if (element) {
78
- handleDisplay();
78
+ // Change focus on slide displayed
79
+ if (isSlideDisplayed && isUserActivated) {
80
+ let elementToFocus: HTMLElement | undefined = slide;
79
81
 
80
- /** If slide is hidden, start observing for elements to block focus */
81
- if (!isSlideDisplayed) {
82
- observer.observe(element, { attributes: true, childList: true, subtree: true });
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
- return () => {
87
- if (!isSlideDisplayed) {
88
- observer.disconnect();
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
- }, [isSlideDisplayed, slideRef]);
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
  };