@cdx-ui/primitives 0.0.1-beta.12 → 0.0.1-beta.14

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.
Files changed (92) hide show
  1. package/README.md +4 -2
  2. package/lib/commonjs/index.js +12 -0
  3. package/lib/commonjs/index.js.map +1 -1
  4. package/lib/commonjs/list-item/createListItemRoot.js +36 -21
  5. package/lib/commonjs/list-item/createListItemRoot.js.map +1 -1
  6. package/lib/commonjs/tile/context.js +30 -0
  7. package/lib/commonjs/tile/context.js.map +1 -0
  8. package/lib/commonjs/tile/createTileContent.js +30 -0
  9. package/lib/commonjs/tile/createTileContent.js.map +1 -0
  10. package/lib/commonjs/tile/createTileDescription.js +28 -0
  11. package/lib/commonjs/tile/createTileDescription.js.map +1 -0
  12. package/lib/commonjs/tile/createTileGroup.js +112 -0
  13. package/lib/commonjs/tile/createTileGroup.js.map +1 -0
  14. package/lib/commonjs/tile/createTileIndicator.js +46 -0
  15. package/lib/commonjs/tile/createTileIndicator.js.map +1 -0
  16. package/lib/commonjs/tile/createTileLeadingSlot.js +34 -0
  17. package/lib/commonjs/tile/createTileLeadingSlot.js.map +1 -0
  18. package/lib/commonjs/tile/createTileRoot.js +133 -0
  19. package/lib/commonjs/tile/createTileRoot.js.map +1 -0
  20. package/lib/commonjs/tile/createTileTitle.js +28 -0
  21. package/lib/commonjs/tile/createTileTitle.js.map +1 -0
  22. package/lib/commonjs/tile/createTileTrailingSlot.js +35 -0
  23. package/lib/commonjs/tile/createTileTrailingSlot.js.map +1 -0
  24. package/lib/commonjs/tile/index.js +55 -0
  25. package/lib/commonjs/tile/index.js.map +1 -0
  26. package/lib/commonjs/tile/types.js +6 -0
  27. package/lib/commonjs/tile/types.js.map +1 -0
  28. package/lib/module/index.js +1 -0
  29. package/lib/module/index.js.map +1 -1
  30. package/lib/module/list-item/createListItemRoot.js +36 -21
  31. package/lib/module/list-item/createListItemRoot.js.map +1 -1
  32. package/lib/module/tile/context.js +21 -0
  33. package/lib/module/tile/context.js.map +1 -0
  34. package/lib/module/tile/createTileContent.js +24 -0
  35. package/lib/module/tile/createTileContent.js.map +1 -0
  36. package/lib/module/tile/createTileDescription.js +22 -0
  37. package/lib/module/tile/createTileDescription.js.map +1 -0
  38. package/lib/module/tile/createTileGroup.js +106 -0
  39. package/lib/module/tile/createTileGroup.js.map +1 -0
  40. package/lib/module/tile/createTileIndicator.js +40 -0
  41. package/lib/module/tile/createTileIndicator.js.map +1 -0
  42. package/lib/module/tile/createTileLeadingSlot.js +28 -0
  43. package/lib/module/tile/createTileLeadingSlot.js.map +1 -0
  44. package/lib/module/tile/createTileRoot.js +127 -0
  45. package/lib/module/tile/createTileRoot.js.map +1 -0
  46. package/lib/module/tile/createTileTitle.js +22 -0
  47. package/lib/module/tile/createTileTitle.js.map +1 -0
  48. package/lib/module/tile/createTileTrailingSlot.js +29 -0
  49. package/lib/module/tile/createTileTrailingSlot.js.map +1 -0
  50. package/lib/module/tile/index.js +39 -0
  51. package/lib/module/tile/index.js.map +1 -0
  52. package/lib/module/tile/types.js +4 -0
  53. package/lib/module/tile/types.js.map +1 -0
  54. package/lib/typescript/index.d.ts +1 -0
  55. package/lib/typescript/index.d.ts.map +1 -1
  56. package/lib/typescript/list-item/createListItemRoot.d.ts.map +1 -1
  57. package/lib/typescript/tile/context.d.ts +9 -0
  58. package/lib/typescript/tile/context.d.ts.map +1 -0
  59. package/lib/typescript/tile/createTileContent.d.ts +3 -0
  60. package/lib/typescript/tile/createTileContent.d.ts.map +1 -0
  61. package/lib/typescript/tile/createTileDescription.d.ts +3 -0
  62. package/lib/typescript/tile/createTileDescription.d.ts.map +1 -0
  63. package/lib/typescript/tile/createTileGroup.d.ts +4 -0
  64. package/lib/typescript/tile/createTileGroup.d.ts.map +1 -0
  65. package/lib/typescript/tile/createTileIndicator.d.ts +4 -0
  66. package/lib/typescript/tile/createTileIndicator.d.ts.map +1 -0
  67. package/lib/typescript/tile/createTileLeadingSlot.d.ts +4 -0
  68. package/lib/typescript/tile/createTileLeadingSlot.d.ts.map +1 -0
  69. package/lib/typescript/tile/createTileRoot.d.ts +4 -0
  70. package/lib/typescript/tile/createTileRoot.d.ts.map +1 -0
  71. package/lib/typescript/tile/createTileTitle.d.ts +3 -0
  72. package/lib/typescript/tile/createTileTitle.d.ts.map +1 -0
  73. package/lib/typescript/tile/createTileTrailingSlot.d.ts +9 -0
  74. package/lib/typescript/tile/createTileTrailingSlot.d.ts.map +1 -0
  75. package/lib/typescript/tile/index.d.ts +15 -0
  76. package/lib/typescript/tile/index.d.ts.map +1 -0
  77. package/lib/typescript/tile/types.d.ts +119 -0
  78. package/lib/typescript/tile/types.d.ts.map +1 -0
  79. package/package.json +2 -2
  80. package/src/index.ts +1 -0
  81. package/src/list-item/createListItemRoot.tsx +37 -22
  82. package/src/tile/context.tsx +23 -0
  83. package/src/tile/createTileContent.tsx +23 -0
  84. package/src/tile/createTileDescription.tsx +19 -0
  85. package/src/tile/createTileGroup.tsx +134 -0
  86. package/src/tile/createTileIndicator.tsx +38 -0
  87. package/src/tile/createTileLeadingSlot.tsx +30 -0
  88. package/src/tile/createTileRoot.tsx +124 -0
  89. package/src/tile/createTileTitle.tsx +19 -0
  90. package/src/tile/createTileTrailingSlot.tsx +25 -0
  91. package/src/tile/index.ts +88 -0
  92. package/src/tile/types.ts +153 -0
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export * from './list-item';
11
11
  export { type EdgeInsets, OverlayInsetsProvider } from './overlay';
12
12
  export * from './select';
13
13
  export * from './switch';
14
+ export * from './tile';
14
15
  export * from './progress';
15
16
  export * from './radio';
16
17
  export type { InteractionState } from './types';
@@ -49,7 +49,17 @@ export const createListItemRoot = <V, P>(
49
49
  }: IListItemProps,
50
50
  ref: React.Ref<unknown>,
51
51
  ) => {
52
- const asChildInteractive = asChild && !!onPress && React.isValidElement(children);
52
+ const childOnPress = React.isValidElement(children)
53
+ ? ((children.props as { onPress?: unknown }).onPress as
54
+ | IListItemProps['onPress']
55
+ | undefined)
56
+ : undefined;
57
+
58
+ // asChild always wins when explicitly set with a valid element — even when the press
59
+ // surface is built internally by the child (e.g. `Link` adds its onPress via `useLink`,
60
+ // not as a JSX-level prop). The clone path below preserves the child's own press by
61
+ // *omitting* `onPress` when there is nothing to compose.
62
+ const asChildInteractive = asChild && React.isValidElement(children);
53
63
  const isPressableRoot = !!onPress && !asChildInteractive;
54
64
 
55
65
  const pressState = usePress({
@@ -74,15 +84,33 @@ export const createListItemRoot = <V, P>(
74
84
  if (asChildInteractive) {
75
85
  const child = children as React.ReactElement<Record<string, unknown>>;
76
86
 
77
- const mergedOnPress = composeEventHandlers(
78
- child.props.onPress as IListItemProps['onPress'],
79
- onPress,
80
- );
81
-
82
87
  const childDisabled = !!(child.props as { disabled?: boolean }).disabled;
83
-
84
88
  const resolvedDisabled = disabled || childDisabled;
85
89
 
90
+ const cloneProps: Record<string, unknown> = {
91
+ ...restProps,
92
+ ...slotAttrs,
93
+ ...dataAttributes({
94
+ active: false,
95
+ hover: false,
96
+ disabled: resolvedDisabled,
97
+ }),
98
+ ...(resolvedDisabled && { accessibilityState: { disabled: true } }),
99
+ disabled: resolvedDisabled,
100
+ ref: mergeRefs(ref, child.props.ref as React.Ref<unknown>),
101
+ style: [rowStyleForCrossAlign(crossAlign), style, child.props.style],
102
+ };
103
+
104
+ // Only override the child's `onPress` when we have something to add (a parent handler)
105
+ // or need to suppress (disabled). Otherwise leave the child's own onPress alone — e.g.
106
+ // `Link` builds its navigation handler inside `useLink`, not as a JSX-level prop, and
107
+ // overriding here would shadow it.
108
+ if (resolvedDisabled) {
109
+ cloneProps.onPress = undefined;
110
+ } else if (onPress) {
111
+ cloneProps.onPress = composeEventHandlers(childOnPress, onPress);
112
+ }
113
+
86
114
  return (
87
115
  <ListItemProvider
88
116
  value={{
@@ -92,20 +120,7 @@ export const createListItemRoot = <V, P>(
92
120
  crossAlign: crossAlign ?? 'center',
93
121
  }}
94
122
  >
95
- {React.cloneElement(child, {
96
- ...restProps,
97
- ...slotAttrs,
98
- ...dataAttributes({
99
- active: false,
100
- hovered: false,
101
- disabled: resolvedDisabled,
102
- }),
103
- ...(resolvedDisabled && { accessibilityState: { disabled: true } }),
104
- disabled: resolvedDisabled,
105
- onPress: resolvedDisabled ? undefined : mergedOnPress,
106
- ref: mergeRefs(ref, child.props.ref as React.Ref<unknown>),
107
- style: [rowStyleForCrossAlign(crossAlign), style, child.props.style],
108
- })}
123
+ {React.cloneElement(child, cloneProps)}
109
124
  </ListItemProvider>
110
125
  );
111
126
  }
@@ -113,7 +128,7 @@ export const createListItemRoot = <V, P>(
113
128
  if (isPressableRoot) {
114
129
  const interactionAttrs = dataAttributes({
115
130
  active: isPressed,
116
- hovered: isHovered,
131
+ hover: isHovered,
117
132
  disabled,
118
133
  });
119
134
 
@@ -0,0 +1,23 @@
1
+ import { createContext as createReactContext, useContext } from 'react';
2
+ import { createContext } from '@cdx-ui/utils';
3
+ import type { ITileContextValue, ITileGroupContextValue } from './types';
4
+
5
+ // Per-tile subtree (selection-aware slots, indicator, etc.)
6
+ export const [TileProvider, useTileContext] = createContext<ITileContextValue>('TileContext');
7
+
8
+ // Per-group subtree (optional — standalone tiles have no provider)
9
+ const TileGroupContext = createReactContext<ITileGroupContextValue | null>(null);
10
+
11
+ export const TileGroupContextProvider = TileGroupContext.Provider;
12
+
13
+ export function useTileGroupContext(): ITileGroupContextValue {
14
+ const ctx = useContext(TileGroupContext);
15
+ if (!ctx) {
16
+ throw new Error('Tile must be used within Tile.Group');
17
+ }
18
+ return ctx;
19
+ }
20
+
21
+ export function useOptionalTileGroupContext(): ITileGroupContextValue | null {
22
+ return useContext(TileGroupContext);
23
+ }
@@ -0,0 +1,23 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import type { ITileContentProps } from './types';
4
+
5
+ const contentStyle = {
6
+ flex: 1,
7
+ flexDirection: 'column' as const,
8
+ minWidth: 0,
9
+ };
10
+
11
+ export const createTileContent = <T,>(Base: React.ComponentType<T>) =>
12
+ forwardRef(({ children, style, ...props }: ITileContentProps, ref: React.Ref<unknown>) => (
13
+ <Base
14
+ {...(props as T)}
15
+ {...dataAttributes({
16
+ slot: 'tile-content',
17
+ })}
18
+ ref={ref as React.Ref<T>}
19
+ style={[contentStyle, style]}
20
+ >
21
+ {children}
22
+ </Base>
23
+ ));
@@ -0,0 +1,19 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import type { ITileDescriptionProps } from './types';
4
+
5
+ const noUnderline = { textDecorationLine: 'none' as const };
6
+
7
+ export const createTileDescription = <T,>(Base: React.ComponentType<T>) =>
8
+ forwardRef(({ children, style, ...props }: ITileDescriptionProps, ref: React.Ref<unknown>) => (
9
+ <Base
10
+ {...(props as T)}
11
+ {...dataAttributes({
12
+ slot: 'tile-description',
13
+ })}
14
+ ref={ref as React.Ref<T>}
15
+ style={[noUnderline, style]}
16
+ >
17
+ {children}
18
+ </Base>
19
+ ));
@@ -0,0 +1,134 @@
1
+ import React, { forwardRef, useCallback, useMemo, type Ref } from 'react';
2
+ import type { ViewProps } from 'react-native';
3
+ import { useControllableState } from '@cdx-ui/utils';
4
+ import { dataAttributes } from '../utils/dataAttributes';
5
+ import { TileGroupContextProvider } from './context';
6
+ import type { ITileGroupProps, TileGroupType } from './types';
7
+
8
+ /**
9
+ * Internal widening of the public discriminated union: the runtime accepts any value
10
+ * shape and reads `type` to decide branch behavior. The public API stays narrow.
11
+ */
12
+ type WidenedTileGroupProps = ViewProps & {
13
+ type: TileGroupType;
14
+ value?: string | string[];
15
+ defaultValue?: string | string[];
16
+ onValueChange?: (value: string | string[]) => void;
17
+ max?: number;
18
+ isDisabled?: boolean;
19
+ 'aria-label'?: string;
20
+ };
21
+
22
+ export const createTileGroup = <T,>(Base: React.ComponentType<T>) =>
23
+ forwardRef((props: ITileGroupProps, ref: Ref<T>) => {
24
+ const {
25
+ children,
26
+ type,
27
+ value: valueProp,
28
+ defaultValue,
29
+ onValueChange,
30
+ max: maxProp,
31
+ isDisabled = false,
32
+ 'aria-label': ariaLabel,
33
+ ...rest
34
+ } = props as WidenedTileGroupProps;
35
+
36
+ const max = maxProp ?? Number.POSITIVE_INFINITY;
37
+
38
+ const defaultProp =
39
+ defaultValue !== undefined ? defaultValue : type === 'multiple' ? [] : undefined;
40
+
41
+ const [state, setState] = useControllableState<string | string[] | undefined>({
42
+ prop: valueProp,
43
+ defaultProp,
44
+ onChange: (next) => {
45
+ if (next !== undefined) {
46
+ onValueChange?.(next);
47
+ }
48
+ },
49
+ });
50
+
51
+ const isSelected = useCallback(
52
+ (tileValue: string) => {
53
+ if (type === 'single') {
54
+ return state === tileValue;
55
+ }
56
+ return Array.isArray(state) && state.includes(tileValue);
57
+ },
58
+ [type, state],
59
+ );
60
+
61
+ const isTileDisabledByGroup = useCallback(
62
+ (tileValue: string) => {
63
+ if (isDisabled) {
64
+ return true;
65
+ }
66
+ if (type !== 'multiple') {
67
+ return false;
68
+ }
69
+ const arr = Array.isArray(state) ? state : [];
70
+ if (arr.length < max) {
71
+ return false;
72
+ }
73
+ return !arr.includes(tileValue);
74
+ },
75
+ [isDisabled, type, state, max],
76
+ );
77
+
78
+ const toggleValue = useCallback(
79
+ (tileValue: string) => {
80
+ if (isDisabled) {
81
+ return;
82
+ }
83
+ if (type === 'single') {
84
+ if (state === tileValue) {
85
+ return;
86
+ }
87
+ setState(tileValue);
88
+ return;
89
+ }
90
+ const arr = Array.isArray(state) ? [...state] : [];
91
+ const idx = arr.indexOf(tileValue);
92
+ if (idx >= 0) {
93
+ arr.splice(idx, 1);
94
+ setState(arr);
95
+ } else if (arr.length < max) {
96
+ arr.push(tileValue);
97
+ setState(arr);
98
+ }
99
+ },
100
+ [isDisabled, type, state, setState, max],
101
+ );
102
+
103
+ const contextValue = useMemo(
104
+ () => ({
105
+ type,
106
+ value: state,
107
+ toggleValue,
108
+ isSelected,
109
+ isTileDisabledByGroup,
110
+ isGroupDisabled: isDisabled,
111
+ max,
112
+ }),
113
+ [type, state, toggleValue, isSelected, isTileDisabledByGroup, isDisabled, max],
114
+ );
115
+
116
+ const role = type === 'single' ? 'radiogroup' : 'group';
117
+
118
+ return (
119
+ <TileGroupContextProvider value={contextValue}>
120
+ <Base
121
+ {...(rest as T)}
122
+ ref={ref}
123
+ role={role}
124
+ {...(type === 'single' ? { accessibilityRole: 'radiogroup' as const } : {})}
125
+ {...(ariaLabel ? { 'aria-label': ariaLabel, accessibilityLabel: ariaLabel } : {})}
126
+ {...dataAttributes({
127
+ slot: 'tile-group',
128
+ })}
129
+ >
130
+ {children}
131
+ </Base>
132
+ </TileGroupContextProvider>
133
+ );
134
+ });
@@ -0,0 +1,38 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import { useTileContext } from './context';
4
+ import type { ITileIndicatorProps } from './types';
5
+
6
+ const shrinkZero = { flexShrink: 0 as const, pointerEvents: 'none' as const };
7
+
8
+ export const createTileIndicator = <T,>(Base: React.ComponentType<T>) =>
9
+ forwardRef(
10
+ (
11
+ { children, style, indicatorType, ...props }: ITileIndicatorProps,
12
+ ref: React.Ref<unknown>,
13
+ ) => {
14
+ const { isSelected, isDisabled, selectionType } = useTileContext();
15
+
16
+ // Effective visual type: explicit prop wins; otherwise infer from group/standalone context.
17
+ const effectiveType: 'radio' | 'checkbox' =
18
+ indicatorType ?? (selectionType === 'single' ? 'radio' : 'checkbox');
19
+
20
+ return (
21
+ <Base
22
+ {...(props as T)}
23
+ accessibilityElementsHidden
24
+ aria-hidden
25
+ {...dataAttributes({
26
+ slot: 'tile-indicator',
27
+ checked: isSelected,
28
+ selectionType: effectiveType === 'radio' ? 'single' : 'multiple',
29
+ disabled: isDisabled,
30
+ })}
31
+ ref={ref as React.Ref<T>}
32
+ style={[shrinkZero, style]}
33
+ >
34
+ {children}
35
+ </Base>
36
+ );
37
+ },
38
+ );
@@ -0,0 +1,30 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import type { ITileLeadingSlotProps } from './types';
4
+
5
+ const shrinkZero = { flexShrink: 0 as const };
6
+
7
+ export const createTileLeadingSlot = <T,>(Base: React.ComponentType<T>) =>
8
+ forwardRef(
9
+ (
10
+ { 'aria-hidden': ariaHidden, style, children, ...props }: ITileLeadingSlotProps,
11
+ ref: React.Ref<unknown>,
12
+ ) => {
13
+ const accessibilityElementsHidden = ariaHidden !== false;
14
+
15
+ return (
16
+ <Base
17
+ {...(props as T)}
18
+ {...dataAttributes({
19
+ slot: 'tile-leading',
20
+ })}
21
+ accessibilityElementsHidden={accessibilityElementsHidden}
22
+ aria-hidden={ariaHidden}
23
+ ref={ref as React.Ref<T>}
24
+ style={[shrinkZero, style]}
25
+ >
26
+ {children}
27
+ </Base>
28
+ );
29
+ },
30
+ );
@@ -0,0 +1,124 @@
1
+ import React, { forwardRef, useMemo } from 'react';
2
+ import { composeEventHandlers, mergeRefs, useControllableState } from '@cdx-ui/utils';
3
+ import { useFocus } from '@react-native-aria/focus';
4
+ import { useHover, usePress } from '@react-native-aria/interactions';
5
+ import { dataAttributes } from '../utils/dataAttributes';
6
+ import { TileProvider, useOptionalTileGroupContext } from './context';
7
+ import type { ITileProps, ITilePressablePassthrough } from './types';
8
+
9
+ const rowStyle = {
10
+ flexDirection: 'row' as const,
11
+ alignSelf: 'stretch' as const,
12
+ alignItems: 'center' as const,
13
+ };
14
+
15
+ export const createTileRoot = <T,>(BasePressable: React.ComponentType<T>) =>
16
+ forwardRef((props: ITileProps, ref: React.Ref<unknown>) => {
17
+ const {
18
+ value,
19
+ disabled: disabledProp = false,
20
+ isSelected: controlledSelected,
21
+ defaultSelected,
22
+ onSelectedChange,
23
+ children,
24
+ onPress,
25
+ onFocus,
26
+ onBlur,
27
+ style,
28
+ /** Consumed by styled `withStyleContext` root. */
29
+ context: _styleContext,
30
+ ...rest
31
+ } = props;
32
+
33
+ const group = useOptionalTileGroupContext();
34
+
35
+ // Standalone selection state — only meaningful when no group owns selection.
36
+ const [standaloneSelected = false, setStandaloneSelected] = useControllableState<boolean>({
37
+ prop: controlledSelected,
38
+ defaultProp: defaultSelected ?? false,
39
+ onChange: (next) => {
40
+ onSelectedChange?.(next);
41
+ },
42
+ });
43
+
44
+ const isSelected = group ? group.isSelected(value) : standaloneSelected;
45
+ const disabledByGroup = group ? group.isTileDisabledByGroup(value) : false;
46
+ const isDisabled = disabledByGroup || disabledProp;
47
+ const cannotToggle = isDisabled;
48
+
49
+ const { focusProps, isFocused } = useFocus();
50
+ const { pressProps, isPressed } = usePress({ isDisabled: cannotToggle });
51
+ const { hoverProps, isHovered } = useHover();
52
+
53
+ // Standalone tiles use checkbox semantics (independent on/off toggle).
54
+ const selectionType = group ? group.type : 'multiple';
55
+
56
+ const accessibilityState = useMemo(
57
+ () => ({
58
+ disabled: isDisabled,
59
+ ...(selectionType === 'single' ? { selected: isSelected } : { checked: isSelected }),
60
+ }),
61
+ [isDisabled, isSelected, selectionType],
62
+ );
63
+
64
+ const tileContext = useMemo(
65
+ () => ({ value, isSelected, isDisabled, selectionType }),
66
+ [value, isSelected, isDisabled, selectionType],
67
+ );
68
+
69
+ const passthrough = rest as ITilePressablePassthrough;
70
+
71
+ const composedOnPress = composeEventHandlers(onPress, () => {
72
+ if (cannotToggle) {
73
+ return;
74
+ }
75
+ if (group) {
76
+ group.toggleValue(value);
77
+ } else {
78
+ setStandaloneSelected(!standaloneSelected);
79
+ }
80
+ });
81
+
82
+ const sharedHandlers = {
83
+ onPress: isDisabled ? undefined : composedOnPress,
84
+ onPressIn: composeEventHandlers(passthrough.onPressIn, pressProps.onPressIn),
85
+ onPressOut: composeEventHandlers(passthrough.onPressOut, pressProps.onPressOut),
86
+ onHoverIn: composeEventHandlers(passthrough.onHoverIn, hoverProps.onHoverIn),
87
+ onHoverOut: composeEventHandlers(passthrough.onHoverOut, hoverProps.onHoverOut),
88
+ onFocus: composeEventHandlers(onFocus, focusProps.onFocus),
89
+ onBlur: composeEventHandlers(onBlur, focusProps.onBlur),
90
+ } as const;
91
+
92
+ const role = selectionType === 'single' ? 'radio' : 'checkbox';
93
+
94
+ const sharedAttrs = {
95
+ accessibilityRole: role,
96
+ role,
97
+ accessibilityState,
98
+ 'aria-checked': isSelected,
99
+ ...(isDisabled ? { 'aria-disabled': true as const } : {}),
100
+ ...dataAttributes({
101
+ slot: 'tile',
102
+ state: isSelected ? 'selected' : 'unselected',
103
+ disabled: isDisabled,
104
+ active: isPressed,
105
+ hover: isHovered,
106
+ focused: isFocused,
107
+ }),
108
+ };
109
+
110
+ return (
111
+ <TileProvider value={tileContext}>
112
+ <BasePressable
113
+ {...(rest as T)}
114
+ context={_styleContext}
115
+ ref={mergeRefs(ref) as React.Ref<T>}
116
+ {...sharedAttrs}
117
+ {...sharedHandlers}
118
+ style={[rowStyle, style]}
119
+ >
120
+ {children}
121
+ </BasePressable>
122
+ </TileProvider>
123
+ );
124
+ });
@@ -0,0 +1,19 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import type { ITileTitleProps } from './types';
4
+
5
+ const noUnderline = { textDecorationLine: 'none' as const };
6
+
7
+ export const createTileTitle = <T,>(Base: React.ComponentType<T>) =>
8
+ forwardRef(({ children, style, ...props }: ITileTitleProps, ref: React.Ref<unknown>) => (
9
+ <Base
10
+ {...(props as T)}
11
+ {...dataAttributes({
12
+ slot: 'tile-title',
13
+ })}
14
+ ref={ref as React.Ref<T>}
15
+ style={[noUnderline, style]}
16
+ >
17
+ {children}
18
+ </Base>
19
+ ));
@@ -0,0 +1,25 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import type { ITileTrailingSlotProps } from './types';
4
+
5
+ const shrinkZero = { flexShrink: 0 as const };
6
+
7
+ /**
8
+ * Generic trailing content (chevron, amount, chip, status). Distinct from `Tile.Indicator`,
9
+ * which is purpose-built for the radio/checkbox selection affordance.
10
+ *
11
+ * Trailing content is meaningful by default (`aria-hidden` falsy), unlike the indicator.
12
+ */
13
+ export const createTileTrailingSlot = <T,>(Base: React.ComponentType<T>) =>
14
+ forwardRef(({ children, style, ...props }: ITileTrailingSlotProps, ref: React.Ref<unknown>) => (
15
+ <Base
16
+ {...(props as T)}
17
+ {...dataAttributes({
18
+ slot: 'tile-trailing',
19
+ })}
20
+ ref={ref as React.Ref<T>}
21
+ style={[shrinkZero, style]}
22
+ >
23
+ {children}
24
+ </Base>
25
+ ));
@@ -0,0 +1,88 @@
1
+ import type React from 'react';
2
+ import { createTileContent } from './createTileContent';
3
+ import { createTileDescription } from './createTileDescription';
4
+ import { createTileGroup } from './createTileGroup';
5
+ import { createTileIndicator } from './createTileIndicator';
6
+ import { createTileLeadingSlot } from './createTileLeadingSlot';
7
+ import { createTileRoot } from './createTileRoot';
8
+ import { createTileTitle } from './createTileTitle';
9
+ import { createTileTrailingSlot } from './createTileTrailingSlot';
10
+ import type { ITileComponentType } from './types';
11
+
12
+ export type {
13
+ ITileComponentType,
14
+ ITileContentProps,
15
+ ITileContextValue,
16
+ ITileDescriptionProps,
17
+ ITileGroupMultipleProps,
18
+ ITileGroupProps,
19
+ ITileGroupSingleProps,
20
+ ITileGroupContextValue,
21
+ ITileIndicatorProps,
22
+ ITileLeadingSlotProps,
23
+ ITilePressablePassthrough,
24
+ ITileProps,
25
+ ITileTitleProps,
26
+ ITileTrailingSlotProps,
27
+ TileGroupType,
28
+ TileGroupValue,
29
+ } from './types';
30
+
31
+ export { TileProvider, useTileContext } from './context';
32
+
33
+ export function createTile<
34
+ Pressable,
35
+ Leading,
36
+ Content,
37
+ Title,
38
+ Description,
39
+ Indicator,
40
+ TrailingSlot,
41
+ Group,
42
+ >(BaseComponents: {
43
+ Pressable: React.ComponentType<Pressable>;
44
+ LeadingSlot: React.ComponentType<Leading>;
45
+ Content: React.ComponentType<Content>;
46
+ Title: React.ComponentType<Title>;
47
+ Description: React.ComponentType<Description>;
48
+ Indicator: React.ComponentType<Indicator>;
49
+ TrailingSlot: React.ComponentType<TrailingSlot>;
50
+ Group: React.ComponentType<Group>;
51
+ }) {
52
+ const Tile = createTileRoot(BaseComponents.Pressable);
53
+ const Group = createTileGroup(BaseComponents.Group);
54
+ const LeadingSlot = createTileLeadingSlot(BaseComponents.LeadingSlot);
55
+ const Content = createTileContent(BaseComponents.Content);
56
+ const Title = createTileTitle(BaseComponents.Title);
57
+ const Description = createTileDescription(BaseComponents.Description);
58
+ const Indicator = createTileIndicator(BaseComponents.Indicator);
59
+ const TrailingSlot = createTileTrailingSlot(BaseComponents.TrailingSlot);
60
+
61
+ Tile.displayName = 'TilePrimitive';
62
+ Group.displayName = 'TilePrimitive.Group';
63
+ LeadingSlot.displayName = 'TilePrimitive.LeadingSlot';
64
+ Content.displayName = 'TilePrimitive.Content';
65
+ Title.displayName = 'TilePrimitive.Title';
66
+ Description.displayName = 'TilePrimitive.Description';
67
+ Indicator.displayName = 'TilePrimitive.Indicator';
68
+ TrailingSlot.displayName = 'TilePrimitive.TrailingSlot';
69
+
70
+ return Object.assign(Tile, {
71
+ Group,
72
+ LeadingSlot,
73
+ Content,
74
+ Title,
75
+ Description,
76
+ Indicator,
77
+ TrailingSlot,
78
+ }) as ITileComponentType<
79
+ Pressable,
80
+ Leading,
81
+ Content,
82
+ Title,
83
+ Description,
84
+ Indicator,
85
+ TrailingSlot,
86
+ Group
87
+ >;
88
+ }