@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.
@@ -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 { mergeRefs } from '@lumx/react/utils/mergeRefs';
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: 'hide',
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 { label, children, className, delay, placement, forceOpen, closeMode, ...forwardedProps } = props;
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 && closeMode === 'unmount') {
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(children, setAnchorElement, isMounted, id, label);
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={mergeRefs(ref, setPopperElement, onPopperMount)}
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`} />
@@ -0,0 +1,8 @@
1
+ import { POPOVER_ZINDEX } from '../popover/constants';
2
+
3
+ export const ARIA_LINK_MODES = ['aria-describedby', 'aria-labelledby'] as const;
4
+
5
+ /**
6
+ * Make sure tooltip appear above popovers.
7
+ */
8
+ export const TOOLTIP_ZINDEX = POPOVER_ZINDEX + 1;
@@ -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: ReactNode,
19
- setAnchorElement: (e: HTMLDivElement) => void,
20
- isOpen: boolean | undefined,
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
- // Add current tooltip to the aria-describedby if the label is not already present
36
- if (label !== props['aria-label'] && describedBy) {
37
- props['aria-describedby'] = [props['aria-describedby'], describedBy].filter(Boolean).join(' ');
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-describedby={describedBy}>
50
+ <div className="lumx-tooltip-anchor-wrapper" ref={setAnchorElement} {...aria}>
45
51
  {children}
46
52
  </div>
47
53
  );
48
- }, [children, setAnchorElement, describedBy, label]);
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
- timer = setTimeout(() => {
34
+ const update = () => {
35
35
  setIsOpen(!!shouldOpen);
36
- }, duration) as any;
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';
package/src/utils/type.ts CHANGED
@@ -59,7 +59,6 @@ export interface HasClassName {
59
59
  className?: string;
60
60
  }
61
61
 
62
-
63
62
  export interface HasCloseMode {
64
63
  /**
65
64
  * Choose how the children are hidden when closed