@zentauri-ui/zentauri-components 1.4.61 → 1.4.62
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/dist/{chunk-UXGHUBNJ.mjs → chunk-2PJF7DLJ.mjs} +3 -3
- package/dist/{chunk-UXGHUBNJ.mjs.map → chunk-2PJF7DLJ.mjs.map} +1 -1
- package/dist/{chunk-WDCIZHXY.mjs → chunk-45FCOQ63.mjs} +5 -3
- package/dist/chunk-45FCOQ63.mjs.map +1 -0
- package/dist/{chunk-RDSPHBHK.mjs → chunk-4ANBTJ5G.mjs} +49 -6
- package/dist/chunk-4ANBTJ5G.mjs.map +1 -0
- package/dist/chunk-4E66ICIR.mjs +158 -0
- package/dist/chunk-4E66ICIR.mjs.map +1 -0
- package/dist/{chunk-XLAFQ24R.js → chunk-4U6FOCFK.js} +22 -14
- package/dist/chunk-4U6FOCFK.js.map +1 -0
- package/dist/{chunk-XWM2S6VV.mjs → chunk-EQSSYK27.mjs} +12 -10
- package/dist/chunk-EQSSYK27.mjs.map +1 -0
- package/dist/{chunk-5QB2KNZQ.js → chunk-FGGYDAX3.js} +5 -3
- package/dist/chunk-FGGYDAX3.js.map +1 -0
- package/dist/{chunk-7HL3A4YF.mjs → chunk-IK75NHRX.mjs} +63 -14
- package/dist/chunk-IK75NHRX.mjs.map +1 -0
- package/dist/{chunk-BORK3BJO.mjs → chunk-J56L4ZQ3.mjs} +10 -10
- package/dist/{chunk-BORK3BJO.mjs.map → chunk-J56L4ZQ3.mjs.map} +1 -1
- package/dist/{chunk-PGH27VTL.mjs → chunk-JF3FKUUP.mjs} +21 -13
- package/dist/chunk-JF3FKUUP.mjs.map +1 -0
- package/dist/{chunk-WZKGRU3U.js → chunk-MQZB5EPD.js} +92 -27
- package/dist/chunk-MQZB5EPD.js.map +1 -0
- package/dist/{chunk-N4NO3SYL.js → chunk-NX3IHMT7.js} +22 -14
- package/dist/chunk-NX3IHMT7.js.map +1 -0
- package/dist/{chunk-BVXTOEBI.mjs → chunk-OG2WM5YK.mjs} +45 -17
- package/dist/chunk-OG2WM5YK.mjs.map +1 -0
- package/dist/{chunk-IXDJ3IPG.mjs → chunk-OXS6UJUG.mjs} +21 -13
- package/dist/chunk-OXS6UJUG.mjs.map +1 -0
- package/dist/{chunk-PCK6LX3K.js → chunk-PFOV3U7W.js} +3 -3
- package/dist/{chunk-PCK6LX3K.js.map → chunk-PFOV3U7W.js.map} +1 -1
- package/dist/{chunk-2PQEXQVR.js → chunk-THCNTPPL.js} +62 -13
- package/dist/chunk-THCNTPPL.js.map +1 -0
- package/dist/chunk-UP6S75V5.js +160 -0
- package/dist/chunk-UP6S75V5.js.map +1 -0
- package/dist/{chunk-P5HUBXUX.js → chunk-V2IWLR4O.js} +48 -5
- package/dist/chunk-V2IWLR4O.js.map +1 -0
- package/dist/{chunk-3OR47XMY.js → chunk-VSKL5LOB.js} +45 -17
- package/dist/chunk-VSKL5LOB.js.map +1 -0
- package/dist/{chunk-E3DZNJAD.js → chunk-Y4EDWZKH.js} +12 -10
- package/dist/chunk-Y4EDWZKH.js.map +1 -0
- package/dist/{chunk-YNCD6TKE.mjs → chunk-Y4IFVO46.mjs} +93 -28
- package/dist/chunk-Y4IFVO46.mjs.map +1 -0
- package/dist/{chunk-BITDSQMR.js → chunk-ZNDHS5OK.js} +10 -10
- package/dist/{chunk-BITDSQMR.js.map → chunk-ZNDHS5OK.js.map} +1 -1
- package/dist/hooks/useFocusManagement/useFocusManagement.d.ts +5 -14
- package/dist/hooks/useFocusManagement/useFocusManagement.d.ts.map +1 -1
- package/dist/hooks/useFocusManagement.js +2 -2
- package/dist/hooks/useFocusManagement.mjs +1 -1
- package/dist/ui/badge/animated.js +2 -2
- package/dist/ui/badge/animated.mjs +1 -1
- package/dist/ui/badge/badge-base.d.ts +1 -1
- package/dist/ui/badge/badge-base.d.ts.map +1 -1
- package/dist/ui/badge/types.d.ts +1 -0
- package/dist/ui/badge/types.d.ts.map +1 -1
- package/dist/ui/badge/variants.d.ts +7 -7
- package/dist/ui/badge.js +4 -4
- package/dist/ui/badge.mjs +2 -2
- package/dist/ui/buttons/animated.js +3 -3
- package/dist/ui/buttons/animated.mjs +1 -1
- package/dist/ui/buttons.js +4 -4
- package/dist/ui/buttons.mjs +2 -2
- package/dist/ui/drawer/animated/drawer-content-animated.d.ts.map +1 -1
- package/dist/ui/drawer/animated.js +17 -18
- package/dist/ui/drawer/animated.js.map +1 -1
- package/dist/ui/drawer/animated.mjs +8 -9
- package/dist/ui/drawer/animated.mjs.map +1 -1
- package/dist/ui/drawer/drawer-base.d.ts +1 -1
- package/dist/ui/drawer/drawer-base.d.ts.map +1 -1
- package/dist/ui/drawer/types.d.ts +1 -0
- package/dist/ui/drawer/types.d.ts.map +1 -1
- package/dist/ui/drawer.js +12 -12
- package/dist/ui/drawer.mjs +2 -2
- package/dist/ui/dropdown/dropdown.d.ts +1 -1
- package/dist/ui/dropdown/dropdown.d.ts.map +1 -1
- package/dist/ui/dropdown/types.d.ts +1 -0
- package/dist/ui/dropdown/types.d.ts.map +1 -1
- package/dist/ui/dropdown/variants.d.ts +1 -1
- package/dist/ui/dropdown.js +25 -7
- package/dist/ui/dropdown.js.map +1 -1
- package/dist/ui/dropdown.mjs +26 -8
- package/dist/ui/dropdown.mjs.map +1 -1
- package/dist/ui/empty-state/animated.js +2 -2
- package/dist/ui/empty-state/animated.mjs +1 -1
- package/dist/ui/empty-state/empty-state-base.d.ts.map +1 -1
- package/dist/ui/empty-state/types.d.ts +1 -0
- package/dist/ui/empty-state/types.d.ts.map +1 -1
- package/dist/ui/empty-state.js +10 -10
- package/dist/ui/empty-state.mjs +2 -2
- package/dist/ui/file-upload/file-upload.d.ts.map +1 -1
- package/dist/ui/file-upload.js +1 -3
- package/dist/ui/file-upload.js.map +1 -1
- package/dist/ui/file-upload.mjs +1 -3
- package/dist/ui/file-upload.mjs.map +1 -1
- package/dist/ui/inputs/input-base.d.ts.map +1 -1
- package/dist/ui/inputs/types.d.ts +3 -1
- package/dist/ui/inputs/types.d.ts.map +1 -1
- package/dist/ui/inputs.js +46 -2
- package/dist/ui/inputs.js.map +1 -1
- package/dist/ui/inputs.mjs +46 -2
- package/dist/ui/inputs.mjs.map +1 -1
- package/dist/ui/modal/animated/modal-content-animated.d.ts.map +1 -1
- package/dist/ui/modal/animated.js +10 -11
- package/dist/ui/modal/animated.js.map +1 -1
- package/dist/ui/modal/animated.mjs +7 -8
- package/dist/ui/modal/animated.mjs.map +1 -1
- package/dist/ui/modal/modal-base.d.ts +4 -2
- package/dist/ui/modal/modal-base.d.ts.map +1 -1
- package/dist/ui/modal.js +13 -13
- package/dist/ui/modal.mjs +3 -3
- package/dist/ui/pagination.js +6 -6
- package/dist/ui/pagination.js.map +1 -1
- package/dist/ui/pagination.mjs +3 -3
- package/dist/ui/pagination.mjs.map +1 -1
- package/dist/ui/progress/animated/progress-animated.d.ts.map +1 -1
- package/dist/ui/progress/animated.js +49 -11
- package/dist/ui/progress/animated.js.map +1 -1
- package/dist/ui/progress/animated.mjs +44 -6
- package/dist/ui/progress/animated.mjs.map +1 -1
- package/dist/ui/progress/progress-base.d.ts.map +1 -1
- package/dist/ui/progress/types.d.ts +3 -0
- package/dist/ui/progress/types.d.ts.map +1 -1
- package/dist/ui/progress.js +9 -9
- package/dist/ui/progress.mjs +2 -2
- package/dist/ui/search/search-bar.d.ts +1 -1
- package/dist/ui/search/search-bar.d.ts.map +1 -1
- package/dist/ui/search.js +2 -0
- package/dist/ui/search.js.map +1 -1
- package/dist/ui/search.mjs +2 -0
- package/dist/ui/search.mjs.map +1 -1
- package/dist/ui/select/select.d.ts +1 -1
- package/dist/ui/select/select.d.ts.map +1 -1
- package/dist/ui/select/types.d.ts +1 -0
- package/dist/ui/select/types.d.ts.map +1 -1
- package/dist/ui/select/variants.d.ts +1 -1
- package/dist/ui/select/variants.d.ts.map +1 -1
- package/dist/ui/select.js +121 -39
- package/dist/ui/select.js.map +1 -1
- package/dist/ui/select.mjs +122 -40
- package/dist/ui/select.mjs.map +1 -1
- package/dist/ui/skeleton/variants.d.ts +1 -1
- package/dist/ui/slider/slider.d.ts.map +1 -1
- package/dist/ui/slider/types.d.ts +8 -2
- package/dist/ui/slider/types.d.ts.map +1 -1
- package/dist/ui/slider.js +43 -7
- package/dist/ui/slider.js.map +1 -1
- package/dist/ui/slider.mjs +43 -7
- package/dist/ui/slider.mjs.map +1 -1
- package/dist/ui/spinner/animated/spinner.d.ts.map +1 -1
- package/dist/ui/spinner/animated.js +62 -50
- package/dist/ui/spinner/animated.js.map +1 -1
- package/dist/ui/spinner/animated.mjs +63 -51
- package/dist/ui/spinner/animated.mjs.map +1 -1
- package/dist/ui/stepper/stepper.d.ts +2 -7
- package/dist/ui/stepper/stepper.d.ts.map +1 -1
- package/dist/ui/stepper/types.d.ts +3 -3
- package/dist/ui/stepper/types.d.ts.map +1 -1
- package/dist/ui/stepper/variants.d.ts +1 -1
- package/dist/ui/stepper.js +7 -5
- package/dist/ui/stepper.js.map +1 -1
- package/dist/ui/stepper.mjs +7 -5
- package/dist/ui/stepper.mjs.map +1 -1
- package/dist/ui/table/animated.js +8 -8
- package/dist/ui/table/animated.mjs +2 -2
- package/dist/ui/table/table-base.d.ts +1 -1
- package/dist/ui/table/table-base.d.ts.map +1 -1
- package/dist/ui/table/types.d.ts +5 -1
- package/dist/ui/table/types.d.ts.map +1 -1
- package/dist/ui/table.js +14 -14
- package/dist/ui/table.mjs +1 -1
- package/dist/ui/tabs/animated.js +2 -2
- package/dist/ui/tabs/animated.mjs +1 -1
- package/dist/ui/tabs/tabs-base.d.ts.map +1 -1
- package/dist/ui/tabs/types.d.ts +2 -1
- package/dist/ui/tabs/types.d.ts.map +1 -1
- package/dist/ui/tabs.js +9 -9
- package/dist/ui/tabs.mjs +1 -1
- package/dist/ui/toast/animated.js +7 -7
- package/dist/ui/toast/animated.mjs +1 -1
- package/dist/ui/toast.js +12 -12
- package/dist/ui/toast.mjs +1 -1
- package/dist/ui/toggle/toggle-base.d.ts.map +1 -1
- package/dist/ui/toggle.js +28 -3
- package/dist/ui/toggle.js.map +1 -1
- package/dist/ui/toggle.mjs +29 -4
- package/dist/ui/toggle.mjs.map +1 -1
- package/dist/ui/tooltip/animated.js +3 -3
- package/dist/ui/tooltip/animated.mjs +1 -1
- package/dist/ui/tooltip/tooltip-base.d.ts.map +1 -1
- package/dist/ui/tooltip/types.d.ts +1 -0
- package/dist/ui/tooltip/types.d.ts.map +1 -1
- package/dist/ui/tooltip.js +7 -7
- package/dist/ui/tooltip.mjs +1 -1
- package/package.json +1 -1
- package/src/hooks/useFocusManagement/useFocusManagement.test.tsx +8 -0
- package/src/hooks/useFocusManagement/useFocusManagement.ts +162 -33
- package/src/ui/badge/badge-base.tsx +4 -1
- package/src/ui/badge/types.ts +1 -0
- package/src/ui/badge/variants.ts +7 -7
- package/src/ui/buttons/button.test.tsx +1 -1
- package/src/ui/buttons/variants.ts +8 -8
- package/src/ui/drawer/animated/drawer-content-animated.tsx +4 -5
- package/src/ui/drawer/drawer-base.tsx +16 -8
- package/src/ui/drawer/types.ts +1 -0
- package/src/ui/dropdown/dropdown.test.tsx +1 -3
- package/src/ui/dropdown/dropdown.tsx +23 -5
- package/src/ui/dropdown/types.ts +1 -0
- package/src/ui/dropdown/variants.ts +2 -2
- package/src/ui/empty-state/empty-state-base.tsx +9 -1
- package/src/ui/empty-state/types.ts +1 -0
- package/src/ui/file-upload/file-upload.tsx +0 -2
- package/src/ui/inputs/input-base.tsx +60 -6
- package/src/ui/inputs/types.ts +3 -1
- package/src/ui/modal/animated/modal-content-animated.tsx +4 -5
- package/src/ui/modal/modal-base.tsx +19 -9
- package/src/ui/modal/modal.test.tsx +38 -0
- package/src/ui/pagination/pagination.tsx +2 -2
- package/src/ui/progress/animated/progress-animated.tsx +42 -3
- package/src/ui/progress/progress-base.tsx +59 -3
- package/src/ui/progress/types.ts +3 -0
- package/src/ui/search/search-bar.tsx +5 -0
- package/src/ui/select/select.tsx +97 -6
- package/src/ui/select/types.ts +1 -0
- package/src/ui/select/variants.ts +5 -3
- package/src/ui/slider/slider.test.tsx +25 -1
- package/src/ui/slider/slider.tsx +45 -4
- package/src/ui/slider/types.ts +8 -2
- package/src/ui/spinner/animated/spinner.tsx +4 -0
- package/src/ui/stepper/stepper.test.tsx +6 -7
- package/src/ui/stepper/stepper.tsx +11 -10
- package/src/ui/stepper/types.ts +7 -3
- package/src/ui/table/table-base.tsx +32 -6
- package/src/ui/table/types.ts +8 -1
- package/src/ui/tabs/tabs-base.tsx +71 -10
- package/src/ui/tabs/types.ts +2 -1
- package/src/ui/tabs/variants.ts +1 -1
- package/src/ui/toast/toast-base.tsx +1 -1
- package/src/ui/toggle/toggle-base.tsx +37 -4
- package/src/ui/tooltip/tooltip-base.tsx +119 -22
- package/src/ui/tooltip/types.ts +1 -0
- package/src/ui/tooltip/variants.ts +2 -2
- package/dist/chunk-2PQEXQVR.js.map +0 -1
- package/dist/chunk-3OR47XMY.js.map +0 -1
- package/dist/chunk-5QB2KNZQ.js.map +0 -1
- package/dist/chunk-7HL3A4YF.mjs.map +0 -1
- package/dist/chunk-BVXTOEBI.mjs.map +0 -1
- package/dist/chunk-E3DZNJAD.js.map +0 -1
- package/dist/chunk-IXDJ3IPG.mjs.map +0 -1
- package/dist/chunk-N4NO3SYL.js.map +0 -1
- package/dist/chunk-P5HUBXUX.js.map +0 -1
- package/dist/chunk-PGH27VTL.mjs.map +0 -1
- package/dist/chunk-RDSPHBHK.mjs.map +0 -1
- package/dist/chunk-WDCIZHXY.mjs.map +0 -1
- package/dist/chunk-WL5I7RVS.mjs +0 -54
- package/dist/chunk-WL5I7RVS.mjs.map +0 -1
- package/dist/chunk-WZKGRU3U.js.map +0 -1
- package/dist/chunk-XLAFQ24R.js.map +0 -1
- package/dist/chunk-XWM2S6VV.mjs.map +0 -1
- package/dist/chunk-YNCD6TKE.mjs.map +0 -1
- package/dist/chunk-YPLVTUYL.js +0 -56
- package/dist/chunk-YPLVTUYL.js.map +0 -1
|
@@ -62,6 +62,7 @@ export function Drawer({
|
|
|
62
62
|
const titleId = `${baseId}-title`;
|
|
63
63
|
const descriptionId = `${baseId}-description`;
|
|
64
64
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
65
|
+
const triggerRef = useRef<HTMLElement | null>(null);
|
|
65
66
|
|
|
66
67
|
const ctx = useMemo(
|
|
67
68
|
() => ({
|
|
@@ -70,6 +71,7 @@ export function Drawer({
|
|
|
70
71
|
titleId,
|
|
71
72
|
descriptionId,
|
|
72
73
|
contentRef,
|
|
74
|
+
triggerRef,
|
|
73
75
|
}),
|
|
74
76
|
[descriptionId, resolvedOpen, setOpen, titleId],
|
|
75
77
|
);
|
|
@@ -86,13 +88,20 @@ export function DrawerTrigger({
|
|
|
86
88
|
children,
|
|
87
89
|
appearance,
|
|
88
90
|
onClick,
|
|
89
|
-
ref,
|
|
91
|
+
ref: refProp,
|
|
90
92
|
...rest
|
|
91
93
|
}: DrawerTriggerProps) {
|
|
92
|
-
const { setOpen } = useDrawerContext("DrawerTrigger");
|
|
94
|
+
const { setOpen, triggerRef } = useDrawerContext("DrawerTrigger");
|
|
93
95
|
return (
|
|
94
96
|
<button
|
|
95
|
-
ref={
|
|
97
|
+
ref={(node) => {
|
|
98
|
+
triggerRef.current = node;
|
|
99
|
+
if (typeof refProp === "function") {
|
|
100
|
+
refProp(node);
|
|
101
|
+
} else if (refProp) {
|
|
102
|
+
(refProp as RefObject<HTMLButtonElement | null>).current = node;
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
96
105
|
type="button"
|
|
97
106
|
data-slot="drawer-trigger"
|
|
98
107
|
className={cn(drawerTriggerVariants({ appearance }), className)}
|
|
@@ -121,7 +130,7 @@ export function DrawerContent({
|
|
|
121
130
|
id,
|
|
122
131
|
style,
|
|
123
132
|
}: DrawerContentProps) {
|
|
124
|
-
const { open, setOpen, titleId, descriptionId, contentRef } =
|
|
133
|
+
const { open, setOpen, titleId, descriptionId, contentRef, triggerRef } =
|
|
125
134
|
useDrawerContext("DrawerContent");
|
|
126
135
|
const resolvedSide = side ?? "right";
|
|
127
136
|
|
|
@@ -129,6 +138,7 @@ export function DrawerContent({
|
|
|
129
138
|
open,
|
|
130
139
|
setOpen,
|
|
131
140
|
contentRef,
|
|
141
|
+
triggerRef,
|
|
132
142
|
});
|
|
133
143
|
|
|
134
144
|
const portalTarget = typeof document !== "undefined" ? document.body : null;
|
|
@@ -139,10 +149,8 @@ export function DrawerContent({
|
|
|
139
149
|
return createPortal(
|
|
140
150
|
open ? (
|
|
141
151
|
<div className="fixed inset-0 z-50" data-slot="drawer-portal">
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
aria-hidden
|
|
145
|
-
tabIndex={-1}
|
|
152
|
+
<div
|
|
153
|
+
role="presentation"
|
|
146
154
|
data-slot="drawer-overlay"
|
|
147
155
|
className={drawerOverlayVariants()}
|
|
148
156
|
onClick={() => setOpen(false)}
|
package/src/ui/drawer/types.ts
CHANGED
|
@@ -104,9 +104,7 @@ describe("Dropdown", () => {
|
|
|
104
104
|
</DropdownContent>
|
|
105
105
|
</Dropdown>,
|
|
106
106
|
);
|
|
107
|
-
const item = screen
|
|
108
|
-
.getByText("Alpha")
|
|
109
|
-
.closest('div[tabindex="0"]') as HTMLElement;
|
|
107
|
+
const item = screen.getByRole("menuitem", { name: "Alpha" });
|
|
110
108
|
item.focus();
|
|
111
109
|
fireEvent.keyDown(item, { key: "Enter" });
|
|
112
110
|
expect(handleSelect).toHaveBeenCalledTimes(1);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { createContext, useContext, useState, useRef, useEffect } from "react";
|
|
3
|
+
import { createContext, useContext, useState, useRef, useEffect, useId } from "react";
|
|
4
4
|
import { FiCheck } from "react-icons/fi";
|
|
5
5
|
import { cn } from "../../lib/utils";
|
|
6
6
|
import type {
|
|
@@ -34,6 +34,7 @@ export const Dropdown = ({
|
|
|
34
34
|
onOpenChange,
|
|
35
35
|
multiSelect = false,
|
|
36
36
|
}: DropdownProps) => {
|
|
37
|
+
const menuId = `${useId()}-menu`;
|
|
37
38
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
38
39
|
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
|
39
40
|
|
|
@@ -70,6 +71,7 @@ export const Dropdown = ({
|
|
|
70
71
|
selectedValues,
|
|
71
72
|
toggleSelect,
|
|
72
73
|
multiSelect,
|
|
74
|
+
menuId,
|
|
73
75
|
}}
|
|
74
76
|
>
|
|
75
77
|
<div className="relative inline-block">{children}</div>
|
|
@@ -85,15 +87,25 @@ export const DropdownTrigger = ({
|
|
|
85
87
|
className,
|
|
86
88
|
variant,
|
|
87
89
|
size,
|
|
90
|
+
onClick,
|
|
88
91
|
...props
|
|
89
92
|
}: DropdownTriggerProps) => {
|
|
90
|
-
const { toggle } = useDropdown();
|
|
93
|
+
const { toggle, open, menuId } = useDropdown();
|
|
91
94
|
|
|
92
95
|
return (
|
|
93
96
|
<button
|
|
94
|
-
|
|
97
|
+
type="button"
|
|
98
|
+
aria-expanded={open}
|
|
99
|
+
aria-haspopup="menu"
|
|
100
|
+
aria-controls={menuId}
|
|
95
101
|
className={cn(triggerVariants({ variant, size }), className)}
|
|
96
102
|
{...props}
|
|
103
|
+
onClick={(event) => {
|
|
104
|
+
onClick?.(event);
|
|
105
|
+
if (!event.defaultPrevented) {
|
|
106
|
+
toggle();
|
|
107
|
+
}
|
|
108
|
+
}}
|
|
97
109
|
>
|
|
98
110
|
{children}
|
|
99
111
|
</button>
|
|
@@ -111,7 +123,7 @@ export const DropdownContent = ({
|
|
|
111
123
|
divider,
|
|
112
124
|
...props
|
|
113
125
|
}: DropdownContentProps) => {
|
|
114
|
-
const { open, setOpen } = useDropdown();
|
|
126
|
+
const { open, setOpen, menuId } = useDropdown();
|
|
115
127
|
const ref = useRef<HTMLDivElement>(null);
|
|
116
128
|
|
|
117
129
|
// click outside
|
|
@@ -122,6 +134,8 @@ export const DropdownContent = ({
|
|
|
122
134
|
return (
|
|
123
135
|
<div
|
|
124
136
|
ref={ref}
|
|
137
|
+
id={menuId}
|
|
138
|
+
role="menu"
|
|
125
139
|
className={cn(
|
|
126
140
|
contentVariants({ placement, spacing }),
|
|
127
141
|
className,
|
|
@@ -157,10 +171,14 @@ export const DropdownItem = ({
|
|
|
157
171
|
|
|
158
172
|
return (
|
|
159
173
|
<div
|
|
174
|
+
role="menuitem"
|
|
160
175
|
tabIndex={0}
|
|
161
176
|
onClick={handleClick}
|
|
162
177
|
onKeyDown={(e) => {
|
|
163
|
-
if (e.key === "Enter")
|
|
178
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
handleClick();
|
|
181
|
+
}
|
|
164
182
|
}}
|
|
165
183
|
className={cn(itemVariants({ variant }), className)}
|
|
166
184
|
{...props}
|
package/src/ui/dropdown/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cva } from "class-variance-authority";
|
|
2
2
|
|
|
3
3
|
export const triggerVariants = cva(
|
|
4
|
-
"inline-flex items-center justify-between rounded-md font-medium transition focus:outline-none cursor-pointer",
|
|
4
|
+
"inline-flex items-center justify-between rounded-md font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500/60 cursor-pointer",
|
|
5
5
|
{
|
|
6
6
|
variants: {
|
|
7
7
|
variant: {
|
|
@@ -81,7 +81,7 @@ export const contentVariants = cva(
|
|
|
81
81
|
);
|
|
82
82
|
|
|
83
83
|
export const itemVariants = cva(
|
|
84
|
-
"flex items-center justify-between px-3 py-2 text-sm cursor-pointer rounded-md transition-colors",
|
|
84
|
+
"flex items-center justify-between px-3 py-2 text-sm cursor-pointer rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500/60",
|
|
85
85
|
{
|
|
86
86
|
variants: {
|
|
87
87
|
variant: {
|
|
@@ -25,17 +25,25 @@ export function EmptyStateBase(props: EmptyStateProps) {
|
|
|
25
25
|
align,
|
|
26
26
|
children,
|
|
27
27
|
ref,
|
|
28
|
+
liveRegion = false,
|
|
28
29
|
as: Wrapper = "section",
|
|
29
30
|
...rest
|
|
30
31
|
} = props;
|
|
31
32
|
const ctx = useMemo(() => size ?? "md", [size]);
|
|
32
33
|
|
|
34
|
+
const liveAria =
|
|
35
|
+
typeof liveRegion === "string"
|
|
36
|
+
? liveRegion
|
|
37
|
+
: liveRegion === true
|
|
38
|
+
? "polite"
|
|
39
|
+
: undefined;
|
|
40
|
+
|
|
33
41
|
return (
|
|
34
42
|
<EmptyStateSizeContext.Provider value={ctx}>
|
|
35
43
|
<Wrapper
|
|
36
44
|
ref={ref}
|
|
37
45
|
data-slot="empty-state"
|
|
38
|
-
aria-live=
|
|
46
|
+
aria-live={liveAria}
|
|
39
47
|
className={cn(
|
|
40
48
|
emptyStateVariants({ size, appearance, align }),
|
|
41
49
|
className,
|
|
@@ -8,6 +8,7 @@ type EmptyStateVariantProps = VariantProps<typeof emptyStateVariants>;
|
|
|
8
8
|
export type EmptyStateProps = EmptyStateVariantProps &
|
|
9
9
|
(Omit<ComponentPropsWithRef<"section">, "children"> & {
|
|
10
10
|
children?: ReactNode;
|
|
11
|
+
liveRegion?: false | true | "polite" | "assertive";
|
|
11
12
|
as?: ElementType;
|
|
12
13
|
});
|
|
13
14
|
|
|
@@ -7,6 +7,18 @@ import { cn } from "../../lib/utils";
|
|
|
7
7
|
import type { InputProps } from "./types";
|
|
8
8
|
import { inputVariants } from "./variants";
|
|
9
9
|
|
|
10
|
+
function mergeDescribedByIds(
|
|
11
|
+
user: string | undefined,
|
|
12
|
+
...generated: (string | undefined)[]
|
|
13
|
+
): string | undefined {
|
|
14
|
+
const ids = [
|
|
15
|
+
...(user ?? "").split(/\s+/).filter(Boolean),
|
|
16
|
+
...generated.filter((id): id is string => Boolean(id)),
|
|
17
|
+
];
|
|
18
|
+
const unique = [...new Set(ids)];
|
|
19
|
+
return unique.length > 0 ? unique.join(" ") : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
export const InputBase = (props: InputProps) => {
|
|
11
23
|
const generatedId = useId();
|
|
12
24
|
|
|
@@ -19,13 +31,17 @@ export const InputBase = (props: InputProps) => {
|
|
|
19
31
|
ref,
|
|
20
32
|
"aria-invalid": ariaInvalidProp,
|
|
21
33
|
errorMessage,
|
|
34
|
+
hint,
|
|
35
|
+
label,
|
|
22
36
|
id,
|
|
23
37
|
as,
|
|
38
|
+
"aria-describedby": ariaDescribedByUser,
|
|
24
39
|
...rest
|
|
25
40
|
} = props;
|
|
26
41
|
|
|
27
42
|
const controlId = id ?? generatedId;
|
|
28
43
|
const errorId = `${controlId}-error`;
|
|
44
|
+
const hintId = `${controlId}-hint`;
|
|
29
45
|
const ariaInvalid =
|
|
30
46
|
ariaInvalidProp !== undefined
|
|
31
47
|
? ariaInvalidProp
|
|
@@ -33,8 +49,27 @@ export const InputBase = (props: InputProps) => {
|
|
|
33
49
|
? true
|
|
34
50
|
: undefined;
|
|
35
51
|
|
|
52
|
+
const describedBy = mergeDescribedByIds(
|
|
53
|
+
ariaDescribedByUser,
|
|
54
|
+
hint !== undefined ? hintId : undefined,
|
|
55
|
+
errorMessage && appearance === "error" ? errorId : undefined,
|
|
56
|
+
);
|
|
57
|
+
|
|
36
58
|
return (
|
|
37
59
|
<>
|
|
60
|
+
{label !== undefined && (
|
|
61
|
+
<label
|
|
62
|
+
htmlFor={controlId}
|
|
63
|
+
className="mb-1 block text-sm font-medium text-slate-200"
|
|
64
|
+
>
|
|
65
|
+
{label}
|
|
66
|
+
</label>
|
|
67
|
+
)}
|
|
68
|
+
{hint !== undefined && (
|
|
69
|
+
<p id={hintId} className="mb-1 text-xs text-slate-400">
|
|
70
|
+
{hint}
|
|
71
|
+
</p>
|
|
72
|
+
)}
|
|
38
73
|
<textarea
|
|
39
74
|
ref={ref}
|
|
40
75
|
id={controlId}
|
|
@@ -44,9 +79,7 @@ export const InputBase = (props: InputProps) => {
|
|
|
44
79
|
className,
|
|
45
80
|
)}
|
|
46
81
|
aria-invalid={ariaInvalid}
|
|
47
|
-
aria-describedby={
|
|
48
|
-
errorMessage && appearance === "error" ? errorId : undefined
|
|
49
|
-
}
|
|
82
|
+
aria-describedby={describedBy}
|
|
50
83
|
{...rest}
|
|
51
84
|
/>
|
|
52
85
|
{errorMessage && appearance === "error" && (
|
|
@@ -69,13 +102,17 @@ export const InputBase = (props: InputProps) => {
|
|
|
69
102
|
ref,
|
|
70
103
|
"aria-invalid": ariaInvalidProp,
|
|
71
104
|
errorMessage,
|
|
105
|
+
hint,
|
|
106
|
+
label,
|
|
72
107
|
id,
|
|
73
108
|
as,
|
|
109
|
+
"aria-describedby": ariaDescribedByUser,
|
|
74
110
|
...rest
|
|
75
111
|
} = props;
|
|
76
112
|
|
|
77
113
|
const controlId = id ?? generatedId;
|
|
78
114
|
const errorId = `${controlId}-error`;
|
|
115
|
+
const hintId = `${controlId}-hint`;
|
|
79
116
|
const ariaInvalid =
|
|
80
117
|
ariaInvalidProp !== undefined
|
|
81
118
|
? ariaInvalidProp
|
|
@@ -83,8 +120,27 @@ export const InputBase = (props: InputProps) => {
|
|
|
83
120
|
? true
|
|
84
121
|
: undefined;
|
|
85
122
|
|
|
123
|
+
const describedBy = mergeDescribedByIds(
|
|
124
|
+
ariaDescribedByUser,
|
|
125
|
+
hint !== undefined ? hintId : undefined,
|
|
126
|
+
errorMessage && appearance === "error" ? errorId : undefined,
|
|
127
|
+
);
|
|
128
|
+
|
|
86
129
|
return (
|
|
87
130
|
<>
|
|
131
|
+
{label !== undefined && (
|
|
132
|
+
<label
|
|
133
|
+
htmlFor={controlId}
|
|
134
|
+
className="mb-1 block text-sm font-medium text-slate-200"
|
|
135
|
+
>
|
|
136
|
+
{label}
|
|
137
|
+
</label>
|
|
138
|
+
)}
|
|
139
|
+
{hint !== undefined && (
|
|
140
|
+
<p id={hintId} className="mb-1 text-xs text-slate-400">
|
|
141
|
+
{hint}
|
|
142
|
+
</p>
|
|
143
|
+
)}
|
|
88
144
|
<input
|
|
89
145
|
ref={ref}
|
|
90
146
|
id={controlId}
|
|
@@ -94,9 +150,7 @@ export const InputBase = (props: InputProps) => {
|
|
|
94
150
|
className,
|
|
95
151
|
)}
|
|
96
152
|
aria-invalid={ariaInvalid}
|
|
97
|
-
aria-describedby={
|
|
98
|
-
errorMessage && appearance === "error" ? errorId : undefined
|
|
99
|
-
}
|
|
153
|
+
aria-describedby={describedBy}
|
|
100
154
|
{...rest}
|
|
101
155
|
/>
|
|
102
156
|
{errorMessage && appearance === "error" && (
|
package/src/ui/inputs/types.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { VariantProps } from "class-variance-authority";
|
|
2
|
-
import type { ComponentPropsWithRef } from "react";
|
|
2
|
+
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
import type { inputVariants } from "./variants";
|
|
5
5
|
|
|
6
6
|
export type InputSharedProps = Omit<VariantProps<typeof inputVariants>, "as"> & {
|
|
7
7
|
errorMessage?: string;
|
|
8
|
+
hint?: ReactNode;
|
|
9
|
+
label?: ReactNode;
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
export type InputProps =
|
|
@@ -22,7 +22,7 @@ export function ModalContentAnimated({
|
|
|
22
22
|
id,
|
|
23
23
|
style,
|
|
24
24
|
}: ModalContentAnimatedProps) {
|
|
25
|
-
const { open, setOpen, titleId, descriptionId, contentRef } =
|
|
25
|
+
const { open, setOpen, titleId, descriptionId, contentRef, triggerRef } =
|
|
26
26
|
useModalContext("ModalContent");
|
|
27
27
|
const reduceMotion = useReducedMotion();
|
|
28
28
|
const overlayMotion =
|
|
@@ -34,6 +34,7 @@ export function ModalContentAnimated({
|
|
|
34
34
|
open,
|
|
35
35
|
setOpen,
|
|
36
36
|
contentRef,
|
|
37
|
+
triggerRef,
|
|
37
38
|
});
|
|
38
39
|
|
|
39
40
|
const portalTarget = typeof document !== "undefined" ? document.body : null;
|
|
@@ -46,10 +47,8 @@ export function ModalContentAnimated({
|
|
|
46
47
|
<AnimatePresence>
|
|
47
48
|
{open ? (
|
|
48
49
|
<div className="fixed inset-0 z-50" data-slot="modal-portal">
|
|
49
|
-
<motion.
|
|
50
|
-
|
|
51
|
-
aria-hidden
|
|
52
|
-
tabIndex={-1}
|
|
50
|
+
<motion.div
|
|
51
|
+
role="presentation"
|
|
53
52
|
data-slot="modal-overlay"
|
|
54
53
|
className={modalOverlayVariants()}
|
|
55
54
|
onClick={() => setOpen(false)}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useMemo,
|
|
9
9
|
useRef,
|
|
10
10
|
useState,
|
|
11
|
+
type RefObject,
|
|
11
12
|
} from "react";
|
|
12
13
|
import { createPortal } from "react-dom";
|
|
13
14
|
|
|
@@ -31,7 +32,8 @@ type ModalCtx = {
|
|
|
31
32
|
setOpen: (next: boolean) => void;
|
|
32
33
|
titleId: string;
|
|
33
34
|
descriptionId: string;
|
|
34
|
-
contentRef:
|
|
35
|
+
contentRef: RefObject<HTMLDivElement | null>;
|
|
36
|
+
triggerRef: RefObject<HTMLElement | null>;
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
const ModalContext = createContext<ModalCtx | null>(null);
|
|
@@ -68,6 +70,7 @@ export function Modal({
|
|
|
68
70
|
const titleId = `${baseId}-title`;
|
|
69
71
|
const descriptionId = `${baseId}-description`;
|
|
70
72
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
73
|
+
const triggerRef = useRef<HTMLElement | null>(null);
|
|
71
74
|
|
|
72
75
|
const ctx = useMemo(
|
|
73
76
|
() => ({
|
|
@@ -76,6 +79,7 @@ export function Modal({
|
|
|
76
79
|
titleId,
|
|
77
80
|
descriptionId,
|
|
78
81
|
contentRef,
|
|
82
|
+
triggerRef,
|
|
79
83
|
}),
|
|
80
84
|
[descriptionId, resolvedOpen, setOpen, titleId],
|
|
81
85
|
);
|
|
@@ -90,13 +94,20 @@ export function ModalTrigger({
|
|
|
90
94
|
children,
|
|
91
95
|
appearance,
|
|
92
96
|
onClick,
|
|
93
|
-
ref,
|
|
97
|
+
ref: refProp,
|
|
94
98
|
...rest
|
|
95
99
|
}: ModalTriggerProps) {
|
|
96
|
-
const { setOpen } = useModalContext("ModalTrigger");
|
|
100
|
+
const { setOpen, triggerRef } = useModalContext("ModalTrigger");
|
|
97
101
|
return (
|
|
98
102
|
<button
|
|
99
|
-
ref={
|
|
103
|
+
ref={(node) => {
|
|
104
|
+
triggerRef.current = node;
|
|
105
|
+
if (typeof refProp === "function") {
|
|
106
|
+
refProp(node);
|
|
107
|
+
} else if (refProp) {
|
|
108
|
+
(refProp as RefObject<HTMLButtonElement | null>).current = node;
|
|
109
|
+
}
|
|
110
|
+
}}
|
|
100
111
|
type="button"
|
|
101
112
|
data-slot="modal-trigger"
|
|
102
113
|
className={cn(modalTriggerVariants({ appearance }), className)}
|
|
@@ -126,13 +137,14 @@ export function ModalContent({
|
|
|
126
137
|
id,
|
|
127
138
|
style,
|
|
128
139
|
}: ModalContentProps) {
|
|
129
|
-
const { open, setOpen, titleId, descriptionId, contentRef } =
|
|
140
|
+
const { open, setOpen, titleId, descriptionId, contentRef, triggerRef } =
|
|
130
141
|
useModalContext("ModalContent");
|
|
131
142
|
|
|
132
143
|
useFocusManagement({
|
|
133
144
|
open,
|
|
134
145
|
setOpen,
|
|
135
146
|
contentRef,
|
|
147
|
+
triggerRef,
|
|
136
148
|
});
|
|
137
149
|
|
|
138
150
|
const portalTarget = typeof document !== "undefined" ? document.body : null;
|
|
@@ -144,10 +156,8 @@ export function ModalContent({
|
|
|
144
156
|
return createPortal(
|
|
145
157
|
open ? (
|
|
146
158
|
<div className="fixed inset-0 z-50" data-slot="modal-portal">
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
aria-hidden
|
|
150
|
-
tabIndex={-1}
|
|
159
|
+
<div
|
|
160
|
+
role="presentation"
|
|
151
161
|
data-slot="modal-overlay"
|
|
152
162
|
className={modalOverlayVariants()}
|
|
153
163
|
onClick={() => setOpen(false)}
|
|
@@ -77,6 +77,21 @@ describe("Modal", () => {
|
|
|
77
77
|
});
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
it("should render overlay as non-focusable presentation surface", async () => {
|
|
81
|
+
render(
|
|
82
|
+
<Modal defaultOpen>
|
|
83
|
+
<ModalContent>
|
|
84
|
+
<ModalTitle>T</ModalTitle>
|
|
85
|
+
</ModalContent>
|
|
86
|
+
</Modal>,
|
|
87
|
+
);
|
|
88
|
+
await waitFor(() => expect(screen.getByRole("dialog")).toBeInTheDocument());
|
|
89
|
+
const overlay = document.querySelector('[data-slot="modal-overlay"]');
|
|
90
|
+
expect(overlay?.tagName.toLowerCase()).toBe("div");
|
|
91
|
+
expect(overlay).toHaveAttribute("role", "presentation");
|
|
92
|
+
expect((overlay as HTMLElement).tabIndex).toBe(-1);
|
|
93
|
+
});
|
|
94
|
+
|
|
80
95
|
it("should close when Escape is pressed", async () => {
|
|
81
96
|
const user = userEvent.setup();
|
|
82
97
|
render(
|
|
@@ -126,4 +141,27 @@ describe("Modal", () => {
|
|
|
126
141
|
await user.keyboard("{Escape}");
|
|
127
142
|
await waitFor(() => expect(handleChange).toHaveBeenLastCalledWith(false));
|
|
128
143
|
});
|
|
144
|
+
|
|
145
|
+
it("should restore focus to the trigger after the dialog closes", async () => {
|
|
146
|
+
const user = userEvent.setup();
|
|
147
|
+
render(
|
|
148
|
+
<Modal>
|
|
149
|
+
<ModalTrigger>Open</ModalTrigger>
|
|
150
|
+
<ModalContent>
|
|
151
|
+
<ModalTitle>T</ModalTitle>
|
|
152
|
+
</ModalContent>
|
|
153
|
+
</Modal>,
|
|
154
|
+
);
|
|
155
|
+
const trigger = screen.getByRole("button", { name: "Open" });
|
|
156
|
+
trigger.focus();
|
|
157
|
+
await user.click(trigger);
|
|
158
|
+
await waitFor(() =>
|
|
159
|
+
expect(screen.getByRole("dialog")).toBeInTheDocument(),
|
|
160
|
+
);
|
|
161
|
+
await user.keyboard("{Escape}");
|
|
162
|
+
await waitFor(() =>
|
|
163
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(),
|
|
164
|
+
);
|
|
165
|
+
expect(trigger).toHaveFocus();
|
|
166
|
+
});
|
|
129
167
|
});
|
|
@@ -192,9 +192,9 @@ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
|
|
|
192
192
|
{item.type === "ellipsis" ? (
|
|
193
193
|
<span
|
|
194
194
|
data-slot="pagination-ellipsis"
|
|
195
|
-
aria-hidden="true"
|
|
196
|
-
title={ellipsisLabel}
|
|
197
195
|
className={paginationEllipsisVariants({ size })}
|
|
196
|
+
role="img"
|
|
197
|
+
aria-label={ellipsisLabel}
|
|
198
198
|
>
|
|
199
199
|
…
|
|
200
200
|
</span>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useMemo } from "react";
|
|
3
|
+
import { useCallback, useId, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { motion } from "framer-motion";
|
|
5
5
|
|
|
6
6
|
import { cn, clamp } from "../../../lib/utils";
|
|
@@ -39,6 +39,32 @@ export function ProgressAnimated({
|
|
|
39
39
|
}: ProgressAnimatedProps) {
|
|
40
40
|
const clamped = clamp(value, min, max);
|
|
41
41
|
const percent = max === min ? 0 : ((clamped - min) / (max - min)) * 100;
|
|
42
|
+
const labelSlotId = `${useId()}-progress-label`;
|
|
43
|
+
const labelSlotCountRef = useRef(0);
|
|
44
|
+
const [labelSlotMounted, setLabelSlotMounted] = useState(false);
|
|
45
|
+
const registerProgressLabel = useCallback(() => {
|
|
46
|
+
labelSlotCountRef.current += 1;
|
|
47
|
+
if (labelSlotCountRef.current === 1) {
|
|
48
|
+
setLabelSlotMounted(true);
|
|
49
|
+
}
|
|
50
|
+
return () => {
|
|
51
|
+
labelSlotCountRef.current -= 1;
|
|
52
|
+
if (labelSlotCountRef.current === 0) {
|
|
53
|
+
setLabelSlotMounted(false);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}, []);
|
|
57
|
+
const hasInlineLabelProp = Boolean(label?.trim().length);
|
|
58
|
+
|
|
59
|
+
const labelingProps = useMemo(() => {
|
|
60
|
+
if (hasInlineLabelProp) {
|
|
61
|
+
return { "aria-label": label?.trim() ?? "Progress" };
|
|
62
|
+
}
|
|
63
|
+
if (labelSlotMounted) {
|
|
64
|
+
return { "aria-labelledby": labelSlotId };
|
|
65
|
+
}
|
|
66
|
+
return { "aria-label": "Progress" };
|
|
67
|
+
}, [hasInlineLabelProp, label, labelSlotId, labelSlotMounted]);
|
|
42
68
|
|
|
43
69
|
const ctx = useMemo<ProgressCtx>(
|
|
44
70
|
() => ({
|
|
@@ -50,8 +76,21 @@ export function ProgressAnimated({
|
|
|
50
76
|
striped: Boolean(striped),
|
|
51
77
|
animated: Boolean(animated),
|
|
52
78
|
appearance: appearance ?? "default",
|
|
79
|
+
labelSlotId,
|
|
80
|
+
registerProgressLabel,
|
|
53
81
|
}),
|
|
54
|
-
[
|
|
82
|
+
[
|
|
83
|
+
animated,
|
|
84
|
+
appearance,
|
|
85
|
+
clamped,
|
|
86
|
+
labelSlotId,
|
|
87
|
+
max,
|
|
88
|
+
min,
|
|
89
|
+
registerProgressLabel,
|
|
90
|
+
shape,
|
|
91
|
+
size,
|
|
92
|
+
striped,
|
|
93
|
+
],
|
|
55
94
|
);
|
|
56
95
|
|
|
57
96
|
const motionProps = progressAnimationPresets[animation];
|
|
@@ -65,8 +104,8 @@ export function ProgressAnimated({
|
|
|
65
104
|
aria-valuemin={min}
|
|
66
105
|
aria-valuemax={max}
|
|
67
106
|
aria-valuenow={clamped}
|
|
68
|
-
aria-label={label}
|
|
69
107
|
aria-busy={busy ? true : undefined}
|
|
108
|
+
{...labelingProps}
|
|
70
109
|
className={cn(
|
|
71
110
|
progressVariants({ appearance, size, shape, striped, animated }),
|
|
72
111
|
className,
|