@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.
Files changed (51) hide show
  1. package/package.json +4 -4
  2. package/src/components/data/avatar-group/index.tsx +224 -0
  3. package/src/components/data/badge-overflow/index.tsx +259 -0
  4. package/src/components/data/circular-progress/index.tsx +358 -0
  5. package/src/components/data/relative-time-card/index.tsx +191 -0
  6. package/src/components/data/stat/index.tsx +140 -0
  7. package/src/components/data/status/index.tsx +80 -0
  8. package/src/components/effects/GlowBackground.tsx +9 -1
  9. package/src/components/effects/swap/index.tsx +289 -0
  10. package/src/components/feedback/banner/index.tsx +693 -0
  11. package/src/components/forms/checkbox-group/index.tsx +243 -0
  12. package/src/components/forms/editable/index.tsx +420 -0
  13. package/src/components/forms/input-otp/index.tsx +12 -3
  14. package/src/components/forms/mask-input/index.tsx +466 -0
  15. package/src/components/forms/otp/index.tsx +12 -8
  16. package/src/components/forms/segmented-input/index.tsx +319 -0
  17. package/src/components/forms/tags-input/index.tsx +896 -0
  18. package/src/components/forms/time-picker/index.tsx +285 -0
  19. package/src/components/index.ts +51 -0
  20. package/src/components/layout/key-value/index.tsx +884 -0
  21. package/src/components/layout/stack/index.tsx +349 -0
  22. package/src/components/navigation/context-menu/index.tsx +9 -6
  23. package/src/components/navigation/stepper/index.tsx +1307 -0
  24. package/src/components/select/multi-select-pro-async.tsx +11 -2
  25. package/src/components/select/multi-select-pro.tsx +11 -2
  26. package/src/components/specialized/presence/index.tsx +181 -0
  27. package/src/components/specialized/primitive/index.tsx +83 -0
  28. package/src/components/specialized/visually-hidden/index.tsx +19 -0
  29. package/src/components/specialized/visually-hidden-input/index.tsx +99 -0
  30. package/src/hooks/dom/index.ts +4 -0
  31. package/src/hooks/dom/useFormReset.ts +49 -0
  32. package/src/hooks/dom/useLayoutEffect.ts +16 -0
  33. package/src/hooks/dom/useSize.ts +57 -0
  34. package/src/hooks/state/index.ts +4 -0
  35. package/src/hooks/state/useCallbackRef.ts +25 -0
  36. package/src/hooks/state/usePrevious.ts +20 -0
  37. package/src/hooks/state/useStateMachine.ts +29 -0
  38. package/src/lib/compose-event-handlers.ts +22 -0
  39. package/src/lib/compose-refs.ts +65 -0
  40. package/src/lib/create-context.tsx +62 -0
  41. package/src/lib/get-element-ref.ts +33 -0
  42. package/src/lib/index.ts +5 -0
  43. package/src/lib/styles.ts +103 -0
  44. package/src/styles/README.md +43 -0
  45. package/src/styles/palette/utils.ts +15 -5
  46. package/src/styles/utilities/animations.css +135 -0
  47. package/src/styles/utilities/display.css +62 -0
  48. package/src/styles/utilities/glass.css +57 -0
  49. package/src/styles/utilities/marquee.css +69 -0
  50. package/src/styles/utilities/step.css +25 -0
  51. 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
- <button
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
- </button>
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
- <button
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
- </button>
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 };
@@ -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 };
@@ -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 };