@jobber/components-native 0.114.13 → 0.114.14
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/package.json +2 -2
- package/dist/src/primitives/Select/SelectPrimitive.js +12 -2
- package/dist/src/primitives/Select/SelectPrimitive.style.js +6 -0
- package/dist/src/primitives/Select/SelectPrimitive.test.js +47 -4
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/primitives/Select/SelectPrimitive.d.ts +1 -1
- package/dist/types/src/primitives/Select/SelectPrimitive.style.d.ts +6 -0
- package/package.json +2 -2
- package/src/Select/Select.guide.md +6 -0
- package/src/primitives/Select/SelectPrimitive.style.ts +8 -0
- package/src/primitives/Select/SelectPrimitive.test.tsx +82 -2
- package/src/primitives/Select/SelectPrimitive.tsx +25 -2
|
@@ -11,7 +11,7 @@ declare function SelectPrimitiveRoot({ ...props }: SelectPrimitiveRootProps): Re
|
|
|
11
11
|
declare function SelectPrimitiveTrigger({ children, style, testID, invalid, ref, ...props }: SelectPrimitiveTriggerProps): React.JSX.Element;
|
|
12
12
|
declare function SelectPrimitiveValue({ style, ...props }: SelectPrimitiveValueProps): React.JSX.Element;
|
|
13
13
|
/** Portals the dropdown into the mounted `AtlantisPortalHost`. */
|
|
14
|
-
declare function SelectPrimitivePortal({ ...props }: SelectPrimitivePortalProps): React.JSX.Element;
|
|
14
|
+
declare function SelectPrimitivePortal({ children, ...props }: SelectPrimitivePortalProps): React.JSX.Element;
|
|
15
15
|
/** Tap-outside dismiss surface behind the dropdown (no scrim). */
|
|
16
16
|
declare function SelectPrimitiveOverlay({ style, testID, ...props }: SelectPrimitiveOverlayProps): React.JSX.Element;
|
|
17
17
|
/**
|
|
@@ -65,6 +65,9 @@ export declare const useStyles: () => {
|
|
|
65
65
|
borderRadius: number;
|
|
66
66
|
padding: number;
|
|
67
67
|
};
|
|
68
|
+
contentDarkElevated: {
|
|
69
|
+
backgroundColor: string;
|
|
70
|
+
};
|
|
68
71
|
item: {
|
|
69
72
|
flexDirection: "row";
|
|
70
73
|
alignItems: "center";
|
|
@@ -76,6 +79,9 @@ export declare const useStyles: () => {
|
|
|
76
79
|
itemPressed: {
|
|
77
80
|
backgroundColor: string;
|
|
78
81
|
};
|
|
82
|
+
itemPressedDarkElevated: {
|
|
83
|
+
backgroundColor: string;
|
|
84
|
+
};
|
|
79
85
|
itemText: {
|
|
80
86
|
flex: number;
|
|
81
87
|
color: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.114.
|
|
3
|
+
"version": "0.114.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native implementation of Atlantis",
|
|
6
6
|
"repository": {
|
|
@@ -123,5 +123,5 @@
|
|
|
123
123
|
"react-native-screens": ">=4.18.0",
|
|
124
124
|
"react-native-svg": ">=12.0.0"
|
|
125
125
|
},
|
|
126
|
-
"gitHead": "
|
|
126
|
+
"gitHead": "3fba9dd8675ceed40979c17792faf2d091ac2ce7"
|
|
127
127
|
}
|
|
@@ -106,6 +106,12 @@ A few decisions shape how the composable parts fit together:
|
|
|
106
106
|
name), so it isn't announced twice — mirroring `InputText` /
|
|
107
107
|
`InputFieldWrapper`.
|
|
108
108
|
|
|
109
|
+
- **The dropdown uses an elevated dark surface.** Native does not have the web
|
|
110
|
+
`data-elevation="elevated"` token remapping layer, so the primitive applies a
|
|
111
|
+
local dark-only elevated surface to the dropdown and its pressed option rows.
|
|
112
|
+
The selected row remains checkmark-only; it does not receive a persistent
|
|
113
|
+
selected background.
|
|
114
|
+
|
|
109
115
|
### Rendering above native-stack modals
|
|
110
116
|
|
|
111
117
|
`Select.Content` renders correctly inside iOS native-stack full-screen modals
|
|
@@ -98,6 +98,10 @@ export const useStyles = buildThemedStyles(tokens => {
|
|
|
98
98
|
...contentShadow,
|
|
99
99
|
},
|
|
100
100
|
|
|
101
|
+
contentDarkElevated: {
|
|
102
|
+
backgroundColor: tokens["color-surface--background--subtle"],
|
|
103
|
+
},
|
|
104
|
+
|
|
101
105
|
item: {
|
|
102
106
|
flexDirection: "row",
|
|
103
107
|
alignItems: "center",
|
|
@@ -111,6 +115,10 @@ export const useStyles = buildThemedStyles(tokens => {
|
|
|
111
115
|
backgroundColor: tokens["color-surface--hover"],
|
|
112
116
|
},
|
|
113
117
|
|
|
118
|
+
itemPressedDarkElevated: {
|
|
119
|
+
backgroundColor: tokens["color-surface--active"],
|
|
120
|
+
},
|
|
121
|
+
|
|
114
122
|
itemText: {
|
|
115
123
|
flex: 1,
|
|
116
124
|
color: tokens["color-text"],
|
|
@@ -5,11 +5,14 @@ import {
|
|
|
5
5
|
userEvent,
|
|
6
6
|
within,
|
|
7
7
|
} from "@testing-library/react-native";
|
|
8
|
+
import { darkTokens } from "@jobber/design";
|
|
8
9
|
import type { TriggerRef } from "@rn-primitives/select";
|
|
10
|
+
import { PortalHost } from "@rn-primitives/portal";
|
|
9
11
|
import { SelectPrimitive } from "./SelectPrimitive";
|
|
10
12
|
import { tokens } from "../../utils/design";
|
|
11
13
|
import { PortalHostWrapper } from "../../utils/test";
|
|
12
14
|
import { Icon } from "../../Icon";
|
|
15
|
+
import { AtlantisThemeContextProvider } from "../../AtlantisThemeContext";
|
|
13
16
|
|
|
14
17
|
const user = userEvent.setup();
|
|
15
18
|
|
|
@@ -159,12 +162,25 @@ function renderSelectWithContent({
|
|
|
159
162
|
onValueChange,
|
|
160
163
|
closeOnPress,
|
|
161
164
|
disabledItem,
|
|
165
|
+
pressedItem,
|
|
166
|
+
theme,
|
|
162
167
|
}: {
|
|
163
168
|
defaultValue?: { value: string; label: string };
|
|
164
169
|
onValueChange?: (option?: { value: string; label: string }) => void;
|
|
165
170
|
closeOnPress?: boolean;
|
|
166
171
|
disabledItem?: string;
|
|
172
|
+
pressedItem?: string;
|
|
173
|
+
theme?: "dark";
|
|
167
174
|
} = {}) {
|
|
175
|
+
const wrapper =
|
|
176
|
+
theme === "dark"
|
|
177
|
+
? ({ children }: { readonly children: React.ReactNode }) => (
|
|
178
|
+
<AtlantisThemeContextProvider dangerouslyOverrideTheme="dark">
|
|
179
|
+
<PortalHostWrapper>{children}</PortalHostWrapper>
|
|
180
|
+
</AtlantisThemeContextProvider>
|
|
181
|
+
)
|
|
182
|
+
: PortalHostWrapper;
|
|
183
|
+
|
|
168
184
|
return render(
|
|
169
185
|
<SelectPrimitive.Root
|
|
170
186
|
defaultValue={defaultValue}
|
|
@@ -175,7 +191,7 @@ function renderSelectWithContent({
|
|
|
175
191
|
</SelectPrimitive.Trigger>
|
|
176
192
|
<SelectPrimitive.Portal>
|
|
177
193
|
<SelectPrimitive.Overlay />
|
|
178
|
-
<SelectPrimitive.Content>
|
|
194
|
+
<SelectPrimitive.Content testID="ATL-Select-Content">
|
|
179
195
|
<SelectPrimitive.Viewport>
|
|
180
196
|
<SelectPrimitive.Group>
|
|
181
197
|
<SelectPrimitive.Label>Cities</SelectPrimitive.Label>
|
|
@@ -186,6 +202,8 @@ function renderSelectWithContent({
|
|
|
186
202
|
label={city.label}
|
|
187
203
|
closeOnPress={closeOnPress}
|
|
188
204
|
disabled={disabledItem === city.value}
|
|
205
|
+
testOnly_pressed={pressedItem === city.value}
|
|
206
|
+
testID={`ATL-Select-Item-${city.value}`}
|
|
189
207
|
>
|
|
190
208
|
<SelectPrimitive.ItemText
|
|
191
209
|
disabled={disabledItem === city.value}
|
|
@@ -201,7 +219,7 @@ function renderSelectWithContent({
|
|
|
201
219
|
</SelectPrimitive.Content>
|
|
202
220
|
</SelectPrimitive.Portal>
|
|
203
221
|
</SelectPrimitive.Root>,
|
|
204
|
-
{ wrapper
|
|
222
|
+
{ wrapper },
|
|
205
223
|
);
|
|
206
224
|
}
|
|
207
225
|
|
|
@@ -231,9 +249,27 @@ describe("SelectPrimitive.Content", () => {
|
|
|
231
249
|
expect(screen.queryByText("Toronto")).toBeNull();
|
|
232
250
|
expect(onValueChange).not.toHaveBeenCalled();
|
|
233
251
|
});
|
|
252
|
+
|
|
253
|
+
it("uses the elevated dark surface style for content", async () => {
|
|
254
|
+
renderSelectWithContent({ theme: "dark" });
|
|
255
|
+
await openSelect();
|
|
256
|
+
|
|
257
|
+
expect(screen.getByTestId("ATL-Select-Content")).toHaveStyle({
|
|
258
|
+
backgroundColor: darkTokens["color-surface--background--subtle"],
|
|
259
|
+
});
|
|
260
|
+
});
|
|
234
261
|
});
|
|
235
262
|
|
|
236
263
|
describe("SelectPrimitive.Item", () => {
|
|
264
|
+
it("uses the elevated dark surface active token when pressed", async () => {
|
|
265
|
+
renderSelectWithContent({ theme: "dark", pressedItem: "tor" });
|
|
266
|
+
await openSelect();
|
|
267
|
+
|
|
268
|
+
expect(screen.getByTestId("ATL-Select-Item-tor")).toHaveStyle({
|
|
269
|
+
backgroundColor: darkTokens["color-surface--active"],
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
237
273
|
it("selects the option, emits { value, label }, and closes", async () => {
|
|
238
274
|
const onValueChange = jest.fn();
|
|
239
275
|
renderSelectWithContent({ onValueChange });
|
|
@@ -341,6 +377,50 @@ describe("SelectPrimitive.Item", () => {
|
|
|
341
377
|
});
|
|
342
378
|
});
|
|
343
379
|
|
|
380
|
+
describe("SelectPrimitive.Portal", () => {
|
|
381
|
+
it("preserves the selected theme when the portal host is outside the provider", async () => {
|
|
382
|
+
render(
|
|
383
|
+
<SelectPrimitive.Root>
|
|
384
|
+
<SelectPrimitive.Trigger>
|
|
385
|
+
<SelectPrimitive.Value placeholder="Select an option" />
|
|
386
|
+
</SelectPrimitive.Trigger>
|
|
387
|
+
<SelectPrimitive.Portal>
|
|
388
|
+
<SelectPrimitive.Content testID="ATL-Select-Content">
|
|
389
|
+
<SelectPrimitive.Viewport>
|
|
390
|
+
<SelectPrimitive.Item
|
|
391
|
+
value="tor"
|
|
392
|
+
label="Toronto"
|
|
393
|
+
testID="ATL-Select-Item-tor"
|
|
394
|
+
>
|
|
395
|
+
<SelectPrimitive.ItemText />
|
|
396
|
+
<SelectPrimitive.ItemIndicator>
|
|
397
|
+
<Icon name="checkmark" />
|
|
398
|
+
</SelectPrimitive.ItemIndicator>
|
|
399
|
+
</SelectPrimitive.Item>
|
|
400
|
+
</SelectPrimitive.Viewport>
|
|
401
|
+
</SelectPrimitive.Content>
|
|
402
|
+
</SelectPrimitive.Portal>
|
|
403
|
+
</SelectPrimitive.Root>,
|
|
404
|
+
{
|
|
405
|
+
wrapper: ({ children }: { readonly children: React.ReactNode }) => (
|
|
406
|
+
<>
|
|
407
|
+
<AtlantisThemeContextProvider dangerouslyOverrideTheme="dark">
|
|
408
|
+
{children}
|
|
409
|
+
</AtlantisThemeContextProvider>
|
|
410
|
+
<PortalHost />
|
|
411
|
+
</>
|
|
412
|
+
),
|
|
413
|
+
},
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
await openSelect();
|
|
417
|
+
|
|
418
|
+
expect(screen.getByTestId("ATL-Select-Content")).toHaveStyle({
|
|
419
|
+
backgroundColor: darkTokens["color-surface--background--subtle"],
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
344
424
|
describe("SelectPrimitive.Value", () => {
|
|
345
425
|
it("shows the placeholder with subdued styling when nothing is selected", () => {
|
|
346
426
|
renderSelect();
|
|
@@ -19,10 +19,18 @@ import type {
|
|
|
19
19
|
SelectPrimitiveViewportProps,
|
|
20
20
|
} from "./types";
|
|
21
21
|
import { tokens as staticTokens } from "../../utils/design";
|
|
22
|
+
import {
|
|
23
|
+
AtlantisThemeContextProvider,
|
|
24
|
+
useAtlantisTheme,
|
|
25
|
+
} from "../../AtlantisThemeContext";
|
|
22
26
|
|
|
23
27
|
/** Default trigger testID. Consumer-supplied `testID`s are used verbatim. */
|
|
24
28
|
export const SELECT_TRIGGER_DEFAULT_TEST_ID = "ATL-Select-Trigger";
|
|
25
29
|
|
|
30
|
+
function useIsDarkTheme() {
|
|
31
|
+
return useAtlantisTheme().effectiveTheme === "dark";
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
function SelectPrimitiveRoot({ ...props }: SelectPrimitiveRootProps) {
|
|
27
35
|
return <Primitive.Root {...props} />;
|
|
28
36
|
}
|
|
@@ -91,8 +99,19 @@ function SelectPrimitiveValue({ style, ...props }: SelectPrimitiveValueProps) {
|
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
/** Portals the dropdown into the mounted `AtlantisPortalHost`. */
|
|
94
|
-
function SelectPrimitivePortal({
|
|
95
|
-
|
|
102
|
+
function SelectPrimitivePortal({
|
|
103
|
+
children,
|
|
104
|
+
...props
|
|
105
|
+
}: SelectPrimitivePortalProps) {
|
|
106
|
+
const { effectiveTheme } = useAtlantisTheme();
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Primitive.Portal {...props}>
|
|
110
|
+
<AtlantisThemeContextProvider dangerouslyOverrideTheme={effectiveTheme}>
|
|
111
|
+
{children}
|
|
112
|
+
</AtlantisThemeContextProvider>
|
|
113
|
+
</Primitive.Portal>
|
|
114
|
+
);
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
/** Tap-outside dismiss surface behind the dropdown (no scrim). */
|
|
@@ -126,6 +145,7 @@ function SelectPrimitiveContent({
|
|
|
126
145
|
...props
|
|
127
146
|
}: SelectPrimitiveContentProps) {
|
|
128
147
|
const styles = useStyles();
|
|
148
|
+
const isDarkTheme = useIsDarkTheme();
|
|
129
149
|
// Read the context directly, not `useSafeAreaInsets` (which throws without a
|
|
130
150
|
// provider): `Content` renders at the portal host, which may sit outside one.
|
|
131
151
|
const safeAreaInsets = React.useContext(SafeAreaInsetsContext) ?? undefined;
|
|
@@ -138,6 +158,7 @@ function SelectPrimitiveContent({
|
|
|
138
158
|
insets={insets ?? safeAreaInsets}
|
|
139
159
|
style={StyleSheet.flatten([
|
|
140
160
|
styles.content,
|
|
161
|
+
isDarkTheme && styles.contentDarkElevated,
|
|
141
162
|
triggerPosition ? { width: triggerPosition.width } : undefined,
|
|
142
163
|
style,
|
|
143
164
|
])}
|
|
@@ -164,6 +185,7 @@ function SelectPrimitiveItem({
|
|
|
164
185
|
...props
|
|
165
186
|
}: SelectPrimitiveItemProps) {
|
|
166
187
|
const styles = useStyles();
|
|
188
|
+
const isDarkTheme = useIsDarkTheme();
|
|
167
189
|
|
|
168
190
|
return (
|
|
169
191
|
<Primitive.Item
|
|
@@ -172,6 +194,7 @@ function SelectPrimitiveItem({
|
|
|
172
194
|
style={({ pressed }) => [
|
|
173
195
|
styles.item,
|
|
174
196
|
pressed && styles.itemPressed,
|
|
197
|
+
pressed && isDarkTheme && styles.itemPressedDarkElevated,
|
|
175
198
|
style,
|
|
176
199
|
]}
|
|
177
200
|
{...props}
|