@lumx/react 3.9.2-alpha.0 → 3.9.2-alpha.10
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/_internal/ClickAwayProvider.js +2 -2
- package/_internal/ClickAwayProvider.js.map +1 -1
- package/index.d.ts +7 -1
- package/index.js +1726 -1672
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/date-picker/DatePickerControlled.tsx +52 -30
- package/src/components/image-lightbox/ImageLightbox.test.tsx +8 -1
- package/src/components/image-lightbox/useImageLightbox.tsx +20 -13
- package/src/components/popover/Popover.tsx +2 -2
- package/src/components/popover/constants.ts +5 -0
- package/src/components/popover/usePopoverStyle.tsx +1 -10
- package/src/components/text-field/TextField.tsx +3 -1
- package/src/components/tooltip/Tooltip.stories.tsx +5 -0
- package/src/components/tooltip/Tooltip.test.tsx +178 -70
- package/src/components/tooltip/Tooltip.tsx +39 -10
- package/src/components/tooltip/constants.ts +8 -0
- package/src/components/tooltip/useInjectTooltipRef.tsx +27 -21
- package/src/components/tooltip/useTooltipOpen.tsx +6 -3
- package/src/constants.ts +5 -0
- package/src/hooks/usePopper.ts +9 -0
- package/src/stories/generated/TextField/Demos.stories.tsx +1 -0
- package/src/utils/type.ts +0 -1
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
2
|
import React, { forwardRef, ReactNode, useState } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
|
-
import { usePopper } from 'react-popper';
|
|
5
4
|
|
|
6
5
|
import classNames from 'classnames';
|
|
7
6
|
|
|
8
7
|
import { DOCUMENT } from '@lumx/react/constants';
|
|
9
8
|
import { Comp, GenericProps, HasCloseMode } from '@lumx/react/utils/type';
|
|
10
9
|
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
11
|
-
import {
|
|
10
|
+
import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
12
11
|
import { Placement } from '@lumx/react/components/popover';
|
|
13
12
|
import { TooltipContextProvider } from '@lumx/react/components/tooltip/context';
|
|
14
13
|
import { useId } from '@lumx/react/hooks/useId';
|
|
14
|
+
import { usePopper } from '@lumx/react/hooks/usePopper';
|
|
15
15
|
|
|
16
|
+
import { ARIA_LINK_MODES, TOOLTIP_ZINDEX } from '@lumx/react/components/tooltip/constants';
|
|
16
17
|
import { useInjectTooltipRef } from './useInjectTooltipRef';
|
|
17
18
|
import { useTooltipOpen } from './useTooltipOpen';
|
|
18
19
|
|
|
@@ -33,6 +34,8 @@ export interface TooltipProps extends GenericProps, HasCloseMode {
|
|
|
33
34
|
label?: string | null | false;
|
|
34
35
|
/** Placement of the tooltip relative to the anchor. */
|
|
35
36
|
placement?: TooltipPlacement;
|
|
37
|
+
/** Choose how the tooltip text should link to the anchor */
|
|
38
|
+
ariaLinkMode?: (typeof ARIA_LINK_MODES)[number];
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
/**
|
|
@@ -50,7 +53,9 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
|
50
53
|
*/
|
|
51
54
|
const DEFAULT_PROPS: Partial<TooltipProps> = {
|
|
52
55
|
placement: Placement.BOTTOM,
|
|
53
|
-
closeMode: '
|
|
56
|
+
closeMode: 'unmount',
|
|
57
|
+
ariaLinkMode: 'aria-describedby',
|
|
58
|
+
zIndex: TOOLTIP_ZINDEX,
|
|
54
59
|
};
|
|
55
60
|
|
|
56
61
|
/**
|
|
@@ -66,9 +71,20 @@ const ARROW_SIZE = 8;
|
|
|
66
71
|
* @return React element.
|
|
67
72
|
*/
|
|
68
73
|
export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, ref) => {
|
|
69
|
-
const {
|
|
74
|
+
const {
|
|
75
|
+
label,
|
|
76
|
+
children,
|
|
77
|
+
className,
|
|
78
|
+
delay,
|
|
79
|
+
placement,
|
|
80
|
+
forceOpen,
|
|
81
|
+
closeMode,
|
|
82
|
+
ariaLinkMode,
|
|
83
|
+
zIndex,
|
|
84
|
+
...forwardedProps
|
|
85
|
+
} = props;
|
|
70
86
|
// Disable in SSR.
|
|
71
|
-
if (!DOCUMENT
|
|
87
|
+
if (!DOCUMENT) {
|
|
72
88
|
return <>{children}</>;
|
|
73
89
|
}
|
|
74
90
|
|
|
@@ -76,7 +92,7 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
|
|
|
76
92
|
|
|
77
93
|
const [popperElement, setPopperElement] = useState<null | HTMLElement>(null);
|
|
78
94
|
const [anchorElement, setAnchorElement] = useState<null | HTMLElement>(null);
|
|
79
|
-
const { styles, attributes } = usePopper(anchorElement, popperElement, {
|
|
95
|
+
const { styles, attributes, update } = usePopper(anchorElement, popperElement, {
|
|
80
96
|
placement,
|
|
81
97
|
modifiers: [
|
|
82
98
|
{
|
|
@@ -89,18 +105,31 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
|
|
|
89
105
|
const position = attributes?.popper?.['data-popper-placement'] ?? placement;
|
|
90
106
|
const { isOpen: isActivated, onPopperMount } = useTooltipOpen(delay, anchorElement);
|
|
91
107
|
const isOpen = (isActivated || forceOpen) && !!label;
|
|
92
|
-
const isMounted = isOpen || closeMode === 'hide';
|
|
93
|
-
const wrappedChildren = useInjectTooltipRef(
|
|
108
|
+
const isMounted = !!label && (isOpen || closeMode === 'hide');
|
|
109
|
+
const wrappedChildren = useInjectTooltipRef({
|
|
110
|
+
children,
|
|
111
|
+
setAnchorElement,
|
|
112
|
+
isMounted,
|
|
113
|
+
id,
|
|
114
|
+
label,
|
|
115
|
+
ariaLinkMode: ariaLinkMode as any,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Update on open
|
|
119
|
+
React.useEffect(() => {
|
|
120
|
+
if (isOpen) update?.();
|
|
121
|
+
}, [isOpen, update]);
|
|
94
122
|
|
|
95
123
|
const labelLines = label ? label.split('\n') : [];
|
|
96
124
|
|
|
125
|
+
const tooltipRef = useMergeRefs(ref, setPopperElement, onPopperMount);
|
|
97
126
|
return (
|
|
98
127
|
<>
|
|
99
128
|
<TooltipContextProvider>{wrappedChildren}</TooltipContextProvider>
|
|
100
129
|
{isMounted &&
|
|
101
130
|
createPortal(
|
|
102
131
|
<div
|
|
103
|
-
ref={
|
|
132
|
+
ref={tooltipRef}
|
|
104
133
|
{...forwardedProps}
|
|
105
134
|
id={id}
|
|
106
135
|
role="tooltip"
|
|
@@ -113,7 +142,7 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
|
|
|
113
142
|
hidden: !isOpen && closeMode === 'hide',
|
|
114
143
|
}),
|
|
115
144
|
)}
|
|
116
|
-
style={styles.popper}
|
|
145
|
+
style={{ ...styles.popper, zIndex }}
|
|
117
146
|
{...attributes.popper}
|
|
118
147
|
>
|
|
119
148
|
<div className={`${CLASSNAME}__arrow`} />
|
|
@@ -2,27 +2,30 @@ import React, { cloneElement, ReactNode, useMemo } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { mergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
4
4
|
|
|
5
|
+
interface Options {
|
|
6
|
+
/** Original tooltip anchor */
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
/** Set tooltip anchor element */
|
|
9
|
+
setAnchorElement: (e: HTMLDivElement) => void;
|
|
10
|
+
/** Whether the tooltip is open or not */
|
|
11
|
+
isMounted: boolean | undefined;
|
|
12
|
+
/** Tooltip id */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Tooltip label*/
|
|
15
|
+
label?: string | null | false;
|
|
16
|
+
/** Choose how the tooltip text should link to the anchor */
|
|
17
|
+
ariaLinkMode: 'aria-describedby' | 'aria-labelledby';
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
/**
|
|
6
21
|
* Add ref and ARIA attribute(s) in tooltip children or wrapped children.
|
|
7
22
|
* Button, IconButton, Icon and React HTML elements don't need to be wrapped but any other kind of children (array, fragment, custom components)
|
|
8
23
|
* will be wrapped in a <span>.
|
|
9
|
-
*
|
|
10
|
-
* @param children Original tooltip anchor.
|
|
11
|
-
* @param setAnchorElement Set tooltip anchor element.
|
|
12
|
-
* @param isOpen Whether the tooltip is open or not.
|
|
13
|
-
* @param id Tooltip id.
|
|
14
|
-
* @param label Tooltip label.
|
|
15
|
-
* @return tooltip anchor.
|
|
16
24
|
*/
|
|
17
|
-
export const useInjectTooltipRef = (
|
|
18
|
-
children
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
id: string,
|
|
22
|
-
label?: string | null | false,
|
|
23
|
-
): ReactNode => {
|
|
24
|
-
// Only add description when open
|
|
25
|
-
const describedBy = isOpen ? id : undefined;
|
|
25
|
+
export const useInjectTooltipRef = (options: Options): ReactNode => {
|
|
26
|
+
const { children, setAnchorElement, isMounted, id, label, ariaLinkMode } = options;
|
|
27
|
+
// Only add link when mounted
|
|
28
|
+
const linkId = isMounted ? id : undefined;
|
|
26
29
|
|
|
27
30
|
return useMemo(() => {
|
|
28
31
|
if (!label) return children;
|
|
@@ -32,18 +35,21 @@ export const useInjectTooltipRef = (
|
|
|
32
35
|
const ref = mergeRefs((children as any).ref, setAnchorElement);
|
|
33
36
|
const props = { ...children.props, ref };
|
|
34
37
|
|
|
35
|
-
//
|
|
36
|
-
if (label !== props['aria-label']
|
|
37
|
-
props[
|
|
38
|
+
// Do not add label/description if the tooltip label is already in aria-label
|
|
39
|
+
if (linkId && label !== props['aria-label']) {
|
|
40
|
+
if (props[ariaLinkMode]) props[ariaLinkMode] += ' ';
|
|
41
|
+
else props[ariaLinkMode] = '';
|
|
42
|
+
props[ariaLinkMode] += linkId;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
return cloneElement(children, props);
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
const aria = linkId ? { [ariaLinkMode]: linkId } : undefined;
|
|
43
49
|
return (
|
|
44
|
-
<div className="lumx-tooltip-anchor-wrapper" ref={setAnchorElement} aria
|
|
50
|
+
<div className="lumx-tooltip-anchor-wrapper" ref={setAnchorElement} {...aria}>
|
|
45
51
|
{children}
|
|
46
52
|
</div>
|
|
47
53
|
);
|
|
48
|
-
}, [children, setAnchorElement,
|
|
54
|
+
}, [label, children, setAnchorElement, linkId, ariaLinkMode]);
|
|
49
55
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MutableRefObject, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { browserDoesNotSupportHover } from '@lumx/react/utils/browserDoesNotSupportHover';
|
|
3
|
-
import { TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/constants';
|
|
3
|
+
import { IS_BROWSER, TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/constants';
|
|
4
4
|
import { useCallbackOnEscape } from '@lumx/react/hooks/useCallbackOnEscape';
|
|
5
5
|
import { isFocusVisible } from '@lumx/react/utils/isFocusVisible';
|
|
6
6
|
|
|
@@ -31,9 +31,12 @@ export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLEle
|
|
|
31
31
|
// Run timer to defer updating the isOpen state.
|
|
32
32
|
const deferUpdate = (duration: number) => {
|
|
33
33
|
if (timer) clearTimeout(timer);
|
|
34
|
-
|
|
34
|
+
const update = () => {
|
|
35
35
|
setIsOpen(!!shouldOpen);
|
|
36
|
-
}
|
|
36
|
+
};
|
|
37
|
+
// Skip timeout in fake browsers
|
|
38
|
+
if (!IS_BROWSER) update();
|
|
39
|
+
else timer = setTimeout(update, duration) as any;
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
const hoverNotSupported = browserDoesNotSupportHover();
|
package/src/constants.ts
CHANGED
|
@@ -15,3 +15,8 @@ export const WINDOW = typeof window !== 'undefined' ? window : undefined;
|
|
|
15
15
|
* Optional global `document` instance (not defined when running SSR).
|
|
16
16
|
*/
|
|
17
17
|
export const DOCUMENT = typeof document !== 'undefined' ? document : undefined;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if we are running in a true browser
|
|
21
|
+
*/
|
|
22
|
+
export const IS_BROWSER = typeof navigator !== 'undefined' && !navigator.userAgent.includes('jsdom');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { usePopper as usePopperHook } from 'react-popper';
|
|
2
|
+
import { IS_BROWSER } from '@lumx/react/constants';
|
|
3
|
+
|
|
4
|
+
/** Stub usePopper for use outside of browsers */
|
|
5
|
+
const useStubPopper: typeof usePopperHook = (_a, _p, { placement }: any) =>
|
|
6
|
+
({ attributes: { popper: { 'data-popper-placement': placement } }, styles: {} }) as any;
|
|
7
|
+
|
|
8
|
+
/** Switch hook implementation between environment */
|
|
9
|
+
export const usePopper: typeof usePopperHook = IS_BROWSER ? usePopperHook : useStubPopper;
|
|
@@ -10,6 +10,7 @@ export { App as Disabled } from './disabled';
|
|
|
10
10
|
export { App as Helper } from './helper';
|
|
11
11
|
export { App as Icon } from './icon';
|
|
12
12
|
export { App as Invalid } from './invalid';
|
|
13
|
+
export { App as Number } from './number';
|
|
13
14
|
export { App as Placeholder } from './placeholder';
|
|
14
15
|
export { App as Required } from './required';
|
|
15
16
|
export { App as TextAreaInvalid } from './text-area-invalid';
|