@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.
@@ -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.13",
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": "0991c3a7ba2e5b5bce8aa8adea0eb60aa9d360f8"
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: PortalHostWrapper },
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({ ...props }: SelectPrimitivePortalProps) {
95
- return <Primitive.Portal {...props} />;
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}