@jobber/components-native 0.105.4 → 0.106.0
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/dist/docs/ActionItem/ActionItem.md +1 -1
- package/dist/docs/ButtonGroup/ButtonGroup.md +2 -2
- package/dist/docs/Chip/Chip.md +4 -4
- package/dist/docs/EmptyState/EmptyState.md +1 -1
- package/dist/docs/Icon/Icon.md +1 -1
- package/dist/docs/IconButton/IconButton.md +1 -1
- package/dist/docs/usage-guidelines/usage-guidelines.md +5 -5
- package/dist/package.json +5 -3
- package/dist/src/primitives/Portal/index.js +5 -0
- package/dist/src/primitives/Select/SelectPrimitive.js +173 -0
- package/dist/src/primitives/Select/SelectPrimitive.style.js +112 -0
- package/dist/src/primitives/Select/SelectPrimitive.test.js +243 -0
- package/dist/src/primitives/Select/index.js +1 -0
- package/dist/src/primitives/Select/types.js +1 -0
- package/dist/src/primitives/index.js +2 -0
- package/dist/src/utils/test/PortalHostWrapper.js +11 -0
- package/dist/src/utils/test/index.js +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/primitives/Portal/index.d.ts +1 -0
- package/dist/types/src/primitives/Select/SelectPrimitive.d.ts +67 -0
- package/dist/types/src/primitives/Select/SelectPrimitive.style.d.ts +105 -0
- package/dist/types/src/primitives/Select/SelectPrimitive.test.d.ts +1 -0
- package/dist/types/src/primitives/Select/index.d.ts +2 -0
- package/dist/types/src/primitives/Select/types.d.ts +41 -0
- package/dist/types/src/primitives/index.d.ts +2 -0
- package/dist/types/src/utils/test/PortalHostWrapper.d.ts +10 -0
- package/dist/types/src/utils/test/index.d.ts +1 -0
- package/package.json +5 -3
- package/src/primitives/Portal/index.ts +5 -0
- package/src/primitives/Select/SelectPrimitive.stories.tsx +221 -0
- package/src/primitives/Select/SelectPrimitive.style.ts +141 -0
- package/src/primitives/Select/SelectPrimitive.test.tsx +372 -0
- package/src/primitives/Select/SelectPrimitive.tsx +292 -0
- package/src/primitives/Select/index.ts +17 -0
- package/src/primitives/Select/types.ts +96 -0
- package/src/primitives/index.ts +2 -0
- package/src/utils/test/PortalHostWrapper.tsx +19 -0
- package/src/utils/test/index.ts +1 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
render,
|
|
4
|
+
screen,
|
|
5
|
+
userEvent,
|
|
6
|
+
within,
|
|
7
|
+
} from "@testing-library/react-native";
|
|
8
|
+
import type { TriggerRef } from "@rn-primitives/select";
|
|
9
|
+
import { SelectPrimitive } from "./SelectPrimitive";
|
|
10
|
+
import { tokens } from "../../utils/design";
|
|
11
|
+
import { PortalHostWrapper } from "../../utils/test";
|
|
12
|
+
import { Icon } from "../../Icon";
|
|
13
|
+
|
|
14
|
+
const user = userEvent.setup();
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
const MockNativeMethods = require("react-native/jest/MockNativeMethods");
|
|
18
|
+
const mockedMethods = MockNativeMethods.default ?? MockNativeMethods;
|
|
19
|
+
mockedMethods.measure.mockImplementation(
|
|
20
|
+
(
|
|
21
|
+
callback: (
|
|
22
|
+
x: number,
|
|
23
|
+
y: number,
|
|
24
|
+
width: number,
|
|
25
|
+
height: number,
|
|
26
|
+
pageX: number,
|
|
27
|
+
pageY: number,
|
|
28
|
+
) => void,
|
|
29
|
+
) => {
|
|
30
|
+
callback(0, 0, 320, 48, 20, 100);
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function renderSelect({
|
|
36
|
+
defaultValue,
|
|
37
|
+
onOpenChange,
|
|
38
|
+
disabled,
|
|
39
|
+
invalid,
|
|
40
|
+
testID,
|
|
41
|
+
triggerRef,
|
|
42
|
+
}: {
|
|
43
|
+
defaultValue?: { value: string; label: string };
|
|
44
|
+
onOpenChange?: (open: boolean) => void;
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
invalid?: boolean;
|
|
47
|
+
testID?: string;
|
|
48
|
+
triggerRef?: React.Ref<TriggerRef>;
|
|
49
|
+
} = {}) {
|
|
50
|
+
return render(
|
|
51
|
+
<SelectPrimitive.Root
|
|
52
|
+
defaultValue={defaultValue}
|
|
53
|
+
onOpenChange={onOpenChange}
|
|
54
|
+
disabled={disabled}
|
|
55
|
+
>
|
|
56
|
+
<SelectPrimitive.Trigger
|
|
57
|
+
ref={triggerRef}
|
|
58
|
+
testID={testID}
|
|
59
|
+
invalid={invalid}
|
|
60
|
+
>
|
|
61
|
+
<SelectPrimitive.Value placeholder="Select an option" />
|
|
62
|
+
</SelectPrimitive.Trigger>
|
|
63
|
+
</SelectPrimitive.Root>,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("SelectPrimitive.Trigger", () => {
|
|
68
|
+
it("renders with the default testID", () => {
|
|
69
|
+
renderSelect();
|
|
70
|
+
expect(screen.getByTestId("ATL-Select-Trigger")).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("namespaces a provided testID", () => {
|
|
74
|
+
renderSelect({ testID: "city" });
|
|
75
|
+
expect(screen.getByTestId("ATL-city-Select-Trigger")).toBeDefined();
|
|
76
|
+
expect(screen.queryByTestId("ATL-Select-Trigger")).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("fires onOpenChange and applies open styling when pressed", async () => {
|
|
80
|
+
const onOpenChange = jest.fn();
|
|
81
|
+
renderSelect({ onOpenChange });
|
|
82
|
+
|
|
83
|
+
const trigger = screen.getByTestId("ATL-Select-Trigger");
|
|
84
|
+
expect(trigger).toHaveStyle({
|
|
85
|
+
borderColor: tokens["color-border--interactive"],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await user.press(trigger);
|
|
89
|
+
|
|
90
|
+
expect(onOpenChange).toHaveBeenCalledWith(true);
|
|
91
|
+
expect(trigger).toHaveStyle({
|
|
92
|
+
borderColor: tokens["color-interactive--subtle"],
|
|
93
|
+
borderWidth: tokens["border-thick"],
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("does not open when disabled", async () => {
|
|
98
|
+
const onOpenChange = jest.fn();
|
|
99
|
+
renderSelect({ onOpenChange, disabled: true });
|
|
100
|
+
|
|
101
|
+
await user.press(screen.getByTestId("ATL-Select-Trigger"));
|
|
102
|
+
|
|
103
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("applies disabled styling: subtle fill and disabled value text", () => {
|
|
107
|
+
renderSelect({ disabled: true });
|
|
108
|
+
|
|
109
|
+
expect(screen.getByTestId("ATL-Select-Trigger")).toHaveStyle({
|
|
110
|
+
backgroundColor: tokens["color-disabled--secondary"],
|
|
111
|
+
});
|
|
112
|
+
expect(screen.getByText("Select an option")).toHaveStyle({
|
|
113
|
+
color: tokens["color-disabled"],
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("applies critical border styling when invalid", () => {
|
|
118
|
+
renderSelect({ invalid: true });
|
|
119
|
+
|
|
120
|
+
expect(screen.getByTestId("ATL-Select-Trigger")).toHaveStyle({
|
|
121
|
+
borderColor: tokens["color-critical"],
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("exposes a combobox role and conveys disabled to the accessibility state", () => {
|
|
126
|
+
renderSelect({ disabled: true });
|
|
127
|
+
|
|
128
|
+
const trigger = screen.getByRole("combobox");
|
|
129
|
+
expect(trigger.props.accessibilityState).toMatchObject({ disabled: true });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("announces the selected option as the trigger's accessibility value", () => {
|
|
133
|
+
renderSelect({ defaultValue: { value: "tor", label: "Toronto" } });
|
|
134
|
+
|
|
135
|
+
const trigger = screen.getByTestId("ATL-Select-Trigger");
|
|
136
|
+
expect(trigger.props.accessibilityValue).toEqual({ text: "Toronto" });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("SelectPrimitive.Trigger ref", () => {
|
|
141
|
+
it("forwards a ref exposing the trigger with open/close", () => {
|
|
142
|
+
const triggerRef = React.createRef<TriggerRef>();
|
|
143
|
+
renderSelect({ triggerRef });
|
|
144
|
+
|
|
145
|
+
expect(triggerRef.current).not.toBeNull();
|
|
146
|
+
expect(typeof triggerRef.current?.open).toBe("function");
|
|
147
|
+
expect(typeof triggerRef.current?.close).toBe("function");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const CITIES = [
|
|
152
|
+
{ value: "tor", label: "Toronto" },
|
|
153
|
+
{ value: "van", label: "Vancouver" },
|
|
154
|
+
{ value: "edm", label: "Edmonton" },
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
function renderSelectWithContent({
|
|
158
|
+
defaultValue,
|
|
159
|
+
onValueChange,
|
|
160
|
+
closeOnPress,
|
|
161
|
+
disabledItem,
|
|
162
|
+
}: {
|
|
163
|
+
defaultValue?: { value: string; label: string };
|
|
164
|
+
onValueChange?: (option?: { value: string; label: string }) => void;
|
|
165
|
+
closeOnPress?: boolean;
|
|
166
|
+
disabledItem?: string;
|
|
167
|
+
} = {}) {
|
|
168
|
+
return render(
|
|
169
|
+
<SelectPrimitive.Root
|
|
170
|
+
defaultValue={defaultValue}
|
|
171
|
+
onValueChange={onValueChange}
|
|
172
|
+
>
|
|
173
|
+
<SelectPrimitive.Trigger>
|
|
174
|
+
<SelectPrimitive.Value placeholder="Select an option" />
|
|
175
|
+
</SelectPrimitive.Trigger>
|
|
176
|
+
<SelectPrimitive.Portal>
|
|
177
|
+
<SelectPrimitive.Overlay />
|
|
178
|
+
<SelectPrimitive.Content>
|
|
179
|
+
<SelectPrimitive.Viewport>
|
|
180
|
+
<SelectPrimitive.Group>
|
|
181
|
+
<SelectPrimitive.Label>Cities</SelectPrimitive.Label>
|
|
182
|
+
{CITIES.map(city => (
|
|
183
|
+
<SelectPrimitive.Item
|
|
184
|
+
key={city.value}
|
|
185
|
+
value={city.value}
|
|
186
|
+
label={city.label}
|
|
187
|
+
closeOnPress={closeOnPress}
|
|
188
|
+
disabled={disabledItem === city.value}
|
|
189
|
+
>
|
|
190
|
+
<SelectPrimitive.ItemText
|
|
191
|
+
disabled={disabledItem === city.value}
|
|
192
|
+
/>
|
|
193
|
+
<SelectPrimitive.ItemIndicator>
|
|
194
|
+
<Icon name="checkmark" />
|
|
195
|
+
</SelectPrimitive.ItemIndicator>
|
|
196
|
+
</SelectPrimitive.Item>
|
|
197
|
+
))}
|
|
198
|
+
</SelectPrimitive.Group>
|
|
199
|
+
<SelectPrimitive.Separator />
|
|
200
|
+
</SelectPrimitive.Viewport>
|
|
201
|
+
</SelectPrimitive.Content>
|
|
202
|
+
</SelectPrimitive.Portal>
|
|
203
|
+
</SelectPrimitive.Root>,
|
|
204
|
+
{ wrapper: PortalHostWrapper },
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function openSelect() {
|
|
209
|
+
await user.press(screen.getByTestId("ATL-Select-Trigger"));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
describe("SelectPrimitive.Content", () => {
|
|
213
|
+
it("renders nothing until the trigger opens it", async () => {
|
|
214
|
+
renderSelectWithContent();
|
|
215
|
+
expect(screen.queryByText("Toronto")).toBeNull();
|
|
216
|
+
|
|
217
|
+
await openSelect();
|
|
218
|
+
|
|
219
|
+
expect(screen.getByText("Toronto")).toBeDefined();
|
|
220
|
+
expect(screen.getByText("Vancouver")).toBeDefined();
|
|
221
|
+
expect(screen.getByText("Cities")).toBeDefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("dismisses when tapping outside (overlay), without selecting", async () => {
|
|
225
|
+
const onValueChange = jest.fn();
|
|
226
|
+
renderSelectWithContent({ onValueChange });
|
|
227
|
+
await openSelect();
|
|
228
|
+
|
|
229
|
+
await user.press(screen.getByTestId("ATL-Select-Overlay"));
|
|
230
|
+
|
|
231
|
+
expect(screen.queryByText("Toronto")).toBeNull();
|
|
232
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("SelectPrimitive.Item", () => {
|
|
237
|
+
it("selects the option, emits { value, label }, and closes", async () => {
|
|
238
|
+
const onValueChange = jest.fn();
|
|
239
|
+
renderSelectWithContent({ onValueChange });
|
|
240
|
+
await openSelect();
|
|
241
|
+
|
|
242
|
+
await user.press(screen.getByText("Vancouver"));
|
|
243
|
+
|
|
244
|
+
expect(onValueChange).toHaveBeenCalledWith({
|
|
245
|
+
value: "van",
|
|
246
|
+
label: "Vancouver",
|
|
247
|
+
});
|
|
248
|
+
// Dropdown closed; trigger now shows the selection.
|
|
249
|
+
expect(screen.queryByText("Cities")).toBeNull();
|
|
250
|
+
expect(screen.getByText("Vancouver")).toBeDefined();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("passes closeOnPress through — dropdown stays open when false", async () => {
|
|
254
|
+
const onValueChange = jest.fn();
|
|
255
|
+
renderSelectWithContent({ onValueChange, closeOnPress: false });
|
|
256
|
+
await openSelect();
|
|
257
|
+
|
|
258
|
+
await user.press(screen.getByText("Vancouver"));
|
|
259
|
+
|
|
260
|
+
expect(onValueChange).toHaveBeenCalled();
|
|
261
|
+
expect(screen.getByText("Cities")).toBeDefined();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("does not select a disabled option", async () => {
|
|
265
|
+
const onValueChange = jest.fn();
|
|
266
|
+
renderSelectWithContent({ onValueChange, disabledItem: "van" });
|
|
267
|
+
await openSelect();
|
|
268
|
+
|
|
269
|
+
await user.press(screen.getByText("Vancouver"));
|
|
270
|
+
|
|
271
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("greys a disabled option's text when ItemText is told it is disabled", async () => {
|
|
275
|
+
renderSelectWithContent({ disabledItem: "van" });
|
|
276
|
+
await openSelect();
|
|
277
|
+
|
|
278
|
+
expect(screen.getByText("Vancouver")).toHaveStyle({
|
|
279
|
+
color: tokens["color-disabled"],
|
|
280
|
+
});
|
|
281
|
+
expect(screen.getByText("Toronto")).toHaveStyle({
|
|
282
|
+
color: tokens["color-text"],
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("shows the checkmark indicator only on the selected option", async () => {
|
|
287
|
+
renderSelectWithContent({
|
|
288
|
+
defaultValue: { value: "tor", label: "Toronto" },
|
|
289
|
+
});
|
|
290
|
+
await openSelect();
|
|
291
|
+
|
|
292
|
+
// One checkmark icon in the dropdown (the selected row's indicator).
|
|
293
|
+
expect(screen.getAllByTestId("checkmark")).toHaveLength(1);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("renders whatever marker is composed into ItemIndicator on the selected row", async () => {
|
|
297
|
+
render(
|
|
298
|
+
<SelectPrimitive.Root defaultValue={{ value: "tor", label: "Toronto" }}>
|
|
299
|
+
<SelectPrimitive.Trigger>
|
|
300
|
+
<SelectPrimitive.Value placeholder="Select an option" />
|
|
301
|
+
</SelectPrimitive.Trigger>
|
|
302
|
+
<SelectPrimitive.Portal>
|
|
303
|
+
<SelectPrimitive.Overlay />
|
|
304
|
+
<SelectPrimitive.Content>
|
|
305
|
+
<SelectPrimitive.Viewport>
|
|
306
|
+
<SelectPrimitive.Item value="tor" label="Toronto">
|
|
307
|
+
<SelectPrimitive.ItemText />
|
|
308
|
+
<SelectPrimitive.ItemIndicator>
|
|
309
|
+
<Icon name="person" />
|
|
310
|
+
</SelectPrimitive.ItemIndicator>
|
|
311
|
+
</SelectPrimitive.Item>
|
|
312
|
+
</SelectPrimitive.Viewport>
|
|
313
|
+
</SelectPrimitive.Content>
|
|
314
|
+
</SelectPrimitive.Portal>
|
|
315
|
+
</SelectPrimitive.Root>,
|
|
316
|
+
{ wrapper: PortalHostWrapper },
|
|
317
|
+
);
|
|
318
|
+
await openSelect();
|
|
319
|
+
|
|
320
|
+
expect(screen.getByTestId("person")).toBeDefined();
|
|
321
|
+
expect(screen.queryByTestId("checkmark")).toBeNull();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("conveys option role plus selected and disabled states to assistive tech", async () => {
|
|
325
|
+
renderSelectWithContent({
|
|
326
|
+
defaultValue: { value: "tor", label: "Toronto" },
|
|
327
|
+
disabledItem: "van",
|
|
328
|
+
});
|
|
329
|
+
await openSelect();
|
|
330
|
+
|
|
331
|
+
const options = screen.getAllByRole("option");
|
|
332
|
+
const optionWith = (label: string) =>
|
|
333
|
+
options.find(option => within(option).queryByText(label));
|
|
334
|
+
|
|
335
|
+
expect(optionWith("Toronto")?.props.accessibilityState).toMatchObject({
|
|
336
|
+
checked: true,
|
|
337
|
+
});
|
|
338
|
+
expect(optionWith("Vancouver")?.props.accessibilityState).toMatchObject({
|
|
339
|
+
disabled: true,
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("SelectPrimitive.Value", () => {
|
|
345
|
+
it("shows the placeholder with subdued styling when nothing is selected", () => {
|
|
346
|
+
renderSelect();
|
|
347
|
+
|
|
348
|
+
const placeholder = screen.getByText("Select an option");
|
|
349
|
+
expect(placeholder).toHaveStyle({
|
|
350
|
+
color: tokens["color-text--secondary"],
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("shows the selected option's label", () => {
|
|
355
|
+
renderSelect({ defaultValue: { value: "tor", label: "Toronto" } });
|
|
356
|
+
|
|
357
|
+
const value = screen.getByText("Toronto");
|
|
358
|
+
expect(value).toHaveStyle({ color: tokens["color-text"] });
|
|
359
|
+
expect(screen.queryByText("Select an option")).toBeNull();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("greys the value when the field is disabled (set on Root)", () => {
|
|
363
|
+
renderSelect({
|
|
364
|
+
disabled: true,
|
|
365
|
+
defaultValue: { value: "tor", label: "Toronto" },
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(screen.getByText("Toronto")).toHaveStyle({
|
|
369
|
+
color: tokens["color-disabled"],
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleSheet } from "react-native";
|
|
3
|
+
import * as Primitive from "@rn-primitives/select";
|
|
4
|
+
import { SafeAreaInsetsContext } from "react-native-safe-area-context";
|
|
5
|
+
import { useStyles } from "./SelectPrimitive.style";
|
|
6
|
+
import type {
|
|
7
|
+
SelectPrimitiveContentProps,
|
|
8
|
+
SelectPrimitiveGroupProps,
|
|
9
|
+
SelectPrimitiveItemIndicatorProps,
|
|
10
|
+
SelectPrimitiveItemProps,
|
|
11
|
+
SelectPrimitiveItemTextProps,
|
|
12
|
+
SelectPrimitiveLabelProps,
|
|
13
|
+
SelectPrimitiveOverlayProps,
|
|
14
|
+
SelectPrimitivePortalProps,
|
|
15
|
+
SelectPrimitiveRootProps,
|
|
16
|
+
SelectPrimitiveSeparatorProps,
|
|
17
|
+
SelectPrimitiveTriggerProps,
|
|
18
|
+
SelectPrimitiveValueProps,
|
|
19
|
+
SelectPrimitiveViewportProps,
|
|
20
|
+
} from "./types";
|
|
21
|
+
import { tokens as staticTokens } from "../../utils/design";
|
|
22
|
+
|
|
23
|
+
function SelectPrimitiveRoot({ ...props }: SelectPrimitiveRootProps) {
|
|
24
|
+
return <Primitive.Root {...props} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Styled trigger field. Renders only its `children` — compose
|
|
29
|
+
* `SelectPrimitive.Value` and any trailing icon (e.g. a chevron) here; the
|
|
30
|
+
* primitive provides no chevron of its own.
|
|
31
|
+
*/
|
|
32
|
+
function SelectPrimitiveTrigger({
|
|
33
|
+
children,
|
|
34
|
+
style,
|
|
35
|
+
testID,
|
|
36
|
+
invalid,
|
|
37
|
+
onPressIn,
|
|
38
|
+
onPressOut,
|
|
39
|
+
ref,
|
|
40
|
+
...props
|
|
41
|
+
}: SelectPrimitiveTriggerProps) {
|
|
42
|
+
const styles = useStyles();
|
|
43
|
+
const { open, value, disabled: rootDisabled } = Primitive.useRootContext();
|
|
44
|
+
const [pressed, setPressed] = React.useState(false);
|
|
45
|
+
|
|
46
|
+
// `disabled` is Root-level; the primitive's Trigger won't read it from
|
|
47
|
+
// context, so resolve it here and pass it down to block interaction.
|
|
48
|
+
const isDisabled = rootDisabled ?? false;
|
|
49
|
+
const isInvalid = !isDisabled && (invalid ?? false);
|
|
50
|
+
|
|
51
|
+
// Announce the selected option as the trigger's value.
|
|
52
|
+
const accessibilityValue = value?.label ? { text: value.label } : undefined;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Primitive.Trigger
|
|
56
|
+
ref={ref}
|
|
57
|
+
testID={computeTriggerTestID(testID)}
|
|
58
|
+
disabled={isDisabled}
|
|
59
|
+
accessibilityValue={accessibilityValue}
|
|
60
|
+
onPressIn={event => {
|
|
61
|
+
setPressed(true);
|
|
62
|
+
onPressIn?.(event);
|
|
63
|
+
}}
|
|
64
|
+
onPressOut={event => {
|
|
65
|
+
setPressed(false);
|
|
66
|
+
onPressOut?.(event);
|
|
67
|
+
}}
|
|
68
|
+
style={[
|
|
69
|
+
styles.trigger,
|
|
70
|
+
pressed && !open && styles.triggerPressed,
|
|
71
|
+
open && styles.triggerOpen,
|
|
72
|
+
isInvalid && styles.triggerInvalid,
|
|
73
|
+
isDisabled && styles.triggerDisabled,
|
|
74
|
+
style,
|
|
75
|
+
]}
|
|
76
|
+
{...props}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</Primitive.Trigger>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function computeTriggerTestID(testID?: string): string {
|
|
84
|
+
return testID ? `ATL-${testID}-Select-Trigger` : "ATL-Select-Trigger";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function SelectPrimitiveValue({ style, ...props }: SelectPrimitiveValueProps) {
|
|
88
|
+
const styles = useStyles();
|
|
89
|
+
const { value, disabled } = Primitive.useRootContext();
|
|
90
|
+
const isEmpty = value?.value === undefined;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Primitive.Value
|
|
94
|
+
style={[
|
|
95
|
+
styles.value,
|
|
96
|
+
isEmpty && styles.valuePlaceholder,
|
|
97
|
+
disabled && styles.valueDisabled,
|
|
98
|
+
style,
|
|
99
|
+
]}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Portals the dropdown into the mounted `AtlantisPortalHost`. */
|
|
106
|
+
function SelectPrimitivePortal({ ...props }: SelectPrimitivePortalProps) {
|
|
107
|
+
return <Primitive.Portal {...props} />;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Tap-outside dismiss surface behind the dropdown (no scrim). */
|
|
111
|
+
function SelectPrimitiveOverlay({
|
|
112
|
+
style,
|
|
113
|
+
testID = "ATL-Select-Overlay",
|
|
114
|
+
...props
|
|
115
|
+
}: SelectPrimitiveOverlayProps) {
|
|
116
|
+
const styles = useStyles();
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<Primitive.Overlay
|
|
120
|
+
testID={testID}
|
|
121
|
+
style={StyleSheet.flatten([styles.overlay, style])}
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Anchored dropdown surface: matches the trigger width, applies safe-area
|
|
129
|
+
* insets, and flips above the trigger when space is tight. Compose it inside
|
|
130
|
+
* `Portal`, alongside `Overlay`, wrapping the items in a `Viewport`.
|
|
131
|
+
*/
|
|
132
|
+
function SelectPrimitiveContent({
|
|
133
|
+
children,
|
|
134
|
+
style,
|
|
135
|
+
sideOffset = staticTokens["space-smaller"],
|
|
136
|
+
insets,
|
|
137
|
+
ref,
|
|
138
|
+
...props
|
|
139
|
+
}: SelectPrimitiveContentProps) {
|
|
140
|
+
const styles = useStyles();
|
|
141
|
+
// Read the context directly, not `useSafeAreaInsets` (which throws without a
|
|
142
|
+
// provider): `Content` renders at the portal host, which may sit outside one.
|
|
143
|
+
const safeAreaInsets = React.useContext(SafeAreaInsetsContext) ?? undefined;
|
|
144
|
+
const { triggerPosition } = Primitive.useRootContext();
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<Primitive.Content
|
|
148
|
+
ref={ref}
|
|
149
|
+
sideOffset={sideOffset}
|
|
150
|
+
insets={insets ?? safeAreaInsets}
|
|
151
|
+
style={StyleSheet.flatten([
|
|
152
|
+
styles.content,
|
|
153
|
+
triggerPosition ? { width: triggerPosition.width } : undefined,
|
|
154
|
+
style,
|
|
155
|
+
])}
|
|
156
|
+
{...props}
|
|
157
|
+
>
|
|
158
|
+
{children}
|
|
159
|
+
</Primitive.Content>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Scroll container for the items (a no-op passthrough on native). */
|
|
164
|
+
function SelectPrimitiveViewport({ ...props }: SelectPrimitiveViewportProps) {
|
|
165
|
+
return <Primitive.Viewport {...props} />;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Styled, pressable option row. Compose its content from `SelectPrimitive.ItemText`
|
|
170
|
+
* and `SelectPrimitive.ItemIndicator` (and anything else) as `children`.
|
|
171
|
+
*/
|
|
172
|
+
function SelectPrimitiveItem({
|
|
173
|
+
style,
|
|
174
|
+
disabled,
|
|
175
|
+
ref,
|
|
176
|
+
...props
|
|
177
|
+
}: SelectPrimitiveItemProps) {
|
|
178
|
+
const styles = useStyles();
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<Primitive.Item
|
|
182
|
+
ref={ref}
|
|
183
|
+
disabled={disabled}
|
|
184
|
+
style={({ pressed }) => [
|
|
185
|
+
styles.item,
|
|
186
|
+
pressed && styles.itemPressed,
|
|
187
|
+
style,
|
|
188
|
+
]}
|
|
189
|
+
{...props}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The option label as styled text (reads the label from the item context). The
|
|
196
|
+
* `disabled` flag is explicit — the composing layer (e.g. the sugared
|
|
197
|
+
* `Select.Item`) forwards the row's disabled state to grey the text.
|
|
198
|
+
*/
|
|
199
|
+
function SelectPrimitiveItemText({
|
|
200
|
+
style,
|
|
201
|
+
disabled,
|
|
202
|
+
ref,
|
|
203
|
+
...props
|
|
204
|
+
}: SelectPrimitiveItemTextProps) {
|
|
205
|
+
const styles = useStyles();
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<Primitive.ItemText
|
|
209
|
+
ref={ref}
|
|
210
|
+
style={[styles.itemText, disabled && styles.itemTextDisabled, style]}
|
|
211
|
+
{...props}
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The selected-state marker slot. Renders its `children` (the marker, e.g. a
|
|
218
|
+
* checkmark) on the selected row only — it self-hides on unselected rows. The
|
|
219
|
+
* marker glyph is supplied by the composing layer, not defaulted here.
|
|
220
|
+
*/
|
|
221
|
+
function SelectPrimitiveItemIndicator({
|
|
222
|
+
style,
|
|
223
|
+
ref,
|
|
224
|
+
...props
|
|
225
|
+
}: SelectPrimitiveItemIndicatorProps) {
|
|
226
|
+
const styles = useStyles();
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<Primitive.ItemIndicator
|
|
230
|
+
ref={ref}
|
|
231
|
+
style={[styles.itemIndicator, style]}
|
|
232
|
+
{...props}
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Groups related options, typically with a `SelectPrimitive.Label` heading.
|
|
239
|
+
*/
|
|
240
|
+
function SelectPrimitiveGroup({ ...props }: SelectPrimitiveGroupProps) {
|
|
241
|
+
return <Primitive.Group {...props} />;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* A styled group heading rendered inside `SelectPrimitive.Group`.
|
|
246
|
+
*/
|
|
247
|
+
function SelectPrimitiveLabel({
|
|
248
|
+
style,
|
|
249
|
+
ref,
|
|
250
|
+
...props
|
|
251
|
+
}: SelectPrimitiveLabelProps) {
|
|
252
|
+
const styles = useStyles();
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<Primitive.Label ref={ref} style={[styles.groupLabel, style]} {...props} />
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* A styled divider between groups or options.
|
|
261
|
+
*/
|
|
262
|
+
function SelectPrimitiveSeparator({
|
|
263
|
+
style,
|
|
264
|
+
ref,
|
|
265
|
+
...props
|
|
266
|
+
}: SelectPrimitiveSeparatorProps) {
|
|
267
|
+
const styles = useStyles();
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<Primitive.Separator
|
|
271
|
+
ref={ref}
|
|
272
|
+
style={[styles.separator, style]}
|
|
273
|
+
{...props}
|
|
274
|
+
/>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export const SelectPrimitive = {
|
|
279
|
+
Root: SelectPrimitiveRoot,
|
|
280
|
+
Trigger: SelectPrimitiveTrigger,
|
|
281
|
+
Value: SelectPrimitiveValue,
|
|
282
|
+
Portal: SelectPrimitivePortal,
|
|
283
|
+
Overlay: SelectPrimitiveOverlay,
|
|
284
|
+
Content: SelectPrimitiveContent,
|
|
285
|
+
Viewport: SelectPrimitiveViewport,
|
|
286
|
+
Item: SelectPrimitiveItem,
|
|
287
|
+
ItemText: SelectPrimitiveItemText,
|
|
288
|
+
ItemIndicator: SelectPrimitiveItemIndicator,
|
|
289
|
+
Group: SelectPrimitiveGroup,
|
|
290
|
+
Label: SelectPrimitiveLabel,
|
|
291
|
+
Separator: SelectPrimitiveSeparator,
|
|
292
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { SelectPrimitive } from "./SelectPrimitive";
|
|
2
|
+
export type {
|
|
3
|
+
SelectPrimitiveContentProps,
|
|
4
|
+
SelectPrimitiveGroupProps,
|
|
5
|
+
SelectPrimitiveItemIndicatorProps,
|
|
6
|
+
SelectPrimitiveItemProps,
|
|
7
|
+
SelectPrimitiveItemTextProps,
|
|
8
|
+
SelectPrimitiveLabelProps,
|
|
9
|
+
SelectPrimitiveOption,
|
|
10
|
+
SelectPrimitiveOverlayProps,
|
|
11
|
+
SelectPrimitivePortalProps,
|
|
12
|
+
SelectPrimitiveRootProps,
|
|
13
|
+
SelectPrimitiveSeparatorProps,
|
|
14
|
+
SelectPrimitiveTriggerProps,
|
|
15
|
+
SelectPrimitiveValueProps,
|
|
16
|
+
SelectPrimitiveViewportProps,
|
|
17
|
+
} from "./types";
|