@seed-design/react-accordion 0.0.0-alpha-20260511052324
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/lib/Accordion-12s-BgNhuqRa.js +356 -0
- package/lib/Accordion-12s-D0hof3B_.cjs +366 -0
- package/lib/index.cjs +23 -0
- package/lib/index.d.ts +2072 -0
- package/lib/index.js +13 -0
- package/package.json +48 -0
- package/src/Accordion.namespace.ts +12 -0
- package/src/Accordion.test.tsx +304 -0
- package/src/Accordion.tsx +127 -0
- package/src/dom.ts +32 -0
- package/src/index.ts +38 -0
- package/src/useAccordion.test.tsx +181 -0
- package/src/useAccordion.ts +142 -0
- package/src/useAccordionContext.tsx +20 -0
- package/src/useAccordionItem.ts +140 -0
- package/src/useAccordionItemContext.tsx +20 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { elementProps, dataAttr, mergeProps } from '@seed-design/dom-utils';
|
|
4
|
+
import { Primitive } from '@seed-design/react-primitive';
|
|
5
|
+
import { composeRefs } from '@radix-ui/react-compose-refs';
|
|
6
|
+
import { useId, useCallback, useMemo, createContext, useContext, forwardRef } from 'react';
|
|
7
|
+
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
8
|
+
import { useCollapsible } from '@seed-design/react-collapsible';
|
|
9
|
+
|
|
10
|
+
const getRootId = (id)=>`accordion:${id}:root`;
|
|
11
|
+
const getTriggerId = (value, id)=>`accordion:${value}:${id}:trigger`;
|
|
12
|
+
const getContentId = (value, id)=>`accordion:${value}:${id}:content`;
|
|
13
|
+
function getRootElement(rootId) {
|
|
14
|
+
return document.querySelector(`[data-accordion-root="${rootId}"]`);
|
|
15
|
+
}
|
|
16
|
+
function getTriggerElements(rootId) {
|
|
17
|
+
const rootEl = getRootElement(rootId);
|
|
18
|
+
if (!rootEl) return [];
|
|
19
|
+
return Array.from(rootEl.querySelectorAll(`[data-ownedby="${rootId}"][data-value]`));
|
|
20
|
+
}
|
|
21
|
+
const getEnabledTriggerElements = (rootId)=>{
|
|
22
|
+
return getTriggerElements(rootId).filter((trigger)=>{
|
|
23
|
+
return !trigger.hasAttribute("disabled") && trigger.getAttribute("aria-disabled") !== "true";
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
const getEnabledValues = (rootId)=>{
|
|
27
|
+
return getEnabledTriggerElements(rootId).map((trigger)=>trigger.getAttribute("data-value")).filter((value)=>value != null);
|
|
28
|
+
};
|
|
29
|
+
const getTriggerByValue = (rootId, value)=>{
|
|
30
|
+
return getEnabledTriggerElements(rootId).find((trigger)=>{
|
|
31
|
+
return trigger.getAttribute("data-value") === value;
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function useAccordion(props) {
|
|
36
|
+
const accordionId = useId();
|
|
37
|
+
const isMultiple = props.multiple === true;
|
|
38
|
+
const disabled = props.disabled ?? false;
|
|
39
|
+
const rootId = getRootId(accordionId);
|
|
40
|
+
const [rawValues, setValues] = useControllableState({
|
|
41
|
+
prop: props.values,
|
|
42
|
+
defaultProp: props.defaultValues ?? [],
|
|
43
|
+
onChange: props.onValuesChange
|
|
44
|
+
});
|
|
45
|
+
const values = isMultiple ? rawValues : rawValues.slice(0, 1);
|
|
46
|
+
const isOpen = useCallback((itemValue)=>values.includes(itemValue), [
|
|
47
|
+
values
|
|
48
|
+
]);
|
|
49
|
+
const getEnabledValues$1 = useCallback(()=>{
|
|
50
|
+
return getEnabledValues(rootId);
|
|
51
|
+
}, [
|
|
52
|
+
rootId
|
|
53
|
+
]);
|
|
54
|
+
const focusTriggerByValue = useCallback((value)=>{
|
|
55
|
+
getTriggerByValue(rootId, value)?.focus();
|
|
56
|
+
}, [
|
|
57
|
+
rootId
|
|
58
|
+
]);
|
|
59
|
+
const focusPrev = useCallback((value)=>{
|
|
60
|
+
const enabledValues = getEnabledValues$1();
|
|
61
|
+
const currentIndex = enabledValues.indexOf(value);
|
|
62
|
+
if (currentIndex === -1) return;
|
|
63
|
+
const prevValue = enabledValues[currentIndex - 1 < 0 ? enabledValues.length - 1 : currentIndex - 1];
|
|
64
|
+
if (!prevValue) return;
|
|
65
|
+
focusTriggerByValue(prevValue);
|
|
66
|
+
}, [
|
|
67
|
+
focusTriggerByValue,
|
|
68
|
+
getEnabledValues$1
|
|
69
|
+
]);
|
|
70
|
+
const focusNext = useCallback((value)=>{
|
|
71
|
+
const enabledValues = getEnabledValues$1();
|
|
72
|
+
const currentIndex = enabledValues.indexOf(value);
|
|
73
|
+
if (currentIndex === -1) return;
|
|
74
|
+
const nextValue = enabledValues[currentIndex + 1 >= enabledValues.length ? 0 : currentIndex + 1];
|
|
75
|
+
if (!nextValue) return;
|
|
76
|
+
focusTriggerByValue(nextValue);
|
|
77
|
+
}, [
|
|
78
|
+
focusTriggerByValue,
|
|
79
|
+
getEnabledValues$1
|
|
80
|
+
]);
|
|
81
|
+
const focusFirst = useCallback(()=>{
|
|
82
|
+
const firstValue = getEnabledValues$1()[0];
|
|
83
|
+
if (!firstValue) return;
|
|
84
|
+
focusTriggerByValue(firstValue);
|
|
85
|
+
}, [
|
|
86
|
+
focusTriggerByValue,
|
|
87
|
+
getEnabledValues$1
|
|
88
|
+
]);
|
|
89
|
+
const focusLast = useCallback(()=>{
|
|
90
|
+
const enabledValues = getEnabledValues$1();
|
|
91
|
+
const lastValue = enabledValues[enabledValues.length - 1];
|
|
92
|
+
if (!lastValue) return;
|
|
93
|
+
focusTriggerByValue(lastValue);
|
|
94
|
+
}, [
|
|
95
|
+
focusTriggerByValue,
|
|
96
|
+
getEnabledValues$1
|
|
97
|
+
]);
|
|
98
|
+
const toggle = useCallback((itemValue)=>{
|
|
99
|
+
if (disabled) return;
|
|
100
|
+
if (!isMultiple) {
|
|
101
|
+
const isCurrentOpen = values[0] === itemValue;
|
|
102
|
+
if (isCurrentOpen) {
|
|
103
|
+
setValues([]);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
setValues([
|
|
107
|
+
itemValue
|
|
108
|
+
]);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
setValues((prev)=>prev.includes(itemValue) ? prev.filter((v)=>v !== itemValue) : [
|
|
112
|
+
...prev,
|
|
113
|
+
itemValue
|
|
114
|
+
]);
|
|
115
|
+
}, [
|
|
116
|
+
disabled,
|
|
117
|
+
isMultiple,
|
|
118
|
+
setValues,
|
|
119
|
+
values
|
|
120
|
+
]);
|
|
121
|
+
return useMemo(()=>({
|
|
122
|
+
accordionId,
|
|
123
|
+
rootId,
|
|
124
|
+
disabled,
|
|
125
|
+
values,
|
|
126
|
+
isOpen,
|
|
127
|
+
toggle,
|
|
128
|
+
focusPrev,
|
|
129
|
+
focusNext,
|
|
130
|
+
focusFirst,
|
|
131
|
+
focusLast,
|
|
132
|
+
rootProps: elementProps({
|
|
133
|
+
id: rootId,
|
|
134
|
+
"data-accordion-root": rootId,
|
|
135
|
+
"data-disabled": dataAttr(disabled)
|
|
136
|
+
})
|
|
137
|
+
}), [
|
|
138
|
+
accordionId,
|
|
139
|
+
disabled,
|
|
140
|
+
focusFirst,
|
|
141
|
+
focusLast,
|
|
142
|
+
focusNext,
|
|
143
|
+
focusPrev,
|
|
144
|
+
isOpen,
|
|
145
|
+
rootId,
|
|
146
|
+
toggle,
|
|
147
|
+
values
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const AccordionContext = /*#__PURE__*/ createContext(null);
|
|
152
|
+
const AccordionProvider = AccordionContext.Provider;
|
|
153
|
+
function useAccordionContext({ strict = true } = {}) {
|
|
154
|
+
const context = useContext(AccordionContext);
|
|
155
|
+
if (!context && strict) {
|
|
156
|
+
throw new Error("useAccordionContext must be used within an AccordionRoot");
|
|
157
|
+
}
|
|
158
|
+
return context;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function useAccordionItem(props) {
|
|
162
|
+
const { value, disabled: itemDisabled } = props;
|
|
163
|
+
const accordion = useAccordionContext();
|
|
164
|
+
const triggerId = getTriggerId(value, accordion.accordionId);
|
|
165
|
+
const contentId = getContentId(value, accordion.accordionId);
|
|
166
|
+
const disabled = itemDisabled || accordion.disabled;
|
|
167
|
+
const open = accordion.isOpen(value);
|
|
168
|
+
const handleOpenChange = useCallback((nextOpen)=>{
|
|
169
|
+
if (nextOpen !== open) {
|
|
170
|
+
accordion.toggle(value);
|
|
171
|
+
}
|
|
172
|
+
}, [
|
|
173
|
+
accordion,
|
|
174
|
+
open,
|
|
175
|
+
value
|
|
176
|
+
]);
|
|
177
|
+
const collapsible = useCollapsible({
|
|
178
|
+
open,
|
|
179
|
+
onOpenChange: handleOpenChange,
|
|
180
|
+
disabled
|
|
181
|
+
});
|
|
182
|
+
const stateProps = useMemo(()=>elementProps({
|
|
183
|
+
"data-disabled": dataAttr(disabled),
|
|
184
|
+
"data-open": dataAttr(open)
|
|
185
|
+
}), [
|
|
186
|
+
disabled,
|
|
187
|
+
open
|
|
188
|
+
]);
|
|
189
|
+
const rootProps = useMemo(()=>mergeProps(collapsible.stateProps, {
|
|
190
|
+
"data-value": value
|
|
191
|
+
}), [
|
|
192
|
+
collapsible.stateProps,
|
|
193
|
+
value
|
|
194
|
+
]);
|
|
195
|
+
const handleTriggerKeyDown = useCallback((event)=>{
|
|
196
|
+
if (event.defaultPrevented) return;
|
|
197
|
+
if (event.nativeEvent.isComposing) return;
|
|
198
|
+
switch(event.key){
|
|
199
|
+
case "ArrowDown":
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
accordion.focusNext(value);
|
|
202
|
+
break;
|
|
203
|
+
case "ArrowUp":
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
accordion.focusPrev(value);
|
|
206
|
+
break;
|
|
207
|
+
case "Home":
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
accordion.focusFirst();
|
|
210
|
+
break;
|
|
211
|
+
case "End":
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
accordion.focusLast();
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}, [
|
|
217
|
+
accordion,
|
|
218
|
+
value
|
|
219
|
+
]);
|
|
220
|
+
const triggerProps = useMemo(()=>mergeProps(stateProps, collapsible.triggerAriaProps, collapsible.triggerHandlers, {
|
|
221
|
+
id: triggerId,
|
|
222
|
+
disabled,
|
|
223
|
+
"data-value": value,
|
|
224
|
+
"data-ownedby": accordion.rootId,
|
|
225
|
+
"aria-controls": contentId,
|
|
226
|
+
onKeyDown: handleTriggerKeyDown
|
|
227
|
+
}), [
|
|
228
|
+
accordion.rootId,
|
|
229
|
+
collapsible.triggerAriaProps,
|
|
230
|
+
collapsible.triggerHandlers,
|
|
231
|
+
contentId,
|
|
232
|
+
disabled,
|
|
233
|
+
handleTriggerKeyDown,
|
|
234
|
+
stateProps,
|
|
235
|
+
triggerId,
|
|
236
|
+
value
|
|
237
|
+
]);
|
|
238
|
+
const contentProps = useMemo(()=>mergeProps(collapsible.contentProps, {
|
|
239
|
+
id: contentId,
|
|
240
|
+
role: "region",
|
|
241
|
+
"aria-labelledby": triggerId
|
|
242
|
+
}), [
|
|
243
|
+
collapsible.contentProps,
|
|
244
|
+
contentId,
|
|
245
|
+
triggerId
|
|
246
|
+
]);
|
|
247
|
+
return useMemo(()=>({
|
|
248
|
+
...collapsible,
|
|
249
|
+
value,
|
|
250
|
+
open,
|
|
251
|
+
disabled,
|
|
252
|
+
triggerId,
|
|
253
|
+
stateProps,
|
|
254
|
+
rootProps,
|
|
255
|
+
triggerProps,
|
|
256
|
+
contentProps
|
|
257
|
+
}), [
|
|
258
|
+
collapsible,
|
|
259
|
+
contentProps,
|
|
260
|
+
disabled,
|
|
261
|
+
open,
|
|
262
|
+
rootProps,
|
|
263
|
+
stateProps,
|
|
264
|
+
triggerId,
|
|
265
|
+
triggerProps,
|
|
266
|
+
value
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const AccordionItemContext = /*#__PURE__*/ createContext(null);
|
|
271
|
+
const AccordionItemProvider = AccordionItemContext.Provider;
|
|
272
|
+
function useAccordionItemContext({ strict = true } = {}) {
|
|
273
|
+
const context = useContext(AccordionItemContext);
|
|
274
|
+
if (!context && strict) {
|
|
275
|
+
throw new Error("useAccordionItemContext must be used within an AccordionItem");
|
|
276
|
+
}
|
|
277
|
+
return context;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const AccordionRoot = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
281
|
+
const { multiple, values, defaultValues, onValuesChange, disabled, ...otherProps } = props;
|
|
282
|
+
const api = useAccordion({
|
|
283
|
+
multiple,
|
|
284
|
+
values,
|
|
285
|
+
defaultValues,
|
|
286
|
+
onValuesChange,
|
|
287
|
+
disabled
|
|
288
|
+
});
|
|
289
|
+
return /*#__PURE__*/ jsx(AccordionProvider, {
|
|
290
|
+
value: api,
|
|
291
|
+
children: /*#__PURE__*/ jsx(Primitive.div, {
|
|
292
|
+
ref: ref,
|
|
293
|
+
...mergeProps(api.rootProps, otherProps)
|
|
294
|
+
})
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
AccordionRoot.displayName = "AccordionRoot";
|
|
298
|
+
const AccordionItem = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
299
|
+
const { value, disabled: itemDisabled, ...rest } = props;
|
|
300
|
+
const itemApi = useAccordionItem({
|
|
301
|
+
value,
|
|
302
|
+
disabled: itemDisabled
|
|
303
|
+
});
|
|
304
|
+
return /*#__PURE__*/ jsx(AccordionItemProvider, {
|
|
305
|
+
value: itemApi,
|
|
306
|
+
children: /*#__PURE__*/ jsx(Primitive.div, {
|
|
307
|
+
ref: ref,
|
|
308
|
+
...mergeProps(rest, itemApi.rootProps)
|
|
309
|
+
})
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
AccordionItem.displayName = "AccordionItem";
|
|
313
|
+
/**
|
|
314
|
+
* `AccordionHeader` wraps the `AccordionTrigger` to provide a semantic heading
|
|
315
|
+
* level for screen readers and document outline.
|
|
316
|
+
*
|
|
317
|
+
* Renders as the requested native heading element. Defaults to `<h3>`.
|
|
318
|
+
*
|
|
319
|
+
* @see https://www.w3.org/WAI/ARIA/apg/patterns/accordion/#wai-ariaroles%2Cstates%2Candproperties
|
|
320
|
+
* — "The title of each accordion header is contained in an element with role `button`.
|
|
321
|
+
* Each accordion header `button` is wrapped in an element with role `heading`..."
|
|
322
|
+
*/ const AccordionHeader = /*#__PURE__*/ forwardRef(({ headingLevel = 3, ...props }, ref)=>{
|
|
323
|
+
const Comp = `h${headingLevel}`;
|
|
324
|
+
return /*#__PURE__*/ jsx(Comp, {
|
|
325
|
+
ref: ref,
|
|
326
|
+
...props
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
AccordionHeader.displayName = "AccordionHeader";
|
|
330
|
+
/**
|
|
331
|
+
* `AccordionTrigger` toggles the open/closed state of an `AccordionItem`.
|
|
332
|
+
* It should always be nested inside of an `AccordionHeader` to preserve the
|
|
333
|
+
* WAI-ARIA accordion pattern (heading > button).
|
|
334
|
+
*
|
|
335
|
+
* Renders as a native `<button>` with `aria-expanded`, `aria-controls`,
|
|
336
|
+
* `aria-disabled` automatically managed via the underlying collapsible.
|
|
337
|
+
*
|
|
338
|
+
* @see https://www.w3.org/WAI/ARIA/apg/patterns/accordion/
|
|
339
|
+
*/ const AccordionTrigger = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
340
|
+
const itemApi = useAccordionItemContext();
|
|
341
|
+
return /*#__PURE__*/ jsx(Primitive.button, {
|
|
342
|
+
ref: ref,
|
|
343
|
+
...mergeProps(props, itemApi.triggerProps)
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
AccordionTrigger.displayName = "AccordionTrigger";
|
|
347
|
+
const AccordionContent = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
348
|
+
const itemApi = useAccordionItemContext();
|
|
349
|
+
return /*#__PURE__*/ jsx(Primitive.div, {
|
|
350
|
+
ref: composeRefs(ref, itemApi.refs.content),
|
|
351
|
+
...mergeProps(itemApi.contentProps, props)
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
AccordionContent.displayName = "AccordionContent";
|
|
355
|
+
|
|
356
|
+
export { AccordionContent as A, AccordionHeader as a, AccordionItem as b, AccordionRoot as c, AccordionTrigger as d, useAccordionItem as e, useAccordionContext as f, AccordionProvider as g, useAccordionItemContext as h, AccordionItemProvider as i, useAccordion as u };
|