@jobber/components-native 0.108.2 → 0.109.1-upgrade-ax-69e6080.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/docs/Select/Select.md +987 -0
  2. package/dist/docs/index.md +1 -0
  3. package/dist/package.json +4 -4
  4. package/dist/src/Select/Select.composable.test.js +38 -0
  5. package/dist/src/Select/Select.js +26 -0
  6. package/dist/src/Select/SelectComposableTypes.js +1 -0
  7. package/dist/src/Select/SelectRoot.js +85 -0
  8. package/dist/src/Select/SelectRoot.style.js +9 -0
  9. package/dist/src/Select/SelectRoot.test.js +157 -0
  10. package/dist/src/Select/components/SelectContent.js +51 -0
  11. package/dist/src/Select/components/SelectContent.test.js +51 -0
  12. package/dist/src/Select/components/SelectFieldContext.js +10 -0
  13. package/dist/src/Select/components/SelectItem.js +29 -0
  14. package/dist/src/Select/components/SelectItem.style.js +6 -0
  15. package/dist/src/Select/components/SelectItem.test.js +98 -0
  16. package/dist/src/Select/components/SelectLabel.js +29 -0
  17. package/dist/src/Select/components/SelectTrigger.js +40 -0
  18. package/dist/src/Select/components/SelectTrigger.style.js +26 -0
  19. package/dist/src/Select/index.js +2 -0
  20. package/dist/src/primitives/Portal/index.js +3 -1
  21. package/dist/src/primitives/Select/SelectPrimitive.js +4 -12
  22. package/dist/src/primitives/Select/SelectPrimitive.style.js +14 -9
  23. package/dist/src/primitives/Select/SelectPrimitive.test.js +2 -2
  24. package/dist/src/primitives/Select/index.js +1 -1
  25. package/dist/src/utils/meta/meta.json +11 -0
  26. package/dist/tsconfig.build.tsbuildinfo +1 -1
  27. package/dist/types/src/Select/Select.composable.test.d.ts +1 -0
  28. package/dist/types/src/Select/Select.d.ts +18 -0
  29. package/dist/types/src/Select/SelectComposableTypes.d.ts +50 -0
  30. package/dist/types/src/Select/SelectRoot.d.ts +12 -0
  31. package/dist/types/src/Select/SelectRoot.style.d.ts +8 -0
  32. package/dist/types/src/Select/SelectRoot.test.d.ts +1 -0
  33. package/dist/types/src/Select/components/SelectContent.d.ts +8 -0
  34. package/dist/types/src/Select/components/SelectContent.test.d.ts +1 -0
  35. package/dist/types/src/Select/components/SelectFieldContext.d.ts +27 -0
  36. package/dist/types/src/Select/components/SelectItem.d.ts +32 -0
  37. package/dist/types/src/Select/components/SelectItem.style.d.ts +5 -0
  38. package/dist/types/src/Select/components/SelectItem.test.d.ts +1 -0
  39. package/dist/types/src/Select/components/SelectLabel.d.ts +22 -0
  40. package/dist/types/src/Select/components/SelectTrigger.d.ts +20 -0
  41. package/dist/types/src/Select/components/SelectTrigger.style.d.ts +21 -0
  42. package/dist/types/src/Select/index.d.ts +2 -1
  43. package/dist/types/src/primitives/Select/SelectPrimitive.d.ts +3 -1
  44. package/dist/types/src/primitives/Select/SelectPrimitive.style.d.ts +6 -3
  45. package/dist/types/src/primitives/Select/index.d.ts +1 -1
  46. package/dist/types/src/primitives/Select/types.d.ts +1 -1
  47. package/package.json +4 -4
  48. package/src/Select/Select.composable.stories.tsx +93 -0
  49. package/src/Select/Select.composable.test.tsx +56 -0
  50. package/src/Select/Select.guide.md +197 -0
  51. package/src/Select/Select.tsx +31 -0
  52. package/src/Select/SelectComposableTypes.ts +57 -0
  53. package/src/Select/SelectRoot.style.ts +10 -0
  54. package/src/Select/SelectRoot.test.tsx +245 -0
  55. package/src/Select/SelectRoot.tsx +120 -0
  56. package/src/Select/components/SelectContent.test.tsx +67 -0
  57. package/src/Select/components/SelectContent.tsx +75 -0
  58. package/src/Select/components/SelectFieldContext.tsx +46 -0
  59. package/src/Select/components/SelectItem.style.ts +7 -0
  60. package/src/Select/components/SelectItem.test.tsx +166 -0
  61. package/src/Select/components/SelectItem.tsx +78 -0
  62. package/src/Select/components/SelectLabel.tsx +57 -0
  63. package/src/Select/components/SelectTrigger.style.ts +29 -0
  64. package/src/Select/components/SelectTrigger.tsx +98 -0
  65. package/src/Select/docs/SelectComposableBasic.tsx +18 -0
  66. package/src/Select/docs/SelectComposableControlledWithRef.tsx +28 -0
  67. package/src/Select/docs/SelectComposableCustomMarker.tsx +25 -0
  68. package/src/Select/docs/SelectComposableDescription.tsx +18 -0
  69. package/src/Select/docs/SelectComposableDisabled.tsx +18 -0
  70. package/src/Select/docs/SelectComposableDisabledOptions.tsx +20 -0
  71. package/src/Select/docs/SelectComposableGroupedOptions.tsx +29 -0
  72. package/src/Select/docs/SelectComposableItemPrefixSuffix.tsx +34 -0
  73. package/src/Select/docs/SelectComposableLabelAbove.tsx +18 -0
  74. package/src/Select/docs/SelectComposableLabelInside.tsx +18 -0
  75. package/src/Select/docs/SelectComposableProvinces.tsx +36 -0
  76. package/src/Select/docs/SelectComposableReadOnly.tsx +18 -0
  77. package/src/Select/docs/SelectComposableValidationFlow.tsx +27 -0
  78. package/src/Select/docs/index.ts +14 -0
  79. package/src/Select/index.ts +10 -1
  80. package/src/primitives/Portal/index.ts +3 -1
  81. package/src/primitives/Select/SelectPrimitive.style.ts +18 -11
  82. package/src/primitives/Select/SelectPrimitive.test.tsx +2 -2
  83. package/src/primitives/Select/SelectPrimitive.tsx +6 -18
  84. package/src/primitives/Select/index.ts +4 -1
  85. package/src/primitives/Select/types.ts +1 -1
  86. package/src/utils/meta/meta.json +11 -0
@@ -52,6 +52,7 @@
52
52
  [Radii](./Radii/Radii.md)
53
53
  [ResponsiveBreakpoint](./ResponsiveBreakpoint/ResponsiveBreakpoint.md)
54
54
  [scaffolding](./scaffolding/scaffolding.md)
55
+ [Select](./Select/Select.md)
55
56
  [settings](./settings/settings.md)
56
57
  [Spacing](./Spacing/Spacing.md)
57
58
  [StatusLabel](./StatusLabel/StatusLabel.md)
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.108.2",
3
+ "version": "0.109.1-upgrade-ax-69e6080.2+69e60801",
4
4
  "license": "MIT",
5
5
  "description": "React Native implementation of Atlantis",
6
6
  "repository": {
@@ -73,8 +73,8 @@
73
73
  "devDependencies": {
74
74
  "@babel/runtime": "^7.29.2",
75
75
  "@gorhom/bottom-sheet": "^5.2.8",
76
- "@jobber/design": "0.108.0",
77
- "@jobber/hooks": "2.21.0",
76
+ "@jobber/design": "0.108.1-upgrade-ax-69e6080.7+69e60801",
77
+ "@jobber/hooks": "2.21.1-upgrade-ax-69e6080.109+69e60801",
78
78
  "@react-native-community/datetimepicker": "^8.4.5",
79
79
  "@react-native/babel-preset": "^0.82.1",
80
80
  "@storybook/addon-a11y": "10.3.5",
@@ -124,5 +124,5 @@
124
124
  "react-native-screens": ">=4.18.0",
125
125
  "react-native-svg": ">=12.0.0"
126
126
  },
127
- "gitHead": "37b9c3bc407715b1dc2190912665cfc41e8cc335"
127
+ "gitHead": "69e60801cfaedb56dbfac2df05b44ace5676c19f"
128
128
  }
@@ -0,0 +1,38 @@
1
+ import React from "react";
2
+ import { render, screen } from "@testing-library/react-native";
3
+ import { Option, Select } from ".";
4
+ import { PortalHostWrapper } from "../utils/test";
5
+ describe("Select composable statics + coexistence", () => {
6
+ it("exposes the composable parts as statics on Select", () => {
7
+ expect(Object.keys(Select)).toEqual(expect.arrayContaining([
8
+ "Root",
9
+ "Trigger",
10
+ "Value",
11
+ "Label",
12
+ "Content",
13
+ "Item",
14
+ "Group",
15
+ "GroupLabel",
16
+ "Separator",
17
+ ]));
18
+ });
19
+ it("renders through the public statics with no react-hook-form context", () => {
20
+ render(React.createElement(Select.Root, null,
21
+ React.createElement(Select.Label, null, "City"),
22
+ React.createElement(Select.Trigger, null,
23
+ React.createElement(Select.Value, { placeholder: "Select a city" })),
24
+ React.createElement(Select.Content, null,
25
+ React.createElement(Select.Item, { value: "tor", label: "Toronto" }))), { wrapper: PortalHostWrapper });
26
+ expect(screen.getByText("City", { includeHiddenElements: true })).toBeDefined();
27
+ expect(screen.getByTestId("ATL-Select-Trigger")).toBeDefined();
28
+ });
29
+ it("keeps the legacy props-driven Select callable and renders Option", () => {
30
+ render(React.createElement(Select, { onChange: jest.fn() },
31
+ React.createElement(Option, { value: "tor" }, "Toronto"),
32
+ React.createElement(Option, { value: "van" }, "Vancouver")));
33
+ // Legacy trigger testID is preserved and does not collide with the
34
+ // composable trigger's ATL-Select-Trigger.
35
+ expect(screen.getByTestId("ATL-Select")).toBeDefined();
36
+ expect(screen.queryByTestId("ATL-Select-Trigger")).toBeNull();
37
+ });
38
+ });
@@ -2,6 +2,12 @@ import React from "react";
2
2
  import { View } from "react-native";
3
3
  import { useStyles } from "./Select.style";
4
4
  import { SelectInternalPicker } from "./components/SelectInternalPicker";
5
+ import { SelectRoot } from "./SelectRoot";
6
+ import { SelectTrigger } from "./components/SelectTrigger";
7
+ import { SelectLabel } from "./components/SelectLabel";
8
+ import { SelectContent } from "./components/SelectContent";
9
+ import { SelectItem, SelectItemPrefix, SelectItemSuffix, } from "./components/SelectItem";
10
+ import { SelectPrimitive } from "../primitives/Select";
5
11
  import { InputFieldWrapper } from "../InputFieldWrapper";
6
12
  import { Icon } from "../Icon";
7
13
  import { Text } from "../Text";
@@ -66,6 +72,26 @@ export function Select({ value, defaultValue, onChange, children, placeholder, l
66
72
  return (activeValue === null || activeValue === void 0 ? void 0 : activeValue.label) || placeholder || t("Select.emptyValue");
67
73
  }
68
74
  }
75
+ /**
76
+ * The composable `Select.*` parts, attached as statics on the function
77
+ * declaration in this file — the docs site's prop generation
78
+ * (react-docgen-typescript) only discovers parts assigned here at compile
79
+ * time, not runtime attachments like `Object.assign`. Additive — the legacy
80
+ * props-driven `Select` + `Option` are unchanged (props, native picker,
81
+ * `ATL-Select` testID). See `Select.guide.md` for the parts and the
82
+ * composition model.
83
+ */
84
+ Select.Root = SelectRoot;
85
+ Select.Trigger = SelectTrigger;
86
+ Select.Value = SelectPrimitive.Value;
87
+ Select.Label = SelectLabel;
88
+ Select.Content = SelectContent;
89
+ Select.Item = SelectItem;
90
+ Select.ItemPrefix = SelectItemPrefix;
91
+ Select.ItemSuffix = SelectItemSuffix;
92
+ Select.Group = SelectPrimitive.Group;
93
+ Select.GroupLabel = SelectPrimitive.Label;
94
+ Select.Separator = SelectPrimitive.Separator;
69
95
  function getTextVariation({ invalid, disabled, }) {
70
96
  if (invalid)
71
97
  return "error";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import React, { useCallback, useMemo, useRef, useState } from "react";
13
+ import { View } from "react-native";
14
+ import { useStyles } from "./SelectRoot.style";
15
+ import { SelectFieldContext } from "./components/SelectFieldContext";
16
+ import { FieldLabel } from "./components/SelectLabel";
17
+ import { HelperText } from "../primitives/HelperText";
18
+ import { SelectPrimitive } from "../primitives/Select";
19
+ /**
20
+ * Composable `Select.Root` chrome: renders the optional `description` / `error`
21
+ * below the field (via `HelperText`), owns the field-state context, and places the
22
+ * field label. The label content is authored via `Select.Label` (registered into
23
+ * this context); `labelPlacement` decides whether it renders in the above slot
24
+ * (here) or the inside slot (`Select.Trigger`). `error` takes priority over
25
+ * `description`. See `Select.guide.md` for the label / accessibility / layering
26
+ * model.
27
+ */
28
+ export function SelectRoot(_a) {
29
+ var { labelPlacement = "inside", accessibilityLabel: accessibilityLabelProp, description, error, invalid, status, readOnly, children } = _a, rootProps = __rest(_a, ["labelPlacement", "accessibilityLabel", "description", "error", "invalid", "status", "readOnly", "children"]);
30
+ const styles = useStyles();
31
+ // An empty / whitespace-only string error counts as "no error".
32
+ const hasError = typeof error === "string" ? error.trim().length > 0 : error != null;
33
+ const isInvalid = invalid === true || status === "critical" || hasError;
34
+ // A composed `Select.Label` registers its content + (for a string) its
35
+ // accessible name here.
36
+ const [registered, setRegistered] = useState();
37
+ // Track how many `Select.Label`s are mounted to warn on unsupported
38
+ // multi-label usage (the registry holds a single label — the last one wins).
39
+ const labelCount = useRef(0);
40
+ const registerLabel = useCallback((content, name) => {
41
+ labelCount.current += 1;
42
+ if (labelCount.current > 1) {
43
+ console.warn("Select: multiple <Select.Label> are composed — only one is supported; the last one wins.");
44
+ }
45
+ setRegistered({ content, name });
46
+ }, []);
47
+ const unregisterLabel = useCallback(() => {
48
+ labelCount.current = Math.max(0, labelCount.current - 1);
49
+ setRegistered(undefined);
50
+ }, []);
51
+ const labelContent = registered === null || registered === void 0 ? void 0 : registered.content;
52
+ const accessibilityLabel = accessibilityLabelProp !== null && accessibilityLabelProp !== void 0 ? accessibilityLabelProp : registered === null || registered === void 0 ? void 0 : registered.name;
53
+ const fieldState = useMemo(() => ({
54
+ labelPlacement,
55
+ labelContent,
56
+ accessibilityLabel,
57
+ invalid: isInvalid,
58
+ readOnly,
59
+ registerLabel,
60
+ unregisterLabel,
61
+ }), [
62
+ labelPlacement,
63
+ labelContent,
64
+ accessibilityLabel,
65
+ isInvalid,
66
+ readOnly,
67
+ registerLabel,
68
+ unregisterLabel,
69
+ ]);
70
+ return (React.createElement(View, { style: styles.container },
71
+ React.createElement(SelectFieldContext.Provider, { value: fieldState },
72
+ React.createElement(SelectPrimitive.Root, Object.assign({ style: styles.field }, rootProps),
73
+ labelPlacement === "above" && React.createElement(FieldLabel, null),
74
+ children)),
75
+ renderBelowField(hasError ? error : undefined, description)));
76
+ }
77
+ function renderBelowField(error, description) {
78
+ if (error != null) {
79
+ return typeof error === "string" ? (React.createElement(HelperText, { message: error, status: "critical" })) : (error);
80
+ }
81
+ if (description != null) {
82
+ return typeof description === "string" ? (React.createElement(HelperText, { message: description, status: "neutral" })) : (description);
83
+ }
84
+ return null;
85
+ }
@@ -0,0 +1,9 @@
1
+ import { buildThemedStyles } from "../AtlantisThemeContext";
2
+ export const useStyles = buildThemedStyles(tokens => ({
3
+ container: {
4
+ gap: tokens["space-smaller"],
5
+ },
6
+ field: {
7
+ gap: tokens["space-small"],
8
+ },
9
+ }));
@@ -0,0 +1,157 @@
1
+ import React from "react";
2
+ import { render, screen, within } from "@testing-library/react-native";
3
+ import { SelectRoot } from "./SelectRoot";
4
+ import { SelectTrigger } from "./components/SelectTrigger";
5
+ import { SelectLabel } from "./components/SelectLabel";
6
+ import { SelectPrimitive } from "../primitives/Select";
7
+ import { Text } from "../Text";
8
+ import { tokens } from "../utils/design";
9
+ function renderRoot(props = {}) {
10
+ return render(React.createElement(SelectRoot, Object.assign({}, props),
11
+ React.createElement(SelectTrigger, null,
12
+ React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
13
+ }
14
+ describe("SelectRoot chrome", () => {
15
+ it("renders a composed label (visible, hidden from assistive tech)", () => {
16
+ render(React.createElement(SelectRoot, null,
17
+ React.createElement(SelectLabel, null, "City"),
18
+ React.createElement(SelectTrigger, null,
19
+ React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
20
+ // The label renders but is hidden from a11y (the trigger carries the name).
21
+ expect(screen.queryByText("City")).toBeNull();
22
+ expect(screen.getByText("City", { includeHiddenElements: true })).toBeDefined();
23
+ });
24
+ it("renders a string description as neutral helper text", () => {
25
+ renderRoot({ description: "Pick your city" });
26
+ expect(screen.getByText("Pick your city")).toBeDefined();
27
+ // Neutral helper text has no alert icon.
28
+ expect(screen.queryByTestId("alert")).toBeNull();
29
+ });
30
+ it("renders an error as critical helper text", () => {
31
+ renderRoot({ error: "This field is required" });
32
+ expect(screen.getByText("This field is required")).toBeDefined();
33
+ expect(screen.getByTestId("alert")).toBeDefined();
34
+ });
35
+ it("gives error priority over description", () => {
36
+ renderRoot({ description: "Pick your city", error: "Required" });
37
+ expect(screen.getByText("Required")).toBeDefined();
38
+ expect(screen.queryByText("Pick your city")).toBeNull();
39
+ });
40
+ it("treats an empty-string error as no error", () => {
41
+ renderRoot({ error: "", description: "Pick your city" });
42
+ // No critical styling, no error helper — the description shows instead.
43
+ expect(screen.getByTestId("ATL-Select-Trigger")).not.toHaveStyle({
44
+ borderColor: tokens["color-critical"],
45
+ });
46
+ expect(screen.queryByTestId("alert")).toBeNull();
47
+ expect(screen.getByText("Pick your city")).toBeDefined();
48
+ });
49
+ it.each([
50
+ ["error", { error: "Required" }],
51
+ ["status critical", { status: "critical" }],
52
+ ["invalid", { invalid: true }],
53
+ ])("applies critical trigger styling from %s", (_label, props) => {
54
+ renderRoot(props);
55
+ expect(screen.getByTestId("ATL-Select-Trigger")).toHaveStyle({
56
+ borderColor: tokens["color-critical"],
57
+ });
58
+ });
59
+ it("makes the trigger read-only (no chevron) via the Root prop", () => {
60
+ renderRoot({ readOnly: true });
61
+ expect(screen.getByTestId("ATL-Select-Trigger")).toHaveStyle({
62
+ backgroundColor: tokens["color-surface--background--subtle"],
63
+ });
64
+ expect(screen.queryByTestId("arrowDown")).toBeNull();
65
+ });
66
+ it.each([false, true])("uses a provided trigger testID verbatim (readOnly=%s)", readOnly => {
67
+ render(React.createElement(SelectRoot, { readOnly: readOnly },
68
+ React.createElement(SelectTrigger, { testID: "city" },
69
+ React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
70
+ expect(screen.getByTestId("city")).toBeDefined();
71
+ expect(screen.queryByTestId("ATL-Select-Trigger")).toBeNull();
72
+ });
73
+ it("greys the value when disabled via the Root prop", () => {
74
+ renderRoot({
75
+ disabled: true,
76
+ defaultValue: { value: "tor", label: "Toronto" },
77
+ });
78
+ expect(screen.getByText("Toronto")).toHaveStyle({
79
+ color: tokens["color-disabled"],
80
+ });
81
+ });
82
+ });
83
+ describe("SelectRoot label placement", () => {
84
+ const hidden = { includeHiddenElements: true };
85
+ function renderComposed(props = {}) {
86
+ return render(React.createElement(SelectRoot, Object.assign({}, props),
87
+ React.createElement(SelectLabel, null, "City"),
88
+ React.createElement(SelectTrigger, null,
89
+ React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
90
+ }
91
+ it("places the label inside the trigger by default (labelPlacement inside)", () => {
92
+ renderComposed();
93
+ const trigger = screen.getByTestId("ATL-Select-Trigger");
94
+ expect(within(trigger).getByText("City", hidden)).toBeDefined();
95
+ });
96
+ it("places the label above the trigger with labelPlacement='above'", () => {
97
+ renderComposed({ labelPlacement: "above" });
98
+ // Rendered exactly once, and outside the trigger.
99
+ expect(screen.getAllByText("City", hidden)).toHaveLength(1);
100
+ const trigger = screen.getByTestId("ATL-Select-Trigger");
101
+ expect(within(trigger).queryByText("City", hidden)).toBeNull();
102
+ });
103
+ it("ignores where Select.Label is written — placement follows the prop", () => {
104
+ render(React.createElement(SelectRoot, { labelPlacement: "inside" },
105
+ React.createElement(SelectTrigger, null,
106
+ React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" })),
107
+ React.createElement(SelectLabel, null, "City")));
108
+ const trigger = screen.getByTestId("ATL-Select-Trigger");
109
+ expect(within(trigger).getByText("City", hidden)).toBeDefined();
110
+ });
111
+ it("auto-publishes a string Select.Label as the trigger's accessible name", () => {
112
+ renderComposed();
113
+ expect(screen.getByTestId("ATL-Select-Trigger").props.accessibilityLabel).toBe("City");
114
+ });
115
+ it("uses an explicit accessibilityLabel for non-string label content", () => {
116
+ render(React.createElement(SelectRoot, { accessibilityLabel: "City" },
117
+ React.createElement(SelectLabel, null,
118
+ React.createElement(Text, null, "City")),
119
+ React.createElement(SelectTrigger, null,
120
+ React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
121
+ expect(screen.getByTestId("ATL-Select-Trigger").props.accessibilityLabel).toBe("City");
122
+ });
123
+ it("warns when more than one Select.Label is composed", () => {
124
+ const warn = jest
125
+ .spyOn(console, "warn")
126
+ .mockImplementation(() => undefined);
127
+ render(React.createElement(SelectRoot, null,
128
+ React.createElement(SelectLabel, null, "City"),
129
+ React.createElement(SelectLabel, null, "Town"),
130
+ React.createElement(SelectTrigger, null,
131
+ React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
132
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("multiple <Select.Label>"));
133
+ warn.mockRestore();
134
+ });
135
+ });
136
+ describe("SelectTrigger ref", () => {
137
+ it("forwards a ref exposing open/close", () => {
138
+ var _a, _b;
139
+ const triggerRef = React.createRef();
140
+ render(React.createElement(SelectRoot, null,
141
+ React.createElement(SelectTrigger, { ref: triggerRef },
142
+ React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
143
+ expect(triggerRef.current).not.toBeNull();
144
+ expect(typeof ((_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.open)).toBe("function");
145
+ expect(typeof ((_b = triggerRef.current) === null || _b === void 0 ? void 0 : _b.close)).toBe("function");
146
+ });
147
+ it("does not attach a ref when readOnly (no interactive trigger)", () => {
148
+ const triggerRef = React.createRef();
149
+ render(React.createElement(SelectRoot, { readOnly: true, defaultValue: { value: "tor", label: "Toronto" } },
150
+ React.createElement(SelectTrigger, { ref: triggerRef },
151
+ React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
152
+ // readOnly mounts a static surface, so the ref stays null and an
153
+ // optional-chained open()/close() is a safe no-op.
154
+ expect(triggerRef.current).toBeNull();
155
+ expect(() => { var _a; return (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.open(); }).not.toThrow();
156
+ });
157
+ });
@@ -0,0 +1,51 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import React from "react";
13
+ import { Platform, StyleSheet, useWindowDimensions } from "react-native";
14
+ import { GestureHandlerRootView, ScrollView, } from "react-native-gesture-handler";
15
+ import { FullWindowOverlay } from "react-native-screens";
16
+ import { SelectPrimitive } from "../../primitives/Select";
17
+ /**
18
+ * iOS: draw the dropdown in a top-level window so it sits above native-stack
19
+ * modals the app-root host can't reach. GestureHandlerRootView restores gestures
20
+ * there so the inner ScrollView scrolls. Passthrough elsewhere.
21
+ */
22
+ function ModalSafeOverlay({ children }) {
23
+ if (Platform.OS !== "ios") {
24
+ return React.createElement(React.Fragment, null, children);
25
+ }
26
+ return (React.createElement(FullWindowOverlay, null,
27
+ React.createElement(GestureHandlerRootView, { style: styles.overlayRoot }, children)));
28
+ }
29
+ // Cap the dropdown height (rn-primitives positions it but never caps it) so long
30
+ // lists scroll; a window fraction adapts across devices.
31
+ const MAX_HEIGHT_RATIO = 0.55;
32
+ /**
33
+ * Folds the primitive Portal / Overlay / Content / Viewport into one dropdown
34
+ * part. Positioning lives in the primitive `Content`; props pass through.
35
+ */
36
+ export function SelectContent(_a) {
37
+ var { children } = _a, props = __rest(_a, ["children"]);
38
+ const { height } = useWindowDimensions();
39
+ const maxHeight = Math.round(height * MAX_HEIGHT_RATIO);
40
+ return (React.createElement(SelectPrimitive.Portal, null,
41
+ React.createElement(ModalSafeOverlay, null,
42
+ React.createElement(SelectPrimitive.Overlay, null,
43
+ React.createElement(SelectPrimitive.Content, Object.assign({}, props),
44
+ React.createElement(SelectPrimitive.Viewport, null,
45
+ React.createElement(ScrollView, { style: { maxHeight }, bounces: false, keyboardShouldPersistTaps: "handled" }, children)))))));
46
+ }
47
+ const styles = StyleSheet.create({
48
+ overlayRoot: {
49
+ flex: 1,
50
+ },
51
+ });
@@ -0,0 +1,51 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import React from "react";
11
+ import { render, screen, userEvent } from "@testing-library/react-native";
12
+ import { SelectContent } from "./SelectContent";
13
+ import { SelectItem } from "./SelectItem";
14
+ import { SelectTrigger } from "./SelectTrigger";
15
+ import { SelectRoot } from "../SelectRoot";
16
+ import { SelectPrimitive } from "../../primitives/Select";
17
+ import { PortalHostWrapper } from "../../utils/test";
18
+ const user = userEvent.setup();
19
+ beforeAll(() => {
20
+ var _a;
21
+ const MockNativeMethods = require("react-native/jest/MockNativeMethods");
22
+ const mockedMethods = (_a = MockNativeMethods.default) !== null && _a !== void 0 ? _a : MockNativeMethods;
23
+ mockedMethods.measure.mockImplementation((callback) => {
24
+ callback(0, 0, 320, 48, 20, 100);
25
+ });
26
+ });
27
+ function renderContent() {
28
+ return render(React.createElement(SelectRoot, null,
29
+ React.createElement(SelectTrigger, null,
30
+ React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" })),
31
+ React.createElement(SelectContent, null,
32
+ React.createElement(SelectItem, { value: "tor", label: "Toronto" }),
33
+ React.createElement(SelectItem, { value: "van", label: "Vancouver" }))), { wrapper: PortalHostWrapper });
34
+ }
35
+ describe("Select.Content", () => {
36
+ it("renders nothing until the trigger opens it", () => __awaiter(void 0, void 0, void 0, function* () {
37
+ renderContent();
38
+ expect(screen.queryByText("Toronto")).toBeNull();
39
+ yield user.press(screen.getByTestId("ATL-Select-Trigger"));
40
+ expect(screen.getByText("Toronto")).toBeDefined();
41
+ }));
42
+ it("folds the portal, overlay, and viewport into one part", () => __awaiter(void 0, void 0, void 0, function* () {
43
+ renderContent();
44
+ yield user.press(screen.getByTestId("ATL-Select-Trigger"));
45
+ // The single Select.Content renders the tap-outside overlay plus the items
46
+ // (portaled + scrolled via the folded Viewport).
47
+ expect(screen.getByTestId("ATL-Select-Overlay")).toBeDefined();
48
+ expect(screen.getByText("Toronto")).toBeDefined();
49
+ expect(screen.getByText("Vancouver")).toBeDefined();
50
+ }));
51
+ });
@@ -0,0 +1,10 @@
1
+ import { createContext, useContext } from "react";
2
+ const noop = () => undefined;
3
+ export const SelectFieldContext = createContext({
4
+ labelPlacement: "inside",
5
+ registerLabel: noop,
6
+ unregisterLabel: noop,
7
+ });
8
+ export function useSelectFieldState() {
9
+ return useContext(SelectFieldContext);
10
+ }
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+ import { useStyles } from "./SelectItem.style";
4
+ import { SelectPrimitive } from "../../primitives/Select";
5
+ import { Icon } from "../../Icon";
6
+ /**
7
+ * An option row. Auto-composes the label + a default checkmark from its
8
+ * `value` / `label` / `disabled` props, and places the `prefix` / `suffix` slot
9
+ * content around the label (prefix → label → suffix → marker). `indicator`
10
+ * overrides the default checkmark; `disabled` greys the label and blocks
11
+ * selection.
12
+ */
13
+ export function SelectItem({ value, label, disabled, closeOnPress, prefix, suffix, indicator, }) {
14
+ return (React.createElement(SelectPrimitive.Item, { value: value, label: label, disabled: disabled, closeOnPress: closeOnPress },
15
+ prefix,
16
+ React.createElement(SelectPrimitive.ItemText, { disabled: disabled }),
17
+ suffix,
18
+ React.createElement(SelectPrimitive.ItemIndicator, null, indicator !== null && indicator !== void 0 ? indicator : React.createElement(Icon, { name: "checkmark" }))));
19
+ }
20
+ /** Styled leading slot — pass to `Select.Item`'s `prefix` prop. */
21
+ export function SelectItemPrefix({ children, }) {
22
+ const styles = useStyles();
23
+ return React.createElement(View, { style: styles.slot }, children);
24
+ }
25
+ /** Styled trailing slot — pass to `Select.Item`'s `suffix` prop. */
26
+ export function SelectItemSuffix({ children, }) {
27
+ const styles = useStyles();
28
+ return React.createElement(View, { style: styles.slot }, children);
29
+ }
@@ -0,0 +1,6 @@
1
+ import { buildThemedStyles } from "../../AtlantisThemeContext";
2
+ export const useStyles = buildThemedStyles(() => ({
3
+ slot: {
4
+ justifyContent: "center",
5
+ },
6
+ }));
@@ -0,0 +1,98 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import React from "react";
11
+ import { render, screen, userEvent } from "@testing-library/react-native";
12
+ import { SelectItem, SelectItemPrefix, SelectItemSuffix } from "./SelectItem";
13
+ import { SelectTrigger } from "./SelectTrigger";
14
+ import { SelectRoot } from "../SelectRoot";
15
+ import { SelectPrimitive } from "../../primitives/Select";
16
+ import { Icon } from "../../Icon";
17
+ import { Text } from "../../Text";
18
+ import { PortalHostWrapper } from "../../utils/test";
19
+ import { tokens } from "../../utils/design";
20
+ const user = userEvent.setup();
21
+ beforeAll(() => {
22
+ var _a;
23
+ const MockNativeMethods = require("react-native/jest/MockNativeMethods");
24
+ const mockedMethods = (_a = MockNativeMethods.default) !== null && _a !== void 0 ? _a : MockNativeMethods;
25
+ mockedMethods.measure.mockImplementation((callback) => {
26
+ callback(0, 0, 320, 48, 20, 100);
27
+ });
28
+ });
29
+ function renderItems(items, defaultValue, onValueChange) {
30
+ return render(React.createElement(SelectRoot, { defaultValue: defaultValue, onValueChange: onValueChange },
31
+ React.createElement(SelectTrigger, null,
32
+ React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" })),
33
+ React.createElement(SelectPrimitive.Portal, null,
34
+ React.createElement(SelectPrimitive.Overlay, null),
35
+ React.createElement(SelectPrimitive.Content, null,
36
+ React.createElement(SelectPrimitive.Viewport, null, items)))), { wrapper: PortalHostWrapper });
37
+ }
38
+ function openSelect() {
39
+ return __awaiter(this, void 0, void 0, function* () {
40
+ yield user.press(screen.getByTestId("ATL-Select-Trigger"));
41
+ });
42
+ }
43
+ describe("Select.Item", () => {
44
+ it("renders the option label from the label prop", () => __awaiter(void 0, void 0, void 0, function* () {
45
+ renderItems(React.createElement(SelectItem, { value: "tor", label: "Toronto" }));
46
+ yield openSelect();
47
+ expect(screen.getByText("Toronto")).toBeDefined();
48
+ }));
49
+ it("shows the default checkmark on the selected row only", () => __awaiter(void 0, void 0, void 0, function* () {
50
+ renderItems(React.createElement(React.Fragment, null,
51
+ React.createElement(SelectItem, { value: "tor", label: "Toronto" }),
52
+ React.createElement(SelectItem, { value: "van", label: "Vancouver" })), { value: "tor", label: "Toronto" });
53
+ yield openSelect();
54
+ // The indicator self-hides on unselected rows, so only the selected row's
55
+ // checkmark is in the tree.
56
+ expect(screen.getAllByTestId("checkmark")).toHaveLength(1);
57
+ }));
58
+ it("renders prefix and suffix slots around the label", () => __awaiter(void 0, void 0, void 0, function* () {
59
+ renderItems(React.createElement(SelectItem, { value: "tor", label: "Toronto", prefix: React.createElement(SelectItemPrefix, null,
60
+ React.createElement(Icon, { name: "home" })), suffix: React.createElement(SelectItemSuffix, null,
61
+ React.createElement(Text, null, "ON")) }));
62
+ yield openSelect();
63
+ expect(screen.getByTestId("home")).toBeDefined();
64
+ expect(screen.getByText("ON")).toBeDefined();
65
+ expect(screen.getByText("Toronto")).toBeDefined();
66
+ }));
67
+ it("shows the suffix alongside the checkmark on the selected row", () => __awaiter(void 0, void 0, void 0, function* () {
68
+ renderItems(React.createElement(SelectItem, { value: "tor", label: "Toronto", suffix: React.createElement(SelectItemSuffix, null,
69
+ React.createElement(Text, null, "ON")) }), { value: "tor", label: "Toronto" });
70
+ yield openSelect();
71
+ // The suffix coexists with the selected-state marker (it doesn't replace it).
72
+ expect(screen.getByText("ON")).toBeDefined();
73
+ expect(screen.getByTestId("checkmark")).toBeDefined();
74
+ }));
75
+ it("overrides the default checkmark via the indicator prop", () => __awaiter(void 0, void 0, void 0, function* () {
76
+ renderItems(React.createElement(SelectItem, { value: "tor", label: "Toronto", indicator: React.createElement(Icon, { name: "starFill" }) }), { value: "tor", label: "Toronto" });
77
+ yield openSelect();
78
+ expect(screen.getByTestId("starFill")).toBeDefined();
79
+ expect(screen.queryByTestId("checkmark")).toBeNull();
80
+ }));
81
+ it("selects an option on press (selection flows through the sugar)", () => __awaiter(void 0, void 0, void 0, function* () {
82
+ const onValueChange = jest.fn();
83
+ renderItems(React.createElement(SelectItem, { value: "tor", label: "Toronto" }), undefined, onValueChange);
84
+ yield openSelect();
85
+ yield user.press(screen.getByText("Toronto"));
86
+ expect(onValueChange).toHaveBeenCalledWith({
87
+ value: "tor",
88
+ label: "Toronto",
89
+ });
90
+ }));
91
+ it("greys a disabled option's label", () => __awaiter(void 0, void 0, void 0, function* () {
92
+ renderItems(React.createElement(SelectItem, { value: "van", label: "Vancouver", disabled: true }));
93
+ yield openSelect();
94
+ expect(screen.getByText("Vancouver")).toHaveStyle({
95
+ color: tokens["color-disabled"],
96
+ });
97
+ }));
98
+ });
@@ -0,0 +1,29 @@
1
+ import React, { useLayoutEffect } from "react";
2
+ import { useSelectFieldState } from "./SelectFieldContext";
3
+ import { Text } from "../../Text";
4
+ /**
5
+ * The field label. Rather than render in place, it **registers** its `children`
6
+ * (and, when a string, the trigger's accessible name) into `Select.Root`, which
7
+ * renders the content in the slot `labelPlacement` selects. Renders nothing where
8
+ * it is composed. See `Select.guide.md`.
9
+ */
10
+ export function SelectLabel({ children }) {
11
+ const { registerLabel, unregisterLabel } = useSelectFieldState();
12
+ useLayoutEffect(() => {
13
+ registerLabel(children, typeof children === "string" ? children : undefined);
14
+ return unregisterLabel;
15
+ }, [children, registerLabel, unregisterLabel]);
16
+ return null;
17
+ }
18
+ /**
19
+ * Internal renderer for the registered label content, shared by the above slot
20
+ * (`Select.Root`) and the inside slot (`Select.Trigger`). Sizes itself from
21
+ * `labelPlacement` (smaller when `inside`). Hidden from assistive tech — the
22
+ * trigger carries the accessible name.
23
+ */
24
+ export function FieldLabel() {
25
+ const { labelContent, labelPlacement } = useSelectFieldState();
26
+ if (labelContent == null)
27
+ return null;
28
+ return typeof labelContent === "string" ? (React.createElement(Text, { variation: "subdued", level: labelPlacement === "inside" ? "textSupporting" : undefined, maxLines: "single", hideFromScreenReader: true }, labelContent)) : (React.createElement(React.Fragment, null, labelContent));
29
+ }