@cdx-ui/components 0.0.1-beta.175 → 0.0.1-beta.177

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.
@@ -1,26 +1,25 @@
1
- import { forwardRef, useMemo, type ReactNode } from 'react';
1
+ import { cloneElement, forwardRef, isValidElement, useMemo, type ReactNode } from 'react';
2
2
  import { Pressable, Text, View, type TextProps, type ViewProps } from 'react-native';
3
3
  import {
4
+ AccordionContentRoot as AccordionContentRootPrimitive,
4
5
  AccordionItemRoot as AccordionItemRootPrimitive,
5
- AccordionPanelRoot as AccordionPanelRootPrimitive,
6
6
  AccordionRoot as AccordionRootPrimitive,
7
7
  AccordionTriggerRoot as AccordionTriggerRootPrimitive,
8
8
  Slot,
9
9
  dataAttributes,
10
10
  mergeDataAttributes,
11
11
  useAccordionItemContext,
12
+ type IAccordionContentProps,
12
13
  type IAccordionItemProps,
13
14
  type IAccordionProps,
14
15
  type IAccordionTriggerProps,
15
16
  } from '@cdx-ui/primitives';
16
17
  import { KeyboardArrowDown } from '@cdx-ui/icons';
17
18
  import { cn, ParentContext, useParentContext, useStyleContext } from '@cdx-ui/utils';
18
- import { Divider } from '../Divider';
19
19
  import { Icon } from '../Icon';
20
20
  import {
21
- accordionBodyVariants,
21
+ accordionContentVariants,
22
22
  accordionDescriptionVariants,
23
- accordionHeadingVariants,
24
23
  accordionIndicatorVariants,
25
24
  accordionItemVariants,
26
25
  accordionLabelVariants,
@@ -29,20 +28,18 @@ import {
29
28
  accordionRootVariants,
30
29
  accordionTitleVariants,
31
30
  accordionTriggerVariants,
32
- type AccordionItemVariantProps,
33
31
  } from './styles';
34
32
 
35
33
  const ITEM_SCOPE = 'ACCORDION_ITEM';
36
34
 
37
- type AccordionItemStyleContext = Pick<AccordionItemVariantProps, 'disableGutters'> & {
38
- showSeparator?: boolean;
39
- };
35
+ interface AccordionItemStyleContext {
36
+ fullBleed?: boolean;
37
+ }
40
38
 
41
- const useAccordionItemStyleContext = (): AccordionItemStyleContext => {
39
+ const useAccordionItemStyleContext = (): Required<AccordionItemStyleContext> => {
42
40
  const ctx = useStyleContext(ITEM_SCOPE) as AccordionItemStyleContext | undefined;
43
41
  return {
44
- disableGutters: ctx?.disableGutters ?? false,
45
- showSeparator: ctx?.showSeparator ?? true,
42
+ fullBleed: ctx?.fullBleed ?? false,
46
43
  };
47
44
  };
48
45
 
@@ -50,15 +47,17 @@ const useAccordionItemStyleContext = (): AccordionItemStyleContext => {
50
47
  // ROOT
51
48
  // =============================================================================
52
49
 
53
- export interface AccordionProps extends IAccordionProps {
50
+ export type AccordionProps = IAccordionProps & {
51
+ asChild?: boolean;
54
52
  className?: string;
55
53
  children?: ReactNode;
56
- }
54
+ };
57
55
 
58
56
  const AccordionRoot = forwardRef<View, AccordionProps>(
59
- ({ className, style, children, ...props }, ref) => (
57
+ ({ asChild, className, style, children, ...props }, ref) => (
60
58
  <AccordionRootPrimitive
61
59
  ref={ref}
60
+ asChild={asChild}
62
61
  className={cn(accordionRootVariants(), className)}
63
62
  style={style}
64
63
  {...props}
@@ -75,32 +74,33 @@ AccordionRoot.displayName = 'Accordion';
75
74
  // =============================================================================
76
75
 
77
76
  export interface AccordionItemProps extends IAccordionItemProps {
77
+ asChild?: boolean;
78
78
  className?: string;
79
79
  children?: ReactNode;
80
- /** Removes horizontal padding on trigger and body. */
81
- disableGutters?: boolean;
82
- /** Renders a Divider below the item when true. @default true */
80
+ /** Removes horizontal padding so trigger and content can render edge to edge. @default false */
81
+ fullBleed?: boolean;
82
+ /** Renders a separator below the item when true. @default true */
83
83
  showSeparator?: boolean;
84
84
  }
85
85
 
86
86
  const AccordionItem = forwardRef<View, AccordionItemProps>(
87
- ({ className, style, children, disableGutters = false, showSeparator = true, ...props }, ref) => {
87
+ (
88
+ { asChild, className, style, children, fullBleed = false, showSeparator = true, ...props },
89
+ ref,
90
+ ) => {
88
91
  const parent = useParentContext();
89
- const ctx = useMemo(
90
- () => ({ ...parent, [ITEM_SCOPE]: { disableGutters, showSeparator } }),
91
- [parent, disableGutters, showSeparator],
92
- );
92
+ const ctx = useMemo(() => ({ ...parent, [ITEM_SCOPE]: { fullBleed } }), [parent, fullBleed]);
93
93
 
94
94
  return (
95
95
  <ParentContext.Provider value={ctx}>
96
96
  <AccordionItemRootPrimitive
97
97
  ref={ref}
98
- className={cn(accordionItemVariants(), className)}
98
+ asChild={asChild}
99
+ className={cn(accordionItemVariants({ showSeparator }), className)}
99
100
  style={style}
100
101
  {...props}
101
102
  >
102
103
  {children}
103
- {showSeparator ? <Divider /> : null}
104
104
  </AccordionItemRootPrimitive>
105
105
  </ParentContext.Provider>
106
106
  );
@@ -122,38 +122,50 @@ export interface AccordionTriggerProps extends IAccordionTriggerProps {
122
122
  * applied so the substituted component's own appearance wins.
123
123
  */
124
124
  asChild?: boolean;
125
+ /**
126
+ * Replaces the default open/closed chevron. Pass `null` to suppress the
127
+ * indicator. When `asChild` is set, only an explicitly provided indicator is
128
+ * appended to the substituted child.
129
+ */
130
+ indicator?: ReactNode;
125
131
  className?: string;
126
132
  children?: ReactNode;
127
133
  }
128
134
 
129
135
  const AccordionTrigger = forwardRef<React.ComponentRef<typeof Pressable>, AccordionTriggerProps>(
130
- ({ className, style, children, asChild, accessibilityLabel, ...props }, ref) => {
131
- const { disableGutters } = useAccordionItemStyleContext();
136
+ ({ className, style, children, asChild, indicator, accessibilityLabel, ...props }, ref) => {
137
+ const { fullBleed } = useAccordionItemStyleContext();
132
138
 
133
139
  const computedClassName = asChild
134
140
  ? className
135
- : cn(accordionTriggerVariants({ disableGutters }), className);
141
+ : cn(accordionTriggerVariants({ fullBleed }), className);
142
+ const triggerIndicator =
143
+ indicator === undefined ? asChild ? undefined : <AccordionIndicator /> : indicator;
144
+ const triggerChildren =
145
+ asChild &&
146
+ triggerIndicator !== undefined &&
147
+ isValidElement<{ children?: ReactNode }>(children)
148
+ ? cloneElement(children, undefined, children.props.children, triggerIndicator)
149
+ : children;
136
150
 
137
151
  return (
138
- <View className={cn(accordionHeadingVariants())}>
139
- <AccordionTriggerRootPrimitive
140
- ref={ref}
141
- asChild={asChild}
142
- accessibilityLabel={accessibilityLabel}
143
- className={computedClassName}
144
- style={style}
145
- {...props}
146
- >
147
- {asChild ? (
148
- children
149
- ) : (
150
- <>
151
- {children}
152
- <AccordionIndicator />
153
- </>
154
- )}
155
- </AccordionTriggerRootPrimitive>
156
- </View>
152
+ <AccordionTriggerRootPrimitive
153
+ ref={ref}
154
+ asChild={asChild}
155
+ accessibilityLabel={accessibilityLabel}
156
+ className={computedClassName}
157
+ style={style}
158
+ {...props}
159
+ >
160
+ {asChild ? (
161
+ triggerChildren
162
+ ) : (
163
+ <>
164
+ {children}
165
+ {triggerIndicator}
166
+ </>
167
+ )}
168
+ </AccordionTriggerRootPrimitive>
157
169
  );
158
170
  },
159
171
  );
@@ -165,26 +177,32 @@ AccordionTrigger.displayName = 'Accordion.Trigger';
165
177
  // =============================================================================
166
178
 
167
179
  export interface AccordionIndicatorProps extends ViewProps {
180
+ asChild?: boolean;
168
181
  className?: string;
182
+ children?: ReactNode;
169
183
  }
170
184
 
171
185
  const AccordionIndicator = forwardRef<View, AccordionIndicatorProps>(
172
- ({ className, style, ...props }, ref) => {
173
- const { isExpanded } = useAccordionItemContext();
186
+ ({ asChild, className, style, children, ...props }, ref) => {
187
+ const { isOpen } = useAccordionItemContext();
188
+ const Comp = asChild ? Slot : View;
174
189
 
175
190
  return (
176
- <View
191
+ <Comp
177
192
  ref={ref}
178
193
  className={cn(accordionIndicatorVariants(), className)}
179
194
  style={style}
180
195
  {...props}
181
- {...dataAttributes({
182
- slot: 'accordion-indicator',
183
- state: isExpanded ? 'open' : 'closed',
184
- })}
196
+ {...mergeDataAttributes(
197
+ props,
198
+ dataAttributes({
199
+ slot: 'accordion-indicator',
200
+ state: isOpen ? 'open' : 'closed',
201
+ }),
202
+ )}
185
203
  >
186
- <Icon as={KeyboardArrowDown} size={24} />
187
- </View>
204
+ {children ?? <Icon as={KeyboardArrowDown} size={24} />}
205
+ </Comp>
188
206
  );
189
207
  },
190
208
  );
@@ -309,53 +327,34 @@ const AccordionMeta = forwardRef<Text, AccordionMetaProps>(
309
327
  AccordionMeta.displayName = 'Accordion.Meta';
310
328
 
311
329
  // =============================================================================
312
- // PANEL / BODY
330
+ // CONTENT
313
331
  // =============================================================================
314
332
 
315
- export interface AccordionPanelProps extends ViewProps {
316
- className?: string;
317
- children?: ReactNode;
318
- }
319
-
320
- const AccordionPanel = forwardRef<View, AccordionPanelProps>(
321
- ({ className, style, children, ...props }, ref) => (
322
- <AccordionPanelRootPrimitive
323
- ref={ref}
324
- className={cn('w-full', className)}
325
- style={style}
326
- {...props}
327
- >
328
- {children}
329
- </AccordionPanelRootPrimitive>
330
- ),
331
- );
332
-
333
- AccordionPanel.displayName = 'Accordion.Panel';
334
-
335
- export interface AccordionBodyProps extends ViewProps {
333
+ export interface AccordionContentProps extends IAccordionContentProps {
334
+ asChild?: boolean;
336
335
  className?: string;
337
336
  children?: ReactNode;
338
337
  }
339
338
 
340
- const AccordionBody = forwardRef<View, AccordionBodyProps>(
341
- ({ className, style, children, ...props }, ref) => {
342
- const { disableGutters } = useAccordionItemStyleContext();
339
+ const AccordionContent = forwardRef<View, AccordionContentProps>(
340
+ ({ asChild, className, style, children, ...props }, ref) => {
341
+ const { fullBleed } = useAccordionItemStyleContext();
343
342
 
344
343
  return (
345
- <View
344
+ <AccordionContentRootPrimitive
346
345
  ref={ref}
347
- className={cn(accordionBodyVariants({ disableGutters }), className)}
346
+ asChild={asChild}
347
+ className={cn(accordionContentVariants({ fullBleed }), className)}
348
348
  style={style}
349
349
  {...props}
350
- {...mergeDataAttributes(props, dataAttributes({ slot: 'accordion-body' }))}
351
350
  >
352
351
  {children}
353
- </View>
352
+ </AccordionContentRootPrimitive>
354
353
  );
355
354
  },
356
355
  );
357
356
 
358
- AccordionBody.displayName = 'Accordion.Body';
357
+ AccordionContent.displayName = 'Accordion.Content';
359
358
 
360
359
  // =============================================================================
361
360
  // COMPOUND EXPORT
@@ -370,8 +369,7 @@ type AccordionCompoundComponent = typeof AccordionRoot & {
370
369
  Description: typeof AccordionDescription;
371
370
  Meta: typeof AccordionMeta;
372
371
  Indicator: typeof AccordionIndicator;
373
- Panel: typeof AccordionPanel;
374
- Body: typeof AccordionBody;
372
+ Content: typeof AccordionContent;
375
373
  };
376
374
 
377
375
  export const Accordion = Object.assign(AccordionRoot, {
@@ -383,6 +381,5 @@ export const Accordion = Object.assign(AccordionRoot, {
383
381
  Description: AccordionDescription,
384
382
  Meta: AccordionMeta,
385
383
  Indicator: AccordionIndicator,
386
- Panel: AccordionPanel,
387
- Body: AccordionBody,
384
+ Content: AccordionContent,
388
385
  }) as AccordionCompoundComponent;
@@ -1,48 +1,50 @@
1
1
  import { Platform } from 'react-native';
2
- import { cva, type VariantProps } from '@cdx-ui/utils';
3
- import { DISABLED_CURSOR, TRANSITION_COLORS } from '../../styles/constants';
2
+ import { cva } from '@cdx-ui/utils';
3
+ import { DISABLED_CURSOR } from '../../styles/constants';
4
4
 
5
5
  export const accordionRootVariants = cva(['flex w-full flex-col']);
6
6
 
7
- export const accordionItemVariants = cva([
8
- 'flex w-full flex-col overflow-clip',
9
- 'data-[disabled=true]:opacity-[var(--opacity-disabled)]',
10
- ]);
11
-
12
- export const accordionHeadingVariants = cva([
13
- 'relative w-full bg-[var(--color-transparent,rgba(255,255,255,0))]',
14
- Platform.select({
15
- web: [
16
- 'before:pointer-events-none before:absolute before:inset-0 before:z-0 before:bg-content-primary before:opacity-0 before:content-[""]',
17
- 'before:transition-opacity before:duration-150 before:ease-out',
18
- // Overlay tracks trigger interaction data-* (Figma Accordion.Heading > [background-color]).
19
- 'has-[[data-hover=true]]:before:opacity-[var(--opacity-hover)]',
20
- 'has-[[data-active=true][data-hover=true]]:before:opacity-[var(--opacity-hover-overlay)]',
21
- 'has-[[data-focus-visible=true]]:before:opacity-[var(--opacity-focus)]',
22
- ].join(' '),
23
- default: '',
24
- }),
25
- ]);
7
+ export const accordionItemVariants = cva(
8
+ ['flex w-full flex-col', 'data-[disabled=true]:opacity-[var(--opacity-disabled)]'],
9
+ {
10
+ variants: {
11
+ showSeparator: {
12
+ true: 'border-b border-solid border-stroke-secondary',
13
+ false: 'border-b-0',
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ showSeparator: true,
18
+ },
19
+ },
20
+ );
26
21
 
27
22
  export const accordionTriggerVariants = cva(
28
23
  [
29
- 'relative z-10 flex w-full flex-row items-center gap-3 min-h-[52px] bg-transparent outline-none',
24
+ 'relative flex w-full flex-row items-center gap-3 min-h-[52px] bg-[var(--color-transparent)] outline-none',
30
25
  'web:outline-none web:focus:outline-none web:focus-visible:outline-none',
31
26
  DISABLED_CURSOR,
32
27
  Platform.select({
33
- web: '',
28
+ web: [
29
+ 'before:pointer-events-none before:absolute before:inset-0 before:z-0 before:bg-content-primary before:opacity-0 before:content-[""]',
30
+ 'before:transition-opacity before:duration-150 before:ease-out',
31
+ 'data-[hover=true]:data-[state="closed"]:before:opacity-[var(--opacity-hover)]',
32
+ 'data-[active=true]:data-[hover=true]:data-[state="closed"]:before:opacity-[var(--opacity-hover-overlay)]',
33
+ 'data-[focus-visible=true]:before:opacity-[var(--opacity-focus)]',
34
+ 'data-[focus-visible=true]:ring-2 data-[focus-visible=true]:ring-stroke-focus data-[focus-visible=true]:ring-offset-2',
35
+ ].join(' '),
34
36
  default: 'data-[active=true]:bg-surface-primary-hover',
35
37
  }),
36
38
  ],
37
39
  {
38
40
  variants: {
39
- disableGutters: {
41
+ fullBleed: {
40
42
  true: 'px-0 py-4',
41
43
  false: 'p-4',
42
44
  },
43
45
  },
44
46
  defaultVariants: {
45
- disableGutters: false,
47
+ fullBleed: false,
46
48
  },
47
49
  },
48
50
  );
@@ -64,19 +66,17 @@ export const accordionMetaVariants = cva(['body-xs text-content-secondary']);
64
66
  export const accordionIndicatorVariants = cva([
65
67
  'relative z-10 flex shrink-0 items-center justify-center text-content-primary',
66
68
  'data-[state=open]:rotate-180',
67
- TRANSITION_COLORS,
69
+ Platform.select({ web: 'transition-transform duration-150 ease-out', default: '' }),
68
70
  ]);
69
71
 
70
- export const accordionBodyVariants = cva(['flex w-full flex-col gap-2 pb-4'], {
72
+ export const accordionContentVariants = cva(['flex w-full flex-col gap-2 pb-4'], {
71
73
  variants: {
72
- disableGutters: {
74
+ fullBleed: {
73
75
  true: 'px-0',
74
76
  false: 'px-4',
75
77
  },
76
78
  },
77
79
  defaultVariants: {
78
- disableGutters: false,
80
+ fullBleed: false,
79
81
  },
80
82
  });
81
-
82
- export type AccordionItemVariantProps = VariantProps<typeof accordionTriggerVariants>;