@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.
@@ -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 };