@jobber/components-native 0.107.0 → 0.107.1-select-nat-3a2699a.22
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/AtlantisThemeContext/AtlantisThemeContext.md +40 -0
- package/dist/docs/Chip/Chip.md +2 -2
- package/dist/docs/Colors/Colors.md +13 -0
- package/dist/docs/ContentOverlay/ContentOverlay.md +4 -0
- package/dist/docs/Form/Form.md +5 -5
- package/dist/docs/Icon/Icon.md +1 -0
- package/dist/docs/InputTime/InputTime.md +8 -0
- package/dist/docs/ProgressBar/ProgressBar.md +9 -0
- package/dist/docs/empty-states/empty-states.md +29 -0
- package/dist/docs/index.md +0 -1
- package/dist/package.json +3 -3
- package/dist/src/ContentOverlay/ContentOverlay.js +15 -10
- package/dist/src/ContentOverlay/computeContentOverlayBehavior.js +7 -4
- package/dist/src/ContentOverlay/computeContentOverlayBehavior.test.js +31 -0
- package/dist/src/Select/Select.composable.test.js +38 -0
- package/dist/src/Select/SelectRoot.js +85 -0
- package/dist/src/Select/SelectRoot.style.js +9 -0
- package/dist/src/Select/SelectRoot.test.js +150 -0
- package/dist/src/Select/components/SelectComposableTypes.js +1 -0
- package/dist/src/Select/components/SelectContent.js +24 -0
- package/dist/src/Select/components/SelectContent.test.js +51 -0
- package/dist/src/Select/components/SelectFieldContext.js +10 -0
- package/dist/src/Select/components/SelectItem.js +29 -0
- package/dist/src/Select/components/SelectItem.style.js +6 -0
- package/dist/src/Select/components/SelectItem.test.js +98 -0
- package/dist/src/Select/components/SelectLabel.js +29 -0
- package/dist/src/Select/components/SelectTrigger.js +39 -0
- package/dist/src/Select/components/SelectTrigger.style.js +19 -0
- package/dist/src/Select/index.js +27 -1
- package/dist/src/utils/meta/meta.json +11 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +1 -1
- package/dist/types/src/ContentOverlay/computeContentOverlayBehavior.d.ts +1 -0
- package/dist/types/src/ContentOverlay/types.d.ts +23 -0
- package/dist/types/src/Select/Select.composable.test.d.ts +1 -0
- package/dist/types/src/Select/SelectRoot.d.ts +12 -0
- package/dist/types/src/Select/SelectRoot.style.d.ts +8 -0
- package/dist/types/src/Select/SelectRoot.test.d.ts +1 -0
- package/dist/types/src/Select/components/SelectComposableTypes.d.ts +50 -0
- package/dist/types/src/Select/components/SelectContent.d.ts +8 -0
- package/dist/types/src/Select/components/SelectContent.test.d.ts +1 -0
- package/dist/types/src/Select/components/SelectFieldContext.d.ts +27 -0
- package/dist/types/src/Select/components/SelectItem.d.ts +32 -0
- package/dist/types/src/Select/components/SelectItem.style.d.ts +5 -0
- package/dist/types/src/Select/components/SelectItem.test.d.ts +1 -0
- package/dist/types/src/Select/components/SelectLabel.d.ts +22 -0
- package/dist/types/src/Select/components/SelectTrigger.d.ts +20 -0
- package/dist/types/src/Select/components/SelectTrigger.style.d.ts +17 -0
- package/dist/types/src/Select/index.d.ts +28 -2
- package/package.json +3 -3
- package/src/ContentOverlay/ContentOverlay.tsx +57 -33
- package/src/ContentOverlay/computeContentOverlayBehavior.test.ts +40 -0
- package/src/ContentOverlay/computeContentOverlayBehavior.ts +8 -4
- package/src/ContentOverlay/types.ts +28 -0
- package/src/Select/Select.composable.stories.tsx +93 -0
- package/src/Select/Select.composable.test.tsx +56 -0
- package/src/Select/Select.guide.md +155 -0
- package/src/Select/SelectRoot.style.ts +10 -0
- package/src/Select/SelectRoot.test.tsx +229 -0
- package/src/Select/SelectRoot.tsx +120 -0
- package/src/Select/components/SelectComposableTypes.ts +57 -0
- package/src/Select/components/SelectContent.test.tsx +67 -0
- package/src/Select/components/SelectContent.tsx +24 -0
- package/src/Select/components/SelectFieldContext.tsx +46 -0
- package/src/Select/components/SelectItem.style.ts +7 -0
- package/src/Select/components/SelectItem.test.tsx +166 -0
- package/src/Select/components/SelectItem.tsx +78 -0
- package/src/Select/components/SelectLabel.tsx +57 -0
- package/src/Select/components/SelectTrigger.style.ts +21 -0
- package/src/Select/components/SelectTrigger.tsx +94 -0
- package/src/Select/docs/SelectComposableBasic.tsx +18 -0
- package/src/Select/docs/SelectComposableControlledWithRef.tsx +28 -0
- package/src/Select/docs/SelectComposableCustomMarker.tsx +25 -0
- package/src/Select/docs/SelectComposableDescription.tsx +18 -0
- package/src/Select/docs/SelectComposableDisabled.tsx +18 -0
- package/src/Select/docs/SelectComposableDisabledOptions.tsx +20 -0
- package/src/Select/docs/SelectComposableGroupedOptions.tsx +29 -0
- package/src/Select/docs/SelectComposableItemPrefixSuffix.tsx +34 -0
- package/src/Select/docs/SelectComposableLabelAbove.tsx +18 -0
- package/src/Select/docs/SelectComposableLabelInside.tsx +18 -0
- package/src/Select/docs/SelectComposableProvinces.tsx +36 -0
- package/src/Select/docs/SelectComposableReadOnly.tsx +18 -0
- package/src/Select/docs/SelectComposableValidationFlow.tsx +27 -0
- package/src/Select/docs/index.ts +14 -0
- package/src/Select/index.ts +41 -2
- package/src/utils/meta/meta.json +11 -0
- package/dist/docs/Select/Select.md +0 -219
|
@@ -0,0 +1,150 @@
|
|
|
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("greys the value when disabled via the Root prop", () => {
|
|
67
|
+
renderRoot({
|
|
68
|
+
disabled: true,
|
|
69
|
+
defaultValue: { value: "tor", label: "Toronto" },
|
|
70
|
+
});
|
|
71
|
+
expect(screen.getByText("Toronto")).toHaveStyle({
|
|
72
|
+
color: tokens["color-disabled"],
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe("SelectRoot label placement", () => {
|
|
77
|
+
const hidden = { includeHiddenElements: true };
|
|
78
|
+
function renderComposed(props = {}) {
|
|
79
|
+
return render(React.createElement(SelectRoot, Object.assign({}, props),
|
|
80
|
+
React.createElement(SelectLabel, null, "City"),
|
|
81
|
+
React.createElement(SelectTrigger, null,
|
|
82
|
+
React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
|
|
83
|
+
}
|
|
84
|
+
it("places the label inside the trigger by default (labelPlacement inside)", () => {
|
|
85
|
+
renderComposed();
|
|
86
|
+
const trigger = screen.getByTestId("ATL-Select-Trigger");
|
|
87
|
+
expect(within(trigger).getByText("City", hidden)).toBeDefined();
|
|
88
|
+
});
|
|
89
|
+
it("places the label above the trigger with labelPlacement='above'", () => {
|
|
90
|
+
renderComposed({ labelPlacement: "above" });
|
|
91
|
+
// Rendered exactly once, and outside the trigger.
|
|
92
|
+
expect(screen.getAllByText("City", hidden)).toHaveLength(1);
|
|
93
|
+
const trigger = screen.getByTestId("ATL-Select-Trigger");
|
|
94
|
+
expect(within(trigger).queryByText("City", hidden)).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
it("ignores where Select.Label is written — placement follows the prop", () => {
|
|
97
|
+
render(React.createElement(SelectRoot, { labelPlacement: "inside" },
|
|
98
|
+
React.createElement(SelectTrigger, null,
|
|
99
|
+
React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" })),
|
|
100
|
+
React.createElement(SelectLabel, null, "City")));
|
|
101
|
+
const trigger = screen.getByTestId("ATL-Select-Trigger");
|
|
102
|
+
expect(within(trigger).getByText("City", hidden)).toBeDefined();
|
|
103
|
+
});
|
|
104
|
+
it("auto-publishes a string Select.Label as the trigger's accessible name", () => {
|
|
105
|
+
renderComposed();
|
|
106
|
+
expect(screen.getByTestId("ATL-Select-Trigger").props.accessibilityLabel).toBe("City");
|
|
107
|
+
});
|
|
108
|
+
it("uses an explicit accessibilityLabel for non-string label content", () => {
|
|
109
|
+
render(React.createElement(SelectRoot, { accessibilityLabel: "City" },
|
|
110
|
+
React.createElement(SelectLabel, null,
|
|
111
|
+
React.createElement(Text, null, "City")),
|
|
112
|
+
React.createElement(SelectTrigger, null,
|
|
113
|
+
React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
|
|
114
|
+
expect(screen.getByTestId("ATL-Select-Trigger").props.accessibilityLabel).toBe("City");
|
|
115
|
+
});
|
|
116
|
+
it("warns when more than one Select.Label is composed", () => {
|
|
117
|
+
const warn = jest
|
|
118
|
+
.spyOn(console, "warn")
|
|
119
|
+
.mockImplementation(() => undefined);
|
|
120
|
+
render(React.createElement(SelectRoot, null,
|
|
121
|
+
React.createElement(SelectLabel, null, "City"),
|
|
122
|
+
React.createElement(SelectLabel, null, "Town"),
|
|
123
|
+
React.createElement(SelectTrigger, null,
|
|
124
|
+
React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
|
|
125
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("multiple <Select.Label>"));
|
|
126
|
+
warn.mockRestore();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe("SelectTrigger ref", () => {
|
|
130
|
+
it("forwards a ref exposing open/close", () => {
|
|
131
|
+
var _a, _b;
|
|
132
|
+
const triggerRef = React.createRef();
|
|
133
|
+
render(React.createElement(SelectRoot, null,
|
|
134
|
+
React.createElement(SelectTrigger, { ref: triggerRef },
|
|
135
|
+
React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
|
|
136
|
+
expect(triggerRef.current).not.toBeNull();
|
|
137
|
+
expect(typeof ((_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.open)).toBe("function");
|
|
138
|
+
expect(typeof ((_b = triggerRef.current) === null || _b === void 0 ? void 0 : _b.close)).toBe("function");
|
|
139
|
+
});
|
|
140
|
+
it("does not attach a ref when readOnly (no interactive trigger)", () => {
|
|
141
|
+
const triggerRef = React.createRef();
|
|
142
|
+
render(React.createElement(SelectRoot, { readOnly: true, defaultValue: { value: "tor", label: "Toronto" } },
|
|
143
|
+
React.createElement(SelectTrigger, { ref: triggerRef },
|
|
144
|
+
React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
|
|
145
|
+
// readOnly mounts a static surface, so the ref stays null and an
|
|
146
|
+
// optional-chained open()/close() is a safe no-op.
|
|
147
|
+
expect(triggerRef.current).toBeNull();
|
|
148
|
+
expect(() => { var _a; return (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.open(); }).not.toThrow();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
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 { SelectPrimitive } from "../../primitives/Select";
|
|
14
|
+
/**
|
|
15
|
+
* Folds the primitive `Portal` / `Overlay` / `Content` / `Viewport` into one
|
|
16
|
+
* dropdown part. Positioning lives in the primitive `Content`; props pass through.
|
|
17
|
+
*/
|
|
18
|
+
export function SelectContent(_a) {
|
|
19
|
+
var { children } = _a, props = __rest(_a, ["children"]);
|
|
20
|
+
return (React.createElement(SelectPrimitive.Portal, null,
|
|
21
|
+
React.createElement(SelectPrimitive.Overlay, null),
|
|
22
|
+
React.createElement(SelectPrimitive.Content, Object.assign({}, props),
|
|
23
|
+
React.createElement(SelectPrimitive.Viewport, null, children))));
|
|
24
|
+
}
|
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useRootContext } from "@rn-primitives/select";
|
|
4
|
+
import { useStyles } from "./SelectTrigger.style";
|
|
5
|
+
import { useSelectFieldState } from "./SelectFieldContext";
|
|
6
|
+
import { FieldLabel } from "./SelectLabel";
|
|
7
|
+
import { SelectPrimitive } from "../../primitives/Select";
|
|
8
|
+
import { Icon } from "../../Icon";
|
|
9
|
+
/**
|
|
10
|
+
* The styled field trigger: composes the chevron, the resolved `invalid` styling,
|
|
11
|
+
* the accessible name, the inside label placement, and the static `readOnly`
|
|
12
|
+
* presentation over `SelectPrimitive.Trigger`. State comes from `Select.Root`
|
|
13
|
+
* (field context) and the primitive root context.
|
|
14
|
+
*/
|
|
15
|
+
export function SelectTrigger({ children, testID, ref, }) {
|
|
16
|
+
const styles = useStyles();
|
|
17
|
+
const { invalid, readOnly, accessibilityLabel, labelPlacement } = useSelectFieldState();
|
|
18
|
+
const { open, value, disabled } = useRootContext();
|
|
19
|
+
const isDisabled = disabled !== null && disabled !== void 0 ? disabled : false;
|
|
20
|
+
// `disabled` wins over `readOnly`.
|
|
21
|
+
const isReadOnly = !isDisabled && (readOnly !== null && readOnly !== void 0 ? readOnly : false);
|
|
22
|
+
const content = (React.createElement(View, { style: styles.content },
|
|
23
|
+
labelPlacement === "inside" && React.createElement(FieldLabel, null),
|
|
24
|
+
children));
|
|
25
|
+
if (isReadOnly) {
|
|
26
|
+
return (React.createElement(View, { testID: computeTriggerTestID(testID), accessible: true, accessibilityLabel: accessibilityLabel, accessibilityValue: (value === null || value === void 0 ? void 0 : value.label) ? { text: value.label } : undefined, style: styles.readOnly }, content));
|
|
27
|
+
}
|
|
28
|
+
return (React.createElement(SelectPrimitive.Trigger, { ref: ref, testID: testID, invalid: invalid, accessibilityLabel: accessibilityLabel },
|
|
29
|
+
content,
|
|
30
|
+
React.createElement(Chevron, { open: open, disabled: isDisabled })));
|
|
31
|
+
}
|
|
32
|
+
/** Trailing chevron: tinted `iconSecondary`, greyed when disabled, rotated when open. */
|
|
33
|
+
function Chevron({ open, disabled, }) {
|
|
34
|
+
return (React.createElement(View, { style: { transform: [{ rotate: open ? "180deg" : "0deg" }] } },
|
|
35
|
+
React.createElement(Icon, { name: "arrowDown", color: disabled ? "disabled" : "iconSecondary" })));
|
|
36
|
+
}
|
|
37
|
+
function computeTriggerTestID(testID) {
|
|
38
|
+
return testID ? `ATL-${testID}-Select-Trigger` : "ATL-Select-Trigger";
|
|
39
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { buildThemedStyles } from "../../AtlantisThemeContext";
|
|
2
|
+
export const useStyles = buildThemedStyles(tokens => ({
|
|
3
|
+
content: {
|
|
4
|
+
flex: 1,
|
|
5
|
+
justifyContent: "center",
|
|
6
|
+
},
|
|
7
|
+
// Static read-only surface — mirrors the trigger shape with a subtle fill, no chevron.
|
|
8
|
+
readOnly: {
|
|
9
|
+
flexDirection: "row",
|
|
10
|
+
alignItems: "center",
|
|
11
|
+
gap: tokens["space-small"],
|
|
12
|
+
minHeight: tokens["space-largest"] + tokens["space-small"],
|
|
13
|
+
paddingHorizontal: tokens["space-slim"],
|
|
14
|
+
backgroundColor: tokens["color-surface--background--subtle"],
|
|
15
|
+
borderWidth: tokens["border-base"],
|
|
16
|
+
borderColor: tokens["color-border"],
|
|
17
|
+
borderRadius: tokens["radius-base"],
|
|
18
|
+
},
|
|
19
|
+
}));
|
package/dist/src/Select/index.js
CHANGED
|
@@ -1,2 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
import { Option, Select as SelectBase } from "./Select";
|
|
2
|
+
import { SelectRoot } from "./SelectRoot";
|
|
3
|
+
import { SelectTrigger } from "./components/SelectTrigger";
|
|
4
|
+
import { SelectLabel } from "./components/SelectLabel";
|
|
5
|
+
import { SelectContent } from "./components/SelectContent";
|
|
6
|
+
import { SelectItem, SelectItemPrefix, SelectItemSuffix, } from "./components/SelectItem";
|
|
7
|
+
import { SelectPrimitive } from "../primitives/Select";
|
|
8
|
+
/**
|
|
9
|
+
* Attaches the composable `Select.*` parts as statics on the existing callable
|
|
10
|
+
* `Select`. Additive — the legacy props-driven `Select` + `Option` are unchanged
|
|
11
|
+
* (props, native picker, `ATL-Select` testID). See `Select.guide.md` for the
|
|
12
|
+
* parts and the composition model.
|
|
13
|
+
*/
|
|
14
|
+
const Select = Object.assign(SelectBase, {
|
|
15
|
+
Root: SelectRoot,
|
|
16
|
+
Trigger: SelectTrigger,
|
|
17
|
+
Value: SelectPrimitive.Value,
|
|
18
|
+
Label: SelectLabel,
|
|
19
|
+
Content: SelectContent,
|
|
20
|
+
Item: SelectItem,
|
|
21
|
+
ItemPrefix: SelectItemPrefix,
|
|
22
|
+
ItemSuffix: SelectItemSuffix,
|
|
23
|
+
Group: SelectPrimitive.Group,
|
|
24
|
+
GroupLabel: SelectPrimitive.Label,
|
|
25
|
+
Separator: SelectPrimitive.Separator,
|
|
26
|
+
});
|
|
27
|
+
export { Select, Option };
|
|
2
28
|
export { SelectPressable } from "./components/SelectPressable";
|
|
@@ -60,6 +60,17 @@
|
|
|
60
60
|
"Option",
|
|
61
61
|
"ProgressBar",
|
|
62
62
|
"Select",
|
|
63
|
+
"Select.Content",
|
|
64
|
+
"Select.Group",
|
|
65
|
+
"Select.GroupLabel",
|
|
66
|
+
"Select.Item",
|
|
67
|
+
"Select.ItemPrefix",
|
|
68
|
+
"Select.ItemSuffix",
|
|
69
|
+
"Select.Label",
|
|
70
|
+
"Select.Root",
|
|
71
|
+
"Select.Separator",
|
|
72
|
+
"Select.Trigger",
|
|
73
|
+
"Select.Value",
|
|
63
74
|
"SelectPressable",
|
|
64
75
|
"StatusLabel",
|
|
65
76
|
"Switch",
|