@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
|
@@ -116,6 +116,46 @@ function ThemedComponent() {
|
|
|
116
116
|
}
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
### Cross-tab theme sync (Web Only)
|
|
120
|
+
|
|
121
|
+
The provider can keep the theme in sync across a user's open browser tabs. This
|
|
122
|
+
is opt-in: pass the `localStorage` key your app writes to as the `storageKey`
|
|
123
|
+
prop. The provider then listens for
|
|
124
|
+
[`storage` events](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event)
|
|
125
|
+
on that key and applies theme changes from other tabs. The `storage` event fires
|
|
126
|
+
only in tabs *other* than the one that wrote the value, so there's no feedback
|
|
127
|
+
loop.
|
|
128
|
+
|
|
129
|
+
**Your app is responsible for writing to `localStorage`** when the theme changes
|
|
130
|
+
— Atlantis only handles the listen-and-react side.
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
<AtlantisThemeContextProvider storageKey="my_app_theme">
|
|
134
|
+
<ThemedComponent />
|
|
135
|
+
</AtlantisThemeContextProvider>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
// Theme toggle — write to the same key after calling updateTheme
|
|
140
|
+
import { updateTheme } from "@jobber/components/AtlantisThemeContext";
|
|
141
|
+
|
|
142
|
+
function toggleTheme(newTheme: "light" | "dark") {
|
|
143
|
+
updateTheme(newTheme); // updates all providers in this tab immediately
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
localStorage.setItem("my_app_theme", newTheme); // signals other tabs
|
|
147
|
+
} catch {
|
|
148
|
+
// localStorage unavailable (e.g. private mode) — degrades gracefully
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Omit `storageKey` and no `storage` listener is added, keeping the previous
|
|
154
|
+
within-tab-only behavior. Multiple dynamic providers on the same page all share
|
|
155
|
+
the same internal store, so a single provider with a `storageKey` is enough — a
|
|
156
|
+
cross-tab signal updates every dynamic provider at once. Providers mounted with
|
|
157
|
+
`dangerouslyOverrideTheme` are unaffected.
|
|
158
|
+
|
|
119
159
|
### Forcing a theme for an AtlantisThemeContextProvider
|
|
120
160
|
|
|
121
161
|
In some scenarios you may want to force a theme for specific components
|
package/dist/docs/Chip/Chip.md
CHANGED
|
@@ -238,8 +238,8 @@ export function ChipInvalidExample(
|
|
|
238
238
|
single-select, multi-select, and add/dismiss functionality "out of the box"
|
|
239
239
|
* [FilterPicker](/components/FilterPicker) is most commonly triggered by a Chip,
|
|
240
240
|
but is a separate component
|
|
241
|
-
* [
|
|
242
|
-
presents as a form element and should be preferred in forms
|
|
241
|
+
* [LegacySelect](/components/LegacySelect) is a simpler single-select "dropdown"
|
|
242
|
+
that presents as a form element and should be preferred in forms
|
|
243
243
|
* [RadioGroup](/components/RadioGroup) should be used to allow the user to
|
|
244
244
|
select "one-of-many" items (single-select) and the labels for the items are
|
|
245
245
|
longer than 1 or 2 words.
|
|
@@ -84,6 +84,19 @@ different.
|
|
|
84
84
|
Used by the `--shadow-focus` property to indicate that an element has received
|
|
85
85
|
focus. Avoid using `--color-focus` directly on UI elements.
|
|
86
86
|
|
|
87
|
+
### Data Visualization
|
|
88
|
+
|
|
89
|
+
Use these colors to distinguish values in charts and other data visualizations.
|
|
90
|
+
They respond to theming and should be preferred over base color tokens.
|
|
91
|
+
|
|
92
|
+
#### Categorical
|
|
93
|
+
|
|
94
|
+
Use categorical colors to distinguish unrelated groups or series. Use `subtle`
|
|
95
|
+
or `bold` when multiple treatments of the first categorical color need to sit
|
|
96
|
+
together in the same visualization.
|
|
97
|
+
|
|
98
|
+
Use additional categorical colors for supporting data series.
|
|
99
|
+
|
|
87
100
|
### Status
|
|
88
101
|
|
|
89
102
|
Use these colors in labels, icons, filters, alerts, and other elements where
|
|
@@ -50,6 +50,9 @@ where it will be a maximum width of 640px and be centered on the screen.
|
|
|
50
50
|
| `children` | `ReactNode` | Yes | — | Content to be passed into the overlay |
|
|
51
51
|
| `accessibilityLabel` | `string` | No | `"Close {title} modal"` | Optional accessibilityLabel describing the overlay. This will read out when the overlay is opened. |
|
|
52
52
|
| `adjustToContentHeight` | `boolean` | No | `false` | If true, automatically adjusts the overlay height to the content height. This will disable the ability to drag the ov... |
|
|
53
|
+
| `allowDragWithBeforeExit` | `boolean` | No | `false` | When `true`, `onBeforeExit` intercepts the dismiss button and back press but does not disable drag. Drag-to-dismiss b... |
|
|
54
|
+
| `enableContentPanningGesture` | `boolean` | No | — | Whether pan on the content area drags the sheet. Set `false` to let nested scrollables own the gesture. Defaults to t... |
|
|
55
|
+
| `enablePanDownToClose` | `boolean` | No | — | Whether panning down on the content area closes the sheet. Set false to prevent the sheet from being closed by pannin... |
|
|
53
56
|
| `fullScreen` | `boolean` | No | `false` | Force overlay height to fill the screen. Width not impacted. |
|
|
54
57
|
| `isDraggable` | `boolean` | No | `true` | If false, hides the handle and turns off dragging. |
|
|
55
58
|
| `keyboardShouldPersistTaps` | `boolean` | No | `false` | Allows taps to be registered behind keyboard if enabled |
|
|
@@ -61,4 +64,5 @@ where it will be a maximum width of 640px and be centered on the screen.
|
|
|
61
64
|
| `ref` | `Ref<{ open?: () => void; close?: () => void; }>` | No | — | Ref to the content overlay component. |
|
|
62
65
|
| `scrollEnabled` | `boolean` | No | `false` | Enables scrolling in the content body of overlay |
|
|
63
66
|
| `showDismiss` | `boolean` | No | `false` | Display the dismiss button in the header of the overlay. |
|
|
67
|
+
| `snapPoints` | `(string | number)[]` | No | — | Explicit snap points for the overlay (percentages or pixel values). When provided, `adjustToContentHeight` is ignored... |
|
|
64
68
|
| `title` | `string` | No | — | Title of overlay, appears in the header next to the close button. |
|
package/dist/docs/Form/Form.md
CHANGED
|
@@ -18,11 +18,11 @@ have a `value` and `onChange` prop.
|
|
|
18
18
|
### Inputs
|
|
19
19
|
|
|
20
20
|
Form can accept various inputs and selection elements such as (but not limited
|
|
21
|
-
to) [InputText](../InputText/InputText.md),
|
|
22
|
-
[
|
|
23
|
-
[Chips](/components/Chips). They should be
|
|
24
|
-
indicate grouping when relevant, and groups
|
|
25
|
-
using ContentSection.
|
|
21
|
+
to) [InputText](../InputText/InputText.md),
|
|
22
|
+
[LegacySelect](/components/LegacySelect), [Switch](../Switch/Switch.md),
|
|
23
|
+
[Checkbox](../Checkbox/Checkbox.md), and [Chips](/components/Chips). They should be
|
|
24
|
+
placed [Cards](../Card/Card.md) to indicate grouping when relevant, and groups
|
|
25
|
+
of Cards can be spaced appropriately using ContentSection.
|
|
26
26
|
|
|
27
27
|
### Save Button label
|
|
28
28
|
|
package/dist/docs/Icon/Icon.md
CHANGED
|
@@ -29,6 +29,14 @@ number automatically fills in the rest of the time. For example, typing `2` will
|
|
|
29
29
|
fill in `2:00 PM` and typing `1` waits for a few milliseconds in case the user
|
|
30
30
|
wants to type `10`, `11`, or `12`.
|
|
31
31
|
|
|
32
|
+
## Sizes and the placeholder
|
|
33
|
+
|
|
34
|
+
Consistent with other inputs, `size="small"` does not show the floating mini
|
|
35
|
+
label. The placeholder appears while the field is empty and unfocused, and is
|
|
36
|
+
hidden while the field is being edited or has a value, keeping the field at its
|
|
37
|
+
small height throughout. The default and `large` sizes float the placeholder up
|
|
38
|
+
as a mini label while editing or once a value is set.
|
|
39
|
+
|
|
32
40
|
|
|
33
41
|
## Props
|
|
34
42
|
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Progress Bar
|
|
2
2
|
|
|
3
|
+
> **Deprecated.** Use [ProgressIndicator](/components/ProgressIndicator) for new
|
|
4
|
+
> work. `ProgressIndicator` is the supported determinate progress indicator
|
|
5
|
+
> going forward — it offers the same semantic with a cleaner prop surface
|
|
6
|
+
> (`value` / `max` instead of `currentStep` / `totalSteps`,
|
|
7
|
+
> `variation="continuous"` instead of `"progress"`, plain `className` / `style`
|
|
8
|
+
> instead of `UNSAFE_*`), theme-adaptive tokens, and a unified
|
|
9
|
+
> `<div role="progressbar">` structure across continuous and stepped variations.
|
|
10
|
+
> `ProgressBar` continues to work unchanged for existing call sites.
|
|
11
|
+
|
|
3
12
|
A ProgressBar is a visual indicator of how close something is to completion.
|
|
4
13
|
|
|
5
14
|
## Design & usage guidelines
|
|
@@ -21,6 +21,7 @@ for the “emptiness” may include, but are not limited to…
|
|
|
21
21
|
* Search results with no match
|
|
22
22
|
* An error has resulted in the user not being able to access content they
|
|
23
23
|
otherwise could
|
|
24
|
+
* A specific field doesn't have a value (for example, a custom field that the user hasn't populated)
|
|
24
25
|
|
|
25
26
|
## Solution
|
|
26
27
|
|
|
@@ -63,6 +64,34 @@ a single Banner can look out of place.
|
|
|
63
64
|
</Box>
|
|
64
65
|
```
|
|
65
66
|
|
|
67
|
+
### Field-level empty states
|
|
68
|
+
|
|
69
|
+
When a field doesn't have a value, use a dash to indicate the lack of a value.
|
|
70
|
+
This provides:
|
|
71
|
+
|
|
72
|
+
* confirmation that nothing was entered
|
|
73
|
+
* confirmation that something *can* be entered there should the information require an update
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<Card>
|
|
77
|
+
<Content>
|
|
78
|
+
<Heading level={4}>Additional details</Heading>
|
|
79
|
+
<Stack gap="smallest">
|
|
80
|
+
<Text size="small" variation="subdued">Preferred service date</Text>
|
|
81
|
+
<Text>June 25, 2026</Text>
|
|
82
|
+
</Stack>
|
|
83
|
+
<Stack gap="smallest">
|
|
84
|
+
<Text size="small" variation="subdued">Gate code</Text>
|
|
85
|
+
<Text>—</Text>
|
|
86
|
+
</Stack>
|
|
87
|
+
<Stack gap="smallest">
|
|
88
|
+
<Text size="small" variation="subdued">Additional requests</Text>
|
|
89
|
+
<Text>—</Text>
|
|
90
|
+
</Stack>
|
|
91
|
+
</Content>
|
|
92
|
+
</Card>
|
|
93
|
+
```
|
|
94
|
+
|
|
66
95
|
### When the user can't add content
|
|
67
96
|
|
|
68
97
|
When a card is empty, and there is no way for the user to add content, you
|
package/dist/docs/index.md
CHANGED
|
@@ -52,7 +52,6 @@
|
|
|
52
52
|
[Radii](./Radii/Radii.md)
|
|
53
53
|
[ResponsiveBreakpoint](./ResponsiveBreakpoint/ResponsiveBreakpoint.md)
|
|
54
54
|
[scaffolding](./scaffolding/scaffolding.md)
|
|
55
|
-
[Select](./Select/Select.md)
|
|
56
55
|
[settings](./settings/settings.md)
|
|
57
56
|
[Spacing](./Spacing/Spacing.md)
|
|
58
57
|
[StatusLabel](./StatusLabel/StatusLabel.md)
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.107.
|
|
3
|
+
"version": "0.107.1-select-nat-3a2699a.22+3a2699a2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native implementation of Atlantis",
|
|
6
6
|
"repository": {
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@babel/runtime": "^7.29.2",
|
|
75
75
|
"@gorhom/bottom-sheet": "^5.2.8",
|
|
76
|
-
"@jobber/design": "0.105.
|
|
76
|
+
"@jobber/design": "0.105.1-select-nat-3a2699a.24+3a2699a2",
|
|
77
77
|
"@jobber/hooks": "2.21.0",
|
|
78
78
|
"@react-native-community/datetimepicker": "^8.4.5",
|
|
79
79
|
"@react-native/babel-preset": "^0.82.1",
|
|
@@ -124,5 +124,5 @@
|
|
|
124
124
|
"react-native-screens": ">=4.18.0",
|
|
125
125
|
"react-native-svg": ">=12.0.0"
|
|
126
126
|
},
|
|
127
|
-
"gitHead": "
|
|
127
|
+
"gitHead": "3a2699a221713d3cf972170175713d309245b653"
|
|
128
128
|
}
|
|
@@ -23,7 +23,7 @@ import { useIsScreenReaderEnabled } from "../hooks";
|
|
|
23
23
|
import { IconButton } from "../IconButton";
|
|
24
24
|
import { Heading } from "../Heading";
|
|
25
25
|
import { useAtlantisI18n } from "../hooks/useAtlantisI18n";
|
|
26
|
-
import { useAtlantisTheme } from "../AtlantisThemeContext";
|
|
26
|
+
import { AtlantisThemeContextProvider, useAtlantisTheme, } from "../AtlantisThemeContext";
|
|
27
27
|
/**
|
|
28
28
|
* Signals whether keyboard handling inside a ContentOverlay is delegated to
|
|
29
29
|
* a keyboard-aware scroll view (e.g. BottomSheetKeyboardAwareScrollView).
|
|
@@ -46,7 +46,7 @@ function getModalBackgroundColor(variation, tokens) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
// eslint-disable-next-line max-statements
|
|
49
|
-
export function ContentOverlay({ children, title, accessibilityLabel, fullScreen = false, showDismiss = false, isDraggable = true, adjustToContentHeight = false, keyboardShouldPersistTaps = false, scrollEnabled = false, modalBackgroundColor = "surface", onClose, onOpen, onBeforeExit, loading = false, ref, }) {
|
|
49
|
+
export function ContentOverlay({ children, title, accessibilityLabel, fullScreen = false, showDismiss = false, isDraggable = true, adjustToContentHeight = false, keyboardShouldPersistTaps = false, scrollEnabled = false, modalBackgroundColor = "surface", onClose, onOpen, onBeforeExit, allowDragWithBeforeExit = false, enablePanDownToClose, enableContentPanningGesture, snapPoints: customSnapPoints, loading = false, ref, }) {
|
|
50
50
|
const insets = useSafeAreaInsets();
|
|
51
51
|
const { width: windowWidth } = useWindowDimensions();
|
|
52
52
|
const bottomSheetModalRef = useRef(null);
|
|
@@ -54,13 +54,14 @@ export function ContentOverlay({ children, title, accessibilityLabel, fullScreen
|
|
|
54
54
|
const [currentPosition, setCurrentPosition] = useState(-1);
|
|
55
55
|
const styles = useStyles();
|
|
56
56
|
const { t } = useAtlantisI18n();
|
|
57
|
-
const { tokens } = useAtlantisTheme();
|
|
57
|
+
const { theme, tokens } = useAtlantisTheme();
|
|
58
58
|
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
|
59
59
|
const behavior = computeContentOverlayBehavior({
|
|
60
60
|
fullScreen,
|
|
61
61
|
adjustToContentHeight,
|
|
62
62
|
isDraggable,
|
|
63
63
|
hasOnBeforeExit: onBeforeExit !== undefined,
|
|
64
|
+
allowDragWithBeforeExit,
|
|
64
65
|
showDismiss,
|
|
65
66
|
}, {
|
|
66
67
|
isScreenReaderEnabled,
|
|
@@ -76,12 +77,15 @@ export function ContentOverlay({ children, title, accessibilityLabel, fullScreen
|
|
|
76
77
|
const scrollViewRef = useRef(null);
|
|
77
78
|
// enableDynamicSizing will add another snap point of the content height
|
|
78
79
|
const snapPoints = useMemo(() => {
|
|
80
|
+
if (customSnapPoints && customSnapPoints.length > 0) {
|
|
81
|
+
return customSnapPoints;
|
|
82
|
+
}
|
|
79
83
|
// There is a bug with "restore" behavior after keyboard is dismissed.
|
|
80
84
|
// https://github.com/gorhom/react-native-bottom-sheet/issues/2465
|
|
81
85
|
// providing a 100% snap point "fixes" it for now, but there is an approved PR to fix it
|
|
82
86
|
// that just needs to be merged and released: https://github.com/gorhom/react-native-bottom-sheet/pull/2511
|
|
83
87
|
return ["100%"];
|
|
84
|
-
}, []);
|
|
88
|
+
}, [customSnapPoints]);
|
|
85
89
|
const onCloseController = () => {
|
|
86
90
|
var _a;
|
|
87
91
|
if (!onBeforeExit) {
|
|
@@ -174,12 +178,13 @@ export function ContentOverlay({ children, title, accessibilityLabel, fullScreen
|
|
|
174
178
|
const backdropComponent = useMemo(() => function ContentOverlayBackdrop(props) {
|
|
175
179
|
return (React.createElement(Backdrop, Object.assign({}, props, { pressBehavior: isCloseableOnOverlayTap ? "close" : "none" })));
|
|
176
180
|
}, [isCloseableOnOverlayTap]);
|
|
177
|
-
return (React.createElement(BottomSheetModal, { ref: bottomSheetModalRef, containerComponent: Platform.OS === "ios" ? Container : undefined, onChange: handleChange, style: sheetStyle, backgroundStyle: backgroundStyle, handleStyle: styles.handleWrapper, handleIndicatorStyle: handleIndicatorStyles, backdropComponent: backdropComponent, snapPoints: snapPoints, enablePanDownToClose: effectiveIsDraggable, enableContentPanningGesture: effectiveIsDraggable, enableHandlePanningGesture: effectiveIsDraggable, enableDynamicSizing: behavior.initialHeight === "contentHeight", keyboardBehavior: "interactive", keyboardBlurBehavior: "restore", topInset: topInset, onDismiss: () => onClose === null || onClose === void 0 ? void 0 : onClose() },
|
|
178
|
-
React.createElement(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
return (React.createElement(BottomSheetModal, { ref: bottomSheetModalRef, containerComponent: Platform.OS === "ios" ? Container : undefined, onChange: handleChange, style: sheetStyle, backgroundStyle: backgroundStyle, handleStyle: styles.handleWrapper, handleIndicatorStyle: handleIndicatorStyles, backdropComponent: backdropComponent, snapPoints: snapPoints, enablePanDownToClose: enablePanDownToClose !== null && enablePanDownToClose !== void 0 ? enablePanDownToClose : effectiveIsDraggable, enableContentPanningGesture: enableContentPanningGesture !== null && enableContentPanningGesture !== void 0 ? enableContentPanningGesture : effectiveIsDraggable, enableHandlePanningGesture: effectiveIsDraggable, enableDynamicSizing: !customSnapPoints && behavior.initialHeight === "contentHeight", keyboardBehavior: "interactive", keyboardBlurBehavior: "restore", topInset: topInset, onDismiss: () => onClose === null || onClose === void 0 ? void 0 : onClose() },
|
|
182
|
+
React.createElement(AtlantisThemeContextProvider, { dangerouslyOverrideTheme: theme },
|
|
183
|
+
React.createElement(ContentOverlayKeyboardContext.Provider, { value: scrollEnabled }, scrollEnabled ? (React.createElement(BottomSheetKeyboardAwareScrollView, { ref: scrollViewRef, contentContainerStyle: { paddingBottom: insets.bottom }, keyboardShouldPersistTaps: keyboardShouldPersistTaps ? "handled" : "never", showsVerticalScrollIndicator: false, onScroll: handleOnScroll, stickyHeaderIndices: [0], bottomOffset: KEYBOARD_TOP_PADDING_AUTO_SCROLL },
|
|
184
|
+
renderHeader(),
|
|
185
|
+
React.createElement(View, { testID: "ATL-Overlay-Children" }, children))) : (React.createElement(BottomSheetView, null,
|
|
186
|
+
renderHeader(),
|
|
187
|
+
React.createElement(View, { style: { paddingBottom: insets.bottom }, testID: "ATL-Overlay-Children" }, children)))))));
|
|
183
188
|
}
|
|
184
189
|
function Backdrop(bottomSheetBackdropProps) {
|
|
185
190
|
const styles = useStyles();
|
|
@@ -42,14 +42,17 @@ function computeInitialHeight(config, isDraggable) {
|
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
44
|
* Draggability determination:
|
|
45
|
-
* - hasOnBeforeExit: true → false (
|
|
45
|
+
* - hasOnBeforeExit: true && !allowDragWithBeforeExit → false (legacy override)
|
|
46
|
+
* - hasOnBeforeExit: true && allowDragWithBeforeExit → use isDraggable prop value
|
|
46
47
|
* - Otherwise → use isDraggable prop value
|
|
47
48
|
*
|
|
48
|
-
*
|
|
49
|
-
* and dragging would bypass that interception.
|
|
49
|
+
* The legacy override exists because onBeforeExit needs to intercept close
|
|
50
|
+
* attempts, and dragging would bypass that interception. Consumers that
|
|
51
|
+
* explicitly accept the asymmetry (e.g. button-press confirmation,
|
|
52
|
+
* drag-dismiss silent) can opt out via `allowDragWithBeforeExit`.
|
|
50
53
|
*/
|
|
51
54
|
function computeIsDraggable(config) {
|
|
52
|
-
if (config.hasOnBeforeExit) {
|
|
55
|
+
if (config.hasOnBeforeExit && !config.allowDragWithBeforeExit) {
|
|
53
56
|
return false;
|
|
54
57
|
}
|
|
55
58
|
return config.isDraggable;
|
|
@@ -5,6 +5,7 @@ const defaultConfig = {
|
|
|
5
5
|
adjustToContentHeight: false,
|
|
6
6
|
isDraggable: true,
|
|
7
7
|
hasOnBeforeExit: false,
|
|
8
|
+
allowDragWithBeforeExit: false,
|
|
8
9
|
showDismiss: false,
|
|
9
10
|
};
|
|
10
11
|
const defaultState = {
|
|
@@ -69,6 +70,36 @@ describe("computeContentOverlayBehavior", () => {
|
|
|
69
70
|
const result = computeContentOverlayBehavior(config, state);
|
|
70
71
|
expect(result.isDraggable).toBe(false);
|
|
71
72
|
});
|
|
73
|
+
it("respects isDraggable=true when onBeforeExit is present and allowDragWithBeforeExit=true", () => {
|
|
74
|
+
const config = aConfig({
|
|
75
|
+
isDraggable: true,
|
|
76
|
+
hasOnBeforeExit: true,
|
|
77
|
+
allowDragWithBeforeExit: true,
|
|
78
|
+
});
|
|
79
|
+
const state = aState();
|
|
80
|
+
const result = computeContentOverlayBehavior(config, state);
|
|
81
|
+
expect(result.isDraggable).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
it("respects isDraggable=false when onBeforeExit is present and allowDragWithBeforeExit=true", () => {
|
|
84
|
+
const config = aConfig({
|
|
85
|
+
isDraggable: false,
|
|
86
|
+
hasOnBeforeExit: true,
|
|
87
|
+
allowDragWithBeforeExit: true,
|
|
88
|
+
});
|
|
89
|
+
const state = aState();
|
|
90
|
+
const result = computeContentOverlayBehavior(config, state);
|
|
91
|
+
expect(result.isDraggable).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
it("still overrides to false when onBeforeExit is present and allowDragWithBeforeExit=false (default)", () => {
|
|
94
|
+
const config = aConfig({
|
|
95
|
+
isDraggable: true,
|
|
96
|
+
hasOnBeforeExit: true,
|
|
97
|
+
allowDragWithBeforeExit: false,
|
|
98
|
+
});
|
|
99
|
+
const state = aState();
|
|
100
|
+
const result = computeContentOverlayBehavior(config, state);
|
|
101
|
+
expect(result.isDraggable).toBe(false);
|
|
102
|
+
});
|
|
72
103
|
});
|
|
73
104
|
describe("showDismiss", () => {
|
|
74
105
|
it("returns true when showDismiss prop is true", () => {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react-native";
|
|
3
|
+
import { Option, Select } from ".";
|
|
4
|
+
import { PortalHostWrapper } from "../utils/test";
|
|
5
|
+
describe("Select composable statics + coexistence", () => {
|
|
6
|
+
it("exposes the composable parts as statics on Select", () => {
|
|
7
|
+
expect(Object.keys(Select)).toEqual(expect.arrayContaining([
|
|
8
|
+
"Root",
|
|
9
|
+
"Trigger",
|
|
10
|
+
"Value",
|
|
11
|
+
"Label",
|
|
12
|
+
"Content",
|
|
13
|
+
"Item",
|
|
14
|
+
"Group",
|
|
15
|
+
"GroupLabel",
|
|
16
|
+
"Separator",
|
|
17
|
+
]));
|
|
18
|
+
});
|
|
19
|
+
it("renders through the public statics with no react-hook-form context", () => {
|
|
20
|
+
render(React.createElement(Select.Root, null,
|
|
21
|
+
React.createElement(Select.Label, null, "City"),
|
|
22
|
+
React.createElement(Select.Trigger, null,
|
|
23
|
+
React.createElement(Select.Value, { placeholder: "Select a city" })),
|
|
24
|
+
React.createElement(Select.Content, null,
|
|
25
|
+
React.createElement(Select.Item, { value: "tor", label: "Toronto" }))), { wrapper: PortalHostWrapper });
|
|
26
|
+
expect(screen.getByText("City", { includeHiddenElements: true })).toBeDefined();
|
|
27
|
+
expect(screen.getByTestId("ATL-Select-Trigger")).toBeDefined();
|
|
28
|
+
});
|
|
29
|
+
it("keeps the legacy props-driven Select callable and renders Option", () => {
|
|
30
|
+
render(React.createElement(Select, { onChange: jest.fn() },
|
|
31
|
+
React.createElement(Option, { value: "tor" }, "Toronto"),
|
|
32
|
+
React.createElement(Option, { value: "van" }, "Vancouver")));
|
|
33
|
+
// Legacy trigger testID is preserved and does not collide with the
|
|
34
|
+
// composable trigger's ATL-Select-Trigger.
|
|
35
|
+
expect(screen.getByTestId("ATL-Select")).toBeDefined();
|
|
36
|
+
expect(screen.queryByTestId("ATL-Select-Trigger")).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
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, { useCallback, useMemo, useRef, useState } from "react";
|
|
13
|
+
import { View } from "react-native";
|
|
14
|
+
import { useStyles } from "./SelectRoot.style";
|
|
15
|
+
import { SelectFieldContext } from "./components/SelectFieldContext";
|
|
16
|
+
import { FieldLabel } from "./components/SelectLabel";
|
|
17
|
+
import { HelperText } from "../primitives/HelperText";
|
|
18
|
+
import { SelectPrimitive } from "../primitives/Select";
|
|
19
|
+
/**
|
|
20
|
+
* Composable `Select.Root` chrome: renders the optional `description` / `error`
|
|
21
|
+
* below the field (via `HelperText`), owns the field-state context, and places the
|
|
22
|
+
* field label. The label content is authored via `Select.Label` (registered into
|
|
23
|
+
* this context); `labelPlacement` decides whether it renders in the above slot
|
|
24
|
+
* (here) or the inside slot (`Select.Trigger`). `error` takes priority over
|
|
25
|
+
* `description`. See `Select.guide.md` for the label / accessibility / layering
|
|
26
|
+
* model.
|
|
27
|
+
*/
|
|
28
|
+
export function SelectRoot(_a) {
|
|
29
|
+
var { labelPlacement = "inside", accessibilityLabel: accessibilityLabelProp, description, error, invalid, status, readOnly, children } = _a, rootProps = __rest(_a, ["labelPlacement", "accessibilityLabel", "description", "error", "invalid", "status", "readOnly", "children"]);
|
|
30
|
+
const styles = useStyles();
|
|
31
|
+
// An empty / whitespace-only string error counts as "no error".
|
|
32
|
+
const hasError = typeof error === "string" ? error.trim().length > 0 : error != null;
|
|
33
|
+
const isInvalid = invalid === true || status === "critical" || hasError;
|
|
34
|
+
// A composed `Select.Label` registers its content + (for a string) its
|
|
35
|
+
// accessible name here.
|
|
36
|
+
const [registered, setRegistered] = useState();
|
|
37
|
+
// Track how many `Select.Label`s are mounted to warn on unsupported
|
|
38
|
+
// multi-label usage (the registry holds a single label — the last one wins).
|
|
39
|
+
const labelCount = useRef(0);
|
|
40
|
+
const registerLabel = useCallback((content, name) => {
|
|
41
|
+
labelCount.current += 1;
|
|
42
|
+
if (labelCount.current > 1) {
|
|
43
|
+
console.warn("Select: multiple <Select.Label> are composed — only one is supported; the last one wins.");
|
|
44
|
+
}
|
|
45
|
+
setRegistered({ content, name });
|
|
46
|
+
}, []);
|
|
47
|
+
const unregisterLabel = useCallback(() => {
|
|
48
|
+
labelCount.current = Math.max(0, labelCount.current - 1);
|
|
49
|
+
setRegistered(undefined);
|
|
50
|
+
}, []);
|
|
51
|
+
const labelContent = registered === null || registered === void 0 ? void 0 : registered.content;
|
|
52
|
+
const accessibilityLabel = accessibilityLabelProp !== null && accessibilityLabelProp !== void 0 ? accessibilityLabelProp : registered === null || registered === void 0 ? void 0 : registered.name;
|
|
53
|
+
const fieldState = useMemo(() => ({
|
|
54
|
+
labelPlacement,
|
|
55
|
+
labelContent,
|
|
56
|
+
accessibilityLabel,
|
|
57
|
+
invalid: isInvalid,
|
|
58
|
+
readOnly,
|
|
59
|
+
registerLabel,
|
|
60
|
+
unregisterLabel,
|
|
61
|
+
}), [
|
|
62
|
+
labelPlacement,
|
|
63
|
+
labelContent,
|
|
64
|
+
accessibilityLabel,
|
|
65
|
+
isInvalid,
|
|
66
|
+
readOnly,
|
|
67
|
+
registerLabel,
|
|
68
|
+
unregisterLabel,
|
|
69
|
+
]);
|
|
70
|
+
return (React.createElement(View, { style: styles.container },
|
|
71
|
+
React.createElement(SelectFieldContext.Provider, { value: fieldState },
|
|
72
|
+
React.createElement(SelectPrimitive.Root, Object.assign({ style: styles.field }, rootProps),
|
|
73
|
+
labelPlacement === "above" && React.createElement(FieldLabel, null),
|
|
74
|
+
children)),
|
|
75
|
+
renderBelowField(hasError ? error : undefined, description)));
|
|
76
|
+
}
|
|
77
|
+
function renderBelowField(error, description) {
|
|
78
|
+
if (error != null) {
|
|
79
|
+
return typeof error === "string" ? (React.createElement(HelperText, { message: error, status: "critical" })) : (error);
|
|
80
|
+
}
|
|
81
|
+
if (description != null) {
|
|
82
|
+
return typeof description === "string" ? (React.createElement(HelperText, { message: description, status: "neutral" })) : (description);
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|