@navikt/ds-react 7.37.0 → 7.39.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/cjs/alert/global-alert/root/GlobalAlertRoot.d.ts +5 -0
- package/cjs/alert/global-alert/root/GlobalAlertRoot.js +14 -2
- package/cjs/alert/global-alert/root/GlobalAlertRoot.js.map +1 -1
- package/cjs/date/Date.Dialog.js +5 -1
- package/cjs/date/Date.Dialog.js.map +1 -1
- package/cjs/dialog/index.d.ts +1 -1
- package/cjs/dialog/index.js +4 -1
- package/cjs/dialog/index.js.map +1 -1
- package/cjs/dialog/popup/DialogPopup.js +6 -1
- package/cjs/dialog/popup/DialogPopup.js.map +1 -1
- package/cjs/dialog/root/DialogRoot.d.ts +5 -5
- package/cjs/dialog/root/DialogRoot.js +12 -11
- package/cjs/dialog/root/DialogRoot.js.map +1 -1
- package/cjs/expansion-card/ExpansionCardContent.js +3 -3
- package/cjs/expansion-card/ExpansionCardContent.js.map +1 -1
- package/cjs/form/combobox/Combobox.d.ts +1 -1
- package/cjs/form/combobox/Input/InputController.d.ts +1 -1
- package/cjs/form/file-upload/useFileUpload.d.ts +1 -1
- package/cjs/layout/base/BasePrimitive.d.ts +4 -4
- package/cjs/layout/base/BasePrimitive.js +0 -1
- package/cjs/layout/base/BasePrimitive.js.map +1 -1
- package/cjs/layout/base/PrimitiveAsChildProps.d.ts +1 -4
- package/cjs/layout/box/Box.darkside.js.map +1 -1
- package/cjs/layout/box/Box.js.map +1 -1
- package/cjs/modal/Modal.js +9 -2
- package/cjs/modal/Modal.js.map +1 -1
- package/cjs/overlays/action-menu/ActionMenu.js +3 -1
- package/cjs/overlays/action-menu/ActionMenu.js.map +1 -1
- package/cjs/overlays/dismissablelayer/DismissableLayer.d.ts +1 -0
- package/cjs/overlays/dismissablelayer/DismissableLayer.js +33 -14
- package/cjs/overlays/dismissablelayer/DismissableLayer.js.map +1 -1
- package/cjs/overlays/dismissablelayer/util/useEscapeKeydown.js +7 -2
- package/cjs/overlays/dismissablelayer/util/useEscapeKeydown.js.map +1 -1
- package/cjs/provider/Provider.d.ts +1 -5
- package/cjs/provider/Provider.js +0 -2
- package/cjs/provider/Provider.js.map +1 -1
- package/cjs/read-more/ReadMore.js +1 -1
- package/cjs/read-more/ReadMore.js.map +1 -1
- package/cjs/slot/Slot.js +12 -5
- package/cjs/slot/Slot.js.map +1 -1
- package/cjs/tabs/Tabs.context.d.ts +1 -1
- package/cjs/tabs/parts/tab/useTab.d.ts +1 -1
- package/cjs/tabs/parts/tab/useTab.js +2 -1
- package/cjs/tabs/parts/tab/useTab.js.map +1 -1
- package/cjs/toggle-group/ToggleGroup.context.d.ts +1 -1
- package/cjs/toggle-group/parts/useToggleItem.d.ts +1 -1
- package/cjs/toggle-group/parts/useToggleItem.js +2 -1
- package/cjs/toggle-group/parts/useToggleItem.js.map +1 -1
- package/cjs/util/hooks/descendants/useDescendant.d.ts +1 -1
- package/cjs/util/hooks/descendants/useDescendant.js +2 -1
- package/cjs/util/hooks/descendants/useDescendant.js.map +1 -1
- package/cjs/util/hooks/useMergeRefs.d.ts +15 -9
- package/cjs/util/hooks/useMergeRefs.js +94 -29
- package/cjs/util/hooks/useMergeRefs.js.map +1 -1
- package/cjs/util/types/AsChildProps.d.ts +0 -4
- package/cjs/util/virtualfocus/Context.d.ts +1 -1
- package/esm/alert/global-alert/root/GlobalAlertRoot.d.ts +5 -0
- package/esm/alert/global-alert/root/GlobalAlertRoot.js +14 -2
- package/esm/alert/global-alert/root/GlobalAlertRoot.js.map +1 -1
- package/esm/date/Date.Dialog.js +5 -1
- package/esm/date/Date.Dialog.js.map +1 -1
- package/esm/dialog/index.d.ts +1 -1
- package/esm/dialog/index.js +1 -1
- package/esm/dialog/index.js.map +1 -1
- package/esm/dialog/popup/DialogPopup.js +6 -1
- package/esm/dialog/popup/DialogPopup.js.map +1 -1
- package/esm/dialog/root/DialogRoot.d.ts +5 -5
- package/esm/dialog/root/DialogRoot.js +5 -5
- package/esm/dialog/root/DialogRoot.js.map +1 -1
- package/esm/expansion-card/ExpansionCardContent.js +3 -3
- package/esm/expansion-card/ExpansionCardContent.js.map +1 -1
- package/esm/form/combobox/Combobox.d.ts +1 -1
- package/esm/form/combobox/Input/InputController.d.ts +1 -1
- package/esm/form/file-upload/useFileUpload.d.ts +1 -1
- package/esm/layout/base/BasePrimitive.d.ts +4 -4
- package/esm/layout/base/BasePrimitive.js +0 -1
- package/esm/layout/base/BasePrimitive.js.map +1 -1
- package/esm/layout/base/PrimitiveAsChildProps.d.ts +1 -4
- package/esm/layout/box/Box.darkside.js.map +1 -1
- package/esm/layout/box/Box.js.map +1 -1
- package/esm/modal/Modal.js +9 -2
- package/esm/modal/Modal.js.map +1 -1
- package/esm/overlays/action-menu/ActionMenu.js +3 -1
- package/esm/overlays/action-menu/ActionMenu.js.map +1 -1
- package/esm/overlays/dismissablelayer/DismissableLayer.d.ts +1 -0
- package/esm/overlays/dismissablelayer/DismissableLayer.js +34 -15
- package/esm/overlays/dismissablelayer/DismissableLayer.js.map +1 -1
- package/esm/overlays/dismissablelayer/util/useEscapeKeydown.js +7 -2
- package/esm/overlays/dismissablelayer/util/useEscapeKeydown.js.map +1 -1
- package/esm/provider/Provider.d.ts +1 -5
- package/esm/provider/Provider.js +0 -2
- package/esm/provider/Provider.js.map +1 -1
- package/esm/read-more/ReadMore.js +1 -1
- package/esm/read-more/ReadMore.js.map +1 -1
- package/esm/slot/Slot.js +12 -5
- package/esm/slot/Slot.js.map +1 -1
- package/esm/tabs/Tabs.context.d.ts +1 -1
- package/esm/tabs/parts/tab/useTab.d.ts +1 -1
- package/esm/tabs/parts/tab/useTab.js +3 -2
- package/esm/tabs/parts/tab/useTab.js.map +1 -1
- package/esm/toggle-group/ToggleGroup.context.d.ts +1 -1
- package/esm/toggle-group/parts/useToggleItem.d.ts +1 -1
- package/esm/toggle-group/parts/useToggleItem.js +3 -2
- package/esm/toggle-group/parts/useToggleItem.js.map +1 -1
- package/esm/util/hooks/descendants/useDescendant.d.ts +1 -1
- package/esm/util/hooks/descendants/useDescendant.js +3 -2
- package/esm/util/hooks/descendants/useDescendant.js.map +1 -1
- package/esm/util/hooks/useMergeRefs.d.ts +15 -9
- package/esm/util/hooks/useMergeRefs.js +93 -25
- package/esm/util/hooks/useMergeRefs.js.map +1 -1
- package/esm/util/types/AsChildProps.d.ts +0 -4
- package/esm/util/virtualfocus/Context.d.ts +1 -1
- package/package.json +5 -5
- package/src/alert/global-alert/root/GlobalAlertRoot.tsx +8 -2
- package/src/date/Date.Dialog.tsx +6 -1
- package/src/dialog/index.ts +1 -1
- package/src/dialog/popup/DialogPopup.tsx +7 -1
- package/src/dialog/root/DialogRoot.tsx +5 -5
- package/src/expansion-card/ExpansionCardContent.tsx +7 -3
- package/src/layout/base/BasePrimitive.tsx +4 -5
- package/src/layout/base/PrimitiveAsChildProps.ts +1 -4
- package/src/layout/box/Box.darkside.tsx +1 -1
- package/src/layout/box/Box.tsx +1 -1
- package/src/modal/Modal.tsx +9 -1
- package/src/overlays/action-menu/ActionMenu.tsx +3 -2
- package/src/overlays/dismissablelayer/DismissableLayer.tsx +52 -16
- package/src/overlays/dismissablelayer/util/useEscapeKeydown.ts +7 -2
- package/src/provider/Provider.tsx +1 -5
- package/src/read-more/ReadMore.tsx +0 -1
- package/src/slot/Slot.tsx +14 -9
- package/src/tabs/parts/tab/useTab.ts +4 -2
- package/src/toggle-group/parts/useToggleItem.ts +4 -2
- package/src/util/__tests__/useMergeRefs.test.ts +92 -0
- package/src/util/hooks/descendants/useDescendant.tsx +4 -2
- package/src/util/hooks/useMergeRefs.ts +147 -24
- package/src/util/types/AsChildProps.ts +0 -4
|
@@ -5,10 +5,6 @@ import { getResponsiveProps, getResponsiveValue } from "../utilities/css";
|
|
|
5
5
|
import { ResponsiveProp, SpacingScale } from "../utilities/types";
|
|
6
6
|
|
|
7
7
|
export type PrimitiveProps = {
|
|
8
|
-
/**
|
|
9
|
-
* @private Hides prop from documentation
|
|
10
|
-
*/
|
|
11
|
-
className?: string;
|
|
12
8
|
/**
|
|
13
9
|
* Padding around children.
|
|
14
10
|
* Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213)
|
|
@@ -190,7 +186,6 @@ export type PrimitiveProps = {
|
|
|
190
186
|
};
|
|
191
187
|
|
|
192
188
|
export const PRIMITIVE_PROPS: (keyof PrimitiveProps)[] = [
|
|
193
|
-
"className",
|
|
194
189
|
"padding",
|
|
195
190
|
"paddingInline",
|
|
196
191
|
"paddingBlock",
|
|
@@ -220,6 +215,10 @@ export const PRIMITIVE_PROPS: (keyof PrimitiveProps)[] = [
|
|
|
220
215
|
|
|
221
216
|
interface BasePrimitiveProps extends PrimitiveProps {
|
|
222
217
|
children: React.ReactElement;
|
|
218
|
+
/**
|
|
219
|
+
* @private Hides prop from documentation
|
|
220
|
+
*/
|
|
221
|
+
className?: string;
|
|
223
222
|
}
|
|
224
223
|
|
|
225
224
|
export const BasePrimitive = ({
|
|
@@ -4,15 +4,14 @@ export type PrimitiveAsChildProps =
|
|
|
4
4
|
/**
|
|
5
5
|
* Renders the component and its child as a single element,
|
|
6
6
|
* merging the props of the component with the props of the child.
|
|
7
|
+
*
|
|
7
8
|
* @example
|
|
8
|
-
* ```tsx
|
|
9
9
|
* <Component asChild data-prop>
|
|
10
10
|
* <ChildComponent data-child />
|
|
11
11
|
* </Component>
|
|
12
12
|
*
|
|
13
13
|
* // Renders
|
|
14
14
|
* <div data-prop data-child />
|
|
15
|
-
* ```
|
|
16
15
|
*/
|
|
17
16
|
asChild: true;
|
|
18
17
|
/**
|
|
@@ -29,14 +28,12 @@ export type PrimitiveAsChildProps =
|
|
|
29
28
|
* merging the props of the component with the props of the child.
|
|
30
29
|
*
|
|
31
30
|
* @example
|
|
32
|
-
* ```tsx
|
|
33
31
|
* <Component asChild data-prop>
|
|
34
32
|
* <ChildComponent data-child />
|
|
35
33
|
* </Component>
|
|
36
34
|
*
|
|
37
35
|
* // Renders
|
|
38
36
|
* <div data-prop data-child />
|
|
39
|
-
* ```
|
|
40
37
|
*/
|
|
41
38
|
asChild?: false;
|
|
42
39
|
};
|
package/src/layout/box/Box.tsx
CHANGED
package/src/modal/Modal.tsx
CHANGED
|
@@ -230,7 +230,7 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
|
|
|
230
230
|
: ariaLabelledby;
|
|
231
231
|
|
|
232
232
|
const component = (
|
|
233
|
-
// eslint-disable-next-line jsx-a11y/
|
|
233
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
|
234
234
|
<dialog
|
|
235
235
|
{...rest}
|
|
236
236
|
ref={mergedRef}
|
|
@@ -248,6 +248,14 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
|
|
|
248
248
|
: onMouseDown
|
|
249
249
|
}
|
|
250
250
|
aria-labelledby={mergedAriaLabelledBy}
|
|
251
|
+
onKeyDown={(e) => {
|
|
252
|
+
/**
|
|
253
|
+
* Stops propagation of Escape key to prevent closing parent modals/dialogs
|
|
254
|
+
*/
|
|
255
|
+
if (e.key === "Escape") {
|
|
256
|
+
e.stopPropagation();
|
|
257
|
+
}
|
|
258
|
+
}}
|
|
251
259
|
>
|
|
252
260
|
<ModalContextProvider
|
|
253
261
|
closeHandler={getCloseHandler(modalRef, header, onBeforeClose)}
|
|
@@ -317,7 +317,6 @@ export const ActionMenuTrigger = forwardRef<
|
|
|
317
317
|
ref,
|
|
318
318
|
) => {
|
|
319
319
|
const context = useActionMenuContext();
|
|
320
|
-
|
|
321
320
|
const mergedRefs = useMergeRefs(ref, context.triggerRef);
|
|
322
321
|
|
|
323
322
|
return (
|
|
@@ -386,7 +385,9 @@ export const ActionMenuContent = forwardRef<
|
|
|
386
385
|
sideOffset={4}
|
|
387
386
|
collisionPadding={10}
|
|
388
387
|
returnFocus={context.triggerRef}
|
|
389
|
-
safeZone={{
|
|
388
|
+
safeZone={{
|
|
389
|
+
anchor: context.triggerRef.current,
|
|
390
|
+
}}
|
|
390
391
|
style={{
|
|
391
392
|
...style,
|
|
392
393
|
...{
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from "react";
|
|
2
8
|
import { Slot } from "../../slot/Slot";
|
|
3
9
|
import { composeEventHandlers } from "../../util/composeEventHandlers";
|
|
4
10
|
import { useMergeRefs } from "../../util/hooks";
|
|
@@ -61,6 +67,7 @@ interface DismissableLayerBaseProps
|
|
|
61
67
|
onDismiss?: (event: Event) => void;
|
|
62
68
|
/**
|
|
63
69
|
* Stops `onDismiss` from beeing called when interacting with the `safeZone` elements.
|
|
70
|
+
* - anchor: The element that should be considered safe to interact with.
|
|
64
71
|
*/
|
|
65
72
|
safeZone?: {
|
|
66
73
|
anchor?: Element | null;
|
|
@@ -112,6 +119,8 @@ const DismissableLayer = forwardRef<HTMLDivElement, DismissableLayerProps>(
|
|
|
112
119
|
) => {
|
|
113
120
|
const context = useContext(DismissableLayerContext);
|
|
114
121
|
|
|
122
|
+
const triggerPointerDownRef = useRef<boolean>(false);
|
|
123
|
+
|
|
115
124
|
const [, forceRerender] = useState({});
|
|
116
125
|
const [node, setNode] = React.useState<DismissableLayerElement | null>(
|
|
117
126
|
null,
|
|
@@ -142,13 +151,10 @@ const DismissableLayer = forwardRef<HTMLDivElement, DismissableLayerProps>(
|
|
|
142
151
|
return;
|
|
143
152
|
}
|
|
144
153
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
hasPointerDownOutside = true;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
154
|
+
const eventType = event.detail.originalEvent.type as
|
|
155
|
+
| "pointerup"
|
|
156
|
+
| "pointerdown"
|
|
157
|
+
| "focusin";
|
|
152
158
|
|
|
153
159
|
const target = event.target as HTMLElement;
|
|
154
160
|
|
|
@@ -160,19 +166,17 @@ const DismissableLayer = forwardRef<HTMLDivElement, DismissableLayerProps>(
|
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* 'focusoutside' event on the container.
|
|
166
|
-
*
|
|
167
|
-
* To handle this, we ignore any 'focusoutside' events if a 'pointerdownoutside' event has already occurred.
|
|
168
|
-
* 'pointerdownoutside' event is sufficient to indicate interaction outside the DismissableLayer.
|
|
169
|
+
* If the target is inside a custom element, event.target will be that element on pointerdown.
|
|
170
|
+
* Therefore, checking anchor.contains(target) etc. won't work in that case.
|
|
169
171
|
*/
|
|
170
172
|
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
eventType === "pointerdown" &&
|
|
174
|
+
triggerPointerDownRef.current === true
|
|
173
175
|
) {
|
|
174
176
|
event.preventDefault();
|
|
175
177
|
}
|
|
178
|
+
|
|
179
|
+
triggerPointerDownRef.current = false;
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
const pointerDownOutside = usePointerDownOutside(
|
|
@@ -283,6 +287,38 @@ const DismissableLayer = forwardRef<HTMLDivElement, DismissableLayerProps>(
|
|
|
283
287
|
enabled,
|
|
284
288
|
);
|
|
285
289
|
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
if (!safeZone?.anchor) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const handlePointerDown = () => {
|
|
296
|
+
triggerPointerDownRef.current = true;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const handlePointerEnd = () => {
|
|
300
|
+
triggerPointerDownRef.current = false;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const anchor = safeZone.anchor;
|
|
304
|
+
|
|
305
|
+
anchor.addEventListener("pointerdown", handlePointerDown, {
|
|
306
|
+
capture: true,
|
|
307
|
+
});
|
|
308
|
+
anchor.addEventListener("pointerup", handlePointerEnd);
|
|
309
|
+
anchor.addEventListener("pointerleave", handlePointerEnd);
|
|
310
|
+
anchor.addEventListener("pointercancel", handlePointerEnd);
|
|
311
|
+
|
|
312
|
+
return () => {
|
|
313
|
+
anchor.removeEventListener("pointerdown", handlePointerDown, {
|
|
314
|
+
capture: true,
|
|
315
|
+
});
|
|
316
|
+
anchor.removeEventListener("pointerup", handlePointerEnd);
|
|
317
|
+
anchor.removeEventListener("pointerleave", handlePointerEnd);
|
|
318
|
+
anchor.removeEventListener("pointercancel", handlePointerEnd);
|
|
319
|
+
};
|
|
320
|
+
}, [safeZone?.anchor]);
|
|
321
|
+
|
|
286
322
|
/**
|
|
287
323
|
* Handles registering `layers` and `layersWithOutsidePointerEventsDisabled`.
|
|
288
324
|
*/
|
|
@@ -19,10 +19,15 @@ export function useEscapeKeydown(
|
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* We use the bubbling phase (not capture) so that elements inside the layer
|
|
24
|
+
* can handle Escape themselves and call stopPropagation() if needed.
|
|
25
|
+
* Layer ordering is handled programmatically via the DismissableLayerContext.
|
|
26
|
+
*/
|
|
27
|
+
ownerDocument.addEventListener("keydown", handleKeyDown);
|
|
23
28
|
|
|
24
29
|
return () => {
|
|
25
|
-
ownerDocument.removeEventListener("keydown", handleKeyDown
|
|
30
|
+
ownerDocument.removeEventListener("keydown", handleKeyDown);
|
|
26
31
|
};
|
|
27
32
|
}, [onEscapeKeyDown, ownerDocument, enabled]);
|
|
28
33
|
}
|
|
@@ -24,12 +24,10 @@ export type ProviderProps = {
|
|
|
24
24
|
* Aksel locale
|
|
25
25
|
* @default nb
|
|
26
26
|
* @example
|
|
27
|
-
* ```jsx
|
|
28
27
|
* import { en } from "@navikt/ds-react/locales";
|
|
29
28
|
* <Provider locale={en}>
|
|
30
|
-
*
|
|
29
|
+
* {app}
|
|
31
30
|
* </Provider>
|
|
32
|
-
* ```
|
|
33
31
|
*/
|
|
34
32
|
locale: Translations;
|
|
35
33
|
/**
|
|
@@ -54,11 +52,9 @@ export const useProvider = () => useContext(ProviderContext);
|
|
|
54
52
|
* @see 🏷️ {@link ProviderProps}
|
|
55
53
|
*
|
|
56
54
|
* @example
|
|
57
|
-
* ```jsx
|
|
58
55
|
* <Provider rootElement={rootElement}>
|
|
59
56
|
* {app}
|
|
60
57
|
* </Provider>
|
|
61
|
-
* ```
|
|
62
58
|
*/
|
|
63
59
|
export const Provider = ({
|
|
64
60
|
children,
|
package/src/slot/Slot.tsx
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { useMergeRefs } from "../util/hooks";
|
|
3
3
|
import { mergeProps } from "./merge-props";
|
|
4
4
|
|
|
5
5
|
interface SlotProps extends React.HTMLAttributes<HTMLElement> {
|
|
6
6
|
children?: React.ReactNode;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
function getChildRef(children: React.ReactNode): React.Ref<HTMLElement> | null {
|
|
10
|
+
if (!React.isValidElement(children)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return Object.prototype.propertyIsEnumerable.call(children.props, "ref")
|
|
14
|
+
? (children.props as any).ref // React 19 (children.ref still works, but gives a warning)
|
|
15
|
+
: (children as any).ref; // React <19
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
const Slot = React.forwardRef<HTMLElement, SlotProps>((props, forwardedRef) => {
|
|
10
19
|
const { children, ...slotProps } = props;
|
|
11
20
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
children.props,
|
|
15
|
-
"ref",
|
|
16
|
-
)
|
|
17
|
-
? (children.props as any).ref // React 19 (children.ref still works, but gives a warning)
|
|
18
|
-
: (children as any).ref; // React <19
|
|
21
|
+
const childRef = getChildRef(children);
|
|
22
|
+
const mergedRef = useMergeRefs(forwardedRef, childRef);
|
|
19
23
|
|
|
24
|
+
if (React.isValidElement(children)) {
|
|
20
25
|
return React.cloneElement<any>(children, {
|
|
21
26
|
...mergeProps(slotProps, children.props as any),
|
|
22
|
-
ref:
|
|
27
|
+
ref: mergedRef,
|
|
23
28
|
});
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { composeEventHandlers } from "../../../util/composeEventHandlers";
|
|
2
|
-
import {
|
|
2
|
+
import { useMergeRefs } from "../../../util/hooks/useMergeRefs";
|
|
3
3
|
import { useTabsContext, useTabsDescendant } from "../../Tabs.context";
|
|
4
4
|
|
|
5
5
|
export interface UseTabProps {
|
|
@@ -40,8 +40,10 @@ export function useTab<P extends UseTabProps>(
|
|
|
40
40
|
selectionFollowsFocus && setSelectedValue(value);
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
const refs = useMergeRefs(register, ref);
|
|
44
|
+
|
|
43
45
|
return {
|
|
44
|
-
ref:
|
|
46
|
+
ref: refs,
|
|
45
47
|
isSelected,
|
|
46
48
|
isFocused: focusedValue === value,
|
|
47
49
|
id: makeTabId(id, value),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import { composeEventHandlers } from "../../util/composeEventHandlers";
|
|
3
|
-
import {
|
|
3
|
+
import { useMergeRefs } from "../../util/hooks/useMergeRefs";
|
|
4
4
|
import {
|
|
5
5
|
useToggleGroupContext,
|
|
6
6
|
useToggleGroupDescendant,
|
|
@@ -96,8 +96,10 @@ export function useToggleItem<P extends UseToggleItemProps>(
|
|
|
96
96
|
[descendants, focusedValue, selectedValue, setFocusedValue],
|
|
97
97
|
);
|
|
98
98
|
|
|
99
|
+
const refs = useMergeRefs(register, ref);
|
|
100
|
+
|
|
99
101
|
return {
|
|
100
|
-
ref:
|
|
102
|
+
ref: refs,
|
|
101
103
|
isSelected,
|
|
102
104
|
isFocused: focusedValue === value,
|
|
103
105
|
onClick: composeEventHandlers(
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { describe, expect, test, vi } from "vitest";
|
|
4
|
+
import { useMergeRefs } from "../hooks/useMergeRefs";
|
|
5
|
+
|
|
6
|
+
describe("useMergeRefs", () => {
|
|
7
|
+
test("returns null when all refs are null or undefined", () => {
|
|
8
|
+
const { result } = renderHook(() => useMergeRefs(null, undefined));
|
|
9
|
+
expect(result.current).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("assigns instance to object ref", () => {
|
|
13
|
+
const { result } = renderHook(() => {
|
|
14
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
|
15
|
+
return { merged: useMergeRefs(ref, null), ref };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const div = document.createElement("div");
|
|
19
|
+
result.current.merged?.(div);
|
|
20
|
+
|
|
21
|
+
expect(result.current.ref.current).toBe(div);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("calls function ref with instance", () => {
|
|
25
|
+
const fnRef = vi.fn();
|
|
26
|
+
const { result } = renderHook(() => useMergeRefs(fnRef, null));
|
|
27
|
+
|
|
28
|
+
const div = document.createElement("div");
|
|
29
|
+
result.current?.(div);
|
|
30
|
+
|
|
31
|
+
expect(fnRef).toHaveBeenCalledWith(div);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("handles mixed ref types", () => {
|
|
35
|
+
const fnRef = vi.fn();
|
|
36
|
+
const { result } = renderHook(() => {
|
|
37
|
+
const objRef = useRef<HTMLDivElement | null>(null);
|
|
38
|
+
return { merged: useMergeRefs(objRef, fnRef, null), objRef };
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const div = document.createElement("div");
|
|
42
|
+
result.current.merged?.(div);
|
|
43
|
+
|
|
44
|
+
expect(result.current.objRef.current).toBe(div);
|
|
45
|
+
expect(fnRef).toHaveBeenCalledWith(div);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("cleanup resets object ref to null", () => {
|
|
49
|
+
const { result } = renderHook(() => {
|
|
50
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
|
51
|
+
return { merged: useMergeRefs(ref, null), ref };
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const div = document.createElement("div");
|
|
55
|
+
result.current.merged?.(div);
|
|
56
|
+
expect(result.current.ref.current).toBe(div);
|
|
57
|
+
|
|
58
|
+
result.current.merged?.(null);
|
|
59
|
+
expect(result.current.ref.current).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("cleanup calls returned cleanup function from callback ref", () => {
|
|
63
|
+
const cleanup = vi.fn();
|
|
64
|
+
const fnRef = vi.fn().mockReturnValue(cleanup);
|
|
65
|
+
const { result } = renderHook(() => useMergeRefs(fnRef, null));
|
|
66
|
+
|
|
67
|
+
const div1 = document.createElement("div");
|
|
68
|
+
result.current?.(div1);
|
|
69
|
+
|
|
70
|
+
const div2 = document.createElement("div");
|
|
71
|
+
result.current?.(div2);
|
|
72
|
+
|
|
73
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(fnRef).not.toHaveBeenCalledWith(null);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("runs previous cleanup before assigning new instance", () => {
|
|
78
|
+
const callOrder: string[] = [];
|
|
79
|
+
const cleanup = vi.fn(() => callOrder.push("cleanup"));
|
|
80
|
+
const fnRef = vi.fn(() => {
|
|
81
|
+
callOrder.push("ref");
|
|
82
|
+
return cleanup;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const { result } = renderHook(() => useMergeRefs(fnRef, null));
|
|
86
|
+
|
|
87
|
+
result.current?.(document.createElement("div"));
|
|
88
|
+
result.current?.(document.createElement("div"));
|
|
89
|
+
|
|
90
|
+
expect(callOrder).toEqual(["ref", "cleanup", "ref"]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import React, { useRef, useState } from "react";
|
|
5
5
|
import { createStrictContext } from "../../create-strict-context";
|
|
6
6
|
import { useClientLayoutEffect } from "../useClientLayoutEffect";
|
|
7
|
-
import {
|
|
7
|
+
import { useMergeRefs } from "../useMergeRefs";
|
|
8
8
|
import { DescendantOptions, DescendantsManager } from "./descendant";
|
|
9
9
|
import { cast } from "./utils";
|
|
10
10
|
|
|
@@ -66,11 +66,13 @@ export function createDescendantContext<
|
|
|
66
66
|
? cast<React.RefCallback<T>>(descendants.register(options))
|
|
67
67
|
: cast<React.RefCallback<T>>(descendants.register);
|
|
68
68
|
|
|
69
|
+
const refs = useMergeRefs(refCallback, ref);
|
|
70
|
+
|
|
69
71
|
return {
|
|
70
72
|
descendants,
|
|
71
73
|
index,
|
|
72
74
|
enabledIndex: descendants.enabledIndexOf(ref.current),
|
|
73
|
-
register:
|
|
75
|
+
register: refs,
|
|
74
76
|
};
|
|
75
77
|
}
|
|
76
78
|
|
|
@@ -1,34 +1,157 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
3
|
-
/* https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/composeRefs.tsx */
|
|
1
|
+
/* https://github.com/mui/base-ui/blob/f2d7a90e3a20dee84955beb5be0d59e50f45ae7e/packages/utils/src/useMergedRefs.ts */
|
|
4
2
|
import React from "react";
|
|
3
|
+
import { useRefWithInit } from "./useRefWithInit";
|
|
4
|
+
|
|
5
|
+
type Empty = null | undefined;
|
|
6
|
+
type InputRef<I> = React.Ref<I> | Empty;
|
|
7
|
+
type Result<I> = React.RefCallback<I> | null;
|
|
8
|
+
type Cleanup = () => void;
|
|
5
9
|
|
|
6
|
-
type
|
|
10
|
+
type ForkRef<I> = {
|
|
11
|
+
callback: React.RefCallback<I> | null;
|
|
12
|
+
cleanup: Cleanup | null;
|
|
13
|
+
refs: InputRef<I>[];
|
|
14
|
+
};
|
|
7
15
|
|
|
8
|
-
// https://github.com/gregberge/react-merge-refs
|
|
9
16
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
17
|
+
* Merges refs into a single memoized callback ref or `null`.
|
|
18
|
+
* This makes sure multiple refs are updated together and have the same value.
|
|
19
|
+
*
|
|
20
|
+
* This function accepts up to four refs. If you need to merge more, or have an unspecified number of refs to merge,
|
|
21
|
+
* use `useMergeRefsN` instead.
|
|
12
22
|
*/
|
|
13
|
-
export function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
export function useMergeRefs<I>(a: InputRef<I>, b: InputRef<I>): Result<I>;
|
|
24
|
+
export function useMergeRefs<I>(
|
|
25
|
+
a: InputRef<I>,
|
|
26
|
+
b: InputRef<I>,
|
|
27
|
+
c: InputRef<I>,
|
|
28
|
+
): Result<I>;
|
|
29
|
+
export function useMergeRefs<I>(
|
|
30
|
+
a: InputRef<I>,
|
|
31
|
+
b: InputRef<I>,
|
|
32
|
+
c: InputRef<I>,
|
|
33
|
+
d: InputRef<I>,
|
|
34
|
+
): Result<I>;
|
|
35
|
+
export function useMergeRefs<I>(
|
|
36
|
+
a: InputRef<I>,
|
|
37
|
+
b: InputRef<I>,
|
|
38
|
+
c?: InputRef<I>,
|
|
39
|
+
d?: InputRef<I>,
|
|
40
|
+
): Result<I> {
|
|
41
|
+
const forkRef = useRefWithInit(createForkRef<I>).current!;
|
|
42
|
+
if (didChange(forkRef, a, b, c, d)) {
|
|
43
|
+
update(forkRef, [a, b, c, d]);
|
|
44
|
+
}
|
|
45
|
+
return forkRef.callback;
|
|
23
46
|
}
|
|
24
47
|
|
|
25
48
|
/**
|
|
26
|
-
* Merges refs
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* @returns React.useCallback(mergeRefs(refs), refs)
|
|
49
|
+
* Merges an array of refs into a single memoized callback ref or `null`.
|
|
50
|
+
*
|
|
51
|
+
* If you need to merge a fixed number (up to four) of refs, use `useMergeRefs` instead for better performance.
|
|
30
52
|
*/
|
|
31
|
-
export function
|
|
32
|
-
|
|
33
|
-
|
|
53
|
+
export function useMergeRefsN<I>(refs: InputRef<I>[]): Result<I> {
|
|
54
|
+
const forkRef = useRefWithInit(createForkRef<I>).current!;
|
|
55
|
+
if (didChangeN(forkRef, refs)) {
|
|
56
|
+
update(forkRef, refs);
|
|
57
|
+
}
|
|
58
|
+
return forkRef.callback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createForkRef<I>(): ForkRef<I> {
|
|
62
|
+
return {
|
|
63
|
+
callback: null,
|
|
64
|
+
cleanup: null as Cleanup | null,
|
|
65
|
+
refs: [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function didChange<I>(
|
|
70
|
+
forkRef: ForkRef<I>,
|
|
71
|
+
a: InputRef<I>,
|
|
72
|
+
b: InputRef<I>,
|
|
73
|
+
c: InputRef<I>,
|
|
74
|
+
d: InputRef<I>,
|
|
75
|
+
) {
|
|
76
|
+
return (
|
|
77
|
+
forkRef.refs[0] !== a ||
|
|
78
|
+
forkRef.refs[1] !== b ||
|
|
79
|
+
forkRef.refs[2] !== c ||
|
|
80
|
+
forkRef.refs[3] !== d
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function didChangeN<I>(forkRef: ForkRef<I>, newRefs: InputRef<I>[]) {
|
|
85
|
+
return (
|
|
86
|
+
forkRef.refs.length !== newRefs.length ||
|
|
87
|
+
forkRef.refs.some((ref, index) => ref !== newRefs[index])
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function update<I>(forkRef: ForkRef<I>, refs: InputRef<I>[]) {
|
|
92
|
+
forkRef.refs = refs;
|
|
93
|
+
|
|
94
|
+
if (refs.every((ref) => ref == null)) {
|
|
95
|
+
forkRef.callback = null;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
forkRef.callback = (instance: I) => {
|
|
100
|
+
if (forkRef.cleanup) {
|
|
101
|
+
forkRef.cleanup();
|
|
102
|
+
forkRef.cleanup = null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (instance != null) {
|
|
106
|
+
const cleanupCallbacks = Array(refs.length).fill(
|
|
107
|
+
null,
|
|
108
|
+
) as (Cleanup | null)[];
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < refs.length; i += 1) {
|
|
111
|
+
const ref = refs[i];
|
|
112
|
+
if (ref == null) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
switch (typeof ref) {
|
|
116
|
+
case "function": {
|
|
117
|
+
const refCleanup = ref(instance);
|
|
118
|
+
if (typeof refCleanup === "function") {
|
|
119
|
+
cleanupCallbacks[i] = refCleanup;
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case "object": {
|
|
124
|
+
(ref as React.MutableRefObject<I | null>).current = instance;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
forkRef.cleanup = () => {
|
|
132
|
+
for (let i = 0; i < refs.length; i += 1) {
|
|
133
|
+
const ref = refs[i];
|
|
134
|
+
if (ref == null) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
switch (typeof ref) {
|
|
138
|
+
case "function": {
|
|
139
|
+
const cleanupCallback = cleanupCallbacks[i];
|
|
140
|
+
if (typeof cleanupCallback === "function") {
|
|
141
|
+
cleanupCallback();
|
|
142
|
+
} else {
|
|
143
|
+
ref(null);
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "object": {
|
|
148
|
+
(ref as React.MutableRefObject<I | null>).current = null;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
default:
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
};
|
|
34
157
|
}
|