@jobber/components-native 0.105.4 → 0.107.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/ActivityIndicator/ActivityIndicator.js +1 -1
- 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/ActivityIndicator/ActivityIndicator.stories.tsx +46 -1
- package/src/ActivityIndicator/ActivityIndicator.tsx +1 -1
- package/src/ActivityIndicator/guide.md +6 -4
- 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,141 @@
|
|
|
1
|
+
import { buildThemedStyles } from "../../AtlantisThemeContext";
|
|
2
|
+
import { getTypographyStyles } from "../../Typography";
|
|
3
|
+
import { tokens as staticTokens } from "../../utils/design";
|
|
4
|
+
|
|
5
|
+
const baseLineHeight = staticTokens["typography--lineHeight-base"];
|
|
6
|
+
|
|
7
|
+
export const useStyles = buildThemedStyles(tokens => {
|
|
8
|
+
const typographyStyles = getTypographyStyles(tokens);
|
|
9
|
+
|
|
10
|
+
// The dark theme merges a web CSS-string `shadow-base` over the native object
|
|
11
|
+
// shadow, leaving stray indexed keys on the value. Pick only the React Native
|
|
12
|
+
// shadow properties so those keys never reach the style (spreading them
|
|
13
|
+
// breaks style application through the dropdown's portal on react-native-web).
|
|
14
|
+
const shadowBase = tokens["shadow-base"];
|
|
15
|
+
const contentShadow = {
|
|
16
|
+
shadowColor: shadowBase.shadowColor,
|
|
17
|
+
shadowOffset: shadowBase.shadowOffset,
|
|
18
|
+
shadowOpacity: shadowBase.shadowOpacity,
|
|
19
|
+
shadowRadius: shadowBase.shadowRadius,
|
|
20
|
+
elevation: shadowBase.elevation,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
trigger: {
|
|
25
|
+
flexDirection: "row",
|
|
26
|
+
alignItems: "center",
|
|
27
|
+
gap: tokens["space-small"],
|
|
28
|
+
minHeight: tokens["space-largest"] + tokens["space-small"],
|
|
29
|
+
paddingHorizontal: tokens["space-slim"],
|
|
30
|
+
backgroundColor: tokens["color-surface"],
|
|
31
|
+
borderWidth: tokens["border-base"],
|
|
32
|
+
borderColor: tokens["color-border--interactive"],
|
|
33
|
+
borderRadius: tokens["radius-base"],
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
triggerOpen: {
|
|
37
|
+
borderWidth: tokens["border-thick"],
|
|
38
|
+
borderColor: tokens["color-interactive--subtle"],
|
|
39
|
+
// Keep the content from shifting when the border thickens.
|
|
40
|
+
paddingHorizontal:
|
|
41
|
+
tokens["space-slim"] - (tokens["border-thick"] - tokens["border-base"]),
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
triggerPressed: {
|
|
45
|
+
borderWidth: tokens["border-thick"],
|
|
46
|
+
borderColor: tokens["color-border--interactive"],
|
|
47
|
+
paddingHorizontal:
|
|
48
|
+
tokens["space-slim"] - (tokens["border-thick"] - tokens["border-base"]),
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Critical sets only the colour; width follows the open/pressed state.
|
|
52
|
+
triggerInvalid: {
|
|
53
|
+
borderColor: tokens["color-critical"],
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
triggerDisabled: {
|
|
57
|
+
backgroundColor: tokens["color-disabled--secondary"],
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
value: {
|
|
61
|
+
flex: 1,
|
|
62
|
+
color: tokens["color-text"],
|
|
63
|
+
fontFamily: typographyStyles.baseRegularRegular.fontFamily,
|
|
64
|
+
fontSize: typographyStyles.defaultSize.fontSize,
|
|
65
|
+
lineHeight: baseLineHeight,
|
|
66
|
+
letterSpacing: typographyStyles.baseLetterSpacing.letterSpacing,
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
valuePlaceholder: {
|
|
70
|
+
color: tokens["color-text--secondary"],
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
valueDisabled: {
|
|
74
|
+
color: tokens["color-disabled"],
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
overlay: {
|
|
78
|
+
position: "absolute",
|
|
79
|
+
top: 0,
|
|
80
|
+
right: 0,
|
|
81
|
+
bottom: 0,
|
|
82
|
+
left: 0,
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
content: {
|
|
86
|
+
backgroundColor: tokens["color-surface"],
|
|
87
|
+
borderWidth: tokens["border-base"],
|
|
88
|
+
borderColor: tokens["color-border"],
|
|
89
|
+
borderRadius: tokens["radius-base"],
|
|
90
|
+
padding: tokens["space-small"],
|
|
91
|
+
...contentShadow,
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
item: {
|
|
95
|
+
flexDirection: "row",
|
|
96
|
+
alignItems: "center",
|
|
97
|
+
gap: tokens["space-small"],
|
|
98
|
+
minHeight: tokens["space-largest"] + tokens["space-small"],
|
|
99
|
+
padding: tokens["space-small"],
|
|
100
|
+
borderRadius: tokens["radius-small"],
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
itemPressed: {
|
|
104
|
+
backgroundColor: tokens["color-surface--hover"],
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
itemText: {
|
|
108
|
+
flex: 1,
|
|
109
|
+
color: tokens["color-text"],
|
|
110
|
+
fontFamily: typographyStyles.baseRegularSemiBold.fontFamily,
|
|
111
|
+
fontSize: typographyStyles.defaultSize.fontSize,
|
|
112
|
+
lineHeight: baseLineHeight,
|
|
113
|
+
letterSpacing: typographyStyles.baseLetterSpacing.letterSpacing,
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
itemTextDisabled: {
|
|
117
|
+
color: tokens["color-disabled"],
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
itemIndicator: {
|
|
121
|
+
justifyContent: "center",
|
|
122
|
+
alignItems: "center",
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
groupLabel: {
|
|
126
|
+
color: tokens["color-text--secondary"],
|
|
127
|
+
fontFamily: typographyStyles.baseRegularRegular.fontFamily,
|
|
128
|
+
fontSize: tokens["typography--fontSize-small"],
|
|
129
|
+
lineHeight: tokens["typography--lineHeight-tight"],
|
|
130
|
+
paddingTop: tokens["space-small"],
|
|
131
|
+
paddingBottom: tokens["space-smaller"],
|
|
132
|
+
paddingHorizontal: tokens["space-small"],
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
separator: {
|
|
136
|
+
height: tokens["border-base"],
|
|
137
|
+
backgroundColor: tokens["color-border"],
|
|
138
|
+
marginVertical: tokens["space-small"],
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
});
|
|
@@ -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
|
+
});
|