@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.
@@ -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
- [Combobox](/components/Combobox)
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
- * [Combobox](/components/Combobox) is most commonly triggered by a Chip, but is
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 Combobox or a date range selector, use the heading to identify the
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 | Notes |
21
- | --------------------- | -------------------------------------------------- | ---------------------------------------- |
22
- | `MultiSelect` | `Autocomplete` with `multiple` prop, or `Combobox` | Fully deprecated, no longer supported. |
23
- | `Chips` (dismissible) | `Autocomplete` with `multiple` prop | Dismissible Chips variant is deprecated. |
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
- | `Combobox` | `Combobox.Activator`, `Combobox.Option`, `Combobox.Action` | `Combobox` > optional `Combobox.Activator`, one or more `Combobox.Option`, optional `Combobox.Action`. Omitting `Combobox.Activator` renders the default activator. |
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.4",
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": "7fb12fadcd1c5e1440bccccae13dd29cf74dae28"
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
- // There is a bug with "restore" behavior after keyboard is dismissed.
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
- // providing a 100% snap point "fixes" it for now, but there is an approved PR to fix it
82
- // that just needs to be merged and released: https://github.com/gorhom/react-native-bottom-sheet/pull/2511
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, 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(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 },
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
+ });