@lumx/react 2.2.5 → 2.2.8

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.
Files changed (73) hide show
  1. package/esm/_internal/AutocompleteMultiple.js.map +1 -1
  2. package/esm/_internal/DatePickerField.js.map +1 -1
  3. package/esm/_internal/Icon2.js +5 -2
  4. package/esm/_internal/Icon2.js.map +1 -1
  5. package/esm/_internal/Lightbox2.js +6 -4
  6. package/esm/_internal/Lightbox2.js.map +1 -1
  7. package/esm/_internal/List2.js.map +1 -1
  8. package/esm/_internal/Notification2.js +2 -1
  9. package/esm/_internal/Notification2.js.map +1 -1
  10. package/esm/_internal/SelectMultiple.js.map +1 -1
  11. package/esm/_internal/SlideshowControls.js +234 -94
  12. package/esm/_internal/SlideshowControls.js.map +1 -1
  13. package/esm/_internal/Thumbnail2.js +1 -1
  14. package/esm/_internal/Thumbnail2.js.map +1 -1
  15. package/esm/_internal/Tooltip2.js +110 -113
  16. package/esm/_internal/Tooltip2.js.map +1 -1
  17. package/esm/_internal/autocomplete.js +0 -1
  18. package/esm/_internal/autocomplete.js.map +1 -1
  19. package/esm/_internal/button.js +0 -1
  20. package/esm/_internal/button.js.map +1 -1
  21. package/esm/_internal/constants.js.map +1 -1
  22. package/esm/_internal/date-picker.js +0 -1
  23. package/esm/_internal/date-picker.js.map +1 -1
  24. package/esm/_internal/expansion-panel.js +0 -1
  25. package/esm/_internal/expansion-panel.js.map +1 -1
  26. package/esm/_internal/getRootClassName.js +19 -1
  27. package/esm/_internal/getRootClassName.js.map +1 -1
  28. package/esm/_internal/lightbox.js +0 -1
  29. package/esm/_internal/lightbox.js.map +1 -1
  30. package/esm/_internal/mergeRefs.js.map +1 -1
  31. package/esm/_internal/select.js +0 -1
  32. package/esm/_internal/select.js.map +1 -1
  33. package/esm/_internal/side-navigation.js +0 -1
  34. package/esm/_internal/side-navigation.js.map +1 -1
  35. package/esm/_internal/slideshow.js +1 -1
  36. package/esm/_internal/text-field.js +0 -1
  37. package/esm/_internal/text-field.js.map +1 -1
  38. package/esm/_internal/tooltip.js +0 -1
  39. package/esm/_internal/tooltip.js.map +1 -1
  40. package/esm/_internal/useRovingTabIndex.js.map +1 -1
  41. package/esm/index.js +1 -1
  42. package/package.json +4 -4
  43. package/src/components/alert-dialog/AlertDialog.test.tsx +1 -0
  44. package/src/components/autocomplete/AutocompleteMultiple.tsx +3 -1
  45. package/src/components/button/__snapshots__/IconButton.test.tsx.snap +0 -5
  46. package/src/components/icon/Icon.tsx +6 -2
  47. package/src/components/image-block/ImageBlock.stories.tsx +1 -2
  48. package/src/components/lightbox/Lightbox.stories.tsx +1 -0
  49. package/src/components/lightbox/Lightbox.tsx +5 -3
  50. package/src/components/notification/Notification.tsx +1 -0
  51. package/src/components/select/SelectMultiple.tsx +0 -1
  52. package/src/components/slideshow/Slideshow.stories.tsx +1 -1
  53. package/src/components/slideshow/Slideshow.tsx +76 -112
  54. package/src/components/slideshow/SlideshowControls.stories.tsx +18 -12
  55. package/src/components/slideshow/SlideshowControls.tsx +11 -7
  56. package/src/components/slideshow/SlideshowItem.tsx +4 -1
  57. package/src/components/slideshow/__snapshots__/Slideshow.test.tsx.snap +52 -17
  58. package/src/components/tabs/state.ts +0 -1
  59. package/src/components/thumbnail/Thumbnail.stories.tsx +25 -1
  60. package/src/components/thumbnail/Thumbnail.test.tsx +9 -1
  61. package/src/components/thumbnail/Thumbnail.tsx +3 -0
  62. package/src/components/thumbnail/__snapshots__/Thumbnail.test.tsx.snap +26 -0
  63. package/src/components/tooltip/Tooltip.tsx +1 -2
  64. package/src/components/tooltip/useTooltipOpen.tsx +90 -91
  65. package/src/constants.ts +7 -1
  66. package/src/hooks/useFocusWithin.ts +33 -0
  67. package/src/hooks/useSlideshowControls.ts +213 -0
  68. package/src/utils/browserDoesNotSupportHover.test.js +24 -0
  69. package/src/utils/browserDoesNotSupportHover.ts +2 -0
  70. package/src/utils/index.tsx +0 -2
  71. package/src/utils/mergeRefs.ts +1 -1
  72. package/types.d.ts +62 -8
  73. package/src/utils/htmlDecode.ts +0 -13
@@ -105,6 +105,8 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
105
105
  crossOrigin,
106
106
  fallback,
107
107
  fillHeight,
108
+ // `focusPoint` needs to be here to remove it from `forwardedProps`.
109
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
108
110
  focusPoint,
109
111
  image,
110
112
  imgProps,
@@ -194,6 +196,7 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
194
196
  isLoading,
195
197
  hasDefinedSize: Boolean(imgProps?.height && imgProps.width),
196
198
  }),
199
+ imgProps?.className,
197
200
  )}
198
201
  crossOrigin={crossOrigin}
199
202
  src={image}
@@ -102,3 +102,29 @@ exports[`<Thumbnail> Snapshots and structure should render story 'WithBadge' 1`]
102
102
  </Badge>
103
103
  </div>
104
104
  `;
105
+
106
+ exports[`<Thumbnail> Snapshots and structure should render story 'WithCustomImageClassName' 1`] = `
107
+ <div
108
+ className="lumx-thumbnail lumx-thumbnail--aspect-ratio-square lumx-thumbnail--size-l lumx-thumbnail--theme-light lumx-thumbnail--variant-rounded lumx-thumbnail--is-loading lumx-thumbnail--has-badge"
109
+ >
110
+ <div
111
+ className="lumx-thumbnail__background"
112
+ >
113
+ <img
114
+ alt="Image alt text"
115
+ className="lumx-thumbnail__image lumx-thumbnail__image--is-loading custom-image-class-name"
116
+ loading="lazy"
117
+ src="/demo-assets/landscape1.jpg"
118
+ style={Object {}}
119
+ />
120
+ </div>
121
+ <Badge
122
+ className="lumx-thumbnail__badge"
123
+ color="primary"
124
+ >
125
+ <Icon
126
+ icon="M4 2A2 2 0 0 0 2 4V12H4V8H6V12H8V4A2 2 0 0 0 6 2H4M4 4H6V6H4M22 15.5V14A2 2 0 0 0 20 12H16V22H20A2 2 0 0 0 22 20V18.5A1.54 1.54 0 0 0 20.5 17A1.54 1.54 0 0 0 22 15.5M20 20H18V18H20V20M20 16H18V14H20M5.79 21.61L4.21 20.39L18.21 2.39L19.79 3.61Z"
127
+ />
128
+ </Badge>
129
+ </div>
130
+ `;
@@ -49,7 +49,6 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
49
49
  * Component default props.
50
50
  */
51
51
  const DEFAULT_PROPS: Partial<TooltipProps> = {
52
- delay: 500,
53
52
  placement: Placement.BOTTOM,
54
53
  };
55
54
 
@@ -90,7 +89,7 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
90
89
  });
91
90
 
92
91
  const position = attributes?.popper?.['data-popper-placement'] ?? placement;
93
- const isOpen = useTooltipOpen(delay as number, anchorElement) || forceOpen;
92
+ const isOpen = useTooltipOpen(delay, anchorElement) || forceOpen;
94
93
  const wrappedChildren = useInjectTooltipRef(children, setAnchorElement, isOpen as boolean, id);
95
94
 
96
95
  return (
@@ -1,60 +1,7 @@
1
- import { Callback, onEscapePressed } from '@lumx/react/utils';
2
- import { useEffect, useRef, useState } from 'react';
3
- import pull from 'lodash/pull';
4
- import debounce from 'lodash/debounce';
5
-
6
- type Tooltip = { open: Callback; close: Callback; anchorElement: HTMLElement };
7
-
8
- /**
9
- * This singleton handle a global `mouseover` event listener on the `document` in order to toggle tooltips when
10
- * entering and leaving their anchor element.
11
- */
12
- const tooltipMouseToggle = (() => {
13
- /** List of tooltips to toggle on anchor enter/leave. */
14
- let tooltips: Array<Tooltip> | undefined;
15
-
16
- /** Global listener added on the document. */
17
- let globalListener: undefined | ((evt: MouseEvent) => void);
18
-
19
- function addGlobalListener() {
20
- if (globalListener) return;
21
- globalListener = debounce((evt) => {
22
- if (!tooltips || !evt.target) return;
23
- for (const { open, close, anchorElement } of tooltips) {
24
- if (anchorElement.contains(evt.target as any)) {
25
- open();
26
- } else {
27
- close();
28
- }
29
- }
30
- }, 10);
31
- document.addEventListener('mouseover', globalListener);
32
- }
33
-
34
- function removeGlobalListener() {
35
- if (!globalListener) return;
36
- document.removeEventListener('mouseover', globalListener);
37
- globalListener = undefined;
38
- }
39
-
40
- return {
41
- addTooltip(tooltip: Tooltip) {
42
- if (!tooltips) {
43
- tooltips = [];
44
- addGlobalListener();
45
- }
46
- tooltips.push(tooltip);
47
- },
48
- removeTooltip(actions: Tooltip) {
49
- if (!tooltips) return;
50
- pull(tooltips, actions);
51
- if (tooltips.length === 0) {
52
- removeGlobalListener();
53
- tooltips = undefined;
54
- }
55
- },
56
- };
57
- })();
1
+ import { onEscapePressed } from '@lumx/react/utils';
2
+ import { useEffect, useState } from 'react';
3
+ import { browserDoesNotSupportHover } from '@lumx/react/utils/browserDoesNotSupportHover';
4
+ import { TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/constants';
58
5
 
59
6
  /**
60
7
  * Hook controlling tooltip visibility using mouse hover the anchor and delay.
@@ -63,50 +10,102 @@ const tooltipMouseToggle = (() => {
63
10
  * @param anchorElement Tooltip anchor element.
64
11
  * @return whether or not to show the tooltip.
65
12
  */
66
- export function useTooltipOpen(delay: number, anchorElement: HTMLElement | null): boolean {
67
- const timer = useRef<number>();
68
- const shouldOpen = useRef<boolean>(false);
13
+ export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLElement | null): boolean {
69
14
  const [isOpen, setIsOpen] = useState(false);
70
15
 
71
16
  useEffect(() => {
72
17
  if (!anchorElement) {
73
18
  return undefined;
74
19
  }
75
- const tooltip: Tooltip = {
76
- anchorElement,
77
- open() {
78
- if (!shouldOpen.current) {
79
- shouldOpen.current = true;
80
- timer.current = setTimeout(() => {
81
- setIsOpen(shouldOpen.current);
82
- }, delay) as any;
83
- }
84
- },
85
- close() {
86
- if (timer.current) {
87
- clearTimeout(timer.current);
88
- timer.current = undefined;
89
- }
90
- if (shouldOpen.current) {
91
- shouldOpen.current = false;
92
- setIsOpen(shouldOpen.current);
93
- }
94
- },
20
+ let timer: number | undefined;
21
+ let openStartTime: number | undefined;
22
+ let shouldOpen: boolean | undefined;
23
+
24
+ // Run timer to defer updating the isOpen state.
25
+ const deferUpdate = (duration: number) => {
26
+ if (timer) clearTimeout(timer);
27
+ timer = setTimeout(() => {
28
+ setIsOpen(!!shouldOpen);
29
+ }, duration) as any;
95
30
  };
96
- const keydown = onEscapePressed(tooltip.close);
97
31
 
98
- tooltipMouseToggle.addTooltip(tooltip);
99
- anchorElement.addEventListener('focusin', tooltip.open);
100
- anchorElement.addEventListener('focusout', tooltip.close);
101
- anchorElement.addEventListener('keydown', keydown);
32
+ const hoverNotSupported = browserDoesNotSupportHover();
33
+ const hasTouch = 'ontouchstart' in window;
34
+
35
+ // Adapt open/close delay
36
+ const openDelay = delay || (hoverNotSupported ? TOOLTIP_LONG_PRESS_DELAY.open : TOOLTIP_HOVER_DELAY.open);
37
+ const closeDelay = hoverNotSupported ? TOOLTIP_LONG_PRESS_DELAY.close : TOOLTIP_HOVER_DELAY.close;
38
+
39
+ // Open (or/and cancel closing) of tooltip.
40
+ const open = () => {
41
+ if (shouldOpen && !timer) return;
42
+ shouldOpen = true;
43
+ openStartTime = Date.now();
44
+ deferUpdate(openDelay);
45
+ };
46
+
47
+ // Close or cancel opening of tooltip
48
+ const close = (overrideDelay = closeDelay) => {
49
+ if (!shouldOpen && !timer) return;
50
+ shouldOpen = false;
51
+ deferUpdate(overrideDelay);
52
+ };
53
+ const closeImmediately = () => close(0);
54
+
55
+ /**
56
+ * Handle touchend event
57
+ * If `touchend` comes before the open delay => cancel tooltip (close immediate).
58
+ * Else if `touchend` comes after the open delay => tooltip takes priority, the anchor's default touch end event is prevented.
59
+ */
60
+ const touchEnd = (evt: Event) => {
61
+ if (!openStartTime) return;
62
+ if (Date.now() - openStartTime >= openDelay) {
63
+ // Tooltip take priority, event prevented.
64
+ evt.stopPropagation();
65
+ evt.preventDefault();
66
+ anchorElement.focus();
67
+ // Close with delay.
68
+ close();
69
+ } else {
70
+ // Close immediately.
71
+ closeImmediately();
72
+ }
73
+ };
74
+
75
+ // Adapt event to browsers with or without `hover` support.
76
+ const events: Array<[Node, Event['type'], any]> = hoverNotSupported
77
+ ? [
78
+ [anchorElement, hasTouch ? 'touchstart' : 'mousedown', open],
79
+ [anchorElement, hasTouch ? 'touchend' : 'mouseup', touchEnd],
80
+ ]
81
+ : [
82
+ [anchorElement, 'mouseenter', open],
83
+ [anchorElement, 'mouseleave', close],
84
+ [anchorElement, 'mouseup', closeImmediately],
85
+ ];
86
+
87
+ // Events always applied no matter the browser:.
88
+ events.push(
89
+ // Open on focus.
90
+ [anchorElement, 'focusin', open],
91
+ // Close on lost focus.
92
+ [anchorElement, 'focusout', closeImmediately],
93
+ // Close on ESC keydown
94
+ [anchorElement, 'keydown', onEscapePressed(closeImmediately)],
95
+ );
96
+
97
+ // Attach events
98
+ for (const [node, eventType, evenHandler] of events) {
99
+ node.addEventListener(eventType, evenHandler);
100
+ }
102
101
  return () => {
103
- tooltipMouseToggle.removeTooltip(tooltip);
104
- anchorElement.removeEventListener('focusin', tooltip.open);
105
- anchorElement.removeEventListener('focusout', tooltip.close);
106
- anchorElement.removeEventListener('keydown', keydown);
107
- tooltip.close();
102
+ // Detach events.
103
+ for (const [node, eventType, evenHandler] of events) {
104
+ node.removeEventListener(eventType, evenHandler);
105
+ }
106
+ closeImmediately();
108
107
  };
109
- }, [anchorElement, delay, timer, shouldOpen]);
108
+ }, [anchorElement, delay]);
110
109
 
111
110
  return isOpen;
112
111
  }
package/src/constants.ts CHANGED
@@ -1,4 +1,10 @@
1
- export { CSS_PREFIX, DIALOG_TRANSITION_DURATION, NOTIFICATION_TRANSITION_DURATION } from '@lumx/core/js/constants';
1
+ export {
2
+ CSS_PREFIX,
3
+ DIALOG_TRANSITION_DURATION,
4
+ NOTIFICATION_TRANSITION_DURATION,
5
+ TOOLTIP_HOVER_DELAY,
6
+ TOOLTIP_LONG_PRESS_DELAY,
7
+ } from '@lumx/core/js/constants';
2
8
 
3
9
  /**
4
10
  * Optional global `window` instance (not defined when running SSR).
@@ -0,0 +1,33 @@
1
+ import { useEffect } from 'react';
2
+
3
+ export interface UseFocusWithinOptions {
4
+ /** element to add the focus within to */
5
+ element: HTMLElement | undefined;
6
+ /** callback to be executed on focus in */
7
+ onFocusIn: (ev: FocusEvent) => void;
8
+ /** callback to be executed on focus out */
9
+ onFocusOut: (ev: FocusEvent) => void;
10
+ }
11
+
12
+ /**
13
+ * Hook that allows to control when there is a focus event within a given element, meaning
14
+ * that any element within the given target will trigger the focus in and focus out events.
15
+ * @param options - UseFocusWithinOptions
16
+ */
17
+ export const useFocusWithin = ({ element, onFocusIn, onFocusOut }: UseFocusWithinOptions) => {
18
+ useEffect(() => {
19
+ if (element) {
20
+ element.addEventListener('focusin', onFocusIn);
21
+
22
+ element.addEventListener('focusout', onFocusOut);
23
+ }
24
+
25
+ return () => {
26
+ if (element) {
27
+ element.removeEventListener('focusin', onFocusIn);
28
+
29
+ element.addEventListener('focusout', onFocusOut);
30
+ }
31
+ };
32
+ }, [onFocusIn, element, onFocusOut]);
33
+ };
@@ -0,0 +1,213 @@
1
+ import { useState, useCallback, useEffect, useMemo } from 'react';
2
+
3
+ import { useInterval } from '@lumx/react/hooks/useInterval';
4
+ import uniqueId from 'lodash/uniqueId';
5
+ import { AUTOPLAY_DEFAULT_INTERVAL } from '@lumx/react/components/slideshow/constants';
6
+
7
+ import { useFocusWithin } from './useFocusWithin';
8
+
9
+ export interface UseSlideshowControlsOptions {
10
+ /** default active index to be displayed */
11
+ defaultActiveIndex?: number;
12
+ /** total slides to display */
13
+ itemsCount: number;
14
+ /** Index of the current slide. */
15
+ activeIndex?: number;
16
+ /** Whether the automatic rotation of the slideshow is enabled or not. */
17
+ autoPlay?: boolean;
18
+ /** Whether the image has to fill its container height or not. */
19
+ fillHeight?: boolean;
20
+ /** Number of slides to group together. */
21
+ groupBy?: number;
22
+ /** Interval between each slide when automatic rotation is enabled. */
23
+ interval?: number;
24
+ /** Callback when slide changes */
25
+ onChange?(index: number): void;
26
+ /** slideshow HTML id attribute */
27
+ id?: string;
28
+ /** slides wrapper HTML id attribute */
29
+ slidesId?: string;
30
+ }
31
+
32
+ export interface UseSlideshowControls {
33
+ /** Index for the first visible slide, should be used when groupBy is passed in */
34
+ startIndexVisible: number;
35
+ /** Index for the last visible slide, should be used when groupBy is passed in */
36
+ endIndexVisible: number;
37
+ /** total slides to be displayed */
38
+ slidesCount: number;
39
+ /** callback to set */
40
+ setSlideshow: (element: HTMLDivElement | undefined) => void;
41
+ /** reference to the slideshow element */
42
+ slideshow: HTMLDivElement | undefined;
43
+ /** id to be used for the slideshow */
44
+ slideshowId: string;
45
+ /** id to be used for the wrapper that contains the slides */
46
+ slideshowSlidesId: string;
47
+ /** callback that triggers the previous slide while using the slideshow controls */
48
+ onPreviousClick: (loopback: boolean) => void;
49
+ /** callback that triggers the next slide while using the slideshow controls */
50
+ onNextClick: (loopback: boolean) => void;
51
+ /** callback that triggers a specific page while using the slideshow controls */
52
+ onPaginationClick: (index: number) => void;
53
+ /** whether the slideshow is autoplaying or not */
54
+ isAutoPlaying: boolean;
55
+ /** callback to change whether the slideshow is autoplaying or not */
56
+ setIsAutoPlaying: (isAutoPlaying: boolean) => void;
57
+ /** current active slide index */
58
+ activeIndex: number;
59
+ /** set the current index as the active one */
60
+ setActiveIndex: (index: number) => void;
61
+ }
62
+
63
+ export const DEFAULT_OPTIONS: Partial<UseSlideshowControlsOptions> = {
64
+ activeIndex: 0,
65
+ groupBy: 1,
66
+ interval: AUTOPLAY_DEFAULT_INTERVAL,
67
+ };
68
+
69
+ export const useSlideshowControls = ({
70
+ activeIndex = DEFAULT_OPTIONS.activeIndex,
71
+ groupBy = DEFAULT_OPTIONS.groupBy,
72
+ interval = DEFAULT_OPTIONS.interval,
73
+ autoPlay,
74
+ defaultActiveIndex,
75
+ onChange,
76
+ itemsCount,
77
+ id,
78
+ slidesId,
79
+ }: UseSlideshowControlsOptions): UseSlideshowControls => {
80
+ const [currentIndex, setCurrentIndex] = useState(activeIndex as number);
81
+ // Use state instead of a ref to make the slideshow controls update directly when the element is set.
82
+ const [element, setElement] = useState<HTMLDivElement>();
83
+
84
+ // Number of slides when using groupBy prop.
85
+ const slidesCount = Math.ceil(itemsCount / Math.min(groupBy as number, itemsCount));
86
+
87
+ // Change current index to display next slide.
88
+ const goToNextSlide = useCallback(
89
+ (loopback = true) => {
90
+ setCurrentIndex((index) => {
91
+ if (loopback && index === slidesCount - 1) {
92
+ // Loopback to the start.
93
+ return 0;
94
+ }
95
+ if (index < slidesCount - 1) {
96
+ // Next slide.
97
+ return index + 1;
98
+ }
99
+ return index;
100
+ });
101
+ },
102
+ [slidesCount, setCurrentIndex],
103
+ );
104
+
105
+ // Change current index to display previous slide.
106
+ const goToPreviousSlide = useCallback(
107
+ (loopback = true) => {
108
+ setCurrentIndex((index) => {
109
+ if (loopback && index === 0) {
110
+ // Loopback to the end.
111
+ return slidesCount - 1;
112
+ }
113
+ if (index > 0) {
114
+ // Previous slide.
115
+ return index - 1;
116
+ }
117
+ return index;
118
+ });
119
+ },
120
+ [slidesCount, setCurrentIndex],
121
+ );
122
+
123
+ // Auto play
124
+ const [isAutoPlaying, setIsAutoPlaying] = useState(Boolean(autoPlay));
125
+ // Start
126
+ useInterval(goToNextSlide, isAutoPlaying && slidesCount > 1 ? (interval as number) : null);
127
+
128
+ // Reset current index if it become invalid.
129
+ useEffect(() => {
130
+ if (currentIndex > slidesCount - 1) {
131
+ setCurrentIndex(defaultActiveIndex as number);
132
+ }
133
+ }, [currentIndex, slidesCount, defaultActiveIndex]);
134
+
135
+ // Handle click on a bullet to go to a specific slide.
136
+ const onPaginationClick = useCallback(
137
+ (index: number) => {
138
+ setIsAutoPlaying(false);
139
+
140
+ if (index >= 0 && index < slidesCount) {
141
+ setCurrentIndex(index);
142
+ }
143
+ },
144
+ [slidesCount, setCurrentIndex],
145
+ );
146
+
147
+ // Handle click or keyboard event to go to next slide.
148
+ const onNextClick = useCallback(
149
+ (loopback = true) => {
150
+ setIsAutoPlaying(false);
151
+ goToNextSlide(loopback);
152
+ },
153
+ [goToNextSlide],
154
+ );
155
+
156
+ // Handle click or keyboard event to go to previous slide.
157
+ const onPreviousClick = useCallback(
158
+ (loopback = true) => {
159
+ setIsAutoPlaying(false);
160
+ goToPreviousSlide(loopback);
161
+ },
162
+ [goToPreviousSlide],
163
+ );
164
+
165
+ // If the activeIndex props changes, update the current slide
166
+ useEffect(() => {
167
+ setCurrentIndex(activeIndex as number);
168
+ }, [activeIndex]);
169
+
170
+ // If the slide changes, with autoplay for example, trigger "onChange"
171
+ useEffect(() => {
172
+ if (!onChange) return;
173
+ onChange(currentIndex);
174
+ }, [currentIndex, onChange]);
175
+
176
+ const slideshowId = useMemo(() => id || uniqueId('slideshow'), [id]);
177
+ const slideshowSlidesId = useMemo(() => slidesId || uniqueId('slideshow-slides'), [slidesId]);
178
+
179
+ const startAutoPlay = () => {
180
+ setIsAutoPlaying(Boolean(autoPlay));
181
+ };
182
+
183
+ const stopAutoPlay = () => {
184
+ setIsAutoPlaying(false);
185
+ };
186
+
187
+ useFocusWithin({
188
+ element,
189
+ onFocusIn: stopAutoPlay,
190
+ onFocusOut: startAutoPlay,
191
+ });
192
+
193
+ // Start index and end index of visible slides.
194
+ const startIndexVisible = currentIndex * (groupBy as number);
195
+ const endIndexVisible = startIndexVisible + (groupBy as number);
196
+
197
+ return {
198
+ startIndexVisible,
199
+ endIndexVisible,
200
+ setSlideshow: setElement,
201
+ slideshow: element,
202
+ slideshowId,
203
+ slideshowSlidesId,
204
+ onPreviousClick,
205
+ onNextClick,
206
+ onPaginationClick,
207
+ isAutoPlaying,
208
+ setIsAutoPlaying,
209
+ activeIndex: currentIndex,
210
+ slidesCount,
211
+ setActiveIndex: setCurrentIndex,
212
+ };
213
+ };
@@ -0,0 +1,24 @@
1
+ import { browserDoesNotSupportHover } from '@lumx/react/utils/browserDoesNotSupportHover';
2
+
3
+ const originalMatchMedia = global.matchMedia;
4
+
5
+ describe('browserDoesNotSupportHover', () => {
6
+ afterAll(() => {
7
+ global.matchMedia = originalMatchMedia;
8
+ });
9
+
10
+ it('should return `false` on browsers that do not support matchMedia', () => {
11
+ global.matchMedia = undefined;
12
+ expect(browserDoesNotSupportHover()).toBe(false);
13
+ });
14
+
15
+ it('should return `false` on browsers that support matchMedia and does support hover', () => {
16
+ global.matchMedia = () => ({ matches: false });
17
+ expect(browserDoesNotSupportHover()).toBe(false);
18
+ });
19
+
20
+ it('should return `true` on browsers that support matchMedia and does not support hover', () => {
21
+ global.matchMedia = () => ({ matches: true });
22
+ expect(browserDoesNotSupportHover()).toBe(true);
23
+ });
24
+ });
@@ -0,0 +1,2 @@
1
+ /** Return true if the browser does not support pointer hover */
2
+ export const browserDoesNotSupportHover = (): boolean => !!window.matchMedia?.('(hover: none)').matches;
@@ -2,8 +2,6 @@ export * from '@lumx/core/js/utils';
2
2
 
3
3
  export * from './getRootClassName';
4
4
 
5
- export * from './htmlDecode';
6
-
7
5
  export * from './partitionMulti';
8
6
 
9
7
  export * from './type';
@@ -1,5 +1,5 @@
1
1
  import { Falsy } from '@lumx/react/utils/type';
2
- import { MutableRefObject, Ref } from 'react';
2
+ import { MutableRefObject } from 'react';
3
3
 
4
4
  type FnRef<T> = (value: T) => void;
5
5
 
package/types.d.ts CHANGED
@@ -1179,6 +1179,8 @@ export interface IconProps extends GenericProps {
1179
1179
  size?: IconSizes;
1180
1180
  /** Theme adapting the component to light or dark background. */
1181
1181
  theme?: Theme;
1182
+ /** Sets an alternative text on the svg. Will set an `img` role to the svg. */
1183
+ alt?: string;
1182
1184
  }
1183
1185
  /**
1184
1186
  * Icon component.
@@ -2097,6 +2099,10 @@ export interface SlideshowProps extends GenericProps {
2097
2099
  theme?: Theme;
2098
2100
  /** Callback when slide changes */
2099
2101
  onChange?(index: number): void;
2102
+ /** slideshow HTML id attribute */
2103
+ id?: string;
2104
+ /** slides wrapper HTML id attribute */
2105
+ slidesId?: string;
2100
2106
  }
2101
2107
  /**
2102
2108
  * Slideshow component.
@@ -2118,6 +2124,58 @@ export declare type SlideshowItemProps = GenericProps;
2118
2124
  * @return React element.
2119
2125
  */
2120
2126
  export declare const SlideshowItem: Comp<SlideshowItemProps, HTMLDivElement>;
2127
+ export interface UseSlideshowControlsOptions {
2128
+ /** default active index to be displayed */
2129
+ defaultActiveIndex?: number;
2130
+ /** total slides to display */
2131
+ itemsCount: number;
2132
+ /** Index of the current slide. */
2133
+ activeIndex?: number;
2134
+ /** Whether the automatic rotation of the slideshow is enabled or not. */
2135
+ autoPlay?: boolean;
2136
+ /** Whether the image has to fill its container height or not. */
2137
+ fillHeight?: boolean;
2138
+ /** Number of slides to group together. */
2139
+ groupBy?: number;
2140
+ /** Interval between each slide when automatic rotation is enabled. */
2141
+ interval?: number;
2142
+ /** Callback when slide changes */
2143
+ onChange?(index: number): void;
2144
+ /** slideshow HTML id attribute */
2145
+ id?: string;
2146
+ /** slides wrapper HTML id attribute */
2147
+ slidesId?: string;
2148
+ }
2149
+ export interface UseSlideshowControls {
2150
+ /** Index for the first visible slide, should be used when groupBy is passed in */
2151
+ startIndexVisible: number;
2152
+ /** Index for the last visible slide, should be used when groupBy is passed in */
2153
+ endIndexVisible: number;
2154
+ /** total slides to be displayed */
2155
+ slidesCount: number;
2156
+ /** callback to set */
2157
+ setSlideshow: (element: HTMLDivElement | undefined) => void;
2158
+ /** reference to the slideshow element */
2159
+ slideshow: HTMLDivElement | undefined;
2160
+ /** id to be used for the slideshow */
2161
+ slideshowId: string;
2162
+ /** id to be used for the wrapper that contains the slides */
2163
+ slideshowSlidesId: string;
2164
+ /** callback that triggers the previous slide while using the slideshow controls */
2165
+ onPreviousClick: (loopback: boolean) => void;
2166
+ /** callback that triggers the next slide while using the slideshow controls */
2167
+ onNextClick: (loopback: boolean) => void;
2168
+ /** callback that triggers a specific page while using the slideshow controls */
2169
+ onPaginationClick: (index: number) => void;
2170
+ /** whether the slideshow is autoplaying or not */
2171
+ isAutoPlaying: boolean;
2172
+ /** callback to change whether the slideshow is autoplaying or not */
2173
+ setIsAutoPlaying: (isAutoPlaying: boolean) => void;
2174
+ /** current active slide index */
2175
+ activeIndex: number;
2176
+ /** set the current index as the active one */
2177
+ setActiveIndex: (index: number) => void;
2178
+ }
2121
2179
  /**
2122
2180
  * Defines the props of the component.
2123
2181
  */
@@ -2141,14 +2199,10 @@ export interface SlideshowControlsProps extends GenericProps {
2141
2199
  /** On previous button click callback. */
2142
2200
  onPreviousClick?(loopback?: boolean): void;
2143
2201
  }
2144
- /**
2145
- * SlideshowControls component.
2146
- *
2147
- * @param props Component props.
2148
- * @param ref Component ref.
2149
- * @return React element.
2150
- */
2151
- export declare const SlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement>;
2202
+ export declare const SlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> & {
2203
+ useSlideshowControls: ({ activeIndex, groupBy, interval, autoPlay, defaultActiveIndex, onChange, itemsCount, id, slidesId, }: import("../../hooks/useSlideshowControls").UseSlideshowControlsOptions) => import("../../hooks/useSlideshowControls").UseSlideshowControls;
2204
+ useSlideshowControlsDefaultOptions: Partial<import("../../hooks/useSlideshowControls").UseSlideshowControlsOptions>;
2205
+ };
2152
2206
  /**
2153
2207
  * Defines the props of the component.
2154
2208
  */