@navikt/ds-react 0.17.16 → 0.17.19

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.
Files changed (43) hide show
  1. package/cjs/form/search/Search.js +11 -17
  2. package/cjs/form/search/SearchButton.js +1 -1
  3. package/cjs/index.js +1 -0
  4. package/cjs/read-more/ReadMore.js +1 -1
  5. package/cjs/tooltip/Tooltip.js +153 -0
  6. package/cjs/tooltip/index.js +23 -0
  7. package/cjs/tooltip/package.json +6 -0
  8. package/cjs/tooltip/portal.js +17 -0
  9. package/cjs/util/index.js +11 -1
  10. package/esm/form/search/Search.d.ts +4 -4
  11. package/esm/form/search/Search.js +13 -19
  12. package/esm/form/search/Search.js.map +1 -1
  13. package/esm/form/search/SearchButton.js +1 -1
  14. package/esm/form/search/SearchButton.js.map +1 -1
  15. package/esm/index.d.ts +1 -0
  16. package/esm/index.js +1 -0
  17. package/esm/index.js.map +1 -1
  18. package/esm/read-more/ReadMore.js +1 -1
  19. package/esm/read-more/ReadMore.js.map +1 -1
  20. package/esm/tooltip/Tooltip.d.ts +51 -0
  21. package/esm/tooltip/Tooltip.js +126 -0
  22. package/esm/tooltip/Tooltip.js.map +1 -0
  23. package/esm/tooltip/index.d.ts +2 -0
  24. package/esm/tooltip/index.js +3 -0
  25. package/esm/tooltip/index.js.map +1 -0
  26. package/esm/tooltip/portal.d.ts +5 -0
  27. package/esm/tooltip/portal.js +13 -0
  28. package/esm/tooltip/portal.js.map +1 -0
  29. package/esm/util/index.d.ts +1 -0
  30. package/esm/util/index.js +9 -0
  31. package/esm/util/index.js.map +1 -1
  32. package/package.json +5 -4
  33. package/src/form/search/Search.tsx +22 -24
  34. package/src/form/search/SearchButton.tsx +1 -1
  35. package/src/form/search/search-themes.stories.tsx +52 -0
  36. package/src/form/search/search.stories.tsx +10 -3
  37. package/src/index.ts +1 -0
  38. package/src/read-more/ReadMore.tsx +1 -0
  39. package/src/tooltip/Tooltip.tsx +301 -0
  40. package/src/tooltip/index.ts +2 -0
  41. package/src/tooltip/portal.tsx +15 -0
  42. package/src/tooltip/tooltip.stories.tsx +144 -0
  43. package/src/util/index.ts +14 -0
@@ -0,0 +1,301 @@
1
+ import cl from "classnames";
2
+ import React, {
3
+ cloneElement,
4
+ forwardRef,
5
+ HTMLAttributes,
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from "react";
12
+ import { composeEventHandlers, Detail, useEventListener } from "..";
13
+ import {
14
+ useFloating,
15
+ arrow as flArrow,
16
+ shift,
17
+ autoUpdate,
18
+ flip,
19
+ hide,
20
+ } from "@floating-ui/react-dom";
21
+ import mergeRefs from "react-merge-refs";
22
+ import Portal from "./portal";
23
+ import { useId } from "../util";
24
+
25
+ export interface TooltipProps extends HTMLAttributes<HTMLDivElement> {
26
+ /**
27
+ * Element tooltip anchors to
28
+ */
29
+ children: React.ReactElement & React.RefAttributes<HTMLElement>;
30
+ /**
31
+ * Open state for contolled tooltip
32
+ */
33
+ open?: boolean;
34
+ /**
35
+ * Tells tooltip to start in open state
36
+ * @note "open"-prop overwrites this
37
+ */
38
+ defaultOpen?: boolean;
39
+ /**
40
+ * Orientation for tooltip
41
+ * @default "top"
42
+ */
43
+ placement?: "top" | "right" | "bottom" | "left";
44
+ /**
45
+ * Toggles rendering of arrow
46
+ * @default true
47
+ */
48
+ arrow?: boolean;
49
+ /**
50
+ * Distance from anchor to tooltip
51
+ * @default 10px with arrow, 2px without arrow
52
+ */
53
+ offset?: number;
54
+ /**
55
+ * Content shown in tooltip
56
+ */
57
+ content: string;
58
+ /**
59
+ * Sets max allowed character length
60
+ * @default 80
61
+ */
62
+ maxChar?: number;
63
+ /**
64
+ * Adds a delay in milliseconds before opening tooltip
65
+ * @default 300
66
+ */
67
+ delay?: number;
68
+ /**
69
+ * List of Keyboard-keys for shortcuts
70
+ */
71
+ keys?: string[];
72
+ }
73
+
74
+ const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
75
+ (
76
+ {
77
+ children,
78
+ className,
79
+ arrow: _arrow = true,
80
+ placement: _placement = "top",
81
+ open,
82
+ defaultOpen = false,
83
+ offset: _offset,
84
+ content,
85
+ delay = 150,
86
+ id,
87
+ keys,
88
+ maxChar = 80,
89
+ ...rest
90
+ },
91
+ ref
92
+ ) => {
93
+ const arrowRef = useRef<HTMLDivElement | null>(null);
94
+ const [isOpen, setIsOpen] = useState(defaultOpen);
95
+ const openTimerRef = useRef(0);
96
+ const leaveTimerRef = useRef(0);
97
+ const isMouseDownRef = useRef(false);
98
+
99
+ const ariaId = useId(id);
100
+
101
+ const {
102
+ x,
103
+ y,
104
+ update,
105
+ placement,
106
+ refs,
107
+ middlewareData: {
108
+ arrow: { x: arrowX, y: arrowY } = {},
109
+ hide: { referenceHidden } = {},
110
+ },
111
+ } = useFloating({
112
+ placement: _placement,
113
+ middleware: [
114
+ shift(),
115
+ flip({ padding: 5, fallbackPlacements: ["bottom", "top"] }),
116
+ flArrow({ element: arrowRef, padding: 5 }),
117
+ hide(),
118
+ ],
119
+ });
120
+
121
+ /* https://floating-ui.com/docs/react-dom#updating */
122
+ useEffect(() => {
123
+ if (!refs.reference.current || !refs.floating.current) {
124
+ return;
125
+ }
126
+
127
+ // Only call this when the floating element is rendered
128
+ return autoUpdate(refs.reference.current, refs.floating.current, update);
129
+ }, [refs.reference, refs.floating, update, open, isOpen]);
130
+
131
+ const handleOpen = useCallback(() => {
132
+ window.clearTimeout(openTimerRef.current);
133
+ window.clearTimeout(leaveTimerRef.current);
134
+ setIsOpen(true);
135
+ }, [setIsOpen]);
136
+
137
+ const handleDelayedOpen = useCallback(() => {
138
+ window.clearTimeout(openTimerRef.current);
139
+ window.clearTimeout(leaveTimerRef.current);
140
+ openTimerRef.current = window.setTimeout(() => {
141
+ setIsOpen(true);
142
+ }, delay);
143
+ }, [delay, setIsOpen]);
144
+
145
+ const handleClose = useCallback(() => {
146
+ window.clearTimeout(openTimerRef.current);
147
+ leaveTimerRef.current = window.setTimeout(() => {
148
+ setIsOpen(false);
149
+ }, 50);
150
+ }, [setIsOpen]);
151
+
152
+ const handleMouseUp = useCallback(
153
+ () => (isMouseDownRef.current = false),
154
+ []
155
+ );
156
+
157
+ useEffect(() => {
158
+ // eslint-disable-next-line react-hooks/exhaustive-deps
159
+ return () => window.clearTimeout(openTimerRef.current);
160
+ }, []);
161
+
162
+ useEffect(() => {
163
+ return () => document.removeEventListener("mouseup", handleMouseUp);
164
+ }, [handleMouseUp]);
165
+
166
+ useEventListener(
167
+ "keydown",
168
+ useCallback((e) => e.key === "Escape" && handleClose(), [handleClose]),
169
+ document
170
+ );
171
+
172
+ /* https://floating-ui.com/docs/react-dom#stable-ref-prop */
173
+ const stableRef = useMemo(() => mergeRefs([ref, refs.floating]), [
174
+ ref,
175
+ refs.floating,
176
+ ]);
177
+
178
+ if (
179
+ !children ||
180
+ children?.type === React.Fragment ||
181
+ (children as any) === React.Fragment
182
+ ) {
183
+ console.error(
184
+ "<Tooltip> children needs to be a single ReactElement and not <React.Fragment/>/<></>"
185
+ );
186
+ return null;
187
+ }
188
+
189
+ if (content?.length > maxChar) {
190
+ console.error(
191
+ `Because of strict accessibility concers we encourage all Tooltips to have less than 80 characters. Can be overwritten with the maxChar-prop`
192
+ );
193
+ return null;
194
+ }
195
+
196
+ return (
197
+ <>
198
+ {cloneElement(children, {
199
+ ...children.props,
200
+ "aria-describedby":
201
+ open || isOpen
202
+ ? cl(ariaId, children?.props["aria-describedby"])
203
+ : children?.props["aria-describedby"],
204
+ ref: mergeRefs([(children as any).ref, refs.reference]),
205
+ onMouseEnter: composeEventHandlers(
206
+ children.props.onMouseEnter,
207
+ handleDelayedOpen
208
+ ),
209
+ onMouseLeave: composeEventHandlers(
210
+ children.props.onMouseLeave,
211
+ handleClose
212
+ ),
213
+ onMouseDown: composeEventHandlers(children.props.onMouseDown, () => {
214
+ isMouseDownRef.current = true;
215
+ document.addEventListener("mouseup", handleMouseUp, { once: true });
216
+ }),
217
+ onFocus: composeEventHandlers(
218
+ children.props.onFocus,
219
+ () => !isMouseDownRef.current && handleOpen()
220
+ ),
221
+ onBlur: composeEventHandlers(children.props.onBlur, handleClose),
222
+ })}
223
+ {(open ?? isOpen) && (
224
+ <Portal>
225
+ <div
226
+ ref={stableRef}
227
+ {...rest}
228
+ onMouseEnter={handleOpen}
229
+ onMouseLeave={handleClose}
230
+ role="tooltip"
231
+ id={ariaId}
232
+ style={{
233
+ position: "absolute",
234
+ top: y ?? "",
235
+ left: x ?? "",
236
+ visibility: referenceHidden ? "hidden" : "visible",
237
+ }}
238
+ data-side={placement}
239
+ className={cl(
240
+ "navds-tooltip",
241
+ "navds-detail navds-detail--small",
242
+ className
243
+ )}
244
+ >
245
+ <div
246
+ className="navds-tooltip__inner"
247
+ style={{
248
+ [{
249
+ top: "marginBottom",
250
+ right: "marginLeft",
251
+ bottom: "marginTop",
252
+ left: "marginRight",
253
+ }[placement]]: _offset ? _offset : _arrow ? 10 : 2,
254
+ }}
255
+ >
256
+ {content}
257
+ {keys && (
258
+ <span className="navds-tooltip__keys">
259
+ {keys.map((key) => (
260
+ <Detail
261
+ size="small"
262
+ as="kbd"
263
+ key={key}
264
+ className="navds-tooltip__key"
265
+ >
266
+ {key}
267
+ </Detail>
268
+ ))}
269
+ </span>
270
+ )}
271
+ {_arrow && (
272
+ <div
273
+ ref={(node) => {
274
+ arrowRef.current = node;
275
+ }}
276
+ className="navds-tooltip__arrow"
277
+ style={{
278
+ left: arrowX != null ? `${arrowX}px` : "",
279
+ top: arrowY != null ? `${arrowY}px` : "",
280
+ right: "",
281
+ bottom: "",
282
+
283
+ [{
284
+ top: "bottom",
285
+ right: "left",
286
+ bottom: "top",
287
+ left: "right",
288
+ }[placement]]: "-3.5px",
289
+ }}
290
+ />
291
+ )}
292
+ </div>
293
+ </div>
294
+ </Portal>
295
+ )}
296
+ </>
297
+ );
298
+ }
299
+ );
300
+
301
+ export default Tooltip;
@@ -0,0 +1,2 @@
1
+ export { default as Tooltip } from "./Tooltip";
2
+ export * from "./Tooltip";
@@ -0,0 +1,15 @@
1
+ /* https://github.com/radix-ui/primitives/blob/main/packages/react/portal/src/Portal.tsx */
2
+ import ReactDOM from "react-dom";
3
+
4
+ const Portal = ({ children }) => {
5
+ const hostElement = globalThis?.document?.body;
6
+
7
+ if (hostElement) {
8
+ return ReactDOM.createPortal(children, hostElement);
9
+ }
10
+
11
+ // bail out of ssr
12
+ return null;
13
+ };
14
+
15
+ export default Portal;
@@ -0,0 +1,144 @@
1
+ import React, { useRef, useState } from "react";
2
+ import { Tooltip } from "../index";
3
+ import { Meta } from "@storybook/react/types-6-0";
4
+ import { Refresh } from "@navikt/ds-icons";
5
+ import { Button } from "../..";
6
+ import { ToggleGroup } from "../toggle-group";
7
+
8
+ export default {
9
+ title: "ds-react/tooltip",
10
+ component: Tooltip,
11
+ parameters: {
12
+ chromatic: { disable: true },
13
+ },
14
+ } as Meta;
15
+
16
+ export const Demo = () => {
17
+ const [open, setOpen] = useState(true);
18
+ const testRef = useRef(null);
19
+ return (
20
+ <div
21
+ style={{
22
+ width: "100vw",
23
+ height: "100vh",
24
+ display: "flex",
25
+ flexDirection: "column",
26
+ gap: 32,
27
+ justifyContent: "center",
28
+ alignItems: "center",
29
+ }}
30
+ >
31
+ <Tooltip
32
+ content="Tooltip example"
33
+ keys={["Cmd", "K"]}
34
+ placement="right"
35
+ open={open}
36
+ ref={testRef}
37
+ >
38
+ <Button aria-describedby="test123" onClick={() => setOpen(!open)}>
39
+ Tooltip C
40
+ </Button>
41
+ </Tooltip>
42
+ <Tooltip
43
+ content="Tooltip example"
44
+ keys={["Cmd", "K"]}
45
+ placement="right"
46
+ defaultOpen
47
+ >
48
+ <Button onClick={() => console.log(testRef.current)}>Tooltip C</Button>
49
+ </Tooltip>
50
+ <ToggleGroup onChange={null} defaultValue="321">
51
+ <Tooltip content="Tooltip" placement="bottom">
52
+ <ToggleGroup.Item value="123">Tekst</ToggleGroup.Item>
53
+ </Tooltip>
54
+ <Tooltip content="Tooltip" placement="bottom">
55
+ <ToggleGroup.Item value="321">tekst 2</ToggleGroup.Item>
56
+ </Tooltip>
57
+ <Tooltip content="Tooltip" placement="bottom">
58
+ <ToggleGroup.Item value="3212">tekst 2</ToggleGroup.Item>
59
+ </Tooltip>
60
+ <Tooltip content="Tooltip" placement="bottom">
61
+ <ToggleGroup.Item value="3213">tekst 2</ToggleGroup.Item>
62
+ </Tooltip>
63
+ <Tooltip content="Tooltip" placement="bottom">
64
+ <ToggleGroup.Item value="3214">tekst 2</ToggleGroup.Item>
65
+ </Tooltip>
66
+ </ToggleGroup>
67
+ </div>
68
+ );
69
+ };
70
+
71
+ export const All = () => {
72
+ const [open, setOpen] = useState(false);
73
+ return (
74
+ <div style={{ margin: "4rem 8rem 4rem 8rem" }}>
75
+ <h2>Controlled</h2>
76
+ <Tooltip open={open} content="Controlled tooltip example" placement="top">
77
+ <Button onClick={() => setOpen((x) => !x)}>Toggle tooltip</Button>
78
+ </Tooltip>
79
+
80
+ <h2>no arrow</h2>
81
+ <Tooltip content="no arrow" placement="top" arrow={false}>
82
+ <Button>Tooltip</Button>
83
+ </Tooltip>
84
+
85
+ <h2>Keys</h2>
86
+ <Tooltip content="Tooltip" placement="top" open keys={["Cmd", "K"]}>
87
+ <Button>Tooltip</Button>
88
+ </Tooltip>
89
+ <h2>more offset</h2>
90
+ <Tooltip content="Tooltip" placement="top" open offset={20}>
91
+ <Button>Tooltip</Button>
92
+ </Tooltip>
93
+
94
+ <h2>all placements</h2>
95
+ <div
96
+ style={{
97
+ display: "flex",
98
+ flexDirection: "column",
99
+ flexWrap: "wrap",
100
+ gap: "3rem",
101
+ }}
102
+ >
103
+ {["top", "left", "bottom", "right"].map((placement) => (
104
+ <div key={placement}>
105
+ <h3>{placement}</h3>
106
+ <div
107
+ style={{
108
+ display: "flex",
109
+ flexDirection: "column",
110
+ flexWrap: "wrap",
111
+ gap: "3rem",
112
+ }}
113
+ >
114
+ <Tooltip
115
+ key={placement}
116
+ defaultOpen
117
+ content={placement}
118
+ placement={placement as any}
119
+ >
120
+ <Refresh aria-hidden tabIndex={0} />
121
+ </Tooltip>
122
+ </div>
123
+ </div>
124
+ ))}
125
+ </div>
126
+ </div>
127
+ );
128
+ };
129
+
130
+ export const UUDemo = () => {
131
+ return (
132
+ <div>
133
+ <Button>Placeholder</Button>
134
+ <br />
135
+ <br />
136
+ <Tooltip content="Shortcut" placement="right" keys={["Cmd", "S"]}>
137
+ <Button>Lagre</Button>
138
+ </Tooltip>
139
+ <br />
140
+ <br />
141
+ <Button>Placeholder</Button>
142
+ </div>
143
+ );
144
+ };
package/src/util/index.ts CHANGED
@@ -44,3 +44,17 @@ export const useEventListener = <T extends ListenerT>(
44
44
  };
45
45
  }, [name, handler, target]);
46
46
  };
47
+
48
+ /* https://github.com/radix-ui/primitives/blob/main/packages/core/primitive/src/primitive.tsx */
49
+ export const composeEventHandlers = <E>(
50
+ originalEventHandler?: (event: E) => void,
51
+ ourEventHandler?: (event: E) => void
52
+ ) => {
53
+ return function handleEvent(event: E) {
54
+ originalEventHandler?.(event);
55
+
56
+ if (!((event as unknown) as Event).defaultPrevented) {
57
+ return ourEventHandler?.(event);
58
+ }
59
+ };
60
+ };