@abuzareal/contour-ui 0.1.1 → 0.2.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/CHANGELOG.md +86 -11
- package/CODE_OF_CONDUCT.md +5 -0
- package/CONTRIBUTING.md +15 -0
- package/NEXTJS.md +51 -0
- package/README.md +176 -76
- package/RELEASING.md +14 -28
- package/SECURITY.md +1 -1
- package/dist/components/hero/SceneBoundary.js +1 -0
- package/dist/components/hero/Sculpture.js +6 -4
- package/dist/components/three/ObjectStudy.js +7 -3
- package/dist/components/ui/Alert.js +1 -0
- package/dist/components/ui/BrandMark.js +1 -0
- package/dist/components/ui/Checkbox.js +1 -0
- package/dist/components/ui/CountUp.js +1 -0
- package/dist/components/ui/DataTable.js +1 -0
- package/dist/components/ui/Dialog.js +1 -0
- package/dist/components/ui/DropdownMenu.d.ts +7 -1
- package/dist/components/ui/DropdownMenu.js +62 -14
- package/dist/components/ui/FlipCard.js +1 -0
- package/dist/components/ui/Magnetic.js +1 -0
- package/dist/components/ui/Pagination.js +1 -0
- package/dist/components/ui/Popover.d.ts +4 -1
- package/dist/components/ui/Popover.js +7 -5
- package/dist/components/ui/RadioGroup.js +1 -0
- package/dist/components/ui/Reveal.js +1 -0
- package/dist/components/ui/ScrollProgress.js +1 -0
- package/dist/components/ui/SegmentedControl.js +1 -0
- package/dist/components/ui/SelectField.d.ts +11 -1
- package/dist/components/ui/SelectField.js +6 -4
- package/dist/components/ui/Slider.js +1 -0
- package/dist/components/ui/Switch.js +1 -0
- package/dist/components/ui/Tabs.d.ts +4 -1
- package/dist/components/ui/Tabs.js +12 -5
- package/dist/components/ui/Tag.js +1 -0
- package/dist/components/ui/TextArea.d.ts +8 -1
- package/dist/components/ui/TextArea.js +21 -7
- package/dist/components/ui/TextField.d.ts +10 -1
- package/dist/components/ui/TextField.js +17 -6
- package/dist/components/ui/ThemeToggle.js +1 -0
- package/dist/components/ui/TiltCard.js +1 -0
- package/dist/components/ui/ToastProvider.js +1 -0
- package/dist/components/ui/Tooltip.js +1 -0
- package/dist/fonts/dm-mono-LICENSE.txt +93 -0
- package/dist/fonts/dm-mono-latin-400-normal.woff2 +0 -0
- package/dist/fonts/dm-sans-LICENSE.txt +93 -0
- package/dist/fonts/dm-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/space-grotesk-LICENSE.txt +93 -0
- package/dist/fonts/space-grotesk-latin-wght-normal.woff2 +0 -0
- package/dist/fonts-latin.css +35 -0
- package/dist/hooks/useClipboardCopy.js +1 -0
- package/dist/hooks/useControlRef.d.ts +6 -0
- package/dist/hooks/useControlRef.js +13 -0
- package/dist/hooks/useDismiss.js +1 -0
- package/dist/hooks/useFormReset.d.ts +3 -0
- package/dist/hooks/useFormReset.js +31 -0
- package/dist/hooks/useHeaderScroll.js +1 -0
- package/dist/hooks/useIdleTrigger.js +1 -0
- package/dist/hooks/useInitialHashScroll.js +1 -0
- package/dist/hooks/useMobileMenu.js +1 -0
- package/dist/hooks/useMotionPreference.js +1 -0
- package/dist/hooks/useOpenState.d.ts +2 -0
- package/dist/hooks/useOpenState.js +15 -0
- package/dist/hooks/usePrintPreparation.js +1 -0
- package/dist/hooks/useScrollSpy.js +1 -0
- package/dist/hooks/useSmoothScroll.js +1 -0
- package/dist/hooks/useTheme.js +1 -0
- package/dist/hooks/useThreeStage.d.ts +4 -0
- package/dist/hooks/useThreeStage.js +165 -128
- package/dist/hooks/useToast.js +1 -0
- package/package.json +29 -12
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
3
|
/** Hero sculpture: three chrome/lime links that follow the pointer and drift while motion is on. */
|
|
3
4
|
import { useRef } from "react";
|
|
@@ -5,10 +6,13 @@ import * as THREE from "three";
|
|
|
5
6
|
import useThreeStage from "../../hooks/useThreeStage.js";
|
|
6
7
|
import { createChrome, createLime, createLinkGeometry, } from "../../lib/threeMaterials.js";
|
|
7
8
|
const restRotation = new THREE.Euler(-0.15, -0.1, -0.1);
|
|
8
|
-
function setupSculpture({ scene }) {
|
|
9
|
+
function setupSculpture({ scene, onDispose }) {
|
|
9
10
|
const chrome = createChrome();
|
|
11
|
+
onDispose?.(() => chrome.dispose());
|
|
10
12
|
const lime = createLime();
|
|
13
|
+
onDispose?.(() => lime.dispose());
|
|
11
14
|
const geometry = createLinkGeometry();
|
|
15
|
+
onDispose?.(() => geometry.dispose());
|
|
12
16
|
// Three interlocking links arranged as one assembly.
|
|
13
17
|
const assembly = new THREE.Group();
|
|
14
18
|
const firstLink = new THREE.Mesh(geometry, chrome);
|
|
@@ -36,9 +40,7 @@ function setupSculpture({ scene }) {
|
|
|
36
40
|
assembly.position.y = 0;
|
|
37
41
|
},
|
|
38
42
|
dispose: () => {
|
|
39
|
-
|
|
40
|
-
chrome.dispose();
|
|
41
|
-
lime.dispose();
|
|
43
|
+
scene.remove(assembly);
|
|
42
44
|
},
|
|
43
45
|
};
|
|
44
46
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
3
|
/** Lays out a set of 3D objects in a responsive grid; each turns slowly and the set leans toward the pointer. */
|
|
3
4
|
import { useRef } from "react";
|
|
@@ -22,8 +23,9 @@ export default function ObjectStudy({ label, motionEnabled, study, }) {
|
|
|
22
23
|
useThreeStage(hostRef, {
|
|
23
24
|
motionEnabled,
|
|
24
25
|
cameraDistance,
|
|
25
|
-
setup: ({ scene, camera }) => {
|
|
26
|
+
setup: ({ scene, camera, onDispose }) => {
|
|
26
27
|
const set = objectStudies[study]();
|
|
28
|
+
onDispose?.(set.dispose);
|
|
27
29
|
const group = new THREE.Group();
|
|
28
30
|
set.objects.forEach((object) => {
|
|
29
31
|
object.rotation.copy(restTilt);
|
|
@@ -42,7 +44,6 @@ export default function ObjectStudy({ label, motionEnabled, study, }) {
|
|
|
42
44
|
arrange();
|
|
43
45
|
return {
|
|
44
46
|
update: ({ delta, elapsed, pointer }) => {
|
|
45
|
-
arrange();
|
|
46
47
|
set.objects.forEach((object, index) => {
|
|
47
48
|
object.rotation.y += delta * 0.45;
|
|
48
49
|
object.rotation.x =
|
|
@@ -57,7 +58,10 @@ export default function ObjectStudy({ label, motionEnabled, study, }) {
|
|
|
57
58
|
group.rotation.set(0, 0, 0);
|
|
58
59
|
set.objects.forEach((object) => object.rotation.copy(restTilt));
|
|
59
60
|
},
|
|
60
|
-
|
|
61
|
+
resize: arrange,
|
|
62
|
+
dispose: () => {
|
|
63
|
+
scene.remove(group);
|
|
64
|
+
},
|
|
61
65
|
};
|
|
62
66
|
},
|
|
63
67
|
});
|
|
@@ -5,10 +5,16 @@ export type MenuItem = {
|
|
|
5
5
|
onSelect: () => void;
|
|
6
6
|
icon?: ReactNode;
|
|
7
7
|
danger?: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
/** Stable identity when labels are duplicated or translated. */
|
|
10
|
+
id?: string;
|
|
8
11
|
};
|
|
9
12
|
export type DropdownMenuProps = {
|
|
10
13
|
label: string;
|
|
11
14
|
items: MenuItem[];
|
|
12
15
|
align?: "start" | "end";
|
|
16
|
+
open?: boolean;
|
|
17
|
+
defaultOpen?: boolean;
|
|
18
|
+
onOpenChange?: (open: boolean) => void;
|
|
13
19
|
};
|
|
14
|
-
export default function DropdownMenu({ label, items, align, }: DropdownMenuProps): import("react").JSX.Element;
|
|
20
|
+
export default function DropdownMenu({ label, items, align, open: controlledOpen, defaultOpen, onOpenChange, }: DropdownMenuProps): import("react").JSX.Element;
|
|
@@ -1,38 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
/** Button-triggered action menu with roving focus, Escape/outside dismissal, and focus return. */
|
|
3
|
-
import { useCallback, useEffect, useId, useRef
|
|
4
|
+
import { useCallback, useEffect, useId, useRef } from "react";
|
|
4
5
|
import { ChevronDown } from "lucide-react";
|
|
6
|
+
import useOpenState from "../../hooks/useOpenState.js";
|
|
5
7
|
import useDismiss from "../../hooks/useDismiss.js";
|
|
6
8
|
import { cx } from "../../lib/classNames.js";
|
|
7
9
|
import { nextIndexForKey } from "../../lib/keyboard.js";
|
|
8
|
-
export default function DropdownMenu({ label, items, align = "start", }) {
|
|
10
|
+
export default function DropdownMenu({ label, items, align = "start", open: controlledOpen, defaultOpen = false, onOpenChange, }) {
|
|
9
11
|
const menuId = useId();
|
|
10
|
-
const [open, setOpen] =
|
|
12
|
+
const [open, setOpen] = useOpenState(controlledOpen, defaultOpen, onOpenChange);
|
|
11
13
|
const container = useRef(null);
|
|
12
14
|
const trigger = useRef(null);
|
|
15
|
+
const initialFocus = useRef("first");
|
|
16
|
+
const search = useRef({ value: "", time: 0 });
|
|
13
17
|
const itemRefs = useRef([]);
|
|
14
18
|
const close = useCallback((returnFocus = true) => {
|
|
15
19
|
setOpen(false);
|
|
16
20
|
if (returnFocus)
|
|
17
21
|
trigger.current?.focus();
|
|
18
|
-
}, []);
|
|
22
|
+
}, [setOpen]);
|
|
19
23
|
useDismiss(container, open, useCallback((reason) => close(reason === "escape"), [close]));
|
|
20
24
|
useEffect(() => {
|
|
21
|
-
if (open)
|
|
22
|
-
|
|
25
|
+
if (!open)
|
|
26
|
+
return;
|
|
27
|
+
search.current = { value: "", time: 0 };
|
|
28
|
+
const enabled = itemRefs.current.filter((item) => item && !item.disabled);
|
|
29
|
+
(initialFocus.current === "last" ? enabled.at(-1) : enabled[0])?.focus();
|
|
23
30
|
}, [open]);
|
|
24
|
-
return (_jsxs("div", { ref: container, className: cx("menu", `menu-${align}`), children: [_jsxs("button", { ref: trigger, type: "button", className: "menu-trigger", "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": menuId, onClick: () =>
|
|
31
|
+
return (_jsxs("div", { ref: container, className: cx("menu", `menu-${align}`), children: [_jsxs("button", { ref: trigger, type: "button", className: "menu-trigger", "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": menuId, onClick: () => {
|
|
32
|
+
initialFocus.current = "first";
|
|
33
|
+
setOpen(!open);
|
|
34
|
+
}, onKeyDown: (event) => {
|
|
35
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp")
|
|
36
|
+
return;
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
initialFocus.current = event.key === "ArrowUp" ? "last" : "first";
|
|
39
|
+
if (open) {
|
|
40
|
+
const enabled = itemRefs.current.filter((item) => item && !item.disabled);
|
|
41
|
+
(initialFocus.current === "last"
|
|
42
|
+
? enabled.at(-1)
|
|
43
|
+
: enabled[0])?.focus();
|
|
44
|
+
}
|
|
45
|
+
else
|
|
46
|
+
setOpen(true);
|
|
47
|
+
}, children: [label, _jsx(ChevronDown, { size: 15, "aria-hidden": "true", className: "menu-chevron" })] }), _jsx("div", { id: menuId, role: "menu", "aria-label": label, className: "menu-list", hidden: !open, children: items.map((item, index) => (_jsxs("button", { ref: (element) => {
|
|
25
48
|
itemRefs.current[index] = element;
|
|
26
|
-
}, type: "button", role: "menuitem", tabIndex: -1, className: cx("menu-item", item.danger && "menu-item-danger"), onClick: () => {
|
|
49
|
+
}, type: "button", role: "menuitem", tabIndex: -1, disabled: item.disabled, className: cx("menu-item", item.danger && "menu-item-danger"), onClick: () => {
|
|
27
50
|
item.onSelect();
|
|
28
51
|
close();
|
|
29
52
|
}, onKeyDown: (event) => {
|
|
30
53
|
if (event.key === "Tab")
|
|
31
54
|
return close(false);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
event.
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
const enabled = items
|
|
56
|
+
.map((entry, position) => (entry.disabled ? -1 : position))
|
|
57
|
+
.filter((position) => position !== -1);
|
|
58
|
+
const next = nextIndexForKey(event.key, enabled.indexOf(index), enabled.length, "vertical");
|
|
59
|
+
if (next !== null) {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
itemRefs.current[enabled[next]]?.focus();
|
|
62
|
+
}
|
|
63
|
+
else if (event.key.length === 1 &&
|
|
64
|
+
!event.ctrlKey &&
|
|
65
|
+
!event.altKey &&
|
|
66
|
+
!event.metaKey) {
|
|
67
|
+
const now = performance.now();
|
|
68
|
+
const previous = now - search.current.time < 500 ? search.current.value : "";
|
|
69
|
+
const value = previous &&
|
|
70
|
+
[...previous].every((letter) => letter === event.key.toLowerCase())
|
|
71
|
+
? event.key.toLowerCase()
|
|
72
|
+
: previous + event.key.toLowerCase();
|
|
73
|
+
search.current = { value, time: now };
|
|
74
|
+
const start = enabled.indexOf(index) + 1;
|
|
75
|
+
const ordered = [
|
|
76
|
+
...enabled.slice(start),
|
|
77
|
+
...enabled.slice(0, start),
|
|
78
|
+
];
|
|
79
|
+
const match = ordered.find((position) => items[position].label.toLowerCase().startsWith(value));
|
|
80
|
+
if (match !== undefined) {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
itemRefs.current[match]?.focus();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}, children: [item.icon, item.label] }, item.id ?? item.label))) })] }));
|
|
38
86
|
}
|
|
@@ -6,5 +6,8 @@ export type PopoverProps = {
|
|
|
6
6
|
title: string;
|
|
7
7
|
children: ReactNode;
|
|
8
8
|
align?: "start" | "end";
|
|
9
|
+
open?: boolean;
|
|
10
|
+
defaultOpen?: boolean;
|
|
11
|
+
onOpenChange?: (open: boolean) => void;
|
|
9
12
|
};
|
|
10
|
-
export default function Popover({ trigger, title, children, align, }: PopoverProps): import("react").JSX.Element;
|
|
13
|
+
export default function Popover({ trigger, title, children, align, open: controlledOpen, defaultOpen, onOpenChange, }: PopoverProps): import("react").JSX.Element;
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
/** Non-modal floating panel anchored to a trigger; closes on Escape or an outside press. */
|
|
3
|
-
import { useCallback, useId, useRef
|
|
4
|
+
import { useCallback, useId, useRef } from "react";
|
|
5
|
+
import useOpenState from "../../hooks/useOpenState.js";
|
|
4
6
|
import useDismiss from "../../hooks/useDismiss.js";
|
|
5
7
|
import { cx } from "../../lib/classNames.js";
|
|
6
|
-
export default function Popover({ trigger, title, children, align = "start", }) {
|
|
8
|
+
export default function Popover({ trigger, title, children, align = "start", open: controlledOpen, defaultOpen = false, onOpenChange, }) {
|
|
7
9
|
const panelId = useId();
|
|
8
10
|
const titleId = useId();
|
|
9
|
-
const [open, setOpen] =
|
|
11
|
+
const [open, setOpen] = useOpenState(controlledOpen, defaultOpen, onOpenChange);
|
|
10
12
|
const container = useRef(null);
|
|
11
13
|
const button = useRef(null);
|
|
12
14
|
useDismiss(container, open, useCallback((reason) => {
|
|
13
15
|
setOpen(false);
|
|
14
16
|
if (reason === "escape")
|
|
15
17
|
button.current?.focus();
|
|
16
|
-
}, []));
|
|
17
|
-
return (_jsxs("div", { ref: container, className: cx("popover", `popover-${align}`), children: [_jsx("button", { ref: button, type: "button", className: "menu-trigger", "aria-expanded": open, "aria-controls": panelId, onClick: () => setOpen(
|
|
18
|
+
}, [setOpen]));
|
|
19
|
+
return (_jsxs("div", { ref: container, className: cx("popover", `popover-${align}`), children: [_jsx("button", { ref: button, type: "button", className: "menu-trigger", "aria-expanded": open, "aria-controls": panelId, onClick: () => setOpen(!open), children: trigger }), _jsxs("div", { id: panelId, role: "dialog", "aria-labelledby": titleId, className: "popover-panel", hidden: !open, children: [_jsx("strong", { id: titleId, className: "popover-title", children: title }), children] })] }));
|
|
18
20
|
}
|
|
@@ -10,4 +10,14 @@ export type SelectFieldProps = SelectHTMLAttributes<HTMLSelectElement> & {
|
|
|
10
10
|
error?: string;
|
|
11
11
|
placeholder?: string;
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
declare const SelectField: import("react").ForwardRefExoticComponent<SelectHTMLAttributes<HTMLSelectElement> & {
|
|
14
|
+
label: string;
|
|
15
|
+
options: {
|
|
16
|
+
value: string;
|
|
17
|
+
label: string;
|
|
18
|
+
}[];
|
|
19
|
+
hint?: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
} & import("react").RefAttributes<HTMLSelectElement>>;
|
|
23
|
+
export default SelectField;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
/** Native select styled as an underlined field with a chevron. */
|
|
3
|
-
import { useId } from "react";
|
|
4
|
+
import { forwardRef, useId } from "react";
|
|
4
5
|
import { ChevronDown } from "lucide-react";
|
|
5
6
|
import { fieldDescribedBy } from "../../lib/a11y.js";
|
|
6
7
|
import Field from "./Field.js";
|
|
7
|
-
|
|
8
|
+
const SelectField = forwardRef(function SelectField({ id: providedId, "aria-describedby": describedBy, label, options, hint, error, placeholder, ...rest }, ref) {
|
|
8
9
|
const generatedId = useId();
|
|
9
10
|
const id = providedId ?? generatedId;
|
|
10
|
-
return (_jsx(Field, { id: id, label: label, hint: hint, error: error, required: rest.required, disabled: rest.disabled, children: _jsxs("div", { className: "field-control field-select", children: [_jsxs("select", { ...rest, id: id, "aria-invalid": error ? true :
|
|
11
|
+
return (_jsx(Field, { id: id, label: label, hint: hint, error: error, required: rest.required, disabled: rest.disabled, children: _jsxs("div", { className: "field-control field-select", children: [_jsxs("select", { ...rest, ref: ref, id: id, "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": [describedBy, fieldDescribedBy(id, hint, error)]
|
|
11
12
|
.filter(Boolean)
|
|
12
13
|
.join(" ") || undefined, children: [placeholder && (_jsx("option", { value: "", disabled: true, children: placeholder })), options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] }), _jsx(ChevronDown, { className: "field-chevron", size: 16, "aria-hidden": "true" })] }) }));
|
|
13
|
-
}
|
|
14
|
+
});
|
|
15
|
+
export default SelectField;
|
|
@@ -9,5 +9,8 @@ export type TabsProps = {
|
|
|
9
9
|
label: string;
|
|
10
10
|
tabs: TabItem[];
|
|
11
11
|
defaultTabId?: string;
|
|
12
|
+
/** Controlled selection. The caller must update this after onTabChange. */
|
|
13
|
+
activeTabId?: string;
|
|
14
|
+
onTabChange?: (id: string) => void;
|
|
12
15
|
};
|
|
13
|
-
export default function Tabs({ label, tabs, defaultTabId }: TabsProps): import("react").JSX.Element;
|
|
16
|
+
export default function Tabs({ label, tabs, defaultTabId, activeTabId, onTabChange, }: TabsProps): import("react").JSX.Element;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
/** Tabs with a sliding underline indicator, arrow-key navigation, and linked panels. */
|
|
3
4
|
import { useId, useLayoutEffect, useRef, useState, } from "react";
|
|
4
5
|
import { nextIndexForKey } from "../../lib/keyboard.js";
|
|
5
|
-
export default function Tabs({ label, tabs, defaultTabId }) {
|
|
6
|
+
export default function Tabs({ label, tabs, defaultTabId, activeTabId, onTabChange, }) {
|
|
6
7
|
const baseId = useId();
|
|
7
8
|
const [activeId, setActiveId] = useState(defaultTabId ?? tabs[0]?.id ?? "");
|
|
8
9
|
const [indicator, setIndicator] = useState({ left: 0, width: 0 });
|
|
9
10
|
const tabRefs = useRef([]);
|
|
10
|
-
const activeIndex = Math.max(0, tabs.findIndex((tab) => tab.id === activeId));
|
|
11
|
+
const activeIndex = Math.max(0, tabs.findIndex((tab) => tab.id === (activeTabId ?? activeId)));
|
|
11
12
|
const selectedId = tabs[activeIndex]?.id;
|
|
12
13
|
useLayoutEffect(() => {
|
|
13
14
|
const measure = () => {
|
|
@@ -18,9 +19,11 @@ export default function Tabs({ label, tabs, defaultTabId }) {
|
|
|
18
19
|
measure();
|
|
19
20
|
window.addEventListener("resize", measure);
|
|
20
21
|
return () => window.removeEventListener("resize", measure);
|
|
21
|
-
}, [activeIndex]);
|
|
22
|
+
}, [activeIndex, tabs]);
|
|
22
23
|
const select = (index) => {
|
|
23
|
-
|
|
24
|
+
if (activeTabId === undefined)
|
|
25
|
+
setActiveId(tabs[index].id);
|
|
26
|
+
onTabChange?.(tabs[index].id);
|
|
24
27
|
tabRefs.current[index]?.focus();
|
|
25
28
|
};
|
|
26
29
|
return (_jsxs("div", { className: "tabs", children: [_jsx("div", { className: "tabs-list", role: "tablist", "aria-label": label, style: {
|
|
@@ -30,7 +33,11 @@ export default function Tabs({ label, tabs, defaultTabId }) {
|
|
|
30
33
|
const selected = tab.id === selectedId;
|
|
31
34
|
return (_jsx("button", { ref: (element) => {
|
|
32
35
|
tabRefs.current[index] = element;
|
|
33
|
-
}, id: `${baseId}-tab-${tab.id}`, type: "button", role: "tab", "aria-selected": selected, "aria-controls": `${baseId}-panel-${tab.id}`, tabIndex: selected ? 0 : -1, className: "tabs-tab", onClick: () =>
|
|
36
|
+
}, id: `${baseId}-tab-${tab.id}`, type: "button", role: "tab", "aria-selected": selected, "aria-controls": `${baseId}-panel-${tab.id}`, tabIndex: selected ? 0 : -1, className: "tabs-tab", onClick: () => {
|
|
37
|
+
if (activeTabId === undefined)
|
|
38
|
+
setActiveId(tab.id);
|
|
39
|
+
onTabChange?.(tab.id);
|
|
40
|
+
}, onKeyDown: (event) => {
|
|
34
41
|
const next = nextIndexForKey(event.key, index, tabs.length);
|
|
35
42
|
if (next === null)
|
|
36
43
|
return;
|
|
@@ -7,4 +7,11 @@ export type TextAreaProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
|
|
7
7
|
/** Grows with its content instead of scrolling. */
|
|
8
8
|
autoResize?: boolean;
|
|
9
9
|
};
|
|
10
|
-
|
|
10
|
+
declare const TextArea: import("react").ForwardRefExoticComponent<TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
|
11
|
+
label: string;
|
|
12
|
+
hint?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
/** Grows with its content instead of scrolling. */
|
|
15
|
+
autoResize?: boolean;
|
|
16
|
+
} & import("react").RefAttributes<HTMLTextAreaElement>>;
|
|
17
|
+
export default TextArea;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
3
|
/** Multi-line text input with optional auto-resize and character counter. */
|
|
3
|
-
import { useId, useLayoutEffect,
|
|
4
|
+
import { forwardRef, useId, useLayoutEffect, useState, } from "react";
|
|
4
5
|
import { fieldDescribedBy } from "../../lib/a11y.js";
|
|
5
6
|
import Field from "./Field.js";
|
|
6
|
-
|
|
7
|
+
import useControlRef from "../../hooks/useControlRef.js";
|
|
8
|
+
import useFormReset from "../../hooks/useFormReset.js";
|
|
9
|
+
const TextArea = forwardRef(function TextArea({ id: providedId, "aria-describedby": describedBy, label, hint, error, autoResize = false, maxLength, onChange, rows = 3, ...rest }, forwardedRef) {
|
|
7
10
|
const generatedId = useId();
|
|
8
11
|
const id = providedId ?? generatedId;
|
|
9
|
-
const ref =
|
|
12
|
+
const { ref, attach } = useControlRef(forwardedRef);
|
|
10
13
|
const [length, setLength] = useState(String(rest.value ?? rest.defaultValue ?? "").length);
|
|
11
14
|
const fitContent = () => {
|
|
12
15
|
const element = ref.current;
|
|
@@ -16,13 +19,24 @@ export default function TextArea({ id: providedId, "aria-describedby": described
|
|
|
16
19
|
element.style.height = `${element.scrollHeight}px`;
|
|
17
20
|
};
|
|
18
21
|
useLayoutEffect(fitContent);
|
|
19
|
-
|
|
22
|
+
useFormReset(ref, () => {
|
|
23
|
+
setLength(ref.current?.value.length ?? 0);
|
|
24
|
+
fitContent();
|
|
25
|
+
});
|
|
26
|
+
return (_jsx(Field, { id: id, label: label, hint: hint, error: error, required: rest.required, disabled: rest.disabled, counter: maxLength !== undefined
|
|
20
27
|
? `${rest.value !== undefined ? String(rest.value).length : length}/${maxLength}`
|
|
21
|
-
: undefined, children: _jsx("div", { className: "field-control", children: _jsx("textarea", { ...rest, ref:
|
|
28
|
+
: undefined, children: _jsx("div", { className: "field-control", children: _jsx("textarea", { ...rest, ref: attach, id: id, rows: rows, maxLength: maxLength, "data-auto-resize": autoResize || undefined, "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": [describedBy, fieldDescribedBy(id, hint, error)]
|
|
22
29
|
.filter(Boolean)
|
|
23
|
-
.join(" ") || undefined,
|
|
30
|
+
.join(" ") || undefined,
|
|
31
|
+
// Native input also runs when React deduplicates a repeated value after form.reset().
|
|
32
|
+
onInput: (event) => {
|
|
33
|
+
setLength(event.currentTarget.value.length);
|
|
34
|
+
fitContent();
|
|
35
|
+
rest.onInput?.(event);
|
|
36
|
+
}, onChange: (event) => {
|
|
24
37
|
setLength(event.target.value.length);
|
|
25
38
|
fitContent();
|
|
26
39
|
onChange?.(event);
|
|
27
40
|
} }) }) }));
|
|
28
|
-
}
|
|
41
|
+
});
|
|
42
|
+
export default TextArea;
|
|
@@ -9,4 +9,13 @@ export type TextFieldProps = InputHTMLAttributes<HTMLInputElement> & {
|
|
|
9
9
|
/** Content after the input, e.g. a unit or an icon button. */
|
|
10
10
|
trailing?: ReactNode;
|
|
11
11
|
};
|
|
12
|
-
|
|
12
|
+
declare const TextField: import("react").ForwardRefExoticComponent<InputHTMLAttributes<HTMLInputElement> & {
|
|
13
|
+
label: string;
|
|
14
|
+
hint?: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
hideLabel?: boolean;
|
|
17
|
+
leadingIcon?: ReactNode;
|
|
18
|
+
/** Content after the input, e.g. a unit or an icon button. */
|
|
19
|
+
trailing?: ReactNode;
|
|
20
|
+
} & import("react").RefAttributes<HTMLInputElement>>;
|
|
21
|
+
export default TextField;
|
|
@@ -1,18 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
/** Single-line text input with underline styling, optional icon/adornment, and character counter. */
|
|
3
|
-
import { useId, useState, } from "react";
|
|
4
|
+
import { forwardRef, useId, useState, } from "react";
|
|
4
5
|
import { fieldDescribedBy } from "../../lib/a11y.js";
|
|
5
6
|
import Field from "./Field.js";
|
|
6
|
-
|
|
7
|
+
import useControlRef from "../../hooks/useControlRef.js";
|
|
8
|
+
import useFormReset from "../../hooks/useFormReset.js";
|
|
9
|
+
const TextField = forwardRef(function TextField({ id: providedId, "aria-describedby": describedBy, label, hint, error, hideLabel, leadingIcon, trailing, maxLength, onChange, ...rest }, forwardedRef) {
|
|
10
|
+
const { ref, attach } = useControlRef(forwardedRef);
|
|
7
11
|
const generatedId = useId();
|
|
8
12
|
const id = providedId ?? generatedId;
|
|
9
13
|
const [length, setLength] = useState(String(rest.value ?? rest.defaultValue ?? "").length);
|
|
10
|
-
|
|
14
|
+
useFormReset(ref, () => setLength(ref.current?.value.length ?? 0));
|
|
15
|
+
return (_jsx(Field, { id: id, label: label, hint: hint, error: error, required: rest.required, hideLabel: hideLabel, disabled: rest.disabled, counter: maxLength !== undefined
|
|
11
16
|
? `${rest.value !== undefined ? String(rest.value).length : length}/${maxLength}`
|
|
12
|
-
: undefined, children: _jsxs("div", { className: "field-control", children: [leadingIcon && _jsx("span", { className: "field-icon", children: leadingIcon }), _jsx("input", { ...rest, id: id, maxLength: maxLength, "aria-invalid": error ? true :
|
|
17
|
+
: undefined, children: _jsxs("div", { className: "field-control", children: [leadingIcon && _jsx("span", { className: "field-icon", children: leadingIcon }), _jsx("input", { ...rest, ref: attach, id: id, maxLength: maxLength, "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": [describedBy, fieldDescribedBy(id, hint, error)]
|
|
13
18
|
.filter(Boolean)
|
|
14
|
-
.join(" ") || undefined,
|
|
19
|
+
.join(" ") || undefined,
|
|
20
|
+
// Native input also runs when React deduplicates a repeated value after form.reset().
|
|
21
|
+
onInput: (event) => {
|
|
22
|
+
setLength(event.currentTarget.value.length);
|
|
23
|
+
rest.onInput?.(event);
|
|
24
|
+
}, onChange: (event) => {
|
|
15
25
|
setLength(event.target.value.length);
|
|
16
26
|
onChange?.(event);
|
|
17
27
|
} }), trailing && _jsx("span", { className: "field-trailing", children: trailing })] }) }));
|
|
18
|
-
}
|
|
28
|
+
});
|
|
29
|
+
export default TextField;
|