@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.
- package/esm/_internal/AutocompleteMultiple.js.map +1 -1
- package/esm/_internal/DatePickerField.js.map +1 -1
- package/esm/_internal/Icon2.js +5 -2
- package/esm/_internal/Icon2.js.map +1 -1
- package/esm/_internal/Lightbox2.js +6 -4
- package/esm/_internal/Lightbox2.js.map +1 -1
- package/esm/_internal/List2.js.map +1 -1
- package/esm/_internal/Notification2.js +2 -1
- package/esm/_internal/Notification2.js.map +1 -1
- package/esm/_internal/SelectMultiple.js.map +1 -1
- package/esm/_internal/SlideshowControls.js +234 -94
- package/esm/_internal/SlideshowControls.js.map +1 -1
- package/esm/_internal/Thumbnail2.js +1 -1
- package/esm/_internal/Thumbnail2.js.map +1 -1
- package/esm/_internal/Tooltip2.js +110 -113
- package/esm/_internal/Tooltip2.js.map +1 -1
- package/esm/_internal/autocomplete.js +0 -1
- package/esm/_internal/autocomplete.js.map +1 -1
- package/esm/_internal/button.js +0 -1
- package/esm/_internal/button.js.map +1 -1
- package/esm/_internal/constants.js.map +1 -1
- package/esm/_internal/date-picker.js +0 -1
- package/esm/_internal/date-picker.js.map +1 -1
- package/esm/_internal/expansion-panel.js +0 -1
- package/esm/_internal/expansion-panel.js.map +1 -1
- package/esm/_internal/getRootClassName.js +19 -1
- package/esm/_internal/getRootClassName.js.map +1 -1
- package/esm/_internal/lightbox.js +0 -1
- package/esm/_internal/lightbox.js.map +1 -1
- package/esm/_internal/mergeRefs.js.map +1 -1
- package/esm/_internal/select.js +0 -1
- package/esm/_internal/select.js.map +1 -1
- package/esm/_internal/side-navigation.js +0 -1
- package/esm/_internal/side-navigation.js.map +1 -1
- package/esm/_internal/slideshow.js +1 -1
- package/esm/_internal/text-field.js +0 -1
- package/esm/_internal/text-field.js.map +1 -1
- package/esm/_internal/tooltip.js +0 -1
- package/esm/_internal/tooltip.js.map +1 -1
- package/esm/_internal/useRovingTabIndex.js.map +1 -1
- package/esm/index.js +1 -1
- package/package.json +4 -4
- package/src/components/alert-dialog/AlertDialog.test.tsx +1 -0
- package/src/components/autocomplete/AutocompleteMultiple.tsx +3 -1
- package/src/components/button/__snapshots__/IconButton.test.tsx.snap +0 -5
- package/src/components/icon/Icon.tsx +6 -2
- package/src/components/image-block/ImageBlock.stories.tsx +1 -2
- package/src/components/lightbox/Lightbox.stories.tsx +1 -0
- package/src/components/lightbox/Lightbox.tsx +5 -3
- package/src/components/notification/Notification.tsx +1 -0
- package/src/components/select/SelectMultiple.tsx +0 -1
- package/src/components/slideshow/Slideshow.stories.tsx +1 -1
- package/src/components/slideshow/Slideshow.tsx +76 -112
- package/src/components/slideshow/SlideshowControls.stories.tsx +18 -12
- package/src/components/slideshow/SlideshowControls.tsx +11 -7
- package/src/components/slideshow/SlideshowItem.tsx +4 -1
- package/src/components/slideshow/__snapshots__/Slideshow.test.tsx.snap +52 -17
- package/src/components/tabs/state.ts +0 -1
- package/src/components/thumbnail/Thumbnail.stories.tsx +25 -1
- package/src/components/thumbnail/Thumbnail.test.tsx +9 -1
- package/src/components/thumbnail/Thumbnail.tsx +3 -0
- package/src/components/thumbnail/__snapshots__/Thumbnail.test.tsx.snap +26 -0
- package/src/components/tooltip/Tooltip.tsx +1 -2
- package/src/components/tooltip/useTooltipOpen.tsx +90 -91
- package/src/constants.ts +7 -1
- package/src/hooks/useFocusWithin.ts +33 -0
- package/src/hooks/useSlideshowControls.ts +213 -0
- package/src/utils/browserDoesNotSupportHover.test.js +24 -0
- package/src/utils/browserDoesNotSupportHover.ts +2 -0
- package/src/utils/index.tsx +0 -2
- package/src/utils/mergeRefs.ts +1 -1
- package/types.d.ts +62 -8
- 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
|
|
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 {
|
|
2
|
-
import { useEffect,
|
|
3
|
-
import
|
|
4
|
-
import
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
// Detach events.
|
|
103
|
+
for (const [node, eventType, evenHandler] of events) {
|
|
104
|
+
node.removeEventListener(eventType, evenHandler);
|
|
105
|
+
}
|
|
106
|
+
closeImmediately();
|
|
108
107
|
};
|
|
109
|
-
}, [anchorElement, delay
|
|
108
|
+
}, [anchorElement, delay]);
|
|
110
109
|
|
|
111
110
|
return isOpen;
|
|
112
111
|
}
|
package/src/constants.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
export {
|
|
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
|
+
});
|
package/src/utils/index.tsx
CHANGED
package/src/utils/mergeRefs.ts
CHANGED
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
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
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
|
*/
|