@cdx-ui/primitives 0.3.2 → 0.3.4
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/lib/commonjs/index.js +12 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/insight/InsightRoot.js +159 -0
- package/lib/commonjs/insight/InsightRoot.js.map +1 -0
- package/lib/commonjs/insight/context.js +11 -0
- package/lib/commonjs/insight/context.js.map +1 -0
- package/lib/commonjs/insight/index.js +26 -0
- package/lib/commonjs/insight/index.js.map +1 -0
- package/lib/commonjs/insight/types.js +6 -0
- package/lib/commonjs/insight/types.js.map +1 -0
- package/lib/commonjs/list-item/ListItemRoot.js +2 -18
- package/lib/commonjs/list-item/ListItemRoot.js.map +1 -1
- package/lib/commonjs/select/SelectContent.js +89 -45
- package/lib/commonjs/select/SelectContent.js.map +1 -1
- package/lib/commonjs/select/SelectDismissButton.js +14 -0
- package/lib/commonjs/select/SelectDismissButton.js.map +1 -0
- package/lib/commonjs/select/SelectDismissButton.web.js +28 -0
- package/lib/commonjs/select/SelectDismissButton.web.js.map +1 -0
- package/lib/commonjs/select/SelectEmpty.js +53 -0
- package/lib/commonjs/select/SelectEmpty.js.map +1 -0
- package/lib/commonjs/select/SelectItem.js +30 -9
- package/lib/commonjs/select/SelectItem.js.map +1 -1
- package/lib/commonjs/select/SelectListbox.js +47 -0
- package/lib/commonjs/select/SelectListbox.js.map +1 -0
- package/lib/commonjs/select/SelectRoot.js +110 -21
- package/lib/commonjs/select/SelectRoot.js.map +1 -1
- package/lib/commonjs/select/SelectSearch.js +81 -0
- package/lib/commonjs/select/SelectSearch.js.map +1 -0
- package/lib/commonjs/select/SelectSearchStatus.js +14 -0
- package/lib/commonjs/select/SelectSearchStatus.js.map +1 -0
- package/lib/commonjs/select/SelectSearchStatus.web.js +24 -0
- package/lib/commonjs/select/SelectSearchStatus.web.js.map +1 -0
- package/lib/commonjs/select/SelectSheetContent.js +67 -0
- package/lib/commonjs/select/SelectSheetContent.js.map +1 -0
- package/lib/commonjs/select/context.js +5 -1
- package/lib/commonjs/select/context.js.map +1 -1
- package/lib/commonjs/select/filter.js +16 -0
- package/lib/commonjs/select/filter.js.map +1 -0
- package/lib/commonjs/select/index.js +34 -0
- package/lib/commonjs/select/index.js.map +1 -1
- package/lib/commonjs/select/popupRole.js +27 -0
- package/lib/commonjs/select/popupRole.js.map +1 -0
- package/lib/commonjs/select/useContentFocus.js +10 -21
- package/lib/commonjs/select/useContentFocus.js.map +1 -1
- package/lib/commonjs/select/useListboxNavigation.js +21 -8
- package/lib/commonjs/select/useListboxNavigation.js.map +1 -1
- package/lib/commonjs/select/useQueryHighlight.js +50 -0
- package/lib/commonjs/select/useQueryHighlight.js.map +1 -0
- package/lib/commonjs/select/useSelectTrigger.js +4 -1
- package/lib/commonjs/select/useSelectTrigger.js.map +1 -1
- package/lib/commonjs/select/useSelectTrigger.web.js +4 -1
- package/lib/commonjs/select/useSelectTrigger.web.js.map +1 -1
- package/lib/commonjs/utils/splitContainerProps.js +26 -0
- package/lib/commonjs/utils/splitContainerProps.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/insight/InsightRoot.js +154 -0
- package/lib/module/insight/InsightRoot.js.map +1 -0
- package/lib/module/insight/context.js +5 -0
- package/lib/module/insight/context.js.map +1 -0
- package/lib/module/insight/index.js +5 -0
- package/lib/module/insight/index.js.map +1 -0
- package/lib/module/insight/types.js +4 -0
- package/lib/module/insight/types.js.map +1 -0
- package/lib/module/list-item/ListItemRoot.js +1 -17
- package/lib/module/list-item/ListItemRoot.js.map +1 -1
- package/lib/module/select/SelectContent.js +94 -48
- package/lib/module/select/SelectContent.js.map +1 -1
- package/lib/module/select/SelectDismissButton.js +10 -0
- package/lib/module/select/SelectDismissButton.js.map +1 -0
- package/lib/module/select/SelectDismissButton.web.js +24 -0
- package/lib/module/select/SelectDismissButton.web.js.map +1 -0
- package/lib/module/select/SelectEmpty.js +49 -0
- package/lib/module/select/SelectEmpty.js.map +1 -0
- package/lib/module/select/SelectItem.js +32 -11
- package/lib/module/select/SelectItem.js.map +1 -1
- package/lib/module/select/SelectListbox.js +43 -0
- package/lib/module/select/SelectListbox.js.map +1 -0
- package/lib/module/select/SelectRoot.js +111 -22
- package/lib/module/select/SelectRoot.js.map +1 -1
- package/lib/module/select/SelectSearch.js +77 -0
- package/lib/module/select/SelectSearch.js.map +1 -0
- package/lib/module/select/SelectSearchStatus.js +10 -0
- package/lib/module/select/SelectSearchStatus.js.map +1 -0
- package/lib/module/select/SelectSearchStatus.web.js +20 -0
- package/lib/module/select/SelectSearchStatus.web.js.map +1 -0
- package/lib/module/select/SelectSheetContent.js +63 -0
- package/lib/module/select/SelectSheetContent.js.map +1 -0
- package/lib/module/select/context.js +2 -0
- package/lib/module/select/context.js.map +1 -1
- package/lib/module/select/filter.js +11 -0
- package/lib/module/select/filter.js.map +1 -0
- package/lib/module/select/index.js +5 -1
- package/lib/module/select/index.js.map +1 -1
- package/lib/module/select/popupRole.js +23 -0
- package/lib/module/select/popupRole.js.map +1 -0
- package/lib/module/select/useContentFocus.js +11 -22
- package/lib/module/select/useContentFocus.js.map +1 -1
- package/lib/module/select/useListboxNavigation.js +21 -8
- package/lib/module/select/useListboxNavigation.js.map +1 -1
- package/lib/module/select/useQueryHighlight.js +46 -0
- package/lib/module/select/useQueryHighlight.js.map +1 -0
- package/lib/module/select/useSelectTrigger.js +4 -1
- package/lib/module/select/useSelectTrigger.js.map +1 -1
- package/lib/module/select/useSelectTrigger.web.js +4 -1
- package/lib/module/select/useSelectTrigger.web.js.map +1 -1
- package/lib/module/utils/splitContainerProps.js +22 -0
- package/lib/module/utils/splitContainerProps.js.map +1 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/insight/InsightRoot.d.ts +6 -0
- package/lib/typescript/insight/InsightRoot.d.ts.map +1 -0
- package/lib/typescript/insight/context.d.ts +6 -0
- package/lib/typescript/insight/context.d.ts.map +1 -0
- package/lib/typescript/insight/index.d.ts +4 -0
- package/lib/typescript/insight/index.d.ts.map +1 -0
- package/lib/typescript/insight/types.d.ts +44 -0
- package/lib/typescript/insight/types.d.ts.map +1 -0
- package/lib/typescript/list-item/ListItemRoot.d.ts.map +1 -1
- package/lib/typescript/select/SelectContent.d.ts.map +1 -1
- package/lib/typescript/select/SelectDismissButton.d.ts +10 -0
- package/lib/typescript/select/SelectDismissButton.d.ts.map +1 -0
- package/lib/typescript/select/SelectDismissButton.web.d.ts +9 -0
- package/lib/typescript/select/SelectDismissButton.web.d.ts.map +1 -0
- package/lib/typescript/select/SelectEmpty.d.ts +12 -0
- package/lib/typescript/select/SelectEmpty.d.ts.map +1 -0
- package/lib/typescript/select/SelectItem.d.ts.map +1 -1
- package/lib/typescript/select/SelectListbox.d.ts +12 -0
- package/lib/typescript/select/SelectListbox.d.ts.map +1 -0
- package/lib/typescript/select/SelectRoot.d.ts +2 -2
- package/lib/typescript/select/SelectRoot.d.ts.map +1 -1
- package/lib/typescript/select/SelectSearch.d.ts +18 -0
- package/lib/typescript/select/SelectSearch.d.ts.map +1 -0
- package/lib/typescript/select/SelectSearchStatus.d.ts +9 -0
- package/lib/typescript/select/SelectSearchStatus.d.ts.map +1 -0
- package/lib/typescript/select/SelectSearchStatus.web.d.ts +8 -0
- package/lib/typescript/select/SelectSearchStatus.web.d.ts.map +1 -0
- package/lib/typescript/select/SelectSheetContent.d.ts +14 -0
- package/lib/typescript/select/SelectSheetContent.d.ts.map +1 -0
- package/lib/typescript/select/context.d.ts +9 -0
- package/lib/typescript/select/context.d.ts.map +1 -1
- package/lib/typescript/select/filter.d.ts +4 -0
- package/lib/typescript/select/filter.d.ts.map +1 -0
- package/lib/typescript/select/index.d.ts +6 -2
- package/lib/typescript/select/index.d.ts.map +1 -1
- package/lib/typescript/select/popupRole.d.ts +18 -0
- package/lib/typescript/select/popupRole.d.ts.map +1 -0
- package/lib/typescript/select/types.d.ts +129 -8
- package/lib/typescript/select/types.d.ts.map +1 -1
- package/lib/typescript/select/useContentFocus.d.ts +4 -7
- package/lib/typescript/select/useContentFocus.d.ts.map +1 -1
- package/lib/typescript/select/useListboxNavigation.d.ts +16 -3
- package/lib/typescript/select/useListboxNavigation.d.ts.map +1 -1
- package/lib/typescript/select/useQueryHighlight.d.ts +21 -0
- package/lib/typescript/select/useQueryHighlight.d.ts.map +1 -0
- package/lib/typescript/select/useSelectTrigger.d.ts.map +1 -1
- package/lib/typescript/select/useSelectTrigger.web.d.ts.map +1 -1
- package/lib/typescript/utils/splitContainerProps.d.ts +9 -0
- package/lib/typescript/utils/splitContainerProps.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/insight/InsightRoot.tsx +162 -0
- package/src/insight/context.tsx +5 -0
- package/src/insight/index.ts +11 -0
- package/src/insight/types.ts +51 -0
- package/src/list-item/ListItemRoot.tsx +1 -14
- package/src/select/SelectContent.tsx +89 -54
- package/src/select/SelectDismissButton.tsx +14 -0
- package/src/select/SelectDismissButton.web.tsx +26 -0
- package/src/select/SelectEmpty.tsx +61 -0
- package/src/select/SelectItem.tsx +42 -10
- package/src/select/SelectListbox.tsx +43 -0
- package/src/select/SelectRoot.tsx +184 -12
- package/src/select/SelectSearch.tsx +98 -0
- package/src/select/SelectSearchStatus.tsx +11 -0
- package/src/select/SelectSearchStatus.web.tsx +18 -0
- package/src/select/SelectSheetContent.tsx +54 -0
- package/src/select/context.tsx +9 -0
- package/src/select/filter.ts +12 -0
- package/src/select/index.ts +14 -1
- package/src/select/popupRole.ts +26 -0
- package/src/select/types.ts +142 -9
- package/src/select/useContentFocus.ts +12 -31
- package/src/select/useListboxNavigation.ts +41 -9
- package/src/select/useQueryHighlight.ts +61 -0
- package/src/select/useSelectTrigger.ts +5 -1
- package/src/select/useSelectTrigger.web.ts +5 -1
- package/src/utils/splitContainerProps.ts +16 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React, { forwardRef, useMemo, type ComponentRef } from 'react';
|
|
2
|
+
import { Pressable, View, type PressableProps, type ViewProps } from 'react-native';
|
|
3
|
+
import { composeEventHandlers } from '@cdx-ui/utils';
|
|
4
|
+
import { useFocus, useFocusRing } from '@react-native-aria/focus';
|
|
5
|
+
import { useHover, usePress } from '@react-native-aria/interactions';
|
|
6
|
+
import { Slot } from '../slot';
|
|
7
|
+
import { dataAttributes, mergeDataAttributes } from '../utils/dataAttributes';
|
|
8
|
+
import { splitContainerProps } from '../utils/splitContainerProps';
|
|
9
|
+
import { InsightProvider } from './context';
|
|
10
|
+
import type {
|
|
11
|
+
IInsightContextValue,
|
|
12
|
+
IInsightProps,
|
|
13
|
+
IInsightSummaryProps,
|
|
14
|
+
InsightLayout,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
export type InsightRootProps = IInsightProps;
|
|
18
|
+
|
|
19
|
+
type InsightRootRuntimeProps = Omit<IInsightSummaryProps, 'layout'> & {
|
|
20
|
+
layout?: InsightLayout;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const InsightRoot = forwardRef<ComponentRef<typeof View>, InsightRootProps>((props, ref) => {
|
|
24
|
+
const {
|
|
25
|
+
layout = 'summary',
|
|
26
|
+
asChild = false,
|
|
27
|
+
children,
|
|
28
|
+
onPress,
|
|
29
|
+
isDisabled = false,
|
|
30
|
+
accessory,
|
|
31
|
+
pressableClassName,
|
|
32
|
+
style,
|
|
33
|
+
role,
|
|
34
|
+
onPressIn: onPressInProp,
|
|
35
|
+
onPressOut: onPressOutProp,
|
|
36
|
+
onHoverIn: onHoverInProp,
|
|
37
|
+
onHoverOut: onHoverOutProp,
|
|
38
|
+
...restProps
|
|
39
|
+
} = props as InsightRootRuntimeProps;
|
|
40
|
+
|
|
41
|
+
const isPressable = layout === 'summary' && !!onPress;
|
|
42
|
+
const asChildElement = asChild && React.isValidElement(children);
|
|
43
|
+
const isSplit = accessory != null && (isPressable || asChildElement);
|
|
44
|
+
|
|
45
|
+
const pressState = usePress({ isDisabled: !isPressable || isDisabled });
|
|
46
|
+
const isPressed = isPressable && pressState.isPressed;
|
|
47
|
+
const pressProps = pressState.pressProps as Pick<PressableProps, 'onPressIn' | 'onPressOut'>;
|
|
48
|
+
const { hoverProps, isHovered } = useHover();
|
|
49
|
+
const { isFocused, focusProps } = useFocus();
|
|
50
|
+
const { isFocusVisible, focusProps: focusRingProps } = useFocusRing() as {
|
|
51
|
+
isFocusVisible: boolean;
|
|
52
|
+
focusProps: typeof focusProps;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const contextValue = useMemo<IInsightContextValue>(
|
|
56
|
+
() => ({ layout, isPressed, isPressable }),
|
|
57
|
+
[layout, isPressed, isPressable],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const interactionAttributes = isPressable
|
|
61
|
+
? dataAttributes({
|
|
62
|
+
active: isPressed,
|
|
63
|
+
hover: isHovered,
|
|
64
|
+
focus: isFocused,
|
|
65
|
+
focusVisible: isFocusVisible,
|
|
66
|
+
disabled: isDisabled,
|
|
67
|
+
})
|
|
68
|
+
: undefined;
|
|
69
|
+
|
|
70
|
+
const {
|
|
71
|
+
onFocus: onFocusProp,
|
|
72
|
+
onBlur: onBlurProp,
|
|
73
|
+
accessibilityState: accessibilityStateProp,
|
|
74
|
+
} = restProps as Pick<PressableProps, 'onFocus' | 'onBlur' | 'accessibilityState'>;
|
|
75
|
+
|
|
76
|
+
const pressRegionProps = (): Record<string, unknown> => ({
|
|
77
|
+
role: role ?? 'button',
|
|
78
|
+
...(isDisabled && { accessibilityState: { ...accessibilityStateProp, disabled: true } }),
|
|
79
|
+
disabled: isDisabled,
|
|
80
|
+
onPress: isDisabled ? undefined : onPress,
|
|
81
|
+
onPressIn: composeEventHandlers(onPressInProp, pressProps.onPressIn),
|
|
82
|
+
onPressOut: composeEventHandlers(onPressOutProp, pressProps.onPressOut),
|
|
83
|
+
onHoverIn: composeEventHandlers(onHoverInProp, hoverProps.onHoverIn),
|
|
84
|
+
onHoverOut: composeEventHandlers(onHoverOutProp, hoverProps.onHoverOut),
|
|
85
|
+
onFocus: composeEventHandlers(
|
|
86
|
+
composeEventHandlers(onFocusProp, focusProps.onFocus),
|
|
87
|
+
focusRingProps.onFocus,
|
|
88
|
+
),
|
|
89
|
+
onBlur: composeEventHandlers(
|
|
90
|
+
composeEventHandlers(onBlurProp, focusProps.onBlur),
|
|
91
|
+
focusRingProps.onBlur,
|
|
92
|
+
),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (isSplit) {
|
|
96
|
+
const { container, region } = splitContainerProps(restProps);
|
|
97
|
+
const regionProps: Record<string, unknown> = {
|
|
98
|
+
...region,
|
|
99
|
+
...(role != null && { role }),
|
|
100
|
+
className: pressableClassName,
|
|
101
|
+
...(isPressable && { ...pressRegionProps(), ...interactionAttributes }),
|
|
102
|
+
};
|
|
103
|
+
const RegionComp = asChildElement ? Slot : isPressable ? Pressable : View;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<InsightProvider value={contextValue}>
|
|
107
|
+
<View
|
|
108
|
+
{...(container as ViewProps)}
|
|
109
|
+
ref={isPressable ? undefined : ref}
|
|
110
|
+
style={style}
|
|
111
|
+
{...mergeDataAttributes(container, interactionAttributes)}
|
|
112
|
+
>
|
|
113
|
+
<RegionComp
|
|
114
|
+
{...(regionProps as PressableProps)}
|
|
115
|
+
ref={isPressable ? (ref as React.Ref<View>) : undefined}
|
|
116
|
+
>
|
|
117
|
+
{children}
|
|
118
|
+
</RegionComp>
|
|
119
|
+
{accessory}
|
|
120
|
+
</View>
|
|
121
|
+
</InsightProvider>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isPressable) {
|
|
126
|
+
const Comp = asChildElement ? Slot : Pressable;
|
|
127
|
+
return (
|
|
128
|
+
<InsightProvider value={contextValue}>
|
|
129
|
+
<Comp
|
|
130
|
+
{...(restProps as PressableProps)}
|
|
131
|
+
{...(pressRegionProps() as PressableProps)}
|
|
132
|
+
ref={ref as React.Ref<View>}
|
|
133
|
+
style={style}
|
|
134
|
+
{...mergeDataAttributes(restProps, interactionAttributes)}
|
|
135
|
+
>
|
|
136
|
+
{children}
|
|
137
|
+
</Comp>
|
|
138
|
+
</InsightProvider>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (asChildElement) {
|
|
143
|
+
return (
|
|
144
|
+
<InsightProvider value={contextValue}>
|
|
145
|
+
<Slot {...(restProps as ViewProps)} ref={ref as React.Ref<View>} style={style} role={role}>
|
|
146
|
+
{children}
|
|
147
|
+
</Slot>
|
|
148
|
+
</InsightProvider>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<InsightProvider value={contextValue}>
|
|
154
|
+
<View {...(restProps as ViewProps)} ref={ref} style={style} role={role}>
|
|
155
|
+
{children}
|
|
156
|
+
{accessory}
|
|
157
|
+
</View>
|
|
158
|
+
</InsightProvider>
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
InsightRoot.displayName = 'InsightRoot';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
IInsightContextValue,
|
|
3
|
+
IInsightDetailProps,
|
|
4
|
+
IInsightPressablePassthrough,
|
|
5
|
+
IInsightProps,
|
|
6
|
+
IInsightSummaryProps,
|
|
7
|
+
InsightLayout,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
export { InsightProvider, useInsightContext } from './context';
|
|
11
|
+
export { InsightRoot, type InsightRootProps } from './InsightRoot';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { PressableProps, ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export type InsightLayout = 'summary' | 'detail';
|
|
5
|
+
|
|
6
|
+
export type IInsightPressablePassthrough = Partial<
|
|
7
|
+
Pick<PressableProps, 'onPressIn' | 'onPressOut' | 'onHoverIn' | 'onHoverOut'>
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
interface IInsightBaseProps extends ViewProps {
|
|
11
|
+
/** Merge props onto a single child instead of rendering `View` or `Pressable`. */
|
|
12
|
+
asChild?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Interactive control that must sit beside the press region, never inside it (the styled
|
|
15
|
+
* layer's close button). On a pressable summary the root renders a non-interactive container
|
|
16
|
+
* holding the press region and the accessory as siblings. The container receives
|
|
17
|
+
* `className`, `style`, and data attributes, and mirrors the region's interaction state as
|
|
18
|
+
* `data-*`; the region receives `ref` and every other prop, so `ref` always targets the
|
|
19
|
+
* focusable press region of a pressable root. A static root renders the accessory after
|
|
20
|
+
* `children` and keeps `ref` on its `View`.
|
|
21
|
+
*/
|
|
22
|
+
accessory?: ReactNode;
|
|
23
|
+
/** Class names for the press region when `accessory` splits the root. */
|
|
24
|
+
pressableClassName?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IInsightSummaryProps extends IInsightBaseProps, IInsightPressablePassthrough {
|
|
28
|
+
/**
|
|
29
|
+
* Inline card that navigates elsewhere when pressed.
|
|
30
|
+
* @default 'summary'
|
|
31
|
+
*/
|
|
32
|
+
layout?: 'summary';
|
|
33
|
+
/** When set, the root renders a press region with `role="button"`. */
|
|
34
|
+
onPress?: PressableProps['onPress'];
|
|
35
|
+
/** Suppresses `onPress` and marks the press region disabled. */
|
|
36
|
+
isDisabled?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IInsightDetailProps extends IInsightBaseProps {
|
|
40
|
+
/** Static container. It holds its own actions, so it is never pressable. */
|
|
41
|
+
layout: 'detail';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Discriminated union: `layout` narrows the press props to the summary layout. */
|
|
45
|
+
export type IInsightProps = IInsightSummaryProps | IInsightDetailProps;
|
|
46
|
+
|
|
47
|
+
export interface IInsightContextValue {
|
|
48
|
+
layout: InsightLayout;
|
|
49
|
+
isPressed: boolean;
|
|
50
|
+
isPressable: boolean;
|
|
51
|
+
}
|
|
@@ -6,6 +6,7 @@ import { useHover, usePress } from '@react-native-aria/interactions';
|
|
|
6
6
|
import type { InteractionState } from '../types';
|
|
7
7
|
import { Slot } from '../slot';
|
|
8
8
|
import { dataAttributes, mergeDataAttributes } from '../utils/dataAttributes';
|
|
9
|
+
import { splitContainerProps } from '../utils/splitContainerProps';
|
|
9
10
|
import { ListItemProvider } from './context';
|
|
10
11
|
import type { IListItemPressablePassthrough, IListItemProps, ListItemCrossAlign } from './types';
|
|
11
12
|
|
|
@@ -17,20 +18,6 @@ function rowStyleForCrossAlign(crossAlign: ListItemCrossAlign) {
|
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
/** Splits the props a split row's container keeps (`className`, data attributes) from the rest. */
|
|
21
|
-
function splitContainerProps(props: Record<string, unknown>) {
|
|
22
|
-
const container: Record<string, unknown> = {};
|
|
23
|
-
const region: Record<string, unknown> = {};
|
|
24
|
-
for (const key in props) {
|
|
25
|
-
if (key === 'className' || key === 'dataSet' || key.startsWith('data-')) {
|
|
26
|
-
container[key] = props[key];
|
|
27
|
-
} else {
|
|
28
|
-
region[key] = props[key];
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return { container, region };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
21
|
export interface ListItemRootProps extends IListItemProps {}
|
|
35
22
|
|
|
36
23
|
export const ListItemRoot = forwardRef<ComponentRef<typeof View>, ListItemRootProps>(
|
|
@@ -1,14 +1,29 @@
|
|
|
1
|
-
import React, { forwardRef, useCallback, useRef, type ComponentRef } from 'react';
|
|
1
|
+
import React, { forwardRef, useCallback, useMemo, useRef, type ComponentRef } from 'react';
|
|
2
2
|
import { Platform, View } from 'react-native';
|
|
3
3
|
import { FadeIn, FadeOut } from 'react-native-reanimated';
|
|
4
4
|
import { composeEventHandlers, mergeRefs } from '@cdx-ui/utils';
|
|
5
|
+
import { FocusScope } from '@react-native-aria/focus';
|
|
5
6
|
import { OverlayContainer, useAnchoredOverlay, useDismissOverlay, useEscapeKey } from '../overlay';
|
|
6
7
|
import { Slot } from '../slot';
|
|
7
8
|
import { dataAttributes, mergeDataAttributes } from '../utils/dataAttributes';
|
|
8
|
-
import { useSelectContext } from './context';
|
|
9
|
+
import { SelectContentProvider, useSelectContext } from './context';
|
|
10
|
+
import { getPopupRoleProps } from './popupRole';
|
|
11
|
+
import { SelectDismissButton } from './SelectDismissButton';
|
|
9
12
|
import type { ISelectContentProps } from './types';
|
|
10
13
|
import { useContentFocus } from './useContentFocus';
|
|
11
14
|
import { useListboxNavigation } from './useListboxNavigation';
|
|
15
|
+
import { useQueryHighlight } from './useQueryHighlight';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Keeps DOM focus in the search input when the pointer goes down elsewhere in the popup, so
|
|
19
|
+
* typing continues after an option is clicked (virtual focus).
|
|
20
|
+
*/
|
|
21
|
+
function keepSearchInputFocus(e: { target: unknown; preventDefault: () => void }) {
|
|
22
|
+
const target = e.target as { closest?: (selector: string) => unknown } | null;
|
|
23
|
+
if (target?.closest?.('input, textarea') == null) {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
12
27
|
|
|
13
28
|
export interface SelectContentProps extends ISelectContentProps {
|
|
14
29
|
asChild?: boolean;
|
|
@@ -27,6 +42,7 @@ export const SelectContent = forwardRef<ComponentRef<typeof View>, SelectContent
|
|
|
27
42
|
placement = 'bottom-start',
|
|
28
43
|
offset = 4,
|
|
29
44
|
crossOffset = 0,
|
|
45
|
+
dismissAccessibilityLabel = 'Dismiss',
|
|
30
46
|
...props
|
|
31
47
|
},
|
|
32
48
|
ref,
|
|
@@ -38,25 +54,22 @@ export const SelectContent = forwardRef<ComponentRef<typeof View>, SelectContent
|
|
|
38
54
|
triggerId,
|
|
39
55
|
activeValue,
|
|
40
56
|
setActiveValue,
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
type,
|
|
58
|
+
selectedValues,
|
|
59
|
+
toggleValue,
|
|
60
|
+
selectValueAndClose,
|
|
61
|
+
isItemDisabled,
|
|
43
62
|
native,
|
|
44
63
|
triggerRef,
|
|
45
|
-
|
|
64
|
+
inputValue,
|
|
65
|
+
visibleItems,
|
|
66
|
+
hasSearch,
|
|
67
|
+
labelId,
|
|
68
|
+
accessibilityLabel,
|
|
46
69
|
} = useSelectContext();
|
|
47
70
|
|
|
48
71
|
const contentRef = useRef<HTMLElement | null>(null);
|
|
49
|
-
|
|
50
|
-
const focusOnMount = useCallback((node: unknown) => {
|
|
51
|
-
contentRef.current = node as HTMLElement | null;
|
|
52
|
-
if (node && Platform.OS === 'web') {
|
|
53
|
-
requestAnimationFrame(() => {
|
|
54
|
-
setTimeout(() => {
|
|
55
|
-
(node as HTMLElement).focus?.();
|
|
56
|
-
}, 100);
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}, []);
|
|
72
|
+
const dismissButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
60
73
|
|
|
61
74
|
const dismiss = useCallback(() => setOpen(false), [setOpen]);
|
|
62
75
|
|
|
@@ -73,35 +86,42 @@ export const SelectContent = forwardRef<ComponentRef<typeof View>, SelectContent
|
|
|
73
86
|
collisionBehavior: 'flip-shift',
|
|
74
87
|
width: 'anchor',
|
|
75
88
|
});
|
|
76
|
-
const mergedRef = mergeRefs(ref,
|
|
89
|
+
const mergedRef = mergeRefs(ref, contentRef as unknown as React.Ref<View>, floatingRef);
|
|
77
90
|
|
|
78
|
-
useDismissOverlay(overlayOpen, contentRef, [triggerRef], dismiss);
|
|
91
|
+
useDismissOverlay(overlayOpen, contentRef, [triggerRef, dismissButtonRef], dismiss);
|
|
79
92
|
useEscapeKey(overlayOpen, dismiss);
|
|
80
93
|
|
|
81
94
|
const { handleKeyDown } = useListboxNavigation({
|
|
82
|
-
items,
|
|
95
|
+
items: visibleItems,
|
|
83
96
|
activeValue,
|
|
84
97
|
setActiveValue,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
type,
|
|
99
|
+
onToggle: toggleValue,
|
|
100
|
+
onSelectAndClose: selectValueAndClose,
|
|
101
|
+
isItemDisabled,
|
|
89
102
|
onDismiss: dismiss,
|
|
103
|
+
isSearch: hasSearch,
|
|
90
104
|
});
|
|
91
105
|
|
|
92
|
-
useContentFocus({
|
|
106
|
+
useContentFocus({ open, triggerRef, setActiveValue });
|
|
107
|
+
|
|
108
|
+
useQueryHighlight({
|
|
93
109
|
open,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
inputValue,
|
|
111
|
+
visibleItems,
|
|
112
|
+
selectedValues,
|
|
113
|
+
isItemDisabled,
|
|
114
|
+
activeValue,
|
|
97
115
|
setActiveValue,
|
|
98
116
|
});
|
|
99
117
|
|
|
118
|
+
const contentContext = useMemo(() => ({ handleKeyDown }), [handleKeyDown]);
|
|
119
|
+
|
|
100
120
|
if (!overlayOpen) {
|
|
101
121
|
return null;
|
|
102
122
|
}
|
|
103
123
|
|
|
104
|
-
const
|
|
124
|
+
const platformProps =
|
|
105
125
|
Platform.OS === 'web'
|
|
106
126
|
? {
|
|
107
127
|
onKeyDown: composeEventHandlers(
|
|
@@ -109,38 +129,53 @@ export const SelectContent = forwardRef<ComponentRef<typeof View>, SelectContent
|
|
|
109
129
|
handleKeyDown,
|
|
110
130
|
),
|
|
111
131
|
tabIndex: -1,
|
|
132
|
+
'aria-multiselectable': (!hasSearch && type === 'multiple') || undefined,
|
|
133
|
+
...(hasSearch ? { onMouseDown: keepSearchInputFocus } : {}),
|
|
112
134
|
}
|
|
113
|
-
: {};
|
|
135
|
+
: { onAccessibilityEscape: composeEventHandlers(props.onAccessibilityEscape, dismiss) };
|
|
136
|
+
|
|
137
|
+
const roleProps = getPopupRoleProps({ hasSearch, labelId, accessibilityLabel, triggerId });
|
|
114
138
|
|
|
115
139
|
const Comp = asChild ? Slot : View;
|
|
116
140
|
|
|
117
141
|
return (
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<Comp
|
|
124
|
-
ref={mergedRef}
|
|
125
|
-
onLayout={composeEventHandlers(onLayout, onFloatingLayout)}
|
|
126
|
-
style={[style, overlayStyle]}
|
|
127
|
-
role="listbox"
|
|
128
|
-
id={contentId}
|
|
129
|
-
aria-labelledby={triggerId}
|
|
130
|
-
{...props}
|
|
131
|
-
{...mergeDataAttributes(
|
|
132
|
-
props,
|
|
133
|
-
dataAttributes({
|
|
134
|
-
state: 'open',
|
|
135
|
-
side: position?.resolvedPlacement.side,
|
|
136
|
-
align: position?.resolvedPlacement.align,
|
|
137
|
-
}),
|
|
138
|
-
)}
|
|
139
|
-
{...webProps}
|
|
142
|
+
<SelectContentProvider value={contentContext}>
|
|
143
|
+
<OverlayContainer
|
|
144
|
+
onDismiss={dismiss}
|
|
145
|
+
entering={enteringAnimation ?? FadeIn.duration(150)}
|
|
146
|
+
exiting={exitingAnimation ?? FadeOut.duration(100)}
|
|
140
147
|
>
|
|
141
|
-
{
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
{/* With search, autofocus lands on the input; otherwise options are not tabbable, so it
|
|
149
|
+
falls back to the focusable popup. Nested in the React tree, the scope also lets focus
|
|
150
|
+
enter this body-portalled popup from inside a `Drawer`'s contained scope. */}
|
|
151
|
+
<FocusScope autoFocus>
|
|
152
|
+
<Comp
|
|
153
|
+
ref={mergedRef}
|
|
154
|
+
onLayout={composeEventHandlers(onLayout, onFloatingLayout)}
|
|
155
|
+
style={[style, overlayStyle]}
|
|
156
|
+
id={contentId}
|
|
157
|
+
{...roleProps}
|
|
158
|
+
{...props}
|
|
159
|
+
{...mergeDataAttributes(
|
|
160
|
+
props,
|
|
161
|
+
dataAttributes({
|
|
162
|
+
state: 'open',
|
|
163
|
+
side: position?.resolvedPlacement.side,
|
|
164
|
+
align: position?.resolvedPlacement.align,
|
|
165
|
+
}),
|
|
166
|
+
)}
|
|
167
|
+
{...platformProps}
|
|
168
|
+
>
|
|
169
|
+
{children}
|
|
170
|
+
</Comp>
|
|
171
|
+
<SelectDismissButton
|
|
172
|
+
ref={dismissButtonRef}
|
|
173
|
+
onDismiss={dismiss}
|
|
174
|
+
accessibilityLabel={dismissAccessibilityLabel}
|
|
175
|
+
/>
|
|
176
|
+
</FocusScope>
|
|
177
|
+
</OverlayContainer>
|
|
178
|
+
</SelectContentProvider>
|
|
144
179
|
);
|
|
145
180
|
},
|
|
146
181
|
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SelectDismissButtonProps {
|
|
4
|
+
onDismiss: () => void;
|
|
5
|
+
accessibilityLabel: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Web-only. Native screen readers close the popup with the iOS escape gesture
|
|
10
|
+
* (`onAccessibilityEscape` on `SelectContent`) and Android back (`onRequestClose`).
|
|
11
|
+
*/
|
|
12
|
+
export const SelectDismissButton = forwardRef<unknown, SelectDismissButtonProps>(() => null);
|
|
13
|
+
|
|
14
|
+
SelectDismissButton.displayName = 'SelectDismissButton';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
import { VisuallyHidden } from '@react-aria/visually-hidden';
|
|
3
|
+
import type { SelectDismissButtonProps } from './SelectDismissButton';
|
|
4
|
+
|
|
5
|
+
export type { SelectDismissButtonProps };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Visually hidden close control for screen-reader users, who cannot press Escape or click
|
|
9
|
+
* outside while browsing the popup. Out of the Tab order (Tab already closes the listbox).
|
|
10
|
+
* Rendered as a sibling of the listbox because a listbox may only own options.
|
|
11
|
+
*/
|
|
12
|
+
export const SelectDismissButton = forwardRef<HTMLButtonElement, SelectDismissButtonProps>(
|
|
13
|
+
({ onDismiss, accessibilityLabel }, ref) => (
|
|
14
|
+
<VisuallyHidden>
|
|
15
|
+
<button
|
|
16
|
+
ref={ref}
|
|
17
|
+
type="button"
|
|
18
|
+
tabIndex={-1}
|
|
19
|
+
aria-label={accessibilityLabel}
|
|
20
|
+
onClick={onDismiss}
|
|
21
|
+
/>
|
|
22
|
+
</VisuallyHidden>
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
SelectDismissButton.displayName = 'SelectDismissButton';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
forwardRef,
|
|
4
|
+
isValidElement,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
type ComponentRef,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { View } from 'react-native';
|
|
10
|
+
import { Slot } from '../slot';
|
|
11
|
+
import { useSelectContext } from './context';
|
|
12
|
+
import type { ISelectEmptyProps } from './types';
|
|
13
|
+
|
|
14
|
+
export interface SelectEmptyProps extends ISelectEmptyProps {
|
|
15
|
+
asChild?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getTextContent(node: ReactNode): string {
|
|
20
|
+
return Children.toArray(node)
|
|
21
|
+
.map((child) => {
|
|
22
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
23
|
+
return String(child);
|
|
24
|
+
}
|
|
25
|
+
if (isValidElement<{ children?: ReactNode }>(child)) {
|
|
26
|
+
return getTextContent(child.props.children);
|
|
27
|
+
}
|
|
28
|
+
return '';
|
|
29
|
+
})
|
|
30
|
+
.join('');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Renders only when no option matches. Its text is also what the search live region
|
|
35
|
+
* announces in that case.
|
|
36
|
+
*/
|
|
37
|
+
export const SelectEmpty = forwardRef<ComponentRef<typeof View>, SelectEmptyProps>(
|
|
38
|
+
({ asChild, children, ...props }, ref) => {
|
|
39
|
+
const { visibleItems, setEmptyText } = useSelectContext();
|
|
40
|
+
const text = getTextContent(children);
|
|
41
|
+
|
|
42
|
+
useLayoutEffect(() => {
|
|
43
|
+
setEmptyText(text || undefined);
|
|
44
|
+
return () => setEmptyText(undefined);
|
|
45
|
+
}, [setEmptyText, text]);
|
|
46
|
+
|
|
47
|
+
if (visibleItems.length > 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const Comp = asChild ? Slot : View;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Comp ref={ref} {...props}>
|
|
55
|
+
{children}
|
|
56
|
+
</Comp>
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
SelectEmpty.displayName = 'SelectEmpty';
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
type ComponentRef,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { Platform, Pressable } from 'react-native';
|
|
3
10
|
import { composeEventHandlers } from '@cdx-ui/utils';
|
|
4
11
|
import { useFocus, useFocusRing } from '@react-native-aria/focus';
|
|
5
12
|
import { useHover, usePress } from '@react-native-aria/interactions';
|
|
@@ -13,6 +20,12 @@ export interface SelectItemProps extends Omit<ISelectItemProps, 'children'> {
|
|
|
13
20
|
children?: ReactNode;
|
|
14
21
|
}
|
|
15
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Options are reached through the highlight (`aria-activedescendant`), not the Tab order, so
|
|
25
|
+
* popup autofocus lands on the search input or the listbox rather than the first option.
|
|
26
|
+
*/
|
|
27
|
+
const WEB_OPTION_PROPS = Platform.OS === 'web' ? { tabIndex: -1 as const } : {};
|
|
28
|
+
|
|
16
29
|
export const SelectItem = forwardRef<ComponentRef<typeof Pressable>, SelectItemProps>(
|
|
17
30
|
(
|
|
18
31
|
{
|
|
@@ -30,18 +43,32 @@ export const SelectItem = forwardRef<ComponentRef<typeof Pressable>, SelectItemP
|
|
|
30
43
|
ref,
|
|
31
44
|
) => {
|
|
32
45
|
const {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
setOpen,
|
|
46
|
+
isSelected,
|
|
47
|
+
commitValue,
|
|
36
48
|
activeValue,
|
|
49
|
+
setActiveValue,
|
|
37
50
|
contentId,
|
|
51
|
+
items,
|
|
38
52
|
disabled: contextDisabled,
|
|
53
|
+
registerDisabledItem,
|
|
54
|
+
isItemVisible,
|
|
39
55
|
} = useSelectContext();
|
|
40
56
|
|
|
41
|
-
const
|
|
42
|
-
const
|
|
57
|
+
const isDisabledByItem = !!disabledProp || !!isDisabledProp;
|
|
58
|
+
const disabled =
|
|
59
|
+
contextDisabled ||
|
|
60
|
+
isDisabledByItem ||
|
|
61
|
+
items.some((item) => item.value === itemValue && !!item.disabled);
|
|
62
|
+
const checked = isSelected(itemValue);
|
|
43
63
|
const highlighted = activeValue === itemValue;
|
|
44
64
|
|
|
65
|
+
useLayoutEffect(() => {
|
|
66
|
+
if (isDisabledByItem) {
|
|
67
|
+
return registerDisabledItem(itemValue);
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}, [isDisabledByItem, itemValue, registerDisabledItem]);
|
|
71
|
+
|
|
45
72
|
const { isFocusVisible, focusProps: focusRingProps } = useFocusRing() as {
|
|
46
73
|
isFocusVisible: boolean;
|
|
47
74
|
focusProps: {
|
|
@@ -55,10 +82,10 @@ export const SelectItem = forwardRef<ComponentRef<typeof Pressable>, SelectItemP
|
|
|
55
82
|
|
|
56
83
|
const handlePress = useCallback(() => {
|
|
57
84
|
if (!disabled) {
|
|
58
|
-
|
|
59
|
-
|
|
85
|
+
setActiveValue(itemValue);
|
|
86
|
+
commitValue(itemValue);
|
|
60
87
|
}
|
|
61
|
-
}, [disabled, itemValue,
|
|
88
|
+
}, [disabled, itemValue, commitValue, setActiveValue]);
|
|
62
89
|
|
|
63
90
|
const interactionState = useMemo(
|
|
64
91
|
() => ({
|
|
@@ -85,6 +112,10 @@ export const SelectItem = forwardRef<ComponentRef<typeof Pressable>, SelectItemP
|
|
|
85
112
|
],
|
|
86
113
|
);
|
|
87
114
|
|
|
115
|
+
if (!isItemVisible(itemValue)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
88
119
|
const Comp = asChild ? Slot : Pressable;
|
|
89
120
|
|
|
90
121
|
return (
|
|
@@ -96,6 +127,7 @@ export const SelectItem = forwardRef<ComponentRef<typeof Pressable>, SelectItemP
|
|
|
96
127
|
aria-selected={checked}
|
|
97
128
|
aria-disabled={disabled}
|
|
98
129
|
disabled={disabled}
|
|
130
|
+
{...WEB_OPTION_PROPS}
|
|
99
131
|
{...props}
|
|
100
132
|
{...mergeDataAttributes(
|
|
101
133
|
props,
|