@lumx/react 3.6.0 → 3.6.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +18 -2
- package/index.js +108 -70
- package/index.js.map +1 -1
- package/package.json +13 -15
- package/src/components/lightbox/Lightbox.stories.tsx +61 -5
- package/src/components/lightbox/Lightbox.tsx +21 -4
- package/src/components/navigation/NavigationItem.tsx +2 -2
- package/src/components/tooltip/Tooltip.stories.tsx +3 -2
- package/src/components/tooltip/Tooltip.test.tsx +32 -0
- package/src/components/tooltip/Tooltip.tsx +4 -3
- package/src/components/tooltip/useInjectTooltipRef.tsx +1 -1
- package/src/components/tooltip/useTooltipOpen.tsx +56 -38
- package/src/components/uploader/Uploader.test.tsx +1 -1
- package/src/stories/controls/image.ts +6 -0
- package/src/utils/forwardRefPolymorphic.ts +2 -2
package/package.json
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@juggle/resize-observer": "^3.2.0",
|
|
10
|
-
"@lumx/core": "^3.6.0",
|
|
11
|
-
"@lumx/icons": "^3.6.0",
|
|
10
|
+
"@lumx/core": "^3.6.1-alpha.0",
|
|
11
|
+
"@lumx/icons": "^3.6.1-alpha.0",
|
|
12
12
|
"@popperjs/core": "^2.5.4",
|
|
13
13
|
"body-scroll-lock": "^3.1.5",
|
|
14
14
|
"classnames": "^2.3.2",
|
|
@@ -30,13 +30,12 @@
|
|
|
30
30
|
"@babel/preset-typescript": "^7.18.6",
|
|
31
31
|
"@rollup/plugin-commonjs": "^15.0.0",
|
|
32
32
|
"@rollup/plugin-node-resolve": "9.0.0",
|
|
33
|
-
"@storybook/addon-a11y": "^7.
|
|
34
|
-
"@storybook/addon-essentials": "^7.
|
|
35
|
-
"@storybook/addon-interactions": "^7.
|
|
36
|
-
"@storybook/blocks": "^7.
|
|
37
|
-
"@storybook/react": "^7.
|
|
38
|
-
"@storybook/react-vite": "^7.
|
|
39
|
-
"@storybook/testing-library": "^0.0.14-next.2",
|
|
33
|
+
"@storybook/addon-a11y": "^7.6.3",
|
|
34
|
+
"@storybook/addon-essentials": "^7.6.3",
|
|
35
|
+
"@storybook/addon-interactions": "^7.6.3",
|
|
36
|
+
"@storybook/blocks": "^7.6.3",
|
|
37
|
+
"@storybook/react": "^7.6.3",
|
|
38
|
+
"@storybook/react-vite": "^7.6.3",
|
|
40
39
|
"@testing-library/jest-dom": "^5.16.4",
|
|
41
40
|
"@testing-library/react": "^12.1.2",
|
|
42
41
|
"@testing-library/user-event": "^14.4.3",
|
|
@@ -44,9 +43,9 @@
|
|
|
44
43
|
"@types/classnames": "^2.2.9",
|
|
45
44
|
"@types/jest": "^29.2.1",
|
|
46
45
|
"@types/lodash": "^4.14.149",
|
|
47
|
-
"@types/react": "^
|
|
48
|
-
"@types/react-dom": "^
|
|
49
|
-
"@types/react-is": "^
|
|
46
|
+
"@types/react": "^17.0.2",
|
|
47
|
+
"@types/react-dom": "^17.0.2",
|
|
48
|
+
"@types/react-is": "^17.0.2",
|
|
50
49
|
"autoprefixer": "^9.7.4",
|
|
51
50
|
"babel-jest": "29.1.2",
|
|
52
51
|
"babel-loader": "^8.0.6",
|
|
@@ -69,8 +68,7 @@
|
|
|
69
68
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
70
69
|
"rollup-plugin-ts-paths-resolve": "^1.3.0",
|
|
71
70
|
"rollup-plugin-typescript-paths": "^1.2.2",
|
|
72
|
-
"storybook": "^7.
|
|
73
|
-
"tsconfig-paths-webpack-plugin": "^3.3.0",
|
|
71
|
+
"storybook": "^7.6.3",
|
|
74
72
|
"typescript": "^4.1.2",
|
|
75
73
|
"vite": "^4.2.1",
|
|
76
74
|
"vite-tsconfig-paths": "^4.0.7",
|
|
@@ -115,5 +113,5 @@
|
|
|
115
113
|
"build:storybook": "storybook build"
|
|
116
114
|
},
|
|
117
115
|
"sideEffects": false,
|
|
118
|
-
"version": "3.6.0"
|
|
116
|
+
"version": "3.6.1-alpha.0"
|
|
119
117
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* eslint-disable react-hooks/rules-of-hooks,react/display-name */
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { ImageBlock, Alignment, Lightbox, Button } from '@lumx/react';
|
|
3
|
+
import { ImageBlock, Alignment, Lightbox, Button, Slideshow, SlideshowItem } from '@lumx/react';
|
|
4
4
|
import { useBooleanState } from '@lumx/react/hooks/useBooleanState';
|
|
5
|
-
import { LANDSCAPE_IMAGES } from '@lumx/react/stories/controls/image';
|
|
5
|
+
import { LANDSCAPE_IMAGES, LANDSCAPE_IMAGES_ALT } from '@lumx/react/stories/controls/image';
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
title: 'LumX components/lightbox/Lightbox',
|
|
@@ -28,9 +28,17 @@ export default {
|
|
|
28
28
|
/**
|
|
29
29
|
* Base LightBox with image block
|
|
30
30
|
*/
|
|
31
|
-
export const
|
|
31
|
+
export const Image = {
|
|
32
32
|
args: {
|
|
33
|
-
|
|
33
|
+
'aria-label': 'Fullscreen image',
|
|
34
|
+
children: (
|
|
35
|
+
<ImageBlock
|
|
36
|
+
align={Alignment.center}
|
|
37
|
+
fillHeight
|
|
38
|
+
image={LANDSCAPE_IMAGES.landscape1}
|
|
39
|
+
alt={LANDSCAPE_IMAGES_ALT.landscape1}
|
|
40
|
+
/>
|
|
41
|
+
),
|
|
34
42
|
},
|
|
35
43
|
};
|
|
36
44
|
|
|
@@ -39,7 +47,55 @@ export const ImageBlock_ = {
|
|
|
39
47
|
*/
|
|
40
48
|
export const WithCloseButton = {
|
|
41
49
|
args: {
|
|
42
|
-
...
|
|
50
|
+
...Image.args,
|
|
43
51
|
closeButtonProps: { label: 'Close' },
|
|
44
52
|
},
|
|
45
53
|
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Demo a LightBox containing an image slideshow
|
|
57
|
+
*/
|
|
58
|
+
export const ImageSlideshow = {
|
|
59
|
+
args: {
|
|
60
|
+
'aria-label': 'Fullscreen image slideshow',
|
|
61
|
+
closeButtonProps: { label: 'Close' },
|
|
62
|
+
children: (
|
|
63
|
+
<Slideshow
|
|
64
|
+
aria-label="Image slideshow"
|
|
65
|
+
theme="dark"
|
|
66
|
+
slideshowControlsProps={{
|
|
67
|
+
nextButtonProps: { label: 'Next image' },
|
|
68
|
+
previousButtonProps: { label: 'Previous image' },
|
|
69
|
+
}}
|
|
70
|
+
slideGroupLabel={(currentGroup, totalGroup) => `${currentGroup} of ${totalGroup}`}
|
|
71
|
+
>
|
|
72
|
+
<SlideshowItem>
|
|
73
|
+
<ImageBlock
|
|
74
|
+
align="center"
|
|
75
|
+
fillHeight
|
|
76
|
+
image={LANDSCAPE_IMAGES.landscape1}
|
|
77
|
+
alt={LANDSCAPE_IMAGES_ALT.landscape1}
|
|
78
|
+
/>
|
|
79
|
+
</SlideshowItem>
|
|
80
|
+
|
|
81
|
+
<SlideshowItem>
|
|
82
|
+
<ImageBlock
|
|
83
|
+
align="center"
|
|
84
|
+
fillHeight
|
|
85
|
+
image={LANDSCAPE_IMAGES.landscape2}
|
|
86
|
+
alt={LANDSCAPE_IMAGES_ALT.landscape2}
|
|
87
|
+
/>
|
|
88
|
+
</SlideshowItem>
|
|
89
|
+
|
|
90
|
+
<SlideshowItem>
|
|
91
|
+
<ImageBlock
|
|
92
|
+
align="center"
|
|
93
|
+
fillHeight
|
|
94
|
+
image={LANDSCAPE_IMAGES.landscape3}
|
|
95
|
+
alt={LANDSCAPE_IMAGES_ALT.landscape3}
|
|
96
|
+
/>
|
|
97
|
+
</SlideshowItem>
|
|
98
|
+
</Slideshow>
|
|
99
|
+
),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { forwardRef, RefObject, useRef, useEffect } from 'react';
|
|
1
|
+
import React, { forwardRef, RefObject, useRef, useEffect, AriaAttributes } from 'react';
|
|
2
2
|
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
@@ -19,7 +19,7 @@ import { useTransitionVisibility } from '@lumx/react/hooks/useTransitionVisibili
|
|
|
19
19
|
/**
|
|
20
20
|
* Defines the props of the component.
|
|
21
21
|
*/
|
|
22
|
-
export interface LightboxProps extends GenericProps, HasTheme {
|
|
22
|
+
export interface LightboxProps extends GenericProps, HasTheme, Pick<AriaAttributes, 'aria-label' | 'aria-labelledby'> {
|
|
23
23
|
/** Props to pass to the close button (minus those already set by the Lightbox props). */
|
|
24
24
|
closeButtonProps?: Pick<IconButtonProps, 'label'> &
|
|
25
25
|
Omit<IconButtonProps, 'label' | 'onClick' | 'icon' | 'emphasis' | 'color'>;
|
|
@@ -27,6 +27,8 @@ export interface LightboxProps extends GenericProps, HasTheme {
|
|
|
27
27
|
isOpen?: boolean;
|
|
28
28
|
/** Reference to the element that triggered modal opening to set focus on. */
|
|
29
29
|
parentElement: RefObject<any>;
|
|
30
|
+
/** Reference to the element that should get the focus when the lightbox opens. By default, the close button or the lightbox itself will take focus. */
|
|
31
|
+
focusElement?: RefObject<HTMLElement>;
|
|
30
32
|
/** Whether to keep the dialog open on clickaway or escape press. */
|
|
31
33
|
preventAutoClose?: boolean;
|
|
32
34
|
/** Z-axis position. */
|
|
@@ -54,13 +56,17 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
|
54
56
|
*/
|
|
55
57
|
export const Lightbox: Comp<LightboxProps, HTMLDivElement> = forwardRef((props, ref) => {
|
|
56
58
|
const {
|
|
57
|
-
|
|
59
|
+
'aria-labelledby': propAriaLabelledBy,
|
|
60
|
+
ariaLabelledBy = propAriaLabelledBy,
|
|
61
|
+
'aria-label': propAriaLabel,
|
|
62
|
+
ariaLabel = propAriaLabel,
|
|
58
63
|
children,
|
|
59
64
|
className,
|
|
60
65
|
closeButtonProps,
|
|
61
66
|
isOpen,
|
|
62
67
|
onClose,
|
|
63
68
|
parentElement,
|
|
69
|
+
focusElement,
|
|
64
70
|
preventAutoClose,
|
|
65
71
|
theme,
|
|
66
72
|
zIndex,
|
|
@@ -75,6 +81,8 @@ export const Lightbox: Comp<LightboxProps, HTMLDivElement> = forwardRef((props,
|
|
|
75
81
|
const childrenRef = useRef<any>(null);
|
|
76
82
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
77
83
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
84
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
85
|
+
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
|
78
86
|
|
|
79
87
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
80
88
|
useDisableBodyScroll(isOpen && wrapperRef.current);
|
|
@@ -84,7 +92,12 @@ export const Lightbox: Comp<LightboxProps, HTMLDivElement> = forwardRef((props,
|
|
|
84
92
|
|
|
85
93
|
// Handle focus trap.
|
|
86
94
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
87
|
-
useFocusTrap(
|
|
95
|
+
useFocusTrap(
|
|
96
|
+
// Focus trap zone
|
|
97
|
+
isOpen && wrapperRef.current,
|
|
98
|
+
// Focus element (fallback on close button and then on the dialog)
|
|
99
|
+
focusElement?.current || closeButtonRef.current || wrapperRef.current,
|
|
100
|
+
);
|
|
88
101
|
|
|
89
102
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
90
103
|
const previousOpen = useRef(isOpen);
|
|
@@ -116,7 +129,10 @@ export const Lightbox: Comp<LightboxProps, HTMLDivElement> = forwardRef((props,
|
|
|
116
129
|
ref={mergeRefs(ref, wrapperRef)}
|
|
117
130
|
{...forwardedProps}
|
|
118
131
|
aria-label={ariaLabel}
|
|
132
|
+
aria-labelledby={ariaLabelledBy}
|
|
119
133
|
aria-modal="true"
|
|
134
|
+
role="dialog"
|
|
135
|
+
tabIndex={-1}
|
|
120
136
|
className={classNames(
|
|
121
137
|
className,
|
|
122
138
|
handleBasicClasses({
|
|
@@ -131,6 +147,7 @@ export const Lightbox: Comp<LightboxProps, HTMLDivElement> = forwardRef((props,
|
|
|
131
147
|
{closeButtonProps && (
|
|
132
148
|
<IconButton
|
|
133
149
|
{...closeButtonProps}
|
|
150
|
+
ref={closeButtonRef}
|
|
134
151
|
className={`${CLASSNAME}__close`}
|
|
135
152
|
color={ColorPalette.light}
|
|
136
153
|
emphasis={Emphasis.low}
|
|
@@ -7,7 +7,7 @@ import { forwardRefPolymorphic } from '@lumx/react/utils/forwardRefPolymorphic';
|
|
|
7
7
|
import { ThemeContext } from '@lumx/react/utils/ThemeContext';
|
|
8
8
|
|
|
9
9
|
type BaseNavigationItemProps = {
|
|
10
|
-
|
|
10
|
+
/** Icon (SVG path). */
|
|
11
11
|
icon?: string;
|
|
12
12
|
/** Label content. */
|
|
13
13
|
label: ReactNode;
|
|
@@ -65,7 +65,7 @@ export const NavigationItem = Object.assign(
|
|
|
65
65
|
prefix: `${CLASSNAME}__link`,
|
|
66
66
|
isSelected: isCurrentPage,
|
|
67
67
|
})}
|
|
68
|
-
ref={ref}
|
|
68
|
+
ref={ref as React.Ref<any>}
|
|
69
69
|
aria-current={isCurrentPage ? 'page' : undefined}
|
|
70
70
|
{...buttonProps}
|
|
71
71
|
{...forwardedProps}
|
|
@@ -56,12 +56,13 @@ export const TooltipWithDropdown = (props: any) => {
|
|
|
56
56
|
const [isOpen, setOpen] = useState(false);
|
|
57
57
|
return (
|
|
58
58
|
<>
|
|
59
|
-
<
|
|
59
|
+
<br />
|
|
60
|
+
<Tooltip label={!isOpen && 'Tooltip'} {...props} placement="top">
|
|
60
61
|
<Button ref={setButton} onClick={() => setOpen((o) => !o)}>
|
|
61
62
|
Anchor
|
|
62
63
|
</Button>
|
|
63
64
|
</Tooltip>
|
|
64
|
-
<Dropdown anchorRef={{ current: button }} isOpen={isOpen}>
|
|
65
|
+
<Dropdown anchorRef={{ current: button }} isOpen={isOpen} onClose={() => setOpen(false)}>
|
|
65
66
|
Dropdown
|
|
66
67
|
</Dropdown>
|
|
67
68
|
</>
|
|
@@ -125,6 +125,38 @@ describe(`<${Tooltip.displayName}>`, () => {
|
|
|
125
125
|
});
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
it('should activate on hover anchor and then tooltip', async () => {
|
|
129
|
+
let { tooltip } = await setup({
|
|
130
|
+
label: 'Tooltip label',
|
|
131
|
+
children: <Button>Anchor</Button>,
|
|
132
|
+
forceOpen: false,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(tooltip).not.toBeInTheDocument();
|
|
136
|
+
|
|
137
|
+
// Hover anchor button
|
|
138
|
+
const button = getByClassName(document.body, Button.className as string);
|
|
139
|
+
await userEvent.hover(button);
|
|
140
|
+
|
|
141
|
+
// Tooltip opened
|
|
142
|
+
tooltip = await findByClassName(document.body, CLASSNAME);
|
|
143
|
+
expect(tooltip).toBeInTheDocument();
|
|
144
|
+
expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
|
|
145
|
+
|
|
146
|
+
// Hover tooltip
|
|
147
|
+
await userEvent.hover(tooltip);
|
|
148
|
+
expect(tooltip).toBeInTheDocument();
|
|
149
|
+
expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
|
|
150
|
+
|
|
151
|
+
// Un-hover tooltip
|
|
152
|
+
userEvent.unhover(tooltip);
|
|
153
|
+
await waitFor(() => {
|
|
154
|
+
expect(button).not.toHaveFocus();
|
|
155
|
+
// Tooltip closed
|
|
156
|
+
expect(tooltip).not.toBeInTheDocument();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
128
160
|
it('should activate on anchor focus', async () => {
|
|
129
161
|
let { tooltip } = await setup({
|
|
130
162
|
label: 'Tooltip label',
|
|
@@ -85,8 +85,9 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
const position = attributes?.popper?.['data-popper-placement'] ?? placement;
|
|
88
|
-
const isOpen = useTooltipOpen(delay, anchorElement)
|
|
89
|
-
const
|
|
88
|
+
const { isOpen: isActivated, onPopperMount } = useTooltipOpen(delay, anchorElement);
|
|
89
|
+
const isOpen = isActivated || forceOpen;
|
|
90
|
+
const wrappedChildren = useInjectTooltipRef(children, setAnchorElement, isOpen, id);
|
|
90
91
|
|
|
91
92
|
return (
|
|
92
93
|
<>
|
|
@@ -94,7 +95,7 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
|
|
|
94
95
|
{isOpen &&
|
|
95
96
|
createPortal(
|
|
96
97
|
<div
|
|
97
|
-
ref={mergeRefs(ref, setPopperElement)}
|
|
98
|
+
ref={mergeRefs(ref, setPopperElement, onPopperMount)}
|
|
98
99
|
{...forwardedProps}
|
|
99
100
|
id={id}
|
|
100
101
|
role="tooltip"
|
|
@@ -18,7 +18,7 @@ import { mergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
|
18
18
|
export const useInjectTooltipRef = (
|
|
19
19
|
children: ReactNode,
|
|
20
20
|
setAnchorElement: (e: HTMLDivElement) => void,
|
|
21
|
-
isOpen: boolean,
|
|
21
|
+
isOpen: boolean | undefined,
|
|
22
22
|
id: string,
|
|
23
23
|
): ReactNode => {
|
|
24
24
|
return useMemo(() => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { MutableRefObject, useEffect, useRef, useState } from 'react';
|
|
3
2
|
import { browserDoesNotSupportHover } from '@lumx/react/utils/browserDoesNotSupportHover';
|
|
4
3
|
import { TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/constants';
|
|
4
|
+
import { useCallbackOnEscape } from '@lumx/react/hooks/useCallbackOnEscape';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Hook controlling tooltip visibility using mouse hover the anchor and delay.
|
|
@@ -10,9 +10,15 @@ import { TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/const
|
|
|
10
10
|
* @param anchorElement Tooltip anchor element.
|
|
11
11
|
* @return whether or not to show the tooltip.
|
|
12
12
|
*/
|
|
13
|
-
export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLElement | null)
|
|
13
|
+
export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLElement | null) {
|
|
14
14
|
const [isOpen, setIsOpen] = useState(false);
|
|
15
15
|
|
|
16
|
+
const onPopperMount = useRef<any>(null) as MutableRefObject<(elem: HTMLElement | null) => void>;
|
|
17
|
+
|
|
18
|
+
// Global close on escape
|
|
19
|
+
const [closeCallback, setCloseCallback] = useState<undefined | (() => void)>(undefined);
|
|
20
|
+
useCallbackOnEscape(closeCallback);
|
|
21
|
+
|
|
16
22
|
useEffect(() => {
|
|
17
23
|
if (!anchorElement) {
|
|
18
24
|
return undefined;
|
|
@@ -45,44 +51,58 @@ export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLEle
|
|
|
45
51
|
};
|
|
46
52
|
|
|
47
53
|
// Close or cancel opening of tooltip
|
|
48
|
-
const
|
|
54
|
+
const getClose = (overrideDelay = closeDelay) => {
|
|
49
55
|
if (!shouldOpen && !timer) return;
|
|
50
56
|
shouldOpen = false;
|
|
51
57
|
deferUpdate(overrideDelay);
|
|
52
58
|
};
|
|
53
|
-
const
|
|
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
|
-
};
|
|
59
|
+
const close = () => getClose(closeDelay);
|
|
60
|
+
const closeImmediately = () => getClose(0);
|
|
61
|
+
setCloseCallback(() => closeImmediately);
|
|
74
62
|
|
|
75
63
|
// Adapt event to browsers with or without `hover` support.
|
|
76
|
-
const events: Array<[Node, Event['type'], any]> =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
64
|
+
const events: Array<[Node, Event['type'], any]> = [];
|
|
65
|
+
if (hoverNotSupported) {
|
|
66
|
+
/**
|
|
67
|
+
* Handle touchend event
|
|
68
|
+
* If end comes before the open delay => cancel tooltip (close immediate).
|
|
69
|
+
* Else if end comes after the open delay => tooltip takes priority, the anchor's default touch end event is prevented.
|
|
70
|
+
*/
|
|
71
|
+
const longPressEnd = (evt: Event) => {
|
|
72
|
+
if (!openStartTime) return;
|
|
73
|
+
if (Date.now() - openStartTime >= openDelay) {
|
|
74
|
+
// Tooltip take priority, event prevented.
|
|
75
|
+
evt.stopPropagation();
|
|
76
|
+
evt.preventDefault();
|
|
77
|
+
anchorElement.focus();
|
|
78
|
+
// Close with delay.
|
|
79
|
+
close();
|
|
80
|
+
} else {
|
|
81
|
+
// Close immediately.
|
|
82
|
+
closeImmediately();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
events.push(
|
|
87
|
+
[anchorElement, hasTouch ? 'touchstart' : 'mousedown', open],
|
|
88
|
+
[anchorElement, hasTouch ? 'touchend' : 'mouseup', longPressEnd],
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
events.push(
|
|
92
|
+
[anchorElement, 'mouseenter', open],
|
|
93
|
+
[anchorElement, 'mouseleave', close],
|
|
94
|
+
[anchorElement, 'mouseup', closeImmediately],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
onPopperMount.current = (popperElement: HTMLElement | null) => {
|
|
98
|
+
if (!popperElement) return;
|
|
99
|
+
// Popper element hover
|
|
100
|
+
popperElement.addEventListener('mouseenter', open);
|
|
101
|
+
popperElement.addEventListener('mouseleave', close);
|
|
102
|
+
// Add to event list to remove on unmount
|
|
103
|
+
events.push([popperElement, 'mouseenter', open], [popperElement, 'mouseleave', close]);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
86
106
|
|
|
87
107
|
// Events always applied no matter the browser:.
|
|
88
108
|
events.push(
|
|
@@ -90,8 +110,6 @@ export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLEle
|
|
|
90
110
|
[anchorElement, 'focusin', open],
|
|
91
111
|
// Close on lost focus.
|
|
92
112
|
[anchorElement, 'focusout', closeImmediately],
|
|
93
|
-
// Close on ESC keydown
|
|
94
|
-
[anchorElement, 'keydown', onEscapePressed(closeImmediately)],
|
|
95
113
|
);
|
|
96
114
|
|
|
97
115
|
// Attach events
|
|
@@ -109,5 +127,5 @@ export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLEle
|
|
|
109
127
|
};
|
|
110
128
|
}, [anchorElement, delay]);
|
|
111
129
|
|
|
112
|
-
return isOpen;
|
|
130
|
+
return { isOpen, onPopperMount: onPopperMount.current };
|
|
113
131
|
}
|
|
@@ -72,7 +72,7 @@ describe(`<${Uploader.displayName}>`, () => {
|
|
|
72
72
|
it('should render file input', () => {
|
|
73
73
|
const label = 'Label';
|
|
74
74
|
const accept = '*';
|
|
75
|
-
const { uploader } = setup({ label, fileInputProps: { accept } });
|
|
75
|
+
const { uploader } = setup({ label, fileInputProps: { accept } as any });
|
|
76
76
|
|
|
77
77
|
expect(uploader.tagName).toBe('LABEL');
|
|
78
78
|
expect(uploader).toHaveTextContent(label);
|
|
@@ -22,6 +22,12 @@ export const SQUARE_IMAGES = { square1, square2 };
|
|
|
22
22
|
export const SVG_IMAGES = { defaultSvg };
|
|
23
23
|
export const EMPTY_IMAGES = { emptyImage };
|
|
24
24
|
export const LANDSCAPE_IMAGES = { landscape1, landscape1s200, landscape2, landscape3 };
|
|
25
|
+
export const LANDSCAPE_IMAGES_ALT: { [key in keyof typeof LANDSCAPE_IMAGES]: string } = {
|
|
26
|
+
landscape1: 'A majestic snowy mountain range with a peak covered in glistening snow',
|
|
27
|
+
landscape1s200: 'A majestic snowy mountain range with a peak covered in glistening snow',
|
|
28
|
+
landscape2: 'A colorful rack of shirts displaying various hues and styles',
|
|
29
|
+
landscape3: 'An open book resting on a table, ready to be explored and read',
|
|
30
|
+
};
|
|
25
31
|
export const PORTRAIT_IMAGES = { portrait1, portrait1s200, portrait2, portrait3 };
|
|
26
32
|
export const IMAGES = {
|
|
27
33
|
...LANDSCAPE_IMAGES,
|
|
@@ -4,6 +4,6 @@ import { ComponentRef } from '@lumx/react/utils/type';
|
|
|
4
4
|
/** Same as `React.forwardRef` but inferring Ref type from the `as` prop. */
|
|
5
5
|
export function forwardRefPolymorphic<E extends ElementType, P extends { as?: E }>(
|
|
6
6
|
render: (props: P, ref: ComponentRef<E>) => React.ReactElement | null,
|
|
7
|
-
) {
|
|
8
|
-
return React.forwardRef(render as any) as
|
|
7
|
+
): (props: P & { ref?: ComponentRef<E> }) => React.ReactElement | null {
|
|
8
|
+
return React.forwardRef(render as any) as any;
|
|
9
9
|
}
|