@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.
Files changed (42) hide show
  1. package/dist/docs/ActionItem/ActionItem.md +1 -1
  2. package/dist/docs/ButtonGroup/ButtonGroup.md +2 -2
  3. package/dist/docs/Chip/Chip.md +4 -4
  4. package/dist/docs/EmptyState/EmptyState.md +1 -1
  5. package/dist/docs/Icon/Icon.md +1 -1
  6. package/dist/docs/IconButton/IconButton.md +1 -1
  7. package/dist/docs/usage-guidelines/usage-guidelines.md +5 -5
  8. package/dist/package.json +5 -3
  9. package/dist/src/ActivityIndicator/ActivityIndicator.js +1 -1
  10. package/dist/src/primitives/Portal/index.js +5 -0
  11. package/dist/src/primitives/Select/SelectPrimitive.js +173 -0
  12. package/dist/src/primitives/Select/SelectPrimitive.style.js +112 -0
  13. package/dist/src/primitives/Select/SelectPrimitive.test.js +243 -0
  14. package/dist/src/primitives/Select/index.js +1 -0
  15. package/dist/src/primitives/Select/types.js +1 -0
  16. package/dist/src/primitives/index.js +2 -0
  17. package/dist/src/utils/test/PortalHostWrapper.js +11 -0
  18. package/dist/src/utils/test/index.js +1 -0
  19. package/dist/tsconfig.build.tsbuildinfo +1 -1
  20. package/dist/types/src/primitives/Portal/index.d.ts +1 -0
  21. package/dist/types/src/primitives/Select/SelectPrimitive.d.ts +67 -0
  22. package/dist/types/src/primitives/Select/SelectPrimitive.style.d.ts +105 -0
  23. package/dist/types/src/primitives/Select/SelectPrimitive.test.d.ts +1 -0
  24. package/dist/types/src/primitives/Select/index.d.ts +2 -0
  25. package/dist/types/src/primitives/Select/types.d.ts +41 -0
  26. package/dist/types/src/primitives/index.d.ts +2 -0
  27. package/dist/types/src/utils/test/PortalHostWrapper.d.ts +10 -0
  28. package/dist/types/src/utils/test/index.d.ts +1 -0
  29. package/package.json +5 -3
  30. package/src/ActivityIndicator/ActivityIndicator.stories.tsx +46 -1
  31. package/src/ActivityIndicator/ActivityIndicator.tsx +1 -1
  32. package/src/ActivityIndicator/guide.md +6 -4
  33. package/src/primitives/Portal/index.ts +5 -0
  34. package/src/primitives/Select/SelectPrimitive.stories.tsx +221 -0
  35. package/src/primitives/Select/SelectPrimitive.style.ts +141 -0
  36. package/src/primitives/Select/SelectPrimitive.test.tsx +372 -0
  37. package/src/primitives/Select/SelectPrimitive.tsx +292 -0
  38. package/src/primitives/Select/index.ts +17 -0
  39. package/src/primitives/Select/types.ts +96 -0
  40. package/src/primitives/index.ts +2 -0
  41. package/src/utils/test/PortalHostWrapper.tsx +19 -0
  42. package/src/utils/test/index.ts +1 -0
@@ -0,0 +1 @@
1
+ export { PortalHost as AtlantisPortalHost } from "@rn-primitives/portal";
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+ import type { SelectPrimitiveContentProps, SelectPrimitiveGroupProps, SelectPrimitiveItemIndicatorProps, SelectPrimitiveItemProps, SelectPrimitiveItemTextProps, SelectPrimitiveLabelProps, SelectPrimitiveOverlayProps, SelectPrimitivePortalProps, SelectPrimitiveRootProps, SelectPrimitiveSeparatorProps, SelectPrimitiveTriggerProps, SelectPrimitiveValueProps, SelectPrimitiveViewportProps } from "./types";
3
+ declare function SelectPrimitiveRoot({ ...props }: SelectPrimitiveRootProps): React.JSX.Element;
4
+ /**
5
+ * Styled trigger field. Renders only its `children` — compose
6
+ * `SelectPrimitive.Value` and any trailing icon (e.g. a chevron) here; the
7
+ * primitive provides no chevron of its own.
8
+ */
9
+ declare function SelectPrimitiveTrigger({ children, style, testID, invalid, onPressIn, onPressOut, ref, ...props }: SelectPrimitiveTriggerProps): React.JSX.Element;
10
+ declare function SelectPrimitiveValue({ style, ...props }: SelectPrimitiveValueProps): React.JSX.Element;
11
+ /** Portals the dropdown into the mounted `AtlantisPortalHost`. */
12
+ declare function SelectPrimitivePortal({ ...props }: SelectPrimitivePortalProps): React.JSX.Element;
13
+ /** Tap-outside dismiss surface behind the dropdown (no scrim). */
14
+ declare function SelectPrimitiveOverlay({ style, testID, ...props }: SelectPrimitiveOverlayProps): React.JSX.Element;
15
+ /**
16
+ * Anchored dropdown surface: matches the trigger width, applies safe-area
17
+ * insets, and flips above the trigger when space is tight. Compose it inside
18
+ * `Portal`, alongside `Overlay`, wrapping the items in a `Viewport`.
19
+ */
20
+ declare function SelectPrimitiveContent({ children, style, sideOffset, insets, ref, ...props }: SelectPrimitiveContentProps): React.JSX.Element;
21
+ /** Scroll container for the items (a no-op passthrough on native). */
22
+ declare function SelectPrimitiveViewport({ ...props }: SelectPrimitiveViewportProps): React.JSX.Element;
23
+ /**
24
+ * Styled, pressable option row. Compose its content from `SelectPrimitive.ItemText`
25
+ * and `SelectPrimitive.ItemIndicator` (and anything else) as `children`.
26
+ */
27
+ declare function SelectPrimitiveItem({ style, disabled, ref, ...props }: SelectPrimitiveItemProps): React.JSX.Element;
28
+ /**
29
+ * The option label as styled text (reads the label from the item context). The
30
+ * `disabled` flag is explicit — the composing layer (e.g. the sugared
31
+ * `Select.Item`) forwards the row's disabled state to grey the text.
32
+ */
33
+ declare function SelectPrimitiveItemText({ style, disabled, ref, ...props }: SelectPrimitiveItemTextProps): React.JSX.Element;
34
+ /**
35
+ * The selected-state marker slot. Renders its `children` (the marker, e.g. a
36
+ * checkmark) on the selected row only — it self-hides on unselected rows. The
37
+ * marker glyph is supplied by the composing layer, not defaulted here.
38
+ */
39
+ declare function SelectPrimitiveItemIndicator({ style, ref, ...props }: SelectPrimitiveItemIndicatorProps): React.JSX.Element;
40
+ /**
41
+ * Groups related options, typically with a `SelectPrimitive.Label` heading.
42
+ */
43
+ declare function SelectPrimitiveGroup({ ...props }: SelectPrimitiveGroupProps): React.JSX.Element;
44
+ /**
45
+ * A styled group heading rendered inside `SelectPrimitive.Group`.
46
+ */
47
+ declare function SelectPrimitiveLabel({ style, ref, ...props }: SelectPrimitiveLabelProps): React.JSX.Element;
48
+ /**
49
+ * A styled divider between groups or options.
50
+ */
51
+ declare function SelectPrimitiveSeparator({ style, ref, ...props }: SelectPrimitiveSeparatorProps): React.JSX.Element;
52
+ export declare const SelectPrimitive: {
53
+ Root: typeof SelectPrimitiveRoot;
54
+ Trigger: typeof SelectPrimitiveTrigger;
55
+ Value: typeof SelectPrimitiveValue;
56
+ Portal: typeof SelectPrimitivePortal;
57
+ Overlay: typeof SelectPrimitiveOverlay;
58
+ Content: typeof SelectPrimitiveContent;
59
+ Viewport: typeof SelectPrimitiveViewport;
60
+ Item: typeof SelectPrimitiveItem;
61
+ ItemText: typeof SelectPrimitiveItemText;
62
+ ItemIndicator: typeof SelectPrimitiveItemIndicator;
63
+ Group: typeof SelectPrimitiveGroup;
64
+ Label: typeof SelectPrimitiveLabel;
65
+ Separator: typeof SelectPrimitiveSeparator;
66
+ };
67
+ export {};
@@ -0,0 +1,105 @@
1
+ export declare const useStyles: () => {
2
+ trigger: {
3
+ flexDirection: "row";
4
+ alignItems: "center";
5
+ gap: number;
6
+ minHeight: number;
7
+ paddingHorizontal: number;
8
+ backgroundColor: string;
9
+ borderWidth: number;
10
+ borderColor: string;
11
+ borderRadius: number;
12
+ };
13
+ triggerOpen: {
14
+ borderWidth: number;
15
+ borderColor: string;
16
+ paddingHorizontal: number;
17
+ };
18
+ triggerPressed: {
19
+ borderWidth: number;
20
+ borderColor: string;
21
+ paddingHorizontal: number;
22
+ };
23
+ triggerInvalid: {
24
+ borderColor: string;
25
+ };
26
+ triggerDisabled: {
27
+ backgroundColor: string;
28
+ };
29
+ value: {
30
+ flex: number;
31
+ color: string;
32
+ fontFamily: string | undefined;
33
+ fontSize: number | undefined;
34
+ lineHeight: number;
35
+ letterSpacing: number | undefined;
36
+ };
37
+ valuePlaceholder: {
38
+ color: string;
39
+ };
40
+ valueDisabled: {
41
+ color: string;
42
+ };
43
+ overlay: {
44
+ position: "absolute";
45
+ top: number;
46
+ right: number;
47
+ bottom: number;
48
+ left: number;
49
+ };
50
+ content: {
51
+ shadowColor: string;
52
+ shadowOffset: {
53
+ width: number;
54
+ height: number;
55
+ };
56
+ shadowOpacity: number;
57
+ shadowRadius: number;
58
+ elevation: number;
59
+ backgroundColor: string;
60
+ borderWidth: number;
61
+ borderColor: string;
62
+ borderRadius: number;
63
+ padding: number;
64
+ };
65
+ item: {
66
+ flexDirection: "row";
67
+ alignItems: "center";
68
+ gap: number;
69
+ minHeight: number;
70
+ padding: number;
71
+ borderRadius: number;
72
+ };
73
+ itemPressed: {
74
+ backgroundColor: string;
75
+ };
76
+ itemText: {
77
+ flex: number;
78
+ color: string;
79
+ fontFamily: string | undefined;
80
+ fontSize: number | undefined;
81
+ lineHeight: number;
82
+ letterSpacing: number | undefined;
83
+ };
84
+ itemTextDisabled: {
85
+ color: string;
86
+ };
87
+ itemIndicator: {
88
+ justifyContent: "center";
89
+ alignItems: "center";
90
+ };
91
+ groupLabel: {
92
+ color: string;
93
+ fontFamily: string | undefined;
94
+ fontSize: number;
95
+ lineHeight: number;
96
+ paddingTop: number;
97
+ paddingBottom: number;
98
+ paddingHorizontal: number;
99
+ };
100
+ separator: {
101
+ height: number;
102
+ backgroundColor: string;
103
+ marginVertical: number;
104
+ };
105
+ };
@@ -0,0 +1,2 @@
1
+ export { SelectPrimitive } from "./SelectPrimitive";
2
+ export type { SelectPrimitiveContentProps, SelectPrimitiveGroupProps, SelectPrimitiveItemIndicatorProps, SelectPrimitiveItemProps, SelectPrimitiveItemTextProps, SelectPrimitiveLabelProps, SelectPrimitiveOption, SelectPrimitiveOverlayProps, SelectPrimitivePortalProps, SelectPrimitiveRootProps, SelectPrimitiveSeparatorProps, SelectPrimitiveTriggerProps, SelectPrimitiveValueProps, SelectPrimitiveViewportProps, } from "./types";
@@ -0,0 +1,41 @@
1
+ import type React from "react";
2
+ import type { StyleProp, ViewStyle } from "react-native";
3
+ import type { ContentProps, ContentRef, GroupProps, GroupRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, ItemTextProps, ItemTextRef, LabelProps, LabelRef, Option, OverlayProps, OverlayRef, PortalProps, RootProps, SeparatorProps, SeparatorRef, TriggerProps, TriggerRef, ValueProps, ViewportProps } from "@rn-primitives/select";
4
+ export type { Option as SelectPrimitiveOption };
5
+ export type SelectPrimitiveRootProps = RootProps;
6
+ export interface SelectPrimitiveTriggerProps extends Omit<TriggerProps, "style" | "children" | "disabled" | "asChild">, React.RefAttributes<TriggerRef> {
7
+ /** Renders as `ATL-<testID>-Select-Trigger`, or `ATL-Select-Trigger` when omitted. */
8
+ readonly testID?: string;
9
+ /** Critical (error) border styling. Style-only — no field/validation logic. */
10
+ readonly invalid?: boolean;
11
+ /** Callback styles are not supported; state styling is composed internally. */
12
+ readonly style?: StyleProp<ViewStyle>;
13
+ readonly children?: React.ReactNode;
14
+ }
15
+ export type SelectPrimitiveValueProps = ValueProps;
16
+ export type SelectPrimitivePortalProps = PortalProps;
17
+ export interface SelectPrimitiveOverlayProps extends Omit<OverlayProps, "style" | "asChild">, React.RefAttributes<OverlayRef> {
18
+ readonly style?: StyleProp<ViewStyle>;
19
+ }
20
+ export interface SelectPrimitiveContentProps extends ContentProps, React.RefAttributes<ContentRef> {
21
+ }
22
+ export type SelectPrimitiveViewportProps = ViewportProps;
23
+ export interface SelectPrimitiveItemProps extends Omit<ItemProps, "style" | "asChild">, React.RefAttributes<ItemRef> {
24
+ /** Callback styles are not supported; pressed styling is composed internally. */
25
+ readonly style?: StyleProp<ViewStyle>;
26
+ /** Compose `ItemText` / `ItemIndicator` (and any other content) as children. */
27
+ readonly children?: React.ReactNode;
28
+ }
29
+ export interface SelectPrimitiveItemTextProps extends Omit<ItemTextProps, "asChild">, React.RefAttributes<ItemTextRef> {
30
+ /** Greys the label; the composing layer forwards the row's disabled state. */
31
+ readonly disabled?: boolean;
32
+ }
33
+ /** The default selected-item checkmark; overridable via `children`. */
34
+ export interface SelectPrimitiveItemIndicatorProps extends ItemIndicatorProps, React.RefAttributes<ItemIndicatorRef> {
35
+ }
36
+ export interface SelectPrimitiveGroupProps extends GroupProps, React.RefAttributes<GroupRef> {
37
+ }
38
+ export interface SelectPrimitiveLabelProps extends LabelProps, React.RefAttributes<LabelRef> {
39
+ }
40
+ export interface SelectPrimitiveSeparatorProps extends SeparatorProps, React.RefAttributes<SeparatorRef> {
41
+ }
@@ -2,3 +2,5 @@
2
2
  * This file exports styled components-native primitives.
3
3
  */
4
4
  export * from "./HelperText";
5
+ export * from "./Portal";
6
+ export * from "./Select";
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ interface PortalHostWrapperProps {
3
+ readonly children: React.ReactNode;
4
+ }
5
+ /**
6
+ * Test wrapper that mounts a `PortalHost` so portal-based overlays (e.g. the
7
+ * Select dropdown) render: `render(ui, { wrapper: PortalHostWrapper })`.
8
+ */
9
+ export declare function PortalHostWrapper({ children }: PortalHostWrapperProps): React.JSX.Element;
10
+ export {};
@@ -1 +1,2 @@
1
1
  export { MockSafeAreaProvider } from "./MockSafeAreaProvider";
2
+ export { PortalHostWrapper } from "./PortalHostWrapper";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.105.4",
3
+ "version": "0.107.0",
4
4
  "license": "MIT",
5
5
  "description": "React Native implementation of Atlantis",
6
6
  "repository": {
@@ -58,6 +58,8 @@
58
58
  "dependencies": {
59
59
  "@react-native-clipboard/clipboard": "^1.11.2",
60
60
  "@react-native-picker/picker": "^2.4.10",
61
+ "@rn-primitives/portal": "^1.4.0",
62
+ "@rn-primitives/select": "^1.4.0",
61
63
  "autolinker": "^4.0.0",
62
64
  "deepmerge": "^4.2.2",
63
65
  "lodash": "^4.17.21",
@@ -71,7 +73,7 @@
71
73
  "devDependencies": {
72
74
  "@babel/runtime": "^7.29.2",
73
75
  "@gorhom/bottom-sheet": "^5.2.8",
74
- "@jobber/design": "0.104.0",
76
+ "@jobber/design": "0.105.0",
75
77
  "@jobber/hooks": "2.21.0",
76
78
  "@react-native-community/datetimepicker": "^8.4.5",
77
79
  "@react-native/babel-preset": "^0.82.1",
@@ -122,5 +124,5 @@
122
124
  "react-native-screens": ">=4.18.0",
123
125
  "react-native-svg": ">=12.0.0"
124
126
  },
125
- "gitHead": "7fb12fadcd1c5e1440bccccae13dd29cf74dae28"
127
+ "gitHead": "5e02a8ecc5f282d152c92e18dde04a27d401ecf5"
126
128
  }
@@ -1,6 +1,11 @@
1
1
  import React from "react";
2
+ import { View } from "react-native";
2
3
  import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
3
- import { ActivityIndicator } from "@jobber/components-native";
4
+ import {
5
+ ActivityIndicator,
6
+ Text,
7
+ useAtlantisTheme,
8
+ } from "@jobber/components-native";
4
9
  import { ActivityIndicatorBasicExample } from "./docs";
5
10
 
6
11
  const meta = {
@@ -28,6 +33,46 @@ export const CustomAccessibilityLabel: Story = {
28
33
  render: () => <ActivityIndicator accessibilityLabel="Uploading file" />,
29
34
  };
30
35
 
36
+ /**
37
+ * The track ring uses `color-surface--active`, which is designed to read as
38
+ * a muted tint of the surface beneath it. This story renders the indicator
39
+ * on each of the standard application surfaces so the contrast between the
40
+ * track and its background can be reviewed at a glance.
41
+ */
42
+ export const OnSurfaces: Story = {
43
+ render: () => <OnSurfacesExample />,
44
+ };
45
+
46
+ function OnSurfacesExample() {
47
+ const { tokens } = useAtlantisTheme();
48
+ const surfaces = [
49
+ { token: "color-surface", label: "surface" },
50
+ { token: "color-surface--background", label: "surface--background" },
51
+ {
52
+ token: "color-surface--background--subtle",
53
+ label: "surface--background--subtle",
54
+ },
55
+ ] as const;
56
+
57
+ return (
58
+ <View style={{ gap: tokens["space-base"] }}>
59
+ {surfaces.map(({ token, label }) => (
60
+ <View
61
+ key={token}
62
+ style={{
63
+ backgroundColor: tokens[token],
64
+ padding: tokens["space-large"],
65
+ gap: tokens["space-small"],
66
+ }}
67
+ >
68
+ <Text>{label}</Text>
69
+ <ActivityIndicator size="base" />
70
+ </View>
71
+ ))}
72
+ </View>
73
+ );
74
+ }
75
+
31
76
  /**
32
77
  * The reduced-motion presentation is driven by the OS-level
33
78
  * "Reduce Motion" setting, not a Storybook control. To preview it:
@@ -146,7 +146,7 @@ export function ActivityIndicator({
146
146
  pixelSize={pixelSize}
147
147
  strokeWidth={strokeWidth}
148
148
  arcColor={themeTokens["color-icon"]}
149
- trackColor={themeTokens["color-disabled--secondary"]}
149
+ trackColor={themeTokens["color-surface--active"]}
150
150
  style={style}
151
151
  testID={testID}
152
152
  a11yProps={a11yProps}
@@ -50,7 +50,7 @@ The previous wrapper accepted React Native's full `ActivityIndicatorProps` via
50
50
  spread. The new public API drops the following intentionally:
51
51
 
52
52
  - `color` — replaced by semantic theme tokens (`color-icon` for the arcs,
53
- `color-disabled-secondary` for the track). The single known consumer of
53
+ `color-surface--active` for the track). The single known consumer of
54
54
  `color="white"` (`jobber-mobile/GalleryHeader`) is migrated as part of the
55
55
  `migrate-activityindicator` change in the jobber-mobile repository.
56
56
  - numeric `size` — sizing is constrained to the two design-system sizes (28px /
@@ -124,7 +124,9 @@ The three concurrent animations:
124
124
  second arc renders 180° out of phase.
125
125
 
126
126
  A static `<Circle>` of the same dimensions renders the track ring beneath the
127
- active arcs, filled with `tokens["color-disabled-secondary"]`.
127
+ active arcs, filled with `themeTokens["color-surface--active"]` so the track
128
+ adapts to whichever surface (`surface`, `surface--background`,
129
+ `surface--background--subtle`) the indicator is placed on.
128
130
 
129
131
  The literal durations match Material Web's published progress motion spec; the
130
132
  same values ship from `@jobber/design`'s `timing.tokens.json` to both web and
@@ -177,8 +179,8 @@ Both components share:
177
179
  - Sizes (28 px small, 44 px base)
178
180
  - Motion durations (`timing-indicator-arc`, `timing-indicator-cycle`,
179
181
  `timing-indicator-linear-rotate`) from the same `@jobber/design` source
180
- - Color tokens (`color-icon` for arcs, `color-disabled-secondary` for track)
181
- from the same `@jobber/design` source
182
+ - Color tokens (`color-icon` for arcs, `color-surface--active` for track) from
183
+ the same `@jobber/design` source
182
184
  - Reduced-motion fallback (opacity 1.0 ↔ 0.35, 2 s ease-in-out alternate)
183
185
 
184
186
  Documented divergences:
@@ -0,0 +1,5 @@
1
+ // The portal host that backs every portal-based primitive (e.g. the Select
2
+ // dropdown). Apps mount one <AtlantisPortalHost/> at their root; it shares this
3
+ // package's bundled @rn-primitives/portal instance with the primitives' Portal,
4
+ // so registered overlays render in the host.
5
+ export { PortalHost as AtlantisPortalHost } from "@rn-primitives/portal";
@@ -0,0 +1,221 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
2
+ import React from "react";
3
+ import { View } from "react-native";
4
+ import { SafeAreaProvider } from "react-native-safe-area-context";
5
+ import { PortalHost } from "@rn-primitives/portal";
6
+ import { useRootContext } from "@rn-primitives/select";
7
+ import { SelectPrimitive } from "./SelectPrimitive";
8
+ import { Text } from "../../Text";
9
+ import { Icon } from "../../Icon";
10
+
11
+ const meta = {
12
+ title: "Components/Primitives/SelectPrimitive",
13
+ component: SelectPrimitive.Root,
14
+ parameters: {
15
+ viewport: { defaultViewport: "mobile1" },
16
+ },
17
+ decorators: [
18
+ (Story: React.ComponentType) => (
19
+ <>
20
+ <SafeAreaProvider>
21
+ <View style={{ width: 320, padding: 16, minHeight: 480 }}>
22
+ <Story />
23
+ </View>
24
+ </SafeAreaProvider>
25
+ <PortalHost />
26
+ </>
27
+ ),
28
+ ],
29
+ } satisfies Meta<typeof SelectPrimitive.Root>;
30
+ export default meta;
31
+ type Story = StoryObj<typeof meta>;
32
+
33
+ const CITIES = [
34
+ { value: "tor", label: "Toronto" },
35
+ { value: "van", label: "Vancouver" },
36
+ { value: "edm", label: "Edmonton" },
37
+ { value: "wpg", label: "Winnipeg" },
38
+ { value: "hal", label: "Halifax" },
39
+ ];
40
+
41
+ /** Composes an option row from the styled `ItemText` + `ItemIndicator` parts. */
42
+ function Option({
43
+ value,
44
+ label,
45
+ disabled,
46
+ }: {
47
+ readonly value: string;
48
+ readonly label: string;
49
+ readonly disabled?: boolean;
50
+ }) {
51
+ return (
52
+ <SelectPrimitive.Item value={value} label={label} disabled={disabled}>
53
+ <SelectPrimitive.ItemText disabled={disabled} />
54
+ <SelectPrimitive.ItemIndicator>
55
+ <Icon name="checkmark" />
56
+ </SelectPrimitive.ItemIndicator>
57
+ </SelectPrimitive.Item>
58
+ );
59
+ }
60
+
61
+ function CityItems() {
62
+ return (
63
+ <>
64
+ {CITIES.map(city => (
65
+ <Option key={city.value} value={city.value} label={city.label} />
66
+ ))}
67
+ </>
68
+ );
69
+ }
70
+
71
+ /** Composes the raw overlay parts (`Portal > Overlay + Content > Viewport`). */
72
+ function Dropdown({ children }: { readonly children: React.ReactNode }) {
73
+ return (
74
+ <SelectPrimitive.Portal>
75
+ <SelectPrimitive.Overlay />
76
+ <SelectPrimitive.Content>
77
+ <SelectPrimitive.Viewport>{children}</SelectPrimitive.Viewport>
78
+ </SelectPrimitive.Content>
79
+ </SelectPrimitive.Portal>
80
+ );
81
+ }
82
+
83
+ /**
84
+ * A trailing chevron composed onto the trigger. The primitive owns no chevron —
85
+ * the icon and its open-state rotation are composed here (this is what the
86
+ * sugared `Select` will do), reading `open` from the primitive root context.
87
+ */
88
+ function Chevron() {
89
+ const { open } = useRootContext();
90
+
91
+ return (
92
+ <View style={{ transform: [{ rotate: open ? "180deg" : "0deg" }] }}>
93
+ <Icon name="arrowDown" color="iconSecondary" />
94
+ </View>
95
+ );
96
+ }
97
+
98
+ /** Composes the trigger: the value plus a composed, rotating chevron. */
99
+ function TriggerField({
100
+ placeholder,
101
+ invalid,
102
+ }: {
103
+ readonly placeholder: string;
104
+ readonly invalid?: boolean;
105
+ }) {
106
+ return (
107
+ <SelectPrimitive.Trigger invalid={invalid}>
108
+ <SelectPrimitive.Value placeholder={placeholder} />
109
+ <Chevron />
110
+ </SelectPrimitive.Trigger>
111
+ );
112
+ }
113
+
114
+ function StateRow({
115
+ caption,
116
+ children,
117
+ }: {
118
+ readonly caption: string;
119
+ readonly children: React.ReactNode;
120
+ }) {
121
+ return (
122
+ <View style={{ gap: 4 }}>
123
+ <Text level="textSupporting" variation="subdued">
124
+ {caption}
125
+ </Text>
126
+ {children}
127
+ </View>
128
+ );
129
+ }
130
+
131
+ export const Basic: Story = {
132
+ render: () => (
133
+ <SelectPrimitive.Root>
134
+ <TriggerField placeholder="Select an option" />
135
+ <Dropdown>
136
+ <CityItems />
137
+ </Dropdown>
138
+ </SelectPrimitive.Root>
139
+ ),
140
+ };
141
+
142
+ export const WithSelection: Story = {
143
+ render: () => (
144
+ <SelectPrimitive.Root defaultValue={{ value: "van", label: "Vancouver" }}>
145
+ <TriggerField placeholder="Select an option" />
146
+ <Dropdown>
147
+ <CityItems />
148
+ </Dropdown>
149
+ </SelectPrimitive.Root>
150
+ ),
151
+ };
152
+
153
+ export const GroupsAndSeparator: Story = {
154
+ render: () => (
155
+ <SelectPrimitive.Root>
156
+ <TriggerField placeholder="Select a city" />
157
+ <Dropdown>
158
+ <SelectPrimitive.Group>
159
+ <SelectPrimitive.Label>West</SelectPrimitive.Label>
160
+ <Option value="van" label="Vancouver" />
161
+ <Option value="edm" label="Edmonton" />
162
+ </SelectPrimitive.Group>
163
+ <SelectPrimitive.Separator />
164
+ <SelectPrimitive.Group>
165
+ <SelectPrimitive.Label>East</SelectPrimitive.Label>
166
+ <Option value="tor" label="Toronto" />
167
+ <Option value="hal" label="Halifax" />
168
+ </SelectPrimitive.Group>
169
+ </Dropdown>
170
+ </SelectPrimitive.Root>
171
+ ),
172
+ };
173
+
174
+ export const DisabledOption: Story = {
175
+ render: () => (
176
+ <SelectPrimitive.Root>
177
+ <TriggerField placeholder="Select an option" />
178
+ <Dropdown>
179
+ <Option value="tor" label="Toronto" />
180
+ <Option value="van" label="Vancouver" disabled />
181
+ <Option value="edm" label="Edmonton" />
182
+ </Dropdown>
183
+ </SelectPrimitive.Root>
184
+ ),
185
+ };
186
+
187
+ export const TriggerStates: Story = {
188
+ render: () => (
189
+ <View style={{ gap: 24 }}>
190
+ <StateRow caption="Default — tap to open">
191
+ <SelectPrimitive.Root>
192
+ <TriggerField placeholder="Select an option" />
193
+ <Dropdown>
194
+ <CityItems />
195
+ </Dropdown>
196
+ </SelectPrimitive.Root>
197
+ </StateRow>
198
+
199
+ <StateRow caption="Critical (invalid) — tap to open">
200
+ <SelectPrimitive.Root>
201
+ <TriggerField placeholder="Select an option" invalid />
202
+ <Dropdown>
203
+ <CityItems />
204
+ </Dropdown>
205
+ </SelectPrimitive.Root>
206
+ </StateRow>
207
+
208
+ <StateRow caption="Disabled — non-interactive, greyed chevron">
209
+ <SelectPrimitive.Root
210
+ disabled
211
+ defaultValue={{ value: "tor", label: "Toronto" }}
212
+ >
213
+ <TriggerField placeholder="Select an option" />
214
+ <Dropdown>
215
+ <CityItems />
216
+ </Dropdown>
217
+ </SelectPrimitive.Root>
218
+ </StateRow>
219
+ </View>
220
+ ),
221
+ };