@octanejs/base-ui 0.1.19 → 0.1.21
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/package.json +4 -4
- package/src/accordion.ts +615 -0
- package/src/collapsible.ts +916 -0
- package/src/index.ts +2 -0
- package/src/utils/collapsibleOpenStateMapping.ts +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/base-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"@floating-ui/utils": "^0.2.12",
|
|
42
42
|
"aria-hidden": "^1.2.6",
|
|
43
43
|
"tabbable": "^6.5.0",
|
|
44
|
-
"@octanejs/floating-ui": "0.1.
|
|
44
|
+
"@octanejs/floating-ui": "0.1.22"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"octane": "0.1.
|
|
47
|
+
"octane": "0.1.23"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@base-ui/react": "^1.6.0",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"react": "^19.2.7",
|
|
54
54
|
"react-dom": "^19.2.7",
|
|
55
55
|
"vitest": "^4.1.10",
|
|
56
|
-
"octane": "0.1.
|
|
56
|
+
"octane": "0.1.23"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"test": "vitest run"
|
package/src/accordion.ts
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/accordion/ (v1.6.0): root/AccordionRootContext,
|
|
2
|
+
// root/AccordionRoot, item/AccordionItemContext, item/stateAttributesMapping, item/AccordionItem,
|
|
3
|
+
// header/AccordionHeader, trigger/AccordionTrigger, panel/AccordionPanel,
|
|
4
|
+
// panel/AccordionPanelCssVars — plus its `index.parts` (the `Accordion` namespace).
|
|
5
|
+
//
|
|
6
|
+
// Accordion is a thin layer over Collapsible: each Item runs its own `useCollapsibleRoot` and
|
|
7
|
+
// PROVIDES `CollapsibleRootContext`, so Trigger and Panel reuse the collapsible open/transition
|
|
8
|
+
// machinery unchanged. Root only owns the value set and single-vs-multiple policy. Reading this
|
|
9
|
+
// file alongside `collapsible.ts` is the intended way to follow it.
|
|
10
|
+
//
|
|
11
|
+
// octane adaptations:
|
|
12
|
+
// 1. `forwardRef` → ref-as-prop; `useMergedRefs` becomes an array ref, which octane accepts.
|
|
13
|
+
// 2. `useIsoLayoutEffect` → `useLayoutEffect`, per the other ports in this package.
|
|
14
|
+
// 3. Events are native, so the handler receives the native event directly.
|
|
15
|
+
// 4. Every hook threads an explicit compiler slot (`S(...)` / `subSlot(...)`), because these
|
|
16
|
+
// are plain `.ts` hooks rather than compiled `.tsrx` call sites.
|
|
17
|
+
// 5. This package's `useCollapsiblePanel` returns `panelRef` and takes no `externalRef`, so the
|
|
18
|
+
// panel composes `ref: [ref, panelRef]` rather than passing a forwarded ref inward.
|
|
19
|
+
import {
|
|
20
|
+
createContext,
|
|
21
|
+
createElement,
|
|
22
|
+
useContext,
|
|
23
|
+
useLayoutEffect,
|
|
24
|
+
useMemo,
|
|
25
|
+
useRef,
|
|
26
|
+
useState,
|
|
27
|
+
} from 'octane';
|
|
28
|
+
|
|
29
|
+
import { S, splitSlot, subSlot } from './internal';
|
|
30
|
+
import { useRenderElement } from './utils/useRenderElement';
|
|
31
|
+
import type { StateAttributesMapping } from './utils/getStateAttributesProps';
|
|
32
|
+
import { useBaseUiId } from './utils/useBaseUiId';
|
|
33
|
+
import { useButton } from './utils/useButton';
|
|
34
|
+
import { useControlled } from './utils/useControlled';
|
|
35
|
+
import { useStableCallback } from './utils/useStableCallback';
|
|
36
|
+
import { resolveStyle } from './utils/resolveStyle';
|
|
37
|
+
import { transitionStatusMapping } from './utils/useTransitionStatus';
|
|
38
|
+
import type { TransitionStatus } from './utils/useTransitionStatus';
|
|
39
|
+
import { CompositeList } from './utils/composite/CompositeList';
|
|
40
|
+
import { useCompositeListItem } from './utils/composite/useCompositeListItem';
|
|
41
|
+
import { useDirection } from './utils/DirectionContext';
|
|
42
|
+
import {
|
|
43
|
+
collapsibleOpenStateMapping,
|
|
44
|
+
collapsibleTriggerOpenStateMapping,
|
|
45
|
+
} from './utils/collapsibleOpenStateMapping';
|
|
46
|
+
import {
|
|
47
|
+
CollapsibleRootContext,
|
|
48
|
+
useCollapsibleRoot,
|
|
49
|
+
useCollapsibleRootContext,
|
|
50
|
+
useCollapsiblePanel,
|
|
51
|
+
} from './collapsible';
|
|
52
|
+
import type { CollapsibleRootContextValue, CollapsibleRootState } from './collapsible';
|
|
53
|
+
|
|
54
|
+
export type AccordionValue<Value = any> = Value[];
|
|
55
|
+
export type Orientation = 'horizontal' | 'vertical';
|
|
56
|
+
|
|
57
|
+
export interface AccordionRootState<Value = any> {
|
|
58
|
+
value: AccordionValue<Value>;
|
|
59
|
+
disabled: boolean;
|
|
60
|
+
/** @deprecated no longer affects keyboard focus behavior, per the APG guidance update. */
|
|
61
|
+
orientation: Orientation;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface AccordionItemState extends AccordionRootState {
|
|
65
|
+
hidden: boolean;
|
|
66
|
+
index: number;
|
|
67
|
+
open: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface AccordionPanelState extends AccordionItemState {
|
|
71
|
+
transitionStatus: TransitionStatus;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// `value` maps to null because the open set is not a serializable attribute — it would otherwise
|
|
75
|
+
// stringify the whole array onto the element.
|
|
76
|
+
const rootStateAttributesMapping: StateAttributesMapping<AccordionRootState> = {
|
|
77
|
+
value: () => null,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const accordionStateAttributesMapping: StateAttributesMapping<AccordionItemState> = {
|
|
81
|
+
...(collapsibleOpenStateMapping as StateAttributesMapping<any>),
|
|
82
|
+
index(value: number) {
|
|
83
|
+
return Number.isInteger(value) ? { 'data-index': String(value) } : null;
|
|
84
|
+
},
|
|
85
|
+
...(transitionStatusMapping as StateAttributesMapping<any>),
|
|
86
|
+
value: () => null,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const triggerStateAttributesMapping: StateAttributesMapping<AccordionItemState> = {
|
|
90
|
+
...(collapsibleTriggerOpenStateMapping as StateAttributesMapping<any>),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const AccordionPanelCssVars = {
|
|
94
|
+
accordionPanelHeight: '--accordion-panel-height',
|
|
95
|
+
accordionPanelWidth: '--accordion-panel-width',
|
|
96
|
+
} as const;
|
|
97
|
+
|
|
98
|
+
// --- Root context -------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
export interface AccordionRootContextValue<Value = any> {
|
|
101
|
+
disabled: boolean;
|
|
102
|
+
handleValueChange: (newValue: Value, nextOpen: boolean, eventDetails: any) => void;
|
|
103
|
+
hiddenUntilFound: boolean;
|
|
104
|
+
keepMounted: boolean;
|
|
105
|
+
state: AccordionRootState<Value>;
|
|
106
|
+
value: AccordionValue<Value>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const AccordionRootContext = createContext<AccordionRootContextValue | undefined>(undefined);
|
|
110
|
+
|
|
111
|
+
export function useAccordionRootContext(): AccordionRootContextValue {
|
|
112
|
+
const context = useContext(AccordionRootContext);
|
|
113
|
+
if (context === undefined) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
'Base UI: AccordionRootContext is missing. Accordion parts must be placed within <Accordion.Root>.',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return context;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Item context -------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
export interface AccordionItemContextValue {
|
|
124
|
+
open: boolean;
|
|
125
|
+
state: AccordionItemState;
|
|
126
|
+
setTriggerId: (id: string | undefined) => void;
|
|
127
|
+
triggerId?: string | undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const AccordionItemContext = createContext<AccordionItemContextValue | undefined>(undefined);
|
|
131
|
+
|
|
132
|
+
export function useAccordionItemContext(): AccordionItemContextValue {
|
|
133
|
+
const context = useContext(AccordionItemContext);
|
|
134
|
+
if (context === undefined) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
'Base UI: AccordionItemContext is missing. Accordion parts must be placed within <Accordion.Item>.',
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return context;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Root ---------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
function AccordionRoot(props: any): any {
|
|
145
|
+
const slot = S('AccordionRoot');
|
|
146
|
+
const {
|
|
147
|
+
render,
|
|
148
|
+
className,
|
|
149
|
+
disabled = false,
|
|
150
|
+
hiddenUntilFound: hiddenUntilFoundProp,
|
|
151
|
+
keepMounted: keepMountedProp,
|
|
152
|
+
loopFocus,
|
|
153
|
+
onValueChange,
|
|
154
|
+
multiple = false,
|
|
155
|
+
orientation = 'vertical',
|
|
156
|
+
value: valueProp,
|
|
157
|
+
defaultValue: defaultValueProp,
|
|
158
|
+
style,
|
|
159
|
+
ref,
|
|
160
|
+
...elementProps
|
|
161
|
+
} = props;
|
|
162
|
+
|
|
163
|
+
const direction = useDirection();
|
|
164
|
+
|
|
165
|
+
useLayoutEffect(
|
|
166
|
+
() => {
|
|
167
|
+
if (hiddenUntilFoundProp && keepMountedProp === false) {
|
|
168
|
+
console.warn(
|
|
169
|
+
'Base UI: The `keepMounted={false}` prop on `Accordion.Root` is ignored when ' +
|
|
170
|
+
'`hiddenUntilFound` is enabled, since panels must remain mounted while closed.',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
[hiddenUntilFoundProp, keepMountedProp],
|
|
175
|
+
subSlot(slot, 'warn'),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Memoized so BOTH `defaultValue` and `value` may be omitted without `useControlled`
|
|
179
|
+
// warning about a component switching between controlled and uncontrolled.
|
|
180
|
+
const defaultValue = useMemo(
|
|
181
|
+
() => {
|
|
182
|
+
if (valueProp === undefined) {
|
|
183
|
+
return defaultValueProp ?? [];
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
},
|
|
187
|
+
[valueProp, defaultValueProp],
|
|
188
|
+
subSlot(slot, 'defaultValue'),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const accordionItemRefs = useRef<(HTMLElement | null)[]>([], subSlot(slot, 'itemRefs'));
|
|
192
|
+
|
|
193
|
+
const [value, setValue] = useControlled<any[]>(
|
|
194
|
+
{ controlled: valueProp, default: defaultValue, name: 'Accordion', state: 'value' },
|
|
195
|
+
subSlot(slot, 'ctrl'),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const handleValueChange = useStableCallback(
|
|
199
|
+
(newValue: any, nextOpen: boolean, details: any) => {
|
|
200
|
+
if (!multiple) {
|
|
201
|
+
const nextValue = value[0] === newValue ? [] : [newValue];
|
|
202
|
+
onValueChange?.(nextValue, details);
|
|
203
|
+
if (details.isCanceled) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
setValue(nextValue);
|
|
207
|
+
} else if (nextOpen) {
|
|
208
|
+
const nextOpenValues = value.slice();
|
|
209
|
+
nextOpenValues.push(newValue);
|
|
210
|
+
onValueChange?.(nextOpenValues, details);
|
|
211
|
+
if (details.isCanceled) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
setValue(nextOpenValues);
|
|
215
|
+
} else {
|
|
216
|
+
const nextOpenValues = value.filter((v: any) => v !== newValue);
|
|
217
|
+
onValueChange?.(nextOpenValues, details);
|
|
218
|
+
if (details.isCanceled) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
setValue(nextOpenValues);
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
subSlot(slot, 'valueChange'),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const state: AccordionRootState = useMemo(
|
|
228
|
+
() => ({ value, disabled, orientation }),
|
|
229
|
+
[value, disabled, orientation],
|
|
230
|
+
subSlot(slot, 'state'),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const contextValue: AccordionRootContextValue = useMemo(
|
|
234
|
+
() => ({
|
|
235
|
+
disabled,
|
|
236
|
+
handleValueChange,
|
|
237
|
+
hiddenUntilFound: hiddenUntilFoundProp ?? false,
|
|
238
|
+
keepMounted: keepMountedProp ?? false,
|
|
239
|
+
state,
|
|
240
|
+
value,
|
|
241
|
+
}),
|
|
242
|
+
[disabled, handleValueChange, hiddenUntilFoundProp, keepMountedProp, state, value],
|
|
243
|
+
subSlot(slot, 'ctx'),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const element = useRenderElement(
|
|
247
|
+
'div',
|
|
248
|
+
{ render, className, style },
|
|
249
|
+
{
|
|
250
|
+
state,
|
|
251
|
+
ref,
|
|
252
|
+
props: [{ dir: direction }, elementProps],
|
|
253
|
+
stateAttributesMapping: rootStateAttributesMapping,
|
|
254
|
+
},
|
|
255
|
+
subSlot(slot, 're'),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
return createElement(AccordionRootContext.Provider, {
|
|
259
|
+
value: contextValue,
|
|
260
|
+
children: createElement(CompositeList, {
|
|
261
|
+
elementsRef: accordionItemRefs,
|
|
262
|
+
children: element,
|
|
263
|
+
}),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// --- Item ---------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
function AccordionItem(props: any): any {
|
|
270
|
+
const slot = S('AccordionItem');
|
|
271
|
+
const {
|
|
272
|
+
className,
|
|
273
|
+
disabled: disabledProp = false,
|
|
274
|
+
onOpenChange: onOpenChangeProp,
|
|
275
|
+
render,
|
|
276
|
+
value: valueProp,
|
|
277
|
+
style,
|
|
278
|
+
ref,
|
|
279
|
+
...elementProps
|
|
280
|
+
} = props;
|
|
281
|
+
|
|
282
|
+
const { ref: listItemRef, index } = useCompositeListItem(subSlot(slot, 'listItem'));
|
|
283
|
+
|
|
284
|
+
const {
|
|
285
|
+
disabled: contextDisabled,
|
|
286
|
+
handleValueChange,
|
|
287
|
+
state: rootState,
|
|
288
|
+
value: openValues,
|
|
289
|
+
} = useAccordionRootContext();
|
|
290
|
+
|
|
291
|
+
const fallbackValue = useBaseUiId(undefined, subSlot(slot, 'fallbackValue'));
|
|
292
|
+
const value = valueProp ?? fallbackValue;
|
|
293
|
+
const disabled = disabledProp || contextDisabled;
|
|
294
|
+
|
|
295
|
+
const isOpen = useMemo(
|
|
296
|
+
() => {
|
|
297
|
+
if (!openValues) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
for (let i = 0; i < openValues.length; i += 1) {
|
|
301
|
+
if (openValues[i] === value) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
},
|
|
307
|
+
[openValues, value],
|
|
308
|
+
subSlot(slot, 'isOpen'),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const onOpenChange = useStableCallback(
|
|
312
|
+
(nextOpen: boolean, eventDetails: any) => {
|
|
313
|
+
onOpenChangeProp?.(nextOpen, eventDetails);
|
|
314
|
+
|
|
315
|
+
if (eventDetails.isCanceled) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
handleValueChange(value, nextOpen, eventDetails);
|
|
320
|
+
},
|
|
321
|
+
subSlot(slot, 'onOpenChange'),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const collapsible = useCollapsibleRoot(
|
|
325
|
+
{ open: isOpen, onOpenChange, disabled },
|
|
326
|
+
subSlot(slot, 'collapsible'),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const collapsibleState: CollapsibleRootState = useMemo(
|
|
330
|
+
() => ({
|
|
331
|
+
open: collapsible.open,
|
|
332
|
+
disabled: collapsible.disabled,
|
|
333
|
+
transitionStatus: collapsible.transitionStatus,
|
|
334
|
+
}),
|
|
335
|
+
[collapsible.open, collapsible.disabled, collapsible.transitionStatus],
|
|
336
|
+
subSlot(slot, 'collapsibleState'),
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const collapsibleContext: CollapsibleRootContextValue = useMemo(
|
|
340
|
+
() => ({ ...collapsible, onOpenChange, state: collapsibleState }),
|
|
341
|
+
[collapsible, collapsibleState, onOpenChange],
|
|
342
|
+
subSlot(slot, 'collapsibleCtx'),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const state: AccordionItemState = useMemo(
|
|
346
|
+
() => ({
|
|
347
|
+
...rootState,
|
|
348
|
+
hidden: !isOpen && !collapsible.mounted,
|
|
349
|
+
index,
|
|
350
|
+
disabled,
|
|
351
|
+
open: isOpen,
|
|
352
|
+
}),
|
|
353
|
+
[collapsible.mounted, disabled, index, isOpen, rootState],
|
|
354
|
+
subSlot(slot, 'state'),
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const defaultTriggerId = useBaseUiId(undefined, subSlot(slot, 'defaultTriggerId'));
|
|
358
|
+
const [triggerId, setTriggerId] = useState<string | undefined>(
|
|
359
|
+
undefined,
|
|
360
|
+
subSlot(slot, 'triggerId'),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const accordionItemContext: AccordionItemContextValue = useMemo(
|
|
364
|
+
() => ({
|
|
365
|
+
open: isOpen,
|
|
366
|
+
state,
|
|
367
|
+
setTriggerId,
|
|
368
|
+
triggerId: triggerId ?? defaultTriggerId,
|
|
369
|
+
}),
|
|
370
|
+
[defaultTriggerId, isOpen, state, setTriggerId, triggerId],
|
|
371
|
+
subSlot(slot, 'itemCtx'),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const element = useRenderElement(
|
|
375
|
+
'div',
|
|
376
|
+
{ render, className, style },
|
|
377
|
+
{
|
|
378
|
+
state,
|
|
379
|
+
ref: [ref, listItemRef],
|
|
380
|
+
props: elementProps,
|
|
381
|
+
stateAttributesMapping: accordionStateAttributesMapping,
|
|
382
|
+
},
|
|
383
|
+
subSlot(slot, 're'),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
return createElement(CollapsibleRootContext.Provider, {
|
|
387
|
+
value: collapsibleContext,
|
|
388
|
+
children: createElement(AccordionItemContext.Provider, {
|
|
389
|
+
value: accordionItemContext,
|
|
390
|
+
children: element,
|
|
391
|
+
}),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// --- Header -------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
function AccordionHeader(props: any): any {
|
|
398
|
+
const slot = S('AccordionHeader');
|
|
399
|
+
const { render, className, style, ref, ...elementProps } = props;
|
|
400
|
+
|
|
401
|
+
const { state } = useAccordionItemContext();
|
|
402
|
+
|
|
403
|
+
return useRenderElement(
|
|
404
|
+
'h3',
|
|
405
|
+
{ render, className, style },
|
|
406
|
+
{
|
|
407
|
+
state,
|
|
408
|
+
ref,
|
|
409
|
+
props: elementProps,
|
|
410
|
+
stateAttributesMapping: accordionStateAttributesMapping,
|
|
411
|
+
},
|
|
412
|
+
subSlot(slot, 're'),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// --- Trigger ------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
function AccordionTrigger(props: any): any {
|
|
419
|
+
const slot = S('AccordionTrigger');
|
|
420
|
+
const {
|
|
421
|
+
disabled: disabledProp,
|
|
422
|
+
className,
|
|
423
|
+
id: idProp,
|
|
424
|
+
render,
|
|
425
|
+
nativeButton = true,
|
|
426
|
+
style,
|
|
427
|
+
ref,
|
|
428
|
+
...elementProps
|
|
429
|
+
} = props;
|
|
430
|
+
|
|
431
|
+
const { panelId, open, handleTrigger, disabled: contextDisabled } = useCollapsibleRootContext();
|
|
432
|
+
|
|
433
|
+
const disabled = disabledProp || contextDisabled;
|
|
434
|
+
|
|
435
|
+
const { getButtonProps, buttonRef } = useButton(
|
|
436
|
+
{ disabled, focusableWhenDisabled: true, native: nativeButton },
|
|
437
|
+
subSlot(slot, 'button'),
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const { state, setTriggerId, triggerId: id } = useAccordionItemContext();
|
|
441
|
+
|
|
442
|
+
useLayoutEffect(
|
|
443
|
+
() => {
|
|
444
|
+
if (idProp) {
|
|
445
|
+
setTriggerId(idProp);
|
|
446
|
+
}
|
|
447
|
+
return () => {
|
|
448
|
+
setTriggerId(undefined);
|
|
449
|
+
};
|
|
450
|
+
},
|
|
451
|
+
[idProp, setTriggerId],
|
|
452
|
+
subSlot(slot, 'registerId'),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
return useRenderElement(
|
|
456
|
+
'button',
|
|
457
|
+
{ render, className, style },
|
|
458
|
+
{
|
|
459
|
+
state,
|
|
460
|
+
ref: [ref, buttonRef],
|
|
461
|
+
props: [
|
|
462
|
+
{
|
|
463
|
+
'aria-controls': open ? panelId : undefined,
|
|
464
|
+
'aria-expanded': open,
|
|
465
|
+
id,
|
|
466
|
+
onClick: handleTrigger,
|
|
467
|
+
},
|
|
468
|
+
elementProps,
|
|
469
|
+
getButtonProps,
|
|
470
|
+
],
|
|
471
|
+
stateAttributesMapping: triggerStateAttributesMapping,
|
|
472
|
+
},
|
|
473
|
+
subSlot(slot, 're'),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// --- Panel --------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
function AccordionPanel(props: any): any {
|
|
480
|
+
const slot = S('AccordionPanel');
|
|
481
|
+
const {
|
|
482
|
+
className,
|
|
483
|
+
hiddenUntilFound: hiddenUntilFoundProp,
|
|
484
|
+
keepMounted: keepMountedProp,
|
|
485
|
+
id: idProp,
|
|
486
|
+
render,
|
|
487
|
+
style,
|
|
488
|
+
ref,
|
|
489
|
+
...elementProps
|
|
490
|
+
} = props;
|
|
491
|
+
|
|
492
|
+
const { hiddenUntilFound: contextHiddenUntilFound, keepMounted: contextKeepMounted } =
|
|
493
|
+
useAccordionRootContext();
|
|
494
|
+
|
|
495
|
+
const {
|
|
496
|
+
mounted,
|
|
497
|
+
onOpenChange,
|
|
498
|
+
open,
|
|
499
|
+
panelId,
|
|
500
|
+
setMounted,
|
|
501
|
+
setOpen,
|
|
502
|
+
setPanelIdState,
|
|
503
|
+
transitionStatus,
|
|
504
|
+
} = useCollapsibleRootContext();
|
|
505
|
+
|
|
506
|
+
const hiddenUntilFound = hiddenUntilFoundProp ?? contextHiddenUntilFound;
|
|
507
|
+
const keepMounted = keepMountedProp ?? contextKeepMounted;
|
|
508
|
+
|
|
509
|
+
useLayoutEffect(
|
|
510
|
+
() => {
|
|
511
|
+
if (keepMountedProp === false && hiddenUntilFound) {
|
|
512
|
+
console.warn(
|
|
513
|
+
'Base UI: The `keepMounted={false}` prop on an `Accordion.Panel` is ignored when ' +
|
|
514
|
+
'`hiddenUntilFound` is enabled on the panel or root, since the panel must remain ' +
|
|
515
|
+
'mounted while closed.',
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
[hiddenUntilFound, keepMountedProp],
|
|
520
|
+
subSlot(slot, 'warn'),
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
useLayoutEffect(
|
|
524
|
+
() => {
|
|
525
|
+
if (idProp) {
|
|
526
|
+
setPanelIdState(idProp);
|
|
527
|
+
return () => {
|
|
528
|
+
setPanelIdState(undefined);
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
return undefined;
|
|
532
|
+
},
|
|
533
|
+
[idProp, setPanelIdState],
|
|
534
|
+
subSlot(slot, 'registerId'),
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const {
|
|
538
|
+
height,
|
|
539
|
+
props: panelProps,
|
|
540
|
+
panelRef,
|
|
541
|
+
shouldPreventOpenAnimation,
|
|
542
|
+
shouldRender,
|
|
543
|
+
transitionStatus: panelTransitionStatus,
|
|
544
|
+
width,
|
|
545
|
+
} = useCollapsiblePanel(
|
|
546
|
+
{
|
|
547
|
+
hiddenUntilFound,
|
|
548
|
+
id: idProp ?? panelId,
|
|
549
|
+
keepMounted,
|
|
550
|
+
mounted,
|
|
551
|
+
onOpenChange,
|
|
552
|
+
open,
|
|
553
|
+
setMounted,
|
|
554
|
+
setOpen,
|
|
555
|
+
transitionStatus,
|
|
556
|
+
},
|
|
557
|
+
subSlot(slot, 'panel'),
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
const { state, triggerId } = useAccordionItemContext();
|
|
561
|
+
|
|
562
|
+
const panelState: AccordionPanelState = {
|
|
563
|
+
...state,
|
|
564
|
+
transitionStatus: panelTransitionStatus,
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const resolvedStyle = resolveStyle(style, panelState);
|
|
568
|
+
|
|
569
|
+
const element = useRenderElement(
|
|
570
|
+
'div',
|
|
571
|
+
{ render, className, style: undefined },
|
|
572
|
+
{
|
|
573
|
+
state: panelState,
|
|
574
|
+
ref: [ref, panelRef],
|
|
575
|
+
props: [
|
|
576
|
+
panelProps,
|
|
577
|
+
{
|
|
578
|
+
'aria-labelledby': triggerId,
|
|
579
|
+
role: 'region',
|
|
580
|
+
style: {
|
|
581
|
+
[AccordionPanelCssVars.accordionPanelHeight]:
|
|
582
|
+
height === undefined ? 'auto' : `${height}px`,
|
|
583
|
+
[AccordionPanelCssVars.accordionPanelWidth]:
|
|
584
|
+
width === undefined ? 'auto' : `${width}px`,
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
elementProps,
|
|
588
|
+
resolvedStyle ? { style: resolvedStyle } : undefined,
|
|
589
|
+
// The public `style` prop is resolved above so a temporary `animationName: 'none'`
|
|
590
|
+
// can still win after the user's inline styles have been merged.
|
|
591
|
+
shouldPreventOpenAnimation ? { style: { animationName: 'none' } } : undefined,
|
|
592
|
+
],
|
|
593
|
+
stateAttributesMapping: accordionStateAttributesMapping,
|
|
594
|
+
},
|
|
595
|
+
subSlot(slot, 're'),
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
if (!shouldRender) {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return element;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// --- Namespace (mirrors `export * as Accordion`) ------------------------------
|
|
606
|
+
|
|
607
|
+
export const Accordion = {
|
|
608
|
+
Root: AccordionRoot,
|
|
609
|
+
Item: AccordionItem,
|
|
610
|
+
Header: AccordionHeader,
|
|
611
|
+
Trigger: AccordionTrigger,
|
|
612
|
+
Panel: AccordionPanel,
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
export { AccordionRoot, AccordionItem, AccordionHeader, AccordionTrigger, AccordionPanel };
|