@tamagui/accordion 1.40.0 → 1.41.1

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/src/Accordion.tsx CHANGED
@@ -1,491 +1,617 @@
1
- export const Accordion = {}
2
-
3
- // import { useComposedRefs } from '@tamagui/compose-refs'
4
- // import { Stack, StackProps, TamaguiElement, composeEventHandlers } from '@tamagui/core'
5
- // import { useId } from '@tamagui/core'
6
- // import { createContextScope } from '@tamagui/create-context'
7
- // import type { Scope } from '@tamagui/create-context'
8
- // import { useControllableState } from '@tamagui/use-controllable-state'
9
- // import React from 'react'
10
-
11
- // const COLLAPSIBLE_NAME = 'Collapsible'
12
-
13
- // const [_createCollapsibleContext, createCollapsibleScope] = createContextScope(COLLAPSIBLE_NAME)
14
-
15
- // /* -------------------------------------------------------------------------------------------------
16
- // * Accordion
17
- // * -----------------------------------------------------------------------------------------------*/
18
-
19
- // const ACCORDION_NAME = 'Accordion'
20
- // const ACCORDION_KEYS = ['Home', 'End', 'ArrowDown', 'ArrowUp']
21
-
22
- // type ScopedProps<P> = P & { __scopeAccordion?: Scope }
23
- // const [createAccordionContext, createAccordionScope] = createContextScope(ACCORDION_NAME, [
24
- // createCollapsibleScope,
25
- // ])
26
- // const useCollapsibleScope = createCollapsibleScope()
27
-
28
- // type AccordionElement = AccordionImplMultipleElement | AccordionImplSingleElement
29
- // interface AccordionSingleProps extends AccordionImplSingleProps {
30
- // type: 'single'
31
- // }
32
- // interface AccordionMultipleProps extends AccordionImplMultipleProps {
33
- // type: 'multiple'
34
- // }
35
-
36
- // const Accordion = React.forwardRef<AccordionElement, AccordionSingleProps | AccordionMultipleProps>(
37
- // (props: ScopedProps<AccordionSingleProps | AccordionMultipleProps>, forwardedRef) => {
38
- // const { type, ...accordionProps } = props
39
- // const singleProps = accordionProps as AccordionImplSingleProps
40
- // const multipleProps = accordionProps as AccordionImplMultipleProps
41
- // return (
42
- // <>
43
- // {type === 'multiple' ? (
44
- // <AccordionImplMultiple {...multipleProps} ref={forwardedRef} />
45
- // ) : (
46
- // <AccordionImplSingle {...singleProps} ref={forwardedRef} />
47
- // )}
48
- // </>
49
- // )
50
- // }
51
- // )
52
-
53
- // Accordion.displayName = ACCORDION_NAME
54
-
55
- // Accordion.propTypes = {
56
- // type(props) {
57
- // const value = props.value || props.defaultValue
58
- // if (props.type && !['single', 'multiple'].includes(props.type)) {
59
- // return new Error(
60
- // 'Invalid prop `type` supplied to `Accordion`. Expected one of `single | multiple`.'
61
- // )
62
- // }
63
- // if (props.type === 'multiple' && typeof value === 'string') {
64
- // return new Error(
65
- // 'Invalid prop `type` supplied to `Accordion`. Expected `single` when `defaultValue` or `value` is type `string`.'
66
- // )
67
- // }
68
- // if (props.type === 'single' && Array.isArray(value)) {
69
- // return new Error(
70
- // 'Invalid prop `type` supplied to `Accordion`. Expected `multiple` when `defaultValue` or `value` is type `string[]`.'
71
- // )
72
- // }
73
- // return null
74
- // },
75
- // }
76
-
77
- // /* -----------------------------------------------------------------------------------------------*/
78
-
79
- // type AccordionValueContextValue = {
80
- // value: string[]
81
- // onItemOpen(value: string): void
82
- // onItemClose(value: string): void
83
- // }
84
-
85
- // const [AccordionValueProvider, useAccordionValueContext] =
86
- // createAccordionContext<AccordionValueContextValue>(ACCORDION_NAME)
87
-
88
- // const [AccordionCollapsibleProvider, useAccordionCollapsibleContext] = createAccordionContext(
89
- // ACCORDION_NAME,
90
- // { collapsible: false }
91
- // )
92
-
93
- // type AccordionImplSingleElement = AccordionImplElement
94
- // interface AccordionImplSingleProps extends AccordionImplProps {
95
- // /**
96
- // * The controlled stateful value of the accordion item whose content is expanded.
97
- // */
98
- // value?: string
99
- // /**
100
- // * The value of the item whose content is expanded when the accordion is initially rendered. Use
101
- // * `defaultValue` if you do not need to control the state of an accordion.
102
- // */
103
- // defaultValue?: string
104
- // /**
105
- // * The callback that fires when the state of the accordion changes.
106
- // */
107
- // onValueChange?(value: string): void
108
- // /**
109
- // * Whether an accordion item can be collapsed after it has been opened.
110
- // * @default false
111
- // */
112
- // collapsible?: boolean
113
- // }
114
-
115
- // const AccordionImplSingle = React.forwardRef<AccordionImplSingleElement, AccordionImplSingleProps>(
116
- // (props: ScopedProps<AccordionImplSingleProps>, forwardedRef) => {
117
- // const {
118
- // value: valueProp,
119
- // defaultValue,
120
- // onValueChange = () => {},
121
- // collapsible = false,
122
- // ...accordionSingleProps
123
- // } = props
124
-
125
- // const [value, setValue] = useControllableState({
126
- // prop: valueProp,
127
- // defaultProp: defaultValue || '',
128
- // onChange: onValueChange,
129
- // })
130
-
131
- // return (
132
- // <AccordionValueProvider
133
- // scope={props.__scopeAccordion}
134
- // value={value ? [value] : []}
135
- // onItemOpen={setValue}
136
- // onItemClose={React.useCallback(() => collapsible && setValue(''), [collapsible, setValue])}
137
- // >
138
- // <AccordionCollapsibleProvider scope={props.__scopeAccordion} collapsible={collapsible}>
139
- // <AccordionImpl {...accordionSingleProps} ref={forwardedRef} />
140
- // </AccordionCollapsibleProvider>
141
- // </AccordionValueProvider>
142
- // )
143
- // }
144
- // )
145
-
146
- // /* -----------------------------------------------------------------------------------------------*/
147
-
148
- // type AccordionImplMultipleElement = AccordionImplElement
149
- // interface AccordionImplMultipleProps extends AccordionImplProps {
150
- // /**
151
- // * The controlled stateful value of the accordion items whose contents are expanded.
152
- // */
153
- // value?: string[]
154
- // /**
155
- // * The value of the items whose contents are expanded when the accordion is initially rendered. Use
156
- // * `defaultValue` if you do not need to control the state of an accordion.
157
- // */
158
- // defaultValue?: string[]
159
- // /**
160
- // * The callback that fires when the state of the accordion changes.
161
- // */
162
- // onValueChange?(value: string[]): void
163
- // }
164
-
165
- // const AccordionImplMultiple = React.forwardRef<
166
- // AccordionImplMultipleElement,
167
- // AccordionImplMultipleProps
168
- // >((props: ScopedProps<AccordionImplMultipleProps>, forwardedRef) => {
169
- // const {
170
- // value: valueProp,
171
- // defaultValue,
172
- // onValueChange = () => {},
173
- // ...accordionMultipleProps
174
- // } = props
175
-
176
- // const [value, setValue] = useControllableState({
177
- // prop: valueProp,
178
- // defaultProp: defaultValue || [],
179
- // onChange: onValueChange,
180
- // })
181
-
182
- // const handleItemOpen = React.useCallback(
183
- // (itemValue: string) => setValue((prevValue = []) => [...prevValue, itemValue]),
184
- // [setValue]
185
- // )
186
-
187
- // const handleItemClose = React.useCallback(
188
- // (itemValue: string) =>
189
- // setValue((prevValue = []) => prevValue.filter((value) => value !== itemValue)),
190
- // [setValue]
191
- // )
192
-
193
- // return (
194
- // <AccordionValueProvider
195
- // scope={props.__scopeAccordion}
196
- // value={value}
197
- // onItemOpen={handleItemOpen}
198
- // onItemClose={handleItemClose}
199
- // >
200
- // <AccordionCollapsibleProvider scope={props.__scopeAccordion} collapsible={true}>
201
- // <AccordionImpl {...accordionMultipleProps} ref={forwardedRef} />
202
- // </AccordionCollapsibleProvider>
203
- // </AccordionValueProvider>
204
- // )
205
- // })
206
-
207
- // /* -----------------------------------------------------------------------------------------------*/
208
-
209
- // type AccordionImplContextValue = {
210
- // disabled?: boolean
211
- // }
212
-
213
- // const [AccordionImplProvider, useAccordionContext] =
214
- // createAccordionContext<AccordionImplContextValue>(ACCORDION_NAME)
215
-
216
- // type AccordionImplElement = TamaguiElement
217
- // type PrimitiveDivProps = StackProps
218
-
219
- // interface AccordionImplProps extends PrimitiveDivProps {
220
- // /**
221
- // * Whether or not an accordion is disabled from user interaction.
222
- // *
223
- // * @defaultValue false
224
- // */
225
- // disabled?: boolean
226
- // }
227
-
228
- // const AccordionImpl = React.forwardRef<AccordionImplElement, AccordionImplProps>(
229
- // (props: ScopedProps<AccordionImplProps>, forwardedRef) => {
230
- // const { __scopeAccordion, disabled, ...accordionProps } = props
231
- // const accordionRef = React.useRef<AccordionImplElement>(null)
232
- // const composedRefs = useComposedRefs(accordionRef, forwardedRef)
233
- // // const getItems = useCollection(__scopeAccordion)
234
-
235
- // const handleKeyDown = (event: KeyboardEvent) => {
236
- // if (!ACCORDION_KEYS.includes(event.key)) return
237
- // const target = event.target as HTMLElement
238
- // const triggerCollection = [] //getItems().filter((item) => !item.ref.current?.disabled)
239
- // const triggerIndex = triggerCollection.findIndex((item) => item.ref.current === target)
240
- // const triggerCount = triggerCollection.length
241
-
242
- // if (triggerIndex === -1) return
243
-
244
- // // Prevents page scroll while user is navigating
245
- // event.preventDefault()
246
-
247
- // let nextIndex = triggerIndex
248
- // switch (event.key) {
249
- // case 'Home':
250
- // nextIndex = 0
251
- // break
252
- // case 'End':
253
- // nextIndex = triggerCount - 1
254
- // break
255
- // case 'ArrowDown':
256
- // nextIndex = triggerIndex + 1
257
- // break
258
- // case 'ArrowUp':
259
- // nextIndex = triggerIndex - 1
260
- // if (nextIndex < 0) {
261
- // nextIndex = triggerCount - 1
262
- // }
263
- // break
264
- // }
265
-
266
- // const clampedIndex = nextIndex % triggerCount
267
- // triggerCollection[clampedIndex].ref.current?.focus()
268
- // }
269
-
270
- // return (
271
- // <AccordionImplProvider scope={__scopeAccordion} disabled={disabled}>
272
- // <Stack
273
- // {...accordionProps}
274
- // // @ts-ignore
275
- // ref={composedRefs}
276
- // // @ts-ignore
277
- // onKeyDown={disabled ? undefined : handleKeyDown}
278
- // />
279
- // </AccordionImplProvider>
280
- // )
281
- // }
282
- // )
283
-
284
- // /* -------------------------------------------------------------------------------------------------
285
- // * AccordionItem
286
- // * -----------------------------------------------------------------------------------------------*/
287
-
288
- // const ITEM_NAME = 'AccordionItem'
289
-
290
- // type AccordionItemContextValue = { open?: boolean; disabled?: boolean; triggerId: string }
291
- // const [AccordionItemProvider, useAccordionItemContext] =
292
- // createAccordionContext<AccordionItemContextValue>(ITEM_NAME)
293
-
294
- // type AccordionItemElement = TamaguiElement
295
- // type CollapsibleProps = Radix.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Root>
296
-
297
- // interface AccordionItemProps
298
- // extends Omit<CollapsibleProps, 'open' | 'defaultOpen' | 'onOpenChange'> {
299
- // /**
300
- // * Whether or not an accordion item is disabled from user interaction.
301
- // *
302
- // * @defaultValue false
303
- // */
304
- // disabled?: boolean
305
- // /**
306
- // * A string value for the accordion item. All items within an accordion should use a unique value.
307
- // */
308
- // value: string
309
- // }
310
-
311
- // /**
312
- // * `AccordionItem` contains all of the parts of a collapsible section inside of an `Accordion`.
313
- // */
314
- // const AccordionItem = React.forwardRef<AccordionItemElement, AccordionItemProps>(
315
- // (props: ScopedProps<AccordionItemProps>, forwardedRef) => {
316
- // const { __scopeAccordion, value, ...accordionItemProps } = props
317
- // const accordionContext = useAccordionContext(ITEM_NAME, __scopeAccordion)
318
- // const valueContext = useAccordionValueContext(ITEM_NAME, __scopeAccordion)
319
- // const collapsibleScope = useCollapsibleScope(__scopeAccordion)
320
- // const triggerId = useId()
321
- // const open = (value && valueContext.value.includes(value)) || false
322
- // const disabled = accordionContext.disabled || props.disabled
323
-
324
- // return (
325
- // <AccordionItemProvider
326
- // scope={__scopeAccordion}
327
- // open={open}
328
- // disabled={disabled}
329
- // triggerId={triggerId}
330
- // >
331
- // <CollapsiblePrimitive.Root
332
- // data-state={open ? 'open' : 'closed'}
333
- // {...collapsibleScope}
334
- // {...accordionItemProps}
335
- // ref={forwardedRef}
336
- // disabled={disabled}
337
- // open={open}
338
- // onOpenChange={(open) => {
339
- // if (open) {
340
- // valueContext.onItemOpen(value)
341
- // } else {
342
- // valueContext.onItemClose(value)
343
- // }
344
- // }}
345
- // />
346
- // </AccordionItemProvider>
347
- // )
348
- // }
349
- // )
350
-
351
- // AccordionItem.displayName = ITEM_NAME
352
-
353
- // /* -------------------------------------------------------------------------------------------------
354
- // * AccordionHeader
355
- // * -----------------------------------------------------------------------------------------------*/
356
-
357
- // const HEADER_NAME = 'AccordionHeader'
358
-
359
- // type AccordionHeaderElement = React.ElementRef<typeof Primitive.h3>
360
- // type PrimitiveHeading3Props = Radix.ComponentPropsWithoutRef<typeof Primitive.h3>
361
- // interface AccordionHeaderProps extends PrimitiveHeading3Props {}
362
-
363
- // /**
364
- // * `AccordionHeader` contains the content for the parts of an `AccordionItem` that will be visible
365
- // * whether or not its content is collapsed.
366
- // */
367
- // const AccordionHeader = React.forwardRef<AccordionHeaderElement, AccordionHeaderProps>(
368
- // (props: ScopedProps<AccordionHeaderProps>, forwardedRef) => {
369
- // const { __scopeAccordion, ...headerProps } = props
370
- // const itemContext = useAccordionItemContext(HEADER_NAME, __scopeAccordion)
371
- // return (
372
- // <Primitive.h3
373
- // data-state={getState(itemContext.open)}
374
- // data-disabled={itemContext.disabled ? '' : undefined}
375
- // {...headerProps}
376
- // ref={forwardedRef}
377
- // />
378
- // )
379
- // }
380
- // )
381
-
382
- // AccordionHeader.displayName = HEADER_NAME
383
-
384
- // /* -------------------------------------------------------------------------------------------------
385
- // * AccordionTrigger
386
- // * -----------------------------------------------------------------------------------------------*/
387
-
388
- // const TRIGGER_NAME = 'AccordionTrigger'
389
-
390
- // type AccordionTriggerElement = React.ElementRef<typeof CollapsiblePrimitive.Trigger>
391
- // type CollapsibleTriggerProps = Radix.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Trigger>
392
- // interface AccordionTriggerProps extends CollapsibleTriggerProps {}
393
-
394
- // /**
395
- // * `AccordionTrigger` is the trigger that toggles the collapsed state of an `AccordionItem`. It
396
- // * should always be nested inside of an `AccordionHeader`.
397
- // */
398
- // const AccordionTrigger = React.forwardRef<AccordionTriggerElement, AccordionTriggerProps>(
399
- // (props: ScopedProps<AccordionTriggerProps>, forwardedRef) => {
400
- // const { __scopeAccordion, ...triggerProps } = props
401
- // const itemContext = useAccordionItemContext(TRIGGER_NAME, __scopeAccordion)
402
- // const collapsibleContext = useAccordionCollapsibleContext(TRIGGER_NAME, __scopeAccordion)
403
- // const collapsibleScope = useCollapsibleScope(__scopeAccordion)
404
- // return (
405
- // <Collection.ItemSlot scope={__scopeAccordion}>
406
- // <CollapsiblePrimitive.Trigger
407
- // aria-disabled={(itemContext.open && !collapsibleContext.collapsible) || undefined}
408
- // id={itemContext.triggerId}
409
- // {...collapsibleScope}
410
- // {...triggerProps}
411
- // ref={forwardedRef}
412
- // />
413
- // </Collection.ItemSlot>
414
- // )
415
- // }
416
- // )
417
-
418
- // AccordionTrigger.displayName = TRIGGER_NAME
419
-
420
- // /* -------------------------------------------------------------------------------------------------
421
- // * AccordionContent
422
- // * -----------------------------------------------------------------------------------------------*/
423
-
424
- // const CONTENT_NAME = 'AccordionContent'
425
-
426
- // type AccordionContentElement = React.ElementRef<typeof CollapsiblePrimitive.Content>
427
- // type CollapsibleContentProps = Radix.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Content>
428
- // interface AccordionContentProps extends CollapsibleContentProps {}
429
-
430
- // /**
431
- // * `AccordionContent` contains the collapsible content for an `AccordionItem`.
432
- // */
433
- // const AccordionContent = React.forwardRef<AccordionContentElement, AccordionContentProps>(
434
- // (props: ScopedProps<AccordionContentProps>, forwardedRef) => {
435
- // const { __scopeAccordion, ...contentProps } = props
436
- // const itemContext = useAccordionItemContext(CONTENT_NAME, __scopeAccordion)
437
- // const collapsibleScope = useCollapsibleScope(__scopeAccordion)
438
- // return (
439
- // <CollapsiblePrimitive.Content
440
- // role="region"
441
- // aria-labelledby={itemContext.triggerId}
442
- // {...collapsibleScope}
443
- // {...contentProps}
444
- // ref={forwardedRef}
445
- // style={{
446
- // ['--radix-accordion-content-height' as any]: 'var(--radix-collapsible-content-height)',
447
- // ['--radix-accordion-content-width' as any]: 'var(--radix-collapsible-content-width)',
448
- // ...props.style,
449
- // }}
450
- // />
451
- // )
452
- // }
453
- // )
454
-
455
- // AccordionContent.displayName = CONTENT_NAME
456
-
457
- // /* -----------------------------------------------------------------------------------------------*/
458
-
459
- // function getState(open?: boolean) {
460
- // return open ? 'open' : 'closed'
461
- // }
462
-
463
- // const Root = Accordion
464
- // const Item = AccordionItem
465
- // const Header = AccordionHeader
466
- // const Trigger = AccordionTrigger
467
- // const Content = AccordionContent
468
-
469
- // export {
470
- // createAccordionScope,
471
- // //
472
- // Accordion,
473
- // AccordionItem,
474
- // AccordionHeader,
475
- // AccordionTrigger,
476
- // AccordionContent,
477
- // //
478
- // Root,
479
- // Item,
480
- // Header,
481
- // Trigger,
482
- // Content,
483
- // }
484
- // export type {
485
- // AccordionSingleProps,
486
- // AccordionMultipleProps,
487
- // AccordionItemProps,
488
- // AccordionHeaderProps,
489
- // AccordionTriggerProps,
490
- // AccordionContentProps,
491
- // }
1
+ import { Collapsible, createCollapsibleScope } from '@tamagui/collapsible'
2
+ import { createCollection } from '@tamagui/collection'
3
+ import type { Scope } from '@tamagui/create-context'
4
+ import { createContextScope } from '@tamagui/create-context'
5
+ import { YStack } from '@tamagui/stacks'
6
+ import { H1, H3 } from '@tamagui/text'
7
+ import { useControllableState } from '@tamagui/use-controllable-state'
8
+ import { useDirection } from '@tamagui/use-direction'
9
+ import {
10
+ GetProps,
11
+ GetRef,
12
+ Stack,
13
+ TamaguiElement,
14
+ composeEventHandlers,
15
+ isWeb,
16
+ styled,
17
+ useComposedRefs,
18
+ withStaticProperties,
19
+ } from '@tamagui/web'
20
+ import * as React from 'react'
21
+
22
+ type Direction = 'ltr' | 'rtl'
23
+
24
+ /* -------------------------------------------------------------------------------------------------
25
+ * Accordion
26
+ * -----------------------------------------------------------------------------------------------*/
27
+
28
+ const ACCORDION_NAME = 'Accordion'
29
+ const ACCORDION_KEYS = ['Home', 'End', 'ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight']
30
+
31
+ const [Collection, useCollection, createCollectionScope] =
32
+ createCollection<AccordionTrigger>(ACCORDION_NAME)
33
+
34
+ type ScopedProps<P> = P & { __scopeAccordion?: Scope }
35
+ const [createAccordionContext, createAccordionScope] = createContextScope(
36
+ ACCORDION_NAME,
37
+ [createCollectionScope, createCollapsibleScope]
38
+ )
39
+
40
+ const useCollapsibleScope = createCollapsibleScope()
41
+ type AccordionElement = AccordionImplMultipleElement | AccordionImplSingleElement
42
+ interface AccordionSingleProps extends AccordionImplSingleProps {
43
+ type: 'single'
44
+ }
45
+ interface AccordionMultipleProps extends AccordionImplMultipleProps {
46
+ type: 'multiple'
47
+ }
48
+
49
+ const AccordionComponent = React.forwardRef<
50
+ AccordionElement,
51
+ AccordionSingleProps | AccordionMultipleProps
52
+ >((props: ScopedProps<AccordionSingleProps | AccordionMultipleProps>, forwardedRef) => {
53
+ const { type, ...accordionProps } = props
54
+ const singleProps = accordionProps as AccordionImplSingleProps
55
+ const multipleProps = accordionProps as AccordionImplMultipleProps
56
+
57
+ return (
58
+ <Collection.Provider scope={props.__scopeAccordion}>
59
+ {type === 'multiple' ? (
60
+ <AccordionImplMultiple {...multipleProps} ref={forwardedRef} />
61
+ ) : (
62
+ <AccordionImplSingle {...singleProps} ref={forwardedRef} />
63
+ )}
64
+ </Collection.Provider>
65
+ )
66
+ })
67
+
68
+ AccordionComponent.displayName = ACCORDION_NAME
69
+
70
+ AccordionComponent.propTypes = {
71
+ type(props) {
72
+ const value = props.value || props.defaultValue
73
+ if (props.type && !['single', 'multiple'].includes(props.type)) {
74
+ return new Error(
75
+ 'Invalid prop `type` supplied to `Accordion`. Expected one of `single | multiple`.'
76
+ )
77
+ }
78
+ if (props.type === 'multiple' && typeof value === 'string') {
79
+ return new Error(
80
+ 'Invalid prop `type` supplied to `Accordion`. Expected `single` when `defaultValue` or `value` is type `string`.'
81
+ )
82
+ }
83
+ if (props.type === 'single' && Array.isArray(value)) {
84
+ return new Error(
85
+ 'Invalid prop `type` supplied to `Accordion`. Expected `multiple` when `defaultValue` or `value` is type `string[]`.'
86
+ )
87
+ }
88
+ return null
89
+ },
90
+ }
91
+
92
+ /* -----------------------------------------------------------------------------------------------*/
93
+
94
+ type AccordionValueContextValue = {
95
+ value: string[]
96
+ onItemOpen(value: string): void
97
+ onItemClose(value: string): void
98
+ }
99
+
100
+ const [AccordionValueProvider, useAccordionValueContext] =
101
+ createAccordionContext<AccordionValueContextValue>(ACCORDION_NAME)
102
+
103
+ const [AccordionCollapsibleProvider, useAccordionCollapsibleContext] =
104
+ createAccordionContext(ACCORDION_NAME, { collapsible: false })
105
+
106
+ type AccordionImplSingleElement = AccordionImplElement
107
+
108
+ interface AccordionImplSingleProps extends AccordionImplProps {
109
+ /**
110
+ * The controlled stateful value of the accordion item whose content is expanded.
111
+ */
112
+ value?: string
113
+ /**
114
+ * The value of the item whose content is expanded when the accordion is initially rendered. Use
115
+ * `defaultValue` if you do not need to control the state of an accordion.
116
+ */
117
+ defaultValue?: string
118
+ /**
119
+ * The callback that fires when the state of the accordion changes.
120
+ */
121
+ onValueChange?(value: string): void
122
+ /**
123
+ * Whether an accordion item can be collapsed after it has been opened.
124
+ * @default false
125
+ */
126
+ collapsible?: boolean
127
+ }
128
+
129
+ const AccordionImplSingle = React.forwardRef<
130
+ AccordionImplSingleElement,
131
+ AccordionImplSingleProps
132
+ >((props: ScopedProps<AccordionImplSingleProps>, forwardedRef) => {
133
+ const {
134
+ value: valueProp,
135
+ defaultValue,
136
+ control,
137
+ onValueChange = () => {},
138
+ collapsible = false,
139
+ ...accordionSingleProps
140
+ } = props
141
+
142
+ const [value, setValue] = useControllableState({
143
+ prop: valueProp,
144
+ defaultProp: defaultValue || '',
145
+ onChange: onValueChange,
146
+ })
147
+
148
+ return (
149
+ <AccordionValueProvider
150
+ scope={props.__scopeAccordion}
151
+ value={value ? [value] : []}
152
+ onItemOpen={setValue}
153
+ onItemClose={React.useCallback(
154
+ () => collapsible && setValue(''),
155
+ [setValue, collapsible]
156
+ )}
157
+ >
158
+ <AccordionCollapsibleProvider
159
+ scope={props.__scopeAccordion}
160
+ collapsible={collapsible}
161
+ >
162
+ <AccordionImpl {...accordionSingleProps} ref={forwardedRef} />
163
+ </AccordionCollapsibleProvider>
164
+ </AccordionValueProvider>
165
+ )
166
+ })
167
+
168
+ /* -----------------------------------------------------------------------------------------------*/
169
+ type AccordionImplMultipleElement = AccordionImplElement
170
+ interface AccordionImplMultipleProps extends AccordionImplProps {
171
+ /**
172
+ * The controlled stateful value of the accordion items whose contents are expanded.
173
+ */
174
+ value?: string[]
175
+ /**
176
+ * The value of the items whose contents are expanded when the accordion is initially rendered. Use
177
+ * `defaultValue` if you do not need to control the state of an accordion.
178
+ */
179
+ defaultValue?: string[]
180
+ /**
181
+ * The callback that fires when the state of the accordion changes.
182
+ */
183
+ onValueChange?(value: string[]): void
184
+ }
185
+
186
+ const AccordionImplMultiple = React.forwardRef<
187
+ AccordionImplMultipleElement,
188
+ AccordionImplMultipleProps
189
+ >((props: ScopedProps<AccordionImplMultipleProps>, forwardedRef) => {
190
+ const {
191
+ value: valueProp,
192
+ defaultValue,
193
+ onValueChange = () => {},
194
+ ...accordionMultipleProps
195
+ } = props
196
+
197
+ const [value, setValue] = useControllableState({
198
+ prop: valueProp,
199
+ defaultProp: defaultValue || [],
200
+ onChange: onValueChange,
201
+ })
202
+
203
+ const handleItemOpen = React.useCallback(
204
+ (itemValue: string) => setValue((prevValue = []) => [...prevValue, itemValue]),
205
+ [setValue]
206
+ )
207
+
208
+ const handleItemClose = React.useCallback(
209
+ (itemValue: string) =>
210
+ setValue((prevValue = []) => prevValue.filter((value) => value !== itemValue)),
211
+ [setValue]
212
+ )
213
+
214
+ return (
215
+ <AccordionValueProvider
216
+ scope={props.__scopeAccordion}
217
+ value={value || []}
218
+ onItemOpen={handleItemOpen}
219
+ onItemClose={handleItemClose}
220
+ >
221
+ <AccordionCollapsibleProvider scope={props.__scopeAccordion} collapsible={true}>
222
+ <AccordionImpl {...accordionMultipleProps} ref={forwardedRef} />
223
+ </AccordionCollapsibleProvider>
224
+ </AccordionValueProvider>
225
+ )
226
+ })
227
+
228
+ /* -----------------------------------------------------------------------------------------------*/
229
+
230
+ type AccordionImplContextValue = {
231
+ disabled?: boolean
232
+ direction: AccordionImplProps['dir']
233
+ orientation: AccordionImplProps['orientation']
234
+ }
235
+
236
+ const [AccordionImplProvider, useAccordionContext] =
237
+ createAccordionContext<AccordionImplContextValue>(ACCORDION_NAME)
238
+
239
+ type AccordionImplElement = TamaguiElement
240
+ type PrimitiveDivProps = React.ComponentPropsWithoutRef<typeof Stack>
241
+ interface AccordionImplProps extends PrimitiveDivProps {
242
+ /**
243
+ * Whether or not an accordion is disabled from user interaction.
244
+ *
245
+ * @defaultValue false
246
+ */
247
+ disabled?: boolean
248
+ /**
249
+ * The layout in which the Accordion operates.
250
+ * @default vertical
251
+ */
252
+ orientation?: React.AriaAttributes['aria-orientation']
253
+ /**
254
+ * The language read direction.
255
+ */
256
+ dir?: Direction
257
+ /**
258
+ * The callback that fires when the state of the accordion changes. for use with `useAccordion`
259
+ * @param selected - The values of the accordion items whose contents are expanded.
260
+ */
261
+ control?(selected: string[]): void
262
+ }
263
+
264
+ const AccordionImpl = React.forwardRef<AccordionImplElement, AccordionImplProps>(
265
+ (props: ScopedProps<AccordionImplProps>, forwardedRef) => {
266
+ const {
267
+ __scopeAccordion,
268
+ disabled,
269
+ dir,
270
+ orientation = 'vertical',
271
+ ...accordionProps
272
+ } = props
273
+
274
+ const accordionRef = React.useRef<AccordionImplElement>(null)
275
+ const composedRef = useComposedRefs(accordionRef, forwardedRef)
276
+ const getItems = useCollection(__scopeAccordion)
277
+ const direction = useDirection(dir)
278
+ const isDirectionLTR = direction === 'ltr'
279
+ const handleKeyDown = composeEventHandlers(
280
+ (props as any).onKeyDown,
281
+ (event: KeyboardEvent) => {
282
+ if (!ACCORDION_KEYS.includes(event.key)) return
283
+ const target = event.target as HTMLElement
284
+ const triggerCollection = getItems().filter((item) => {
285
+ const el = item.ref.current as { disabled?: boolean } | null
286
+ return !el?.disabled
287
+ })
288
+ const triggerIndex = triggerCollection.findIndex(
289
+ (item) => item.ref.current === target
290
+ )
291
+ const triggerCount = triggerCollection.length
292
+
293
+ if (triggerIndex === -1) return
294
+
295
+ // Prevents page scroll while user is navigating
296
+ event.preventDefault()
297
+
298
+ let nextIndex = triggerIndex
299
+ const homeIndex = 0
300
+ const endIndex = triggerCount - 1
301
+
302
+ const moveNext = () => {
303
+ nextIndex = triggerIndex + 1
304
+ if (nextIndex > endIndex) {
305
+ nextIndex = homeIndex
306
+ }
307
+ }
308
+
309
+ const movePrev = () => {
310
+ nextIndex = triggerIndex - 1
311
+ if (nextIndex < homeIndex) {
312
+ nextIndex = endIndex
313
+ }
314
+ }
315
+
316
+ switch (event.key) {
317
+ case 'Home':
318
+ nextIndex = homeIndex
319
+ break
320
+ case 'End':
321
+ nextIndex = endIndex
322
+ break
323
+ case 'ArrowRight':
324
+ if (orientation === 'horizontal') {
325
+ if (isDirectionLTR) {
326
+ moveNext()
327
+ } else {
328
+ movePrev()
329
+ }
330
+ }
331
+ break
332
+ case 'ArrowDown':
333
+ if (orientation === 'vertical') {
334
+ moveNext()
335
+ }
336
+ break
337
+ case 'ArrowLeft':
338
+ if (orientation === 'horizontal') {
339
+ if (isDirectionLTR) {
340
+ movePrev()
341
+ } else {
342
+ moveNext()
343
+ }
344
+ }
345
+ break
346
+ case 'ArrowUp':
347
+ if (orientation === 'vertical') {
348
+ movePrev()
349
+ }
350
+ break
351
+ }
352
+
353
+ const clampedIndex = nextIndex % triggerCount
354
+ triggerCollection[clampedIndex].ref.current?.focus()
355
+ }
356
+ )
357
+
358
+ return (
359
+ <AccordionImplProvider
360
+ scope={__scopeAccordion}
361
+ disabled={disabled}
362
+ direction={dir}
363
+ orientation={orientation}
364
+ >
365
+ <Collection.Slot scope={__scopeAccordion}>
366
+ <YStack
367
+ data-orientation={orientation}
368
+ ref={composedRef}
369
+ {...accordionProps}
370
+ {...(isWeb && {
371
+ onKeyDown: handleKeyDown,
372
+ })}
373
+ />
374
+ </Collection.Slot>
375
+ </AccordionImplProvider>
376
+ )
377
+ }
378
+ )
379
+
380
+ /* -------------------------------------------------------------------------------------------------
381
+ * AccordionItem
382
+ * -----------------------------------------------------------------------------------------------*/
383
+
384
+ const ITEM_NAME = 'AccordionItem'
385
+
386
+ type AccordionItemContextValue = { open?: boolean; disabled?: boolean; triggerId: string }
387
+ const [AccordionItemProvider, useAccordionItemContext] =
388
+ createAccordionContext<AccordionItemContextValue>(ITEM_NAME)
389
+ type AccordionItemElement = React.ElementRef<typeof Collapsible>
390
+ type CollapsibleProps = React.ComponentPropsWithoutRef<typeof Collapsible>
391
+ interface AccordionItemProps
392
+ extends Omit<CollapsibleProps, 'open' | 'defaultOpen' | 'onOpenChange'> {
393
+ /**
394
+ * Whether or not an accordion item is disabled from user interaction.
395
+ *
396
+ * @defaultValue false
397
+ */
398
+ disabled?: boolean
399
+ /**
400
+ * A string value for the accordion item. All items within an accordion should use a unique value.
401
+ */
402
+ value: string
403
+ }
404
+
405
+ /**
406
+ * `AccordionItem` contains all of the parts of a collapsible section inside of an `Accordion`.
407
+ */
408
+
409
+ const AccordionItem = React.forwardRef<AccordionItemElement, AccordionItemProps>(
410
+ (props: ScopedProps<AccordionItemProps>, forwardedRef) => {
411
+ const { __scopeAccordion, value, ...accordionItemProps } = props
412
+ const accordionContext = useAccordionContext(ITEM_NAME, __scopeAccordion)
413
+ const valueContext = useAccordionValueContext(ITEM_NAME, __scopeAccordion)
414
+ const collapsibleScope = useCollapsibleScope(__scopeAccordion)
415
+ const triggerId = React.useId()
416
+ const open = (value && valueContext.value.includes(value)) || false
417
+ const disabled = accordionContext.disabled || props.disabled
418
+
419
+ return (
420
+ <AccordionItemProvider
421
+ scope={__scopeAccordion}
422
+ open={open}
423
+ disabled={disabled}
424
+ triggerId={triggerId}
425
+ >
426
+ <Collapsible
427
+ data-orientation={accordionContext.orientation}
428
+ data-state={open ? 'open' : 'closed'}
429
+ {...collapsibleScope}
430
+ {...accordionItemProps}
431
+ ref={forwardedRef}
432
+ disabled={disabled}
433
+ open={open}
434
+ onOpenChange={(open) => {
435
+ if (open) {
436
+ valueContext.onItemOpen(value)
437
+ } else {
438
+ valueContext.onItemClose(value)
439
+ }
440
+ }}
441
+ />
442
+ </AccordionItemProvider>
443
+ )
444
+ }
445
+ )
446
+
447
+ AccordionItem.displayName = ITEM_NAME
448
+
449
+ /* -------------------------------------------------------------------------------------------------
450
+ * AccordionHeader
451
+ * -----------------------------------------------------------------------------------------------*/
452
+
453
+ const HEADER_NAME = 'AccordionHeader'
454
+
455
+ type AccordionHeaderElement = React.ElementRef<typeof H3>
456
+ type PrimitiveHeading3Props = React.ComponentPropsWithoutRef<typeof H3>
457
+ type AccordionHeaderProps = PrimitiveHeading3Props
458
+
459
+ /**
460
+ * `AccordionHeader` contains the content for the parts of an `AccordionItem` that will be visible
461
+ * whether or not its content is collapsed.
462
+ */
463
+ const AccordionHeader = React.forwardRef<AccordionHeaderElement, AccordionHeaderProps>(
464
+ (props: ScopedProps<AccordionHeaderProps>, forwardedRef) => {
465
+ const { __scopeAccordion, ...headerProps } = props
466
+ const accordionContext = useAccordionContext(ACCORDION_NAME, __scopeAccordion)
467
+ const itemContext = useAccordionItemContext(HEADER_NAME, __scopeAccordion)
468
+ return (
469
+ <H1
470
+ data-orientation={accordionContext.orientation}
471
+ data-state={getState(itemContext.open)}
472
+ data-disabled={itemContext.disabled ? '' : undefined}
473
+ {...headerProps}
474
+ ref={forwardedRef}
475
+ />
476
+ )
477
+ }
478
+ )
479
+
480
+ AccordionHeader.displayName = HEADER_NAME
481
+
482
+ /* -------------------------------------------------------------------------------------------------
483
+ * AccordionTrigger
484
+ * -----------------------------------------------------------------------------------------------*/
485
+
486
+ const AccordionTriggerFrame = styled(Collapsible.Trigger, {
487
+ variants: {
488
+ unstyled: {
489
+ false: {
490
+ cursor: 'pointer',
491
+ backgroundColor: '$background',
492
+ borderColor: '$borderColor',
493
+ borderWidth: 1,
494
+ padding: '$true',
495
+
496
+ hoverStyle: {
497
+ backgroundColor: '$backgroundHover',
498
+ },
499
+
500
+ focusStyle: {
501
+ backgroundColor: '$backgroundFocus',
502
+ },
503
+
504
+ pressStyle: {
505
+ backgroundColor: '$backgroundPress',
506
+ },
507
+ },
508
+ },
509
+ } as const,
510
+
511
+ defaultVariants: {
512
+ unstyled: false,
513
+ },
514
+ })
515
+
516
+ const TRIGGER_NAME = 'AccordionTrigger'
517
+
518
+ type AccordionTrigger = GetRef<typeof AccordionTriggerFrame>
519
+ type AccordionTriggerProps = GetProps<typeof AccordionTriggerFrame>
520
+
521
+ /**
522
+ * `AccordionTrigger` is the trigger that toggles the collapsed state of an `AccordionItem`. It
523
+ * should always be nested inside of an `AccordionHeader`.
524
+ */
525
+ const AccordionTrigger = AccordionTriggerFrame.styleable(function AccordionTrigger(
526
+ props: ScopedProps<AccordionTriggerProps>,
527
+ forwardedRef
528
+ ) {
529
+ const { __scopeAccordion, ...triggerProps } = props
530
+ const accordionContext = useAccordionContext(ACCORDION_NAME, __scopeAccordion)
531
+ const itemContext = useAccordionItemContext(TRIGGER_NAME, __scopeAccordion)
532
+ const collapsibleContext = useAccordionCollapsibleContext(
533
+ TRIGGER_NAME,
534
+ __scopeAccordion
535
+ )
536
+ const collapsibleScope = useCollapsibleScope(__scopeAccordion)
537
+
538
+ return (
539
+ <Collection.ItemSlot scope={__scopeAccordion}>
540
+ <AccordionTriggerFrame
541
+ aria-disabled={(itemContext.open && !collapsibleContext.collapsible) || undefined}
542
+ data-orientation={accordionContext.orientation}
543
+ id={itemContext.triggerId}
544
+ {...collapsibleScope}
545
+ {...triggerProps}
546
+ ref={forwardedRef}
547
+ />
548
+ </Collection.ItemSlot>
549
+ )
550
+ })
551
+
552
+ /* -------------------------------------------------------------------------------------------------
553
+ * AccordionContent
554
+ * -----------------------------------------------------------------------------------------------*/
555
+
556
+ const AccordionContentFrame = styled(Collapsible.Content, {
557
+ variants: {
558
+ unstyled: {
559
+ false: {
560
+ padding: '$true',
561
+ backgroundColor: '$background',
562
+ },
563
+ },
564
+ } as const,
565
+
566
+ defaultVariants: {
567
+ unstyled: false,
568
+ },
569
+ })
570
+
571
+ type AccordionContentProps = GetProps<typeof AccordionContentFrame>
572
+
573
+ /**
574
+ * `AccordionContent` contains the collapsible content for an `AccordionItem`.
575
+ */
576
+ const AccordionContent = AccordionContentFrame.styleable(function AccordionContent(
577
+ props: ScopedProps<AccordionContentProps>,
578
+ forwardedRef
579
+ ) {
580
+ const { __scopeAccordion, ...contentProps } = props
581
+ const accordionContext = useAccordionContext(ACCORDION_NAME, __scopeAccordion)
582
+ const itemContext = useAccordionItemContext('AccordionContent', __scopeAccordion)
583
+ const collapsibleScope = useCollapsibleScope(__scopeAccordion)
584
+ return (
585
+ <AccordionContentFrame
586
+ role="region"
587
+ aria-labelledby={itemContext.triggerId}
588
+ data-orientation={accordionContext.orientation}
589
+ {...collapsibleScope}
590
+ {...contentProps}
591
+ ref={forwardedRef}
592
+ />
593
+ )
594
+ })
595
+
596
+ /* -----------------------------------------------------------------------------------------------*/
597
+
598
+ function getState(open?: boolean) {
599
+ return open ? 'open' : 'closed'
600
+ }
601
+ const Accordion = withStaticProperties(AccordionComponent, {
602
+ Trigger: AccordionTrigger,
603
+ Header: AccordionHeader,
604
+ Content: AccordionContent,
605
+ Item: AccordionItem,
606
+ })
607
+
608
+ export { Accordion, createAccordionScope }
609
+
610
+ export type {
611
+ AccordionContentProps,
612
+ AccordionHeaderProps,
613
+ AccordionItemProps,
614
+ AccordionMultipleProps,
615
+ AccordionSingleProps,
616
+ AccordionTriggerProps,
617
+ }