@djangocfg/ui-core 2.1.412 → 2.1.413
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 +4 -4
- package/src/components/data/avatar-group/index.tsx +224 -0
- package/src/components/data/badge-overflow/index.tsx +259 -0
- package/src/components/data/circular-progress/index.tsx +358 -0
- package/src/components/data/relative-time-card/index.tsx +191 -0
- package/src/components/data/stat/index.tsx +140 -0
- package/src/components/data/status/index.tsx +80 -0
- package/src/components/effects/GlowBackground.tsx +9 -1
- package/src/components/effects/swap/index.tsx +289 -0
- package/src/components/feedback/banner/index.tsx +693 -0
- package/src/components/forms/checkbox-group/index.tsx +243 -0
- package/src/components/forms/editable/index.tsx +420 -0
- package/src/components/forms/input-otp/index.tsx +12 -3
- package/src/components/forms/mask-input/index.tsx +466 -0
- package/src/components/forms/otp/index.tsx +12 -8
- package/src/components/forms/segmented-input/index.tsx +319 -0
- package/src/components/forms/tags-input/index.tsx +896 -0
- package/src/components/forms/time-picker/index.tsx +285 -0
- package/src/components/index.ts +51 -0
- package/src/components/layout/key-value/index.tsx +884 -0
- package/src/components/layout/stack/index.tsx +349 -0
- package/src/components/navigation/context-menu/index.tsx +9 -6
- package/src/components/navigation/stepper/index.tsx +1307 -0
- package/src/components/select/multi-select-pro-async.tsx +11 -2
- package/src/components/select/multi-select-pro.tsx +11 -2
- package/src/components/specialized/presence/index.tsx +181 -0
- package/src/components/specialized/primitive/index.tsx +83 -0
- package/src/components/specialized/visually-hidden/index.tsx +19 -0
- package/src/components/specialized/visually-hidden-input/index.tsx +99 -0
- package/src/hooks/dom/index.ts +4 -0
- package/src/hooks/dom/useFormReset.ts +49 -0
- package/src/hooks/dom/useLayoutEffect.ts +16 -0
- package/src/hooks/dom/useSize.ts +57 -0
- package/src/hooks/state/index.ts +4 -0
- package/src/hooks/state/useCallbackRef.ts +25 -0
- package/src/hooks/state/usePrevious.ts +20 -0
- package/src/hooks/state/useStateMachine.ts +29 -0
- package/src/lib/compose-event-handlers.ts +22 -0
- package/src/lib/compose-refs.ts +65 -0
- package/src/lib/create-context.tsx +62 -0
- package/src/lib/get-element-ref.ts +33 -0
- package/src/lib/index.ts +5 -0
- package/src/lib/styles.ts +103 -0
- package/src/styles/README.md +43 -0
- package/src/styles/palette/utils.ts +15 -5
- package/src/styles/utilities/animations.css +135 -0
- package/src/styles/utilities/display.css +62 -0
- package/src/styles/utilities/glass.css +57 -0
- package/src/styles/utilities/marquee.css +69 -0
- package/src/styles/utilities/step.css +25 -0
- package/src/styles/utilities.css +6 -259
|
@@ -542,16 +542,25 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
|
|
|
542
542
|
</div>
|
|
543
543
|
<div className="flex items-center gap-1 ml-2">
|
|
544
544
|
{selectedValues.length > 0 && !disabled && (
|
|
545
|
-
<
|
|
545
|
+
<span
|
|
546
|
+
role="button"
|
|
547
|
+
tabIndex={0}
|
|
546
548
|
onClick={(e) => {
|
|
547
549
|
e.stopPropagation()
|
|
548
550
|
handleClearAll()
|
|
549
551
|
}}
|
|
552
|
+
onKeyDown={(e) => {
|
|
553
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
554
|
+
e.preventDefault()
|
|
555
|
+
e.stopPropagation()
|
|
556
|
+
handleClearAll()
|
|
557
|
+
}
|
|
558
|
+
}}
|
|
550
559
|
className="rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
551
560
|
aria-label={translations.clearAll}
|
|
552
561
|
>
|
|
553
562
|
<XCircle className="h-4 w-4 shrink-0 opacity-50" />
|
|
554
|
-
</
|
|
563
|
+
</span>
|
|
555
564
|
)}
|
|
556
565
|
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
|
557
566
|
</div>
|
|
@@ -559,16 +559,25 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
|
|
|
559
559
|
</div>
|
|
560
560
|
<div className="flex items-center gap-1 ml-2">
|
|
561
561
|
{selectedValues.length > 0 && !disabled && (
|
|
562
|
-
<
|
|
562
|
+
<span
|
|
563
|
+
role="button"
|
|
564
|
+
tabIndex={0}
|
|
563
565
|
onClick={(e) => {
|
|
564
566
|
e.stopPropagation()
|
|
565
567
|
handleClearAll()
|
|
566
568
|
}}
|
|
569
|
+
onKeyDown={(e) => {
|
|
570
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
571
|
+
e.preventDefault()
|
|
572
|
+
e.stopPropagation()
|
|
573
|
+
handleClearAll()
|
|
574
|
+
}
|
|
575
|
+
}}
|
|
567
576
|
className="rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
568
577
|
aria-label={translations.clearAll}
|
|
569
578
|
>
|
|
570
579
|
<XCircle className="h-4 w-4 shrink-0 opacity-50" />
|
|
571
|
-
</
|
|
580
|
+
</span>
|
|
572
581
|
)}
|
|
573
582
|
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
|
574
583
|
</div>
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useLayoutEffect } from "../../../hooks/dom/useLayoutEffect";
|
|
5
|
+
import { useStateMachine } from "../../../hooks/state/useStateMachine";
|
|
6
|
+
import { useComposedRefs } from "../../../lib/compose-refs";
|
|
7
|
+
import { getElementRef } from "../../../lib/get-element-ref";
|
|
8
|
+
|
|
9
|
+
interface PresenceProps {
|
|
10
|
+
children:
|
|
11
|
+
| React.ReactElement
|
|
12
|
+
| ((props: { present: boolean }) => React.ReactElement);
|
|
13
|
+
present: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Presence: React.FC<PresenceProps> = (props) => {
|
|
17
|
+
const { present, children } = props;
|
|
18
|
+
const presence = usePresence(present);
|
|
19
|
+
|
|
20
|
+
const child = (
|
|
21
|
+
typeof children === "function"
|
|
22
|
+
? children({ present: presence.isPresent })
|
|
23
|
+
: React.Children.only(children)
|
|
24
|
+
) as React.ReactElement<{ ref?: React.Ref<HTMLElement> }>;
|
|
25
|
+
|
|
26
|
+
const ref = useComposedRefs(presence.ref, getElementRef(child));
|
|
27
|
+
const forceMount = typeof children === "function";
|
|
28
|
+
return forceMount || presence.isPresent
|
|
29
|
+
? React.cloneElement(child, { ref })
|
|
30
|
+
: null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
Presence.displayName = "Presence";
|
|
34
|
+
|
|
35
|
+
function usePresence(present: boolean) {
|
|
36
|
+
const [node, setNode] = React.useState<HTMLElement>();
|
|
37
|
+
const stylesRef = React.useRef<CSSStyleDeclaration>(
|
|
38
|
+
{} as unknown as CSSStyleDeclaration,
|
|
39
|
+
);
|
|
40
|
+
const prevPresentRef = React.useRef(present);
|
|
41
|
+
const prevAnimationNameRef = React.useRef<string>("none");
|
|
42
|
+
const initialState = present ? "mounted" : "unmounted";
|
|
43
|
+
const [state, send] = useStateMachine({
|
|
44
|
+
initial: initialState,
|
|
45
|
+
states: {
|
|
46
|
+
mounted: {
|
|
47
|
+
UNMOUNT: "unmounted",
|
|
48
|
+
ANIMATION_OUT: "unmountSuspended",
|
|
49
|
+
},
|
|
50
|
+
unmountSuspended: {
|
|
51
|
+
MOUNT: "mounted",
|
|
52
|
+
ANIMATION_END: "unmounted",
|
|
53
|
+
},
|
|
54
|
+
unmounted: {
|
|
55
|
+
MOUNT: "mounted",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
const currentAnimationName = getAnimationName(stylesRef.current);
|
|
62
|
+
prevAnimationNameRef.current =
|
|
63
|
+
state === "mounted" ? currentAnimationName : "none";
|
|
64
|
+
}, [state]);
|
|
65
|
+
|
|
66
|
+
useLayoutEffect(() => {
|
|
67
|
+
const styles = stylesRef.current;
|
|
68
|
+
const wasPresent = prevPresentRef.current;
|
|
69
|
+
const hasPresentChanged = wasPresent !== present;
|
|
70
|
+
|
|
71
|
+
if (hasPresentChanged) {
|
|
72
|
+
const prevAnimationName = prevAnimationNameRef.current;
|
|
73
|
+
const currentAnimationName = getAnimationName(styles);
|
|
74
|
+
|
|
75
|
+
if (present) {
|
|
76
|
+
send("MOUNT");
|
|
77
|
+
} else if (
|
|
78
|
+
currentAnimationName === "none" ||
|
|
79
|
+
styles?.display === "none"
|
|
80
|
+
) {
|
|
81
|
+
// If there is no exit animation or the element is hidden, animations won't run
|
|
82
|
+
// so we unmount instantly
|
|
83
|
+
send("UNMOUNT");
|
|
84
|
+
} else {
|
|
85
|
+
/**
|
|
86
|
+
* When `present` changes to `false`, we check changes to animation-name to
|
|
87
|
+
* determine whether an animation has started. We chose this approach (reading
|
|
88
|
+
* computed styles) because there is no `animationrun` event and `animationstart`
|
|
89
|
+
* fires after `animation-delay` has expired which would be too late.
|
|
90
|
+
*/
|
|
91
|
+
const isAnimating = prevAnimationName !== currentAnimationName;
|
|
92
|
+
|
|
93
|
+
if (wasPresent && isAnimating) {
|
|
94
|
+
send("ANIMATION_OUT");
|
|
95
|
+
} else {
|
|
96
|
+
send("UNMOUNT");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
prevPresentRef.current = present;
|
|
101
|
+
}
|
|
102
|
+
}, [present, send]);
|
|
103
|
+
|
|
104
|
+
useLayoutEffect(() => {
|
|
105
|
+
if (node) {
|
|
106
|
+
let timeoutId: number;
|
|
107
|
+
const ownerWindow = node.ownerDocument.defaultView ?? window;
|
|
108
|
+
/**
|
|
109
|
+
* Triggering an ANIMATION_OUT during an ANIMATION_IN will fire an `animationcancel`
|
|
110
|
+
* event for ANIMATION_IN after we have entered `unmountSuspended` state. So, we
|
|
111
|
+
* make sure we only trigger ANIMATION_END for the currently active animation.
|
|
112
|
+
*/
|
|
113
|
+
function onAnimationEnd(event: AnimationEvent) {
|
|
114
|
+
const currentAnimationName = getAnimationName(stylesRef.current);
|
|
115
|
+
const isCurrentAnimation = currentAnimationName.includes(
|
|
116
|
+
event.animationName,
|
|
117
|
+
);
|
|
118
|
+
if (event.target === node && isCurrentAnimation) {
|
|
119
|
+
// With React 18 concurrency this update is applied a frame after the
|
|
120
|
+
// animation ends, creating a flash of visible content. By setting the
|
|
121
|
+
// animation fill mode to "forwards", we force the node to keep the
|
|
122
|
+
// styles of the last keyframe, removing the flash.
|
|
123
|
+
//
|
|
124
|
+
// Previously we flushed the update via ReactDom.flushSync, but with
|
|
125
|
+
// exit animations this resulted in the node being removed from the
|
|
126
|
+
// DOM before the synthetic animationEnd event was dispatched, meaning
|
|
127
|
+
// user-provided event handlers would not be called.
|
|
128
|
+
// https://github.com/radix-ui/primitives/pull/1849
|
|
129
|
+
send("ANIMATION_END");
|
|
130
|
+
if (!prevPresentRef.current) {
|
|
131
|
+
const currentFillMode = node.style.animationFillMode;
|
|
132
|
+
node.style.animationFillMode = "forwards";
|
|
133
|
+
// Reset the style after the node had time to unmount (for cases
|
|
134
|
+
// where the component chooses not to unmount). Doing this any
|
|
135
|
+
// sooner than `setTimeout` (e.g. with `requestAnimationFrame`)
|
|
136
|
+
// still causes a flash.
|
|
137
|
+
timeoutId = ownerWindow.setTimeout(() => {
|
|
138
|
+
if (node.style.animationFillMode === "forwards") {
|
|
139
|
+
node.style.animationFillMode = currentFillMode;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function onAnimationStart(event: AnimationEvent) {
|
|
146
|
+
if (event.target === node) {
|
|
147
|
+
// if animation occurred, store its name as the previous animation.
|
|
148
|
+
prevAnimationNameRef.current = getAnimationName(stylesRef.current);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
node.addEventListener("animationstart", onAnimationStart);
|
|
152
|
+
node.addEventListener("animationcancel", onAnimationEnd);
|
|
153
|
+
node.addEventListener("animationend", onAnimationEnd);
|
|
154
|
+
return () => {
|
|
155
|
+
ownerWindow.clearTimeout(timeoutId);
|
|
156
|
+
node.removeEventListener("animationstart", onAnimationStart);
|
|
157
|
+
node.removeEventListener("animationcancel", onAnimationEnd);
|
|
158
|
+
node.removeEventListener("animationend", onAnimationEnd);
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Transition to the unmounted state if the node is removed prematurely.
|
|
163
|
+
// We avoid doing so during cleanup as the node may change but still exist.
|
|
164
|
+
send("ANIMATION_END");
|
|
165
|
+
}, [node, send]);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
isPresent: ["mounted", "unmountSuspended"].includes(state),
|
|
169
|
+
ref: React.useCallback((node: HTMLElement) => {
|
|
170
|
+
if (node) stylesRef.current = getComputedStyle(node);
|
|
171
|
+
setNode(node);
|
|
172
|
+
}, []),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getAnimationName(styles?: CSSStyleDeclaration) {
|
|
177
|
+
return styles?.animationName ?? "none";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export type { PresenceProps };
|
|
181
|
+
export { Presence };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as ReactDOM from "react-dom";
|
|
5
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
6
|
+
|
|
7
|
+
type IntrinsicElementsKeys = keyof React.JSX.IntrinsicElements;
|
|
8
|
+
|
|
9
|
+
type PrimitivePropsWithRef<E extends IntrinsicElementsKeys> = Omit<
|
|
10
|
+
React.JSX.IntrinsicElements[E],
|
|
11
|
+
"ref"
|
|
12
|
+
> & {
|
|
13
|
+
/** Whether to render the wrapped component and merge their props. */
|
|
14
|
+
asChild?: boolean;
|
|
15
|
+
ref?: React.Ref<React.ElementRef<E>>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type PrimitiveForwardRefComponent<E extends IntrinsicElementsKeys> =
|
|
19
|
+
React.ForwardRefExoticComponent<PrimitivePropsWithRef<E>>;
|
|
20
|
+
|
|
21
|
+
function createPrimitive<E extends IntrinsicElementsKeys>(
|
|
22
|
+
element: E,
|
|
23
|
+
): PrimitiveForwardRefComponent<E> {
|
|
24
|
+
const Primitive = React.forwardRef<
|
|
25
|
+
React.ElementRef<E>,
|
|
26
|
+
PrimitivePropsWithRef<E>
|
|
27
|
+
>((props, forwardedRef) => {
|
|
28
|
+
const { asChild, ...primitiveProps } = props;
|
|
29
|
+
|
|
30
|
+
if (asChild) {
|
|
31
|
+
return React.createElement(Slot, {
|
|
32
|
+
...primitiveProps,
|
|
33
|
+
ref: forwardedRef as React.Ref<HTMLElement>,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return React.createElement(element, {
|
|
38
|
+
...primitiveProps,
|
|
39
|
+
ref: forwardedRef,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
Primitive.displayName = `Primitive.${String(element)}`;
|
|
44
|
+
return Primitive as PrimitiveForwardRefComponent<E>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type Primitives = {
|
|
48
|
+
[E in IntrinsicElementsKeys]: PrimitiveForwardRefComponent<E>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const cache = new Map<
|
|
52
|
+
IntrinsicElementsKeys,
|
|
53
|
+
PrimitiveForwardRefComponent<IntrinsicElementsKeys>
|
|
54
|
+
>();
|
|
55
|
+
|
|
56
|
+
const Primitive = new Proxy(
|
|
57
|
+
{},
|
|
58
|
+
{
|
|
59
|
+
get: (_, element: PropertyKey) => {
|
|
60
|
+
const key = element as IntrinsicElementsKeys;
|
|
61
|
+
if (!cache.has(key)) {
|
|
62
|
+
cache.set(key, createPrimitive(key));
|
|
63
|
+
}
|
|
64
|
+
return cache.get(key);
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
) as Primitives;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Flush custom event dispatch for React 18 batching
|
|
71
|
+
* @see https://github.com/facebook/react/blob/a8a4742f1c54493df00da648a3f9d26e3db9c8b5/packages/react-dom/src/events/ReactDOMEventListener.js#L294-L350
|
|
72
|
+
*/
|
|
73
|
+
function dispatchDiscreteCustomEvent<E extends CustomEvent>(
|
|
74
|
+
target: E["target"],
|
|
75
|
+
event: E,
|
|
76
|
+
) {
|
|
77
|
+
if (!target) return;
|
|
78
|
+
|
|
79
|
+
ReactDOM.flushSync(() => target.dispatchEvent(event));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type { PrimitivePropsWithRef };
|
|
83
|
+
export { dispatchDiscreteCustomEvent, Primitive };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { visuallyHidden } from "../../../lib/styles";
|
|
5
|
+
|
|
6
|
+
const VisuallyHidden = React.forwardRef<
|
|
7
|
+
HTMLSpanElement,
|
|
8
|
+
React.ComponentPropsWithoutRef<"span">
|
|
9
|
+
>((props, ref) => (
|
|
10
|
+
<span
|
|
11
|
+
ref={ref}
|
|
12
|
+
style={visuallyHidden}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
));
|
|
16
|
+
|
|
17
|
+
VisuallyHidden.displayName = "VisuallyHidden";
|
|
18
|
+
|
|
19
|
+
export { VisuallyHidden };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { usePrevious } from "../../../hooks/state/usePrevious";
|
|
5
|
+
import { useSize } from "../../../hooks/dom/useSize";
|
|
6
|
+
import { useFormReset } from "../../../hooks/dom/useFormReset";
|
|
7
|
+
import { visuallyHidden } from "../../../lib/styles";
|
|
8
|
+
|
|
9
|
+
type InputValue = string[] | string;
|
|
10
|
+
|
|
11
|
+
interface VisuallyHiddenInputProps<T = InputValue>
|
|
12
|
+
extends Omit<
|
|
13
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
14
|
+
"value" | "checked" | "onReset"
|
|
15
|
+
> {
|
|
16
|
+
value?: T;
|
|
17
|
+
checked?: boolean;
|
|
18
|
+
control: HTMLElement | null;
|
|
19
|
+
bubbles?: boolean;
|
|
20
|
+
onReset?: (value: T) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function VisuallyHiddenInput<T = InputValue>(
|
|
24
|
+
props: VisuallyHiddenInputProps<T>,
|
|
25
|
+
) {
|
|
26
|
+
const {
|
|
27
|
+
control,
|
|
28
|
+
value,
|
|
29
|
+
checked,
|
|
30
|
+
bubbles = true,
|
|
31
|
+
type = "hidden",
|
|
32
|
+
onReset,
|
|
33
|
+
style,
|
|
34
|
+
...inputProps
|
|
35
|
+
} = props;
|
|
36
|
+
|
|
37
|
+
const isCheckInput =
|
|
38
|
+
type === "checkbox" || type === "radio" || type === "switch";
|
|
39
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
40
|
+
const prevValue = usePrevious(type === "hidden" ? value : checked);
|
|
41
|
+
const controlSize = useSize(control);
|
|
42
|
+
|
|
43
|
+
// Bubble value/checked change to parents
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
const input = inputRef.current;
|
|
46
|
+
if (!input) return;
|
|
47
|
+
const inputProto = window.HTMLInputElement.prototype;
|
|
48
|
+
|
|
49
|
+
const propertyKey = isCheckInput ? "checked" : "value";
|
|
50
|
+
const eventType = isCheckInput ? "click" : "input";
|
|
51
|
+
const currentValue = isCheckInput ? checked : JSON.stringify(value);
|
|
52
|
+
|
|
53
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
54
|
+
inputProto,
|
|
55
|
+
propertyKey,
|
|
56
|
+
) as PropertyDescriptor;
|
|
57
|
+
const setter = descriptor.set;
|
|
58
|
+
|
|
59
|
+
if (prevValue !== currentValue && setter) {
|
|
60
|
+
const event = new Event(eventType, { bubbles });
|
|
61
|
+
setter.call(input, currentValue);
|
|
62
|
+
input.dispatchEvent(event);
|
|
63
|
+
}
|
|
64
|
+
}, [prevValue, value, checked, bubbles, isCheckInput]);
|
|
65
|
+
|
|
66
|
+
// Trigger on onReset callback when form is reset
|
|
67
|
+
useFormReset({
|
|
68
|
+
form: inputRef.current?.form ?? null,
|
|
69
|
+
defaultValue: isCheckInput ? (checked as T) : value,
|
|
70
|
+
onReset: (resetValue: T) => {
|
|
71
|
+
onReset?.(resetValue);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const composedStyle = React.useMemo<React.CSSProperties>(() => {
|
|
76
|
+
return {
|
|
77
|
+
...style,
|
|
78
|
+
...(controlSize?.width !== undefined && controlSize?.height !== undefined
|
|
79
|
+
? controlSize
|
|
80
|
+
: {}),
|
|
81
|
+
...visuallyHidden,
|
|
82
|
+
};
|
|
83
|
+
}, [style, controlSize]);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<input
|
|
87
|
+
type={type}
|
|
88
|
+
{...inputProps}
|
|
89
|
+
ref={inputRef}
|
|
90
|
+
aria-hidden={isCheckInput}
|
|
91
|
+
tabIndex={-1}
|
|
92
|
+
defaultChecked={isCheckInput ? checked : undefined}
|
|
93
|
+
style={composedStyle}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type { VisuallyHiddenInputProps, InputValue };
|
|
99
|
+
export { VisuallyHiddenInput };
|
package/src/hooks/dom/index.ts
CHANGED
|
@@ -10,3 +10,7 @@ export {
|
|
|
10
10
|
useIsScrolling,
|
|
11
11
|
} from './useScroll';
|
|
12
12
|
export type { ScrollSnapshot, ScrollDirection, ScrollTarget } from './useScroll';
|
|
13
|
+
export { useLayoutEffect } from './useLayoutEffect';
|
|
14
|
+
export { useSize } from './useSize';
|
|
15
|
+
export { useFormReset } from './useFormReset';
|
|
16
|
+
export type { UseFormResetParams } from './useFormReset';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useCallbackRef } from "../state/useCallbackRef";
|
|
5
|
+
|
|
6
|
+
interface UseFormResetParams<T> {
|
|
7
|
+
/**
|
|
8
|
+
* The form element to attach reset handler to.
|
|
9
|
+
*/
|
|
10
|
+
form?: HTMLFormElement | null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The default value to reset to.
|
|
14
|
+
*/
|
|
15
|
+
defaultValue?: T;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Callback fired when form is reset.
|
|
19
|
+
*/
|
|
20
|
+
onReset?: (value: T) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A hook to handle form reset events.
|
|
25
|
+
* Can be triggered by onReset callback or by form reset event.
|
|
26
|
+
*/
|
|
27
|
+
function useFormReset<T>({
|
|
28
|
+
form,
|
|
29
|
+
defaultValue,
|
|
30
|
+
onReset,
|
|
31
|
+
}: UseFormResetParams<T>) {
|
|
32
|
+
const onResetCallback = useCallbackRef(onReset);
|
|
33
|
+
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
if (!form) return;
|
|
36
|
+
|
|
37
|
+
function onFormReset() {
|
|
38
|
+
if (defaultValue !== undefined) {
|
|
39
|
+
onResetCallback?.(defaultValue);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
form.addEventListener("reset", onFormReset);
|
|
44
|
+
return () => form.removeEventListener("reset", onFormReset);
|
|
45
|
+
}, [form, defaultValue, onResetCallback]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { useFormReset };
|
|
49
|
+
export type { UseFormResetParams };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* On the server, React emits a warning when calling `useLayoutEffect`.
|
|
7
|
+
* This is because neither `useLayoutEffect` nor `useEffect` run on the server.
|
|
8
|
+
* We use this safe version which suppresses the warning by replacing it with a noop on the server.
|
|
9
|
+
*
|
|
10
|
+
* @see https://react.dev/reference/react/useLayoutEffect
|
|
11
|
+
*/
|
|
12
|
+
const useLayoutEffect = globalThis?.document
|
|
13
|
+
? React.useLayoutEffect
|
|
14
|
+
: () => {};
|
|
15
|
+
|
|
16
|
+
export { useLayoutEffect };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useLayoutEffect } from "./useLayoutEffect";
|
|
5
|
+
|
|
6
|
+
function useSize(element: HTMLElement | null) {
|
|
7
|
+
const [size, setSize] = React.useState<
|
|
8
|
+
{ width: number; height: number } | undefined
|
|
9
|
+
>(undefined);
|
|
10
|
+
|
|
11
|
+
useLayoutEffect(() => {
|
|
12
|
+
if (element) {
|
|
13
|
+
// Provide size as early as possible
|
|
14
|
+
setSize({ width: element.offsetWidth, height: element.offsetHeight });
|
|
15
|
+
|
|
16
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
17
|
+
if (!Array.isArray(entries)) return;
|
|
18
|
+
|
|
19
|
+
// Since we only observe the one element, we don't need to loop over the array
|
|
20
|
+
if (!entries.length) return;
|
|
21
|
+
|
|
22
|
+
const entry = entries[0];
|
|
23
|
+
let width: number;
|
|
24
|
+
let height: number;
|
|
25
|
+
|
|
26
|
+
if (entry && "borderBoxSize" in entry) {
|
|
27
|
+
const borderSizeEntry = entry.borderBoxSize;
|
|
28
|
+
// iron out differences between browsers
|
|
29
|
+
const borderSize = Array.isArray(borderSizeEntry)
|
|
30
|
+
? borderSizeEntry[0]
|
|
31
|
+
: borderSizeEntry;
|
|
32
|
+
width = borderSize.inlineSize;
|
|
33
|
+
height = borderSize.blockSize;
|
|
34
|
+
} else {
|
|
35
|
+
// For browsers that don't support `borderBoxSize`
|
|
36
|
+
// we calculate it ourselves to get the correct border box.
|
|
37
|
+
width = element.offsetWidth;
|
|
38
|
+
height = element.offsetHeight;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setSize({ width, height });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
resizeObserver.observe(element, { box: "border-box" });
|
|
45
|
+
|
|
46
|
+
return () => resizeObserver.unobserve(element);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// We only want to reset to `undefined` when the element becomes `null`,
|
|
50
|
+
// not if it changes to another element.
|
|
51
|
+
setSize(undefined);
|
|
52
|
+
}, [element]);
|
|
53
|
+
|
|
54
|
+
return size;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { useSize };
|
package/src/hooks/state/index.ts
CHANGED
|
@@ -15,3 +15,7 @@ export type {
|
|
|
15
15
|
UseStoredValueReturn,
|
|
16
16
|
StorageType,
|
|
17
17
|
} from './useStoredValue';
|
|
18
|
+
export { useStateMachine } from './useStateMachine';
|
|
19
|
+
export type { StateMachineConfig } from './useStateMachine';
|
|
20
|
+
export { useCallbackRef } from './useCallbackRef';
|
|
21
|
+
export { usePrevious } from './usePrevious';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a
|
|
7
|
+
* prop or avoid re-executing effects when passed as a dependency
|
|
8
|
+
*/
|
|
9
|
+
function useCallbackRef<T extends (...args: never[]) => unknown>(
|
|
10
|
+
callback: T | undefined,
|
|
11
|
+
): T {
|
|
12
|
+
const callbackRef = React.useRef(callback);
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
callbackRef.current = callback;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// https://github.com/facebook/react/issues/19240
|
|
19
|
+
return React.useMemo(
|
|
20
|
+
() => ((...args) => callbackRef.current?.(...args)) as T,
|
|
21
|
+
[],
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { useCallbackRef };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
function usePrevious<T>(value: T) {
|
|
6
|
+
const ref = React.useRef({ value, previous: value });
|
|
7
|
+
|
|
8
|
+
// We compare values before making an update to ensure that
|
|
9
|
+
// a change has been made. This ensures the previous value is
|
|
10
|
+
// persisted correctly between renders.
|
|
11
|
+
return React.useMemo(() => {
|
|
12
|
+
if (ref.current.value !== value) {
|
|
13
|
+
ref.current.previous = ref.current.value;
|
|
14
|
+
ref.current.value = value;
|
|
15
|
+
}
|
|
16
|
+
return ref.current.previous;
|
|
17
|
+
}, [value]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { usePrevious };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
interface StateMachineConfig<TState extends string, TEvent extends string> {
|
|
6
|
+
initial: TState;
|
|
7
|
+
states: Record<TState, Partial<Record<TEvent, TState>>>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function useStateMachine<TState extends string, TEvent extends string>(
|
|
11
|
+
config: StateMachineConfig<TState, TEvent>,
|
|
12
|
+
) {
|
|
13
|
+
const [state, setState] = React.useState<TState>(config.initial);
|
|
14
|
+
|
|
15
|
+
const send = React.useCallback(
|
|
16
|
+
(event: TEvent) => {
|
|
17
|
+
setState((currentState) => {
|
|
18
|
+
const transition = config.states[currentState]?.[event];
|
|
19
|
+
return transition ?? currentState;
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
[config.states],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return [state, send] as const;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { useStateMachine };
|
|
29
|
+
export type { StateMachineConfig };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
function composeEventHandlers<E>(
|
|
4
|
+
originalEventHandler?: (event: E) => void,
|
|
5
|
+
ourEventHandler?: (event: E) => void,
|
|
6
|
+
{ checkForDefaultPrevented = true } = {},
|
|
7
|
+
) {
|
|
8
|
+
return function handleEvent(event: E) {
|
|
9
|
+
originalEventHandler?.(event);
|
|
10
|
+
|
|
11
|
+
if (
|
|
12
|
+
checkForDefaultPrevented &&
|
|
13
|
+
(event as unknown as Event).defaultPrevented
|
|
14
|
+
) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
ourEventHandler?.(event);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { composeEventHandlers };
|