@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/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.0",
10
- "@lumx/icons": "^3.9.4-alpha.0",
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.0"
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 ? slideGroupLabel(index + 1, groups.length) : undefined}
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
- * Classname set on elements whose focus was blocked.
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 BLOCKED_FOCUS_CLASSNAME = 'focus-blocked';
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
- useEffect(() => {
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
- const isUserActivated = slidesRef?.current?.dataset.lumxUserActivated === 'true';
28
- let focusableElements: Array<HTMLElement> = [];
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
- * Hide given slide from screen readers and block focus on all focusable elements within.
47
- */
48
- const blockSlide = () => {
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
- focusableElements = getFocusableElements(slide);
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.classList.add(BLOCKED_FOCUS_CLASSNAME);
69
+ focusableElement.setAttribute(BLOCKED_FOCUS, '');
55
70
  }
56
- };
57
-
58
- const handleDisplay = () => {
59
- if (isSlideDisplayed) {
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
- // 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();
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
- handleDisplay();
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
- // Change focus on slide displayed
79
- if (isSlideDisplayed && isUserActivated) {
80
- let elementToFocus: HTMLElement | undefined = slide;
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
- // We have exactly one focusable element => focus it
83
- if (focusableElements.length === 1) {
84
- // eslint-disable-next-line prefer-destructuring
85
- elementToFocus = focusableElements[0];
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
- // We have not focusable element => focus the pagination item
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);