@cdx-ui/primitives 0.0.1-beta.11 → 0.0.1-beta.13
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/README.md +1 -0
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/list-item/createListItemRoot.js +36 -21
- package/lib/commonjs/list-item/createListItemRoot.js.map +1 -1
- package/lib/commonjs/tile/context.js +30 -0
- package/lib/commonjs/tile/context.js.map +1 -0
- package/lib/commonjs/tile/createTileContent.js +30 -0
- package/lib/commonjs/tile/createTileContent.js.map +1 -0
- package/lib/commonjs/tile/createTileDescription.js +28 -0
- package/lib/commonjs/tile/createTileDescription.js.map +1 -0
- package/lib/commonjs/tile/createTileGroup.js +112 -0
- package/lib/commonjs/tile/createTileGroup.js.map +1 -0
- package/lib/commonjs/tile/createTileIndicator.js +46 -0
- package/lib/commonjs/tile/createTileIndicator.js.map +1 -0
- package/lib/commonjs/tile/createTileLeadingSlot.js +34 -0
- package/lib/commonjs/tile/createTileLeadingSlot.js.map +1 -0
- package/lib/commonjs/tile/createTileRoot.js +133 -0
- package/lib/commonjs/tile/createTileRoot.js.map +1 -0
- package/lib/commonjs/tile/createTileTitle.js +28 -0
- package/lib/commonjs/tile/createTileTitle.js.map +1 -0
- package/lib/commonjs/tile/createTileTrailingSlot.js +35 -0
- package/lib/commonjs/tile/createTileTrailingSlot.js.map +1 -0
- package/lib/commonjs/tile/index.js +55 -0
- package/lib/commonjs/tile/index.js.map +1 -0
- package/lib/commonjs/tile/types.js +6 -0
- package/lib/commonjs/tile/types.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/list-item/createListItemRoot.js +36 -21
- package/lib/module/list-item/createListItemRoot.js.map +1 -1
- package/lib/module/tile/context.js +21 -0
- package/lib/module/tile/context.js.map +1 -0
- package/lib/module/tile/createTileContent.js +24 -0
- package/lib/module/tile/createTileContent.js.map +1 -0
- package/lib/module/tile/createTileDescription.js +22 -0
- package/lib/module/tile/createTileDescription.js.map +1 -0
- package/lib/module/tile/createTileGroup.js +106 -0
- package/lib/module/tile/createTileGroup.js.map +1 -0
- package/lib/module/tile/createTileIndicator.js +40 -0
- package/lib/module/tile/createTileIndicator.js.map +1 -0
- package/lib/module/tile/createTileLeadingSlot.js +28 -0
- package/lib/module/tile/createTileLeadingSlot.js.map +1 -0
- package/lib/module/tile/createTileRoot.js +127 -0
- package/lib/module/tile/createTileRoot.js.map +1 -0
- package/lib/module/tile/createTileTitle.js +22 -0
- package/lib/module/tile/createTileTitle.js.map +1 -0
- package/lib/module/tile/createTileTrailingSlot.js +29 -0
- package/lib/module/tile/createTileTrailingSlot.js.map +1 -0
- package/lib/module/tile/index.js +39 -0
- package/lib/module/tile/index.js.map +1 -0
- package/lib/module/tile/types.js +4 -0
- package/lib/module/tile/types.js.map +1 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/list-item/createListItemRoot.d.ts.map +1 -1
- package/lib/typescript/tile/context.d.ts +9 -0
- package/lib/typescript/tile/context.d.ts.map +1 -0
- package/lib/typescript/tile/createTileContent.d.ts +3 -0
- package/lib/typescript/tile/createTileContent.d.ts.map +1 -0
- package/lib/typescript/tile/createTileDescription.d.ts +3 -0
- package/lib/typescript/tile/createTileDescription.d.ts.map +1 -0
- package/lib/typescript/tile/createTileGroup.d.ts +4 -0
- package/lib/typescript/tile/createTileGroup.d.ts.map +1 -0
- package/lib/typescript/tile/createTileIndicator.d.ts +4 -0
- package/lib/typescript/tile/createTileIndicator.d.ts.map +1 -0
- package/lib/typescript/tile/createTileLeadingSlot.d.ts +4 -0
- package/lib/typescript/tile/createTileLeadingSlot.d.ts.map +1 -0
- package/lib/typescript/tile/createTileRoot.d.ts +4 -0
- package/lib/typescript/tile/createTileRoot.d.ts.map +1 -0
- package/lib/typescript/tile/createTileTitle.d.ts +3 -0
- package/lib/typescript/tile/createTileTitle.d.ts.map +1 -0
- package/lib/typescript/tile/createTileTrailingSlot.d.ts +9 -0
- package/lib/typescript/tile/createTileTrailingSlot.d.ts.map +1 -0
- package/lib/typescript/tile/index.d.ts +15 -0
- package/lib/typescript/tile/index.d.ts.map +1 -0
- package/lib/typescript/tile/types.d.ts +119 -0
- package/lib/typescript/tile/types.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/list-item/createListItemRoot.tsx +37 -22
- package/src/tile/context.tsx +23 -0
- package/src/tile/createTileContent.tsx +23 -0
- package/src/tile/createTileDescription.tsx +19 -0
- package/src/tile/createTileGroup.tsx +134 -0
- package/src/tile/createTileIndicator.tsx +38 -0
- package/src/tile/createTileLeadingSlot.tsx +30 -0
- package/src/tile/createTileRoot.tsx +124 -0
- package/src/tile/createTileTitle.tsx +19 -0
- package/src/tile/createTileTrailingSlot.tsx +25 -0
- package/src/tile/index.ts +88 -0
- 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
|
|
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
|
-
|
|
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
|
+
}
|