@jobber/components-native 0.105.4 → 0.105.5-contentove-a24d18c.6
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/Chip/Chip.md +4 -4
- package/dist/docs/usage-guidelines/usage-guidelines.md +5 -5
- package/dist/package.json +2 -2
- package/dist/src/ContentOverlay/ContentOverlay.js +59 -8
- package/dist/src/ContentOverlay/ContentOverlay.test.js +35 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +1 -1
- package/dist/types/src/ContentOverlay/types.d.ts +23 -0
- package/package.json +2 -2
- package/src/ContentOverlay/ContentOverlay.stories.tsx +15 -1
- package/src/ContentOverlay/ContentOverlay.test.tsx +56 -0
- package/src/ContentOverlay/ContentOverlay.tsx +98 -9
- package/src/ContentOverlay/docs/ContentOverlayWithFooter.tsx +85 -0
- package/src/ContentOverlay/docs/index.ts +1 -0
- package/src/ContentOverlay/types.ts +23 -0
package/dist/docs/Chip/Chip.md
CHANGED
|
@@ -4,7 +4,7 @@ Chip is a flexible component that can be used for
|
|
|
4
4
|
|
|
5
5
|
* inline single- or multi-selection of items
|
|
6
6
|
* triggering filtering and selection components like
|
|
7
|
-
[
|
|
7
|
+
[FilterPicker](/components/FilterPicker)
|
|
8
8
|
* presenting grouped items that can be added or removed
|
|
9
9
|
|
|
10
10
|
```tsx
|
|
@@ -236,8 +236,8 @@ export function ChipInvalidExample(
|
|
|
236
236
|
|
|
237
237
|
* [Chips](/components/Chips) is a convenience wrapper that offers the
|
|
238
238
|
single-select, multi-select, and add/dismiss functionality "out of the box"
|
|
239
|
-
* [
|
|
240
|
-
a separate component
|
|
239
|
+
* [FilterPicker](/components/FilterPicker) is most commonly triggered by a Chip,
|
|
240
|
+
but is a separate component
|
|
241
241
|
* [Select](../Select/Select.md) is a simpler single-select "dropdown" that
|
|
242
242
|
presents as a form element and should be preferred in forms
|
|
243
243
|
* [RadioGroup](/components/RadioGroup) should be used to allow the user to
|
|
@@ -256,7 +256,7 @@ ideally 1–2 words. If any of the options in the group may have longer labels,
|
|
|
256
256
|
consider Checkbox or Radio as necessary for your selection type.
|
|
257
257
|
|
|
258
258
|
In cases where a Chip displays name of its selections, such as when used to
|
|
259
|
-
trigger a
|
|
259
|
+
trigger a FilterPicker or a date range selector, use the heading to identify the
|
|
260
260
|
"category" and the label to identify the selected items.
|
|
261
261
|
|
|
262
262
|
## Accessibility
|
|
@@ -17,10 +17,10 @@ deprecated.
|
|
|
17
17
|
|
|
18
18
|
## Deprecated components — do not use
|
|
19
19
|
|
|
20
|
-
| Deprecated | Replacement
|
|
21
|
-
| --------------------- |
|
|
22
|
-
| `MultiSelect` | `Autocomplete` with `multiple` prop, or `
|
|
23
|
-
| `Chips` (dismissible) | `Autocomplete` with `multiple` prop
|
|
20
|
+
| Deprecated | Replacement | Notes |
|
|
21
|
+
| --------------------- | ------------------------------------------------------ | ---------------------------------------- |
|
|
22
|
+
| `MultiSelect` | `Autocomplete` with `multiple` prop, or `FilterPicker` | Fully deprecated, no longer supported. |
|
|
23
|
+
| `Chips` (dismissible) | `Autocomplete` with `multiple` prop | Dismissible Chips variant is deprecated. |
|
|
24
24
|
|
|
25
25
|
### Deprecated props on still-supported components
|
|
26
26
|
|
|
@@ -77,7 +77,7 @@ parent reads its compound children to wire up state, accessibility, and styling.
|
|
|
77
77
|
| `Modal` v2 | `Modal.Provider`, `Modal.Activator`, `Modal.Content`, `Modal.Header`, `Modal.Actions` | Wrap with `Modal.Provider`. `Modal.Header` and `Modal.Actions` must be **direct children** of `Modal.Content` for sticky behavior to work. Use `Modal.Activator` to control focus return when the trigger element unmounts. |
|
|
78
78
|
| `Banner` | `Banner.Provider`, `Banner.Content`, `Banner.Icon`, `Banner.DismissButton`, `Banner.Action` | Either the simple `<Banner type="...">children</Banner>` form, or the Provider form: `Banner.Provider` > `Banner.Content` (+ optional `Banner.Icon` / `Banner.Action` / `Banner.DismissButton`). |
|
|
79
79
|
| `Card` | `Card.Header`, `Card.Body` | `Card` > `Card.Header` + `Card.Body`. The legacy `header` string/element prop still works for simple cases. Use composition for any header that needs richer markup. |
|
|
80
|
-
| `
|
|
80
|
+
| `FilterPicker` | `FilterPicker.Activator`, `FilterPicker.Option`, `FilterPicker.Action` | `FilterPicker` > optional `FilterPicker.Activator`, one or more `FilterPicker.Option`, optional `FilterPicker.Action`. Omitting `FilterPicker.Activator` renders the default activator. |
|
|
81
81
|
| `DataTable` | `DataTable.Container`, `DataTable.Table`, `DataTable.Header`, `DataTable.HeaderCell`, `DataTable.SortableHeader`, `DataTable.Body`, `DataTable.Row`, `DataTable.Cell`, `DataTable.RowActions`, `DataTable.Actions`, `DataTable.Footer`, `DataTable.Pagination`, `DataTable.PaginationButton` | `DataTable.Footer` is a sibling of `DataTable.Body`, not a child. Place pagination outside the table (not inside the footer). For `layout="fixed"`, pass explicit widths via `style={{ width: "..." }}` on each `HeaderCell`. |
|
|
82
82
|
| `Menu` (web composable) | `Menu.Trigger`, `Menu.Content`, `Menu.Section`, `Menu.Header`, `Menu.HeaderLabel`, `Menu.Item`, `Menu.ItemIcon`, `Menu.ItemLabel`, `Menu.Separator` | `Menu` > `Menu.Trigger` + `Menu.Content` containing `Menu.Item`s (optionally grouped in `Menu.Section`s). Each `Menu.Item` must include `textValue` for type-ahead and screen readers. |
|
|
83
83
|
| `Page` | `Page.Header`, `Page.HeaderContent`, `Page.TitleBar`, `Page.Title`, `Page.Subtitle`, `Page.Intro`, `Page.Actions`, `Page.ActionPrimary`, `Page.ActionSecondary`, `Page.ActionMenu`, `Page.PrimaryButton`, `Page.SecondaryButton`, `Page.Menu`, `Page.Body` | Composable `Page` API: `Page` > `Page.Header` (containing `Page.HeaderContent` with `Page.TitleBar` > `Page.Title` / `Page.Subtitle`, and `Page.Actions` with `Page.ActionPrimary` / `Page.ActionSecondary` / `Page.ActionMenu`) + `Page.Body`. The legacy props-based API (`title`, `subtitle`, `intro`, etc.) still works. |
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.105.
|
|
3
|
+
"version": "0.105.5-contentove-a24d18c.6+a24d18c5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native implementation of Atlantis",
|
|
6
6
|
"repository": {
|
|
@@ -122,5 +122,5 @@
|
|
|
122
122
|
"react-native-screens": ">=4.18.0",
|
|
123
123
|
"react-native-svg": ">=12.0.0"
|
|
124
124
|
},
|
|
125
|
-
"gitHead": "
|
|
125
|
+
"gitHead": "a24d18c5b55ad4dcee34ba37652c831b13f89928"
|
|
126
126
|
}
|
|
@@ -12,11 +12,12 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
12
12
|
import React, { createContext, useContext, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
13
13
|
import { AccessibilityInfo, Platform, View, findNodeHandle, useWindowDimensions, } from "react-native";
|
|
14
14
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
15
|
-
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetView, } from "@gorhom/bottom-sheet";
|
|
15
|
+
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView, BottomSheetView, } from "@gorhom/bottom-sheet";
|
|
16
16
|
import { FullWindowOverlay } from "react-native-screens";
|
|
17
17
|
import { BottomSheetKeyboardAwareScrollView } from "./BottomSheetKeyboardAwareScrollView";
|
|
18
18
|
import { useStyles } from "./ContentOverlay.style";
|
|
19
19
|
import { useBottomSheetModalBackHandler } from "./hooks/useBottomSheetModalBackHandler";
|
|
20
|
+
import { useKeyboardVisibility } from "./hooks/useKeyboardVisibility";
|
|
20
21
|
import { computeContentOverlayBehavior } from "./computeContentOverlayBehavior";
|
|
21
22
|
import { KEYBOARD_TOP_PADDING_AUTO_SCROLL } from "./constants";
|
|
22
23
|
import { useIsScreenReaderEnabled } from "../hooks";
|
|
@@ -46,8 +47,9 @@ function getModalBackgroundColor(variation, tokens) {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
// 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, }) {
|
|
50
|
+
export function ContentOverlay({ children, title, accessibilityLabel, fullScreen = false, showDismiss = false, isDraggable = true, adjustToContentHeight = false, keyboardShouldPersistTaps = false, scrollEnabled = false, modalBackgroundColor = "surface", footerComponent, snapPoints: snapPointsProp, onClose, onOpen, onBeforeExit, loading = false, ref, }) {
|
|
50
51
|
const insets = useSafeAreaInsets();
|
|
52
|
+
const { isKeyboardVisible } = useKeyboardVisibility();
|
|
51
53
|
const { width: windowWidth } = useWindowDimensions();
|
|
52
54
|
const bottomSheetModalRef = useRef(null);
|
|
53
55
|
const previousIndexRef = useRef(-1);
|
|
@@ -68,20 +70,47 @@ export function ContentOverlay({ children, title, accessibilityLabel, fullScreen
|
|
|
68
70
|
});
|
|
69
71
|
const effectiveIsDraggable = behavior.isDraggable;
|
|
70
72
|
const shouldShowDismiss = behavior.showDismiss;
|
|
73
|
+
// renderHeader() only renders when there's a title or dismiss button. Making a
|
|
74
|
+
// null first child "sticky" breaks the scroll view (phantom sticky layer that
|
|
75
|
+
// blocks scrolling), so only set sticky header indices when a header exists.
|
|
76
|
+
const hasHeader = Boolean(title || shouldShowDismiss);
|
|
77
|
+
const stickyHeaderIndices = hasHeader ? [0] : undefined;
|
|
71
78
|
const isCloseableOnOverlayTap = onBeforeExit === undefined;
|
|
72
79
|
// Prevent the Overlay from being flush with the top of the screen, even if we are "100%" or "fullscreen"
|
|
73
80
|
const topInset = insets.top || tokens["space-larger"];
|
|
74
81
|
const [showHeaderShadow, setShowHeaderShadow] = useState(false);
|
|
75
82
|
const overlayHeader = useRef(null);
|
|
76
83
|
const scrollViewRef = useRef(null);
|
|
84
|
+
// Footer/list pattern: footerComponent is rendered by us as the bottom row of
|
|
85
|
+
// a flex column (see the body below), not via gorhom's footer. gorhom owns the
|
|
86
|
+
// keyboard — the footer input registers with it, so focusing it lifts the whole
|
|
87
|
+
// sheet above the keyboard, carrying the footer with it. See OpenSpec change
|
|
88
|
+
// contentoverlay-footer-and-scroll-fix (design.md D1).
|
|
89
|
+
const hasFooter = footerComponent !== undefined;
|
|
90
|
+
const isContentHeight = behavior.initialHeight === "contentHeight";
|
|
91
|
+
const hasCustomSnapPoints = snapPointsProp !== undefined && snapPointsProp.length > 0;
|
|
77
92
|
// enableDynamicSizing will add another snap point of the content height
|
|
78
93
|
const snapPoints = useMemo(() => {
|
|
79
|
-
//
|
|
94
|
+
// A consumer-provided snap point gives the overlay a stable, fixed height
|
|
95
|
+
// (dynamic sizing is turned off below). This generalizes the hardcoded
|
|
96
|
+
// ["100%"]: any explicit detent also satisfies gorhom's keyboard "restore".
|
|
97
|
+
if (hasCustomSnapPoints) {
|
|
98
|
+
return snapPointsProp;
|
|
99
|
+
}
|
|
100
|
+
// Only drop the ["100%"] workaround in the footer path WHEN dynamic sizing
|
|
101
|
+
// is actually on — gorhom then provides the content-height detent. If we
|
|
102
|
+
// dropped it without dynamic sizing (e.g. footer + fullScreen) the sheet
|
|
103
|
+
// would have no detents at all.
|
|
104
|
+
if (hasFooter && isContentHeight) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
// There is a bug with "restore" behavior after the keyboard is dismissed.
|
|
80
108
|
// https://github.com/gorhom/react-native-bottom-sheet/issues/2465
|
|
81
|
-
//
|
|
82
|
-
//
|
|
109
|
+
// Providing a 100% snap point "fixes" it for now. The upstream fix
|
|
110
|
+
// (https://github.com/gorhom/react-native-bottom-sheet/pull/2511) was
|
|
111
|
+
// closed without merging, so there is no released fix to wait for.
|
|
83
112
|
return ["100%"];
|
|
84
|
-
}, []);
|
|
113
|
+
}, [hasCustomSnapPoints, snapPointsProp, hasFooter, isContentHeight]);
|
|
85
114
|
const onCloseController = () => {
|
|
86
115
|
var _a;
|
|
87
116
|
if (!onBeforeExit) {
|
|
@@ -174,8 +203,30 @@ export function ContentOverlay({ children, title, accessibilityLabel, fullScreen
|
|
|
174
203
|
const backdropComponent = useMemo(() => function ContentOverlayBackdrop(props) {
|
|
175
204
|
return (React.createElement(Backdrop, Object.assign({}, props, { pressBehavior: isCloseableOnOverlayTap ? "close" : "none" })));
|
|
176
205
|
}, [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,
|
|
178
|
-
|
|
206
|
+
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,
|
|
207
|
+
// A scrollable body (gorhom's BottomSheetScrollView/KASV) only wires up its
|
|
208
|
+
// scroll gesture when content panning is enabled, so it must stay on even
|
|
209
|
+
// when the sheet itself isn't draggable — otherwise the list won't scroll.
|
|
210
|
+
// Drag-to-dismiss is still gated by enablePanDownToClose/handle panning.
|
|
211
|
+
enableContentPanningGesture: effectiveIsDraggable || scrollEnabled, enableHandlePanningGesture: effectiveIsDraggable, enableDynamicSizing: !hasCustomSnapPoints && isContentHeight, keyboardBehavior: "interactive", keyboardBlurBehavior: "restore", topInset: topInset, onDismiss: () => onClose === null || onClose === void 0 ? void 0 : onClose() },
|
|
212
|
+
React.createElement(ContentOverlayKeyboardContext.Provider, { value: scrollEnabled && !hasFooter }, hasFooter ? (
|
|
213
|
+
// Footer/list pattern: footerComponent is the bottom row of a flex
|
|
214
|
+
// column, so it stays pinned to the bottom of the sheet while the body
|
|
215
|
+
// scrolls above it. gorhom's interactive keyboardBehavior lifts the
|
|
216
|
+
// whole sheet when the footer input is focused, carrying the footer
|
|
217
|
+
// above the keyboard — no gorhom BottomSheetFooter needed.
|
|
218
|
+
React.createElement(View, { style: { flex: 1 } },
|
|
219
|
+
scrollEnabled ? (React.createElement(BottomSheetScrollView, { ref: scrollViewRef, style: { flex: 1 }, keyboardShouldPersistTaps: keyboardShouldPersistTaps ? "handled" : "never", showsVerticalScrollIndicator: false, onScroll: handleOnScroll, stickyHeaderIndices: stickyHeaderIndices },
|
|
220
|
+
renderHeader(),
|
|
221
|
+
React.createElement(View, { testID: "ATL-Overlay-Children" }, children))) : (React.createElement(BottomSheetView, { style: { flex: 1 } },
|
|
222
|
+
renderHeader(),
|
|
223
|
+
React.createElement(View, { testID: "ATL-Overlay-Children" }, children))),
|
|
224
|
+
React.createElement(View, { style: {
|
|
225
|
+
// Only reserve the safe-area inset when the keyboard is hidden;
|
|
226
|
+
// while it's open the keyboard already bounds the bottom, so the
|
|
227
|
+
// inset would just add a gap between the footer and the keyboard.
|
|
228
|
+
paddingBottom: isKeyboardVisible ? 0 : insets.bottom,
|
|
229
|
+
} }, footerComponent))) : scrollEnabled ? (React.createElement(BottomSheetKeyboardAwareScrollView, { ref: scrollViewRef, contentContainerStyle: { paddingBottom: insets.bottom }, keyboardShouldPersistTaps: keyboardShouldPersistTaps ? "handled" : "never", showsVerticalScrollIndicator: false, onScroll: handleOnScroll, stickyHeaderIndices: stickyHeaderIndices, bottomOffset: KEYBOARD_TOP_PADDING_AUTO_SCROLL },
|
|
179
230
|
renderHeader(),
|
|
180
231
|
React.createElement(View, { testID: "ATL-Overlay-Children" }, children))) : (React.createElement(BottomSheetView, null,
|
|
181
232
|
renderHeader(),
|
|
@@ -295,3 +295,38 @@ describe("title prop", () => {
|
|
|
295
295
|
}));
|
|
296
296
|
});
|
|
297
297
|
});
|
|
298
|
+
describe("when a footerComponent is provided", () => {
|
|
299
|
+
function renderWithFooter() {
|
|
300
|
+
const contentOverlayRef = createRef();
|
|
301
|
+
render(React.createElement(AtlantisOverlayProvider, null,
|
|
302
|
+
React.createElement(View, null,
|
|
303
|
+
React.createElement(Button, { label: "Open Content Overlay", onPress: () => { var _a, _b; return (_b = (_a = contentOverlayRef.current) === null || _a === void 0 ? void 0 : _a.open) === null || _b === void 0 ? void 0 : _b.call(_a); } }),
|
|
304
|
+
React.createElement(ContentOverlay, { ref: contentOverlayRef, adjustToContentHeight: true, scrollEnabled: true, onOpen: jest.fn(), footerComponent: React.createElement(Text, null, "Search team") },
|
|
305
|
+
React.createElement(Content, null,
|
|
306
|
+
React.createElement(Text, null, "Marty Fayes"))))));
|
|
307
|
+
return contentOverlayRef;
|
|
308
|
+
}
|
|
309
|
+
function openWithFooter() {
|
|
310
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
311
|
+
jest.useFakeTimers();
|
|
312
|
+
renderWithFooter();
|
|
313
|
+
yield user.press(screen.getByLabelText("Open Content Overlay"));
|
|
314
|
+
yield act(() => __awaiter(this, void 0, void 0, function* () {
|
|
315
|
+
jest.runAllTimers();
|
|
316
|
+
}));
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
it("renders the footer content alongside the body content", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
320
|
+
yield openWithFooter();
|
|
321
|
+
yield waitFor(() => {
|
|
322
|
+
expect(screen.getByText("Search team")).toBeDefined();
|
|
323
|
+
expect(screen.getByText("Marty Fayes")).toBeDefined();
|
|
324
|
+
});
|
|
325
|
+
}));
|
|
326
|
+
it("still exposes the overlay body content container", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
327
|
+
yield openWithFooter();
|
|
328
|
+
yield waitFor(() => {
|
|
329
|
+
expect(screen.getByTestId("ATL-Overlay-Children")).toBeDefined();
|
|
330
|
+
});
|
|
331
|
+
}));
|
|
332
|
+
});
|