@lumx/react 3.6.0 → 3.6.1-alpha-chore-dependency-updates.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/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-chore-dependency-updates.0",
11
+ "@lumx/icons": "^3.6.1-alpha-chore-dependency-updates.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.0.20",
34
- "@storybook/addon-essentials": "^7.0.20",
35
- "@storybook/addon-interactions": "^7.0.20",
36
- "@storybook/blocks": "^7.0.20",
37
- "@storybook/react": "^7.0.20",
38
- "@storybook/react-vite": "^7.0.20",
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",
@@ -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.0.20",
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-chore-dependency-updates.0"
119
117
  }
@@ -56,12 +56,13 @@ export const TooltipWithDropdown = (props: any) => {
56
56
  const [isOpen, setOpen] = useState(false);
57
57
  return (
58
58
  <>
59
- <Tooltip label={!isOpen && 'Tooltip'} {...props}>
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) || forceOpen;
89
- const wrappedChildren = useInjectTooltipRef(children, setAnchorElement, isOpen as boolean, id);
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 { onEscapePressed } from '@lumx/react/utils/event';
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): boolean {
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 close = (overrideDelay = closeDelay) => {
54
+ const getClose = (overrideDelay = closeDelay) => {
49
55
  if (!shouldOpen && !timer) return;
50
56
  shouldOpen = false;
51
57
  deferUpdate(overrideDelay);
52
58
  };
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
- };
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]> = 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
- ];
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
  }