@jobber/components-native 0.105.4 → 0.105.5-contentove-4cd673f.10

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
@@ -50,6 +50,7 @@ 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
+ | `footerComponent` | `ReactNode` | No | `undefined` | Content pinned to the bottom of the overlay, above the keyboard — e.g. a persistent search input (results scroll abov... |
53
54
  | `fullScreen` | `boolean` | No | `false` | Force overlay height to fill the screen. Width not impacted. |
54
55
  | `isDraggable` | `boolean` | No | `true` | If false, hides the handle and turns off dragging. |
55
56
  | `keyboardShouldPersistTaps` | `boolean` | No | `false` | Allows taps to be registered behind keyboard if enabled |
@@ -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-4cd673f.10+4cd673ff",
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": "4cd673ff20841cf9e6ea375cf0577310ffaae365"
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, 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,18 +70,22 @@ export function ContentOverlay({ children, title, accessibilityLabel, fullScreen
68
70
  });
69
71
  const effectiveIsDraggable = behavior.isDraggable;
70
72
  const shouldShowDismiss = behavior.showDismiss;
73
+ const hasHeader = Boolean(title || shouldShowDismiss);
74
+ const stickyHeaderIndices = hasHeader ? [0] : undefined;
71
75
  const isCloseableOnOverlayTap = onBeforeExit === undefined;
72
76
  // Prevent the Overlay from being flush with the top of the screen, even if we are "100%" or "fullscreen"
73
77
  const topInset = insets.top || tokens["space-larger"];
74
78
  const [showHeaderShadow, setShowHeaderShadow] = useState(false);
75
79
  const overlayHeader = useRef(null);
76
80
  const scrollViewRef = useRef(null);
81
+ const hasFooter = footerComponent !== undefined;
77
82
  // enableDynamicSizing will add another snap point of the content height
78
83
  const snapPoints = useMemo(() => {
79
- // There is a bug with "restore" behavior after keyboard is dismissed.
84
+ // There is a bug with "restore" behavior after the keyboard is dismissed.
80
85
  // 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
86
+ // Providing a 100% snap point "fixes" it for now. The upstream fix
87
+ // (https://github.com/gorhom/react-native-bottom-sheet/pull/2511) was
88
+ // closed without merging, so there is no released fix to wait for.
83
89
  return ["100%"];
84
90
  }, []);
85
91
  const onCloseController = () => {
@@ -174,8 +180,16 @@ export function ContentOverlay({ children, title, accessibilityLabel, fullScreen
174
180
  const backdropComponent = useMemo(() => function ContentOverlayBackdrop(props) {
175
181
  return (React.createElement(Backdrop, Object.assign({}, props, { pressBehavior: isCloseableOnOverlayTap ? "close" : "none" })));
176
182
  }, [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 },
183
+ 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 || scrollEnabled, enableHandlePanningGesture: effectiveIsDraggable, enableDynamicSizing: behavior.initialHeight === "contentHeight", keyboardBehavior: "interactive", keyboardBlurBehavior: "restore", topInset: topInset, onDismiss: () => onClose === null || onClose === void 0 ? void 0 : onClose() },
184
+ React.createElement(ContentOverlayKeyboardContext.Provider, { value: scrollEnabled && !hasFooter }, hasFooter ? (React.createElement(View, { style: { flex: 1 } },
185
+ scrollEnabled ? (React.createElement(BottomSheetScrollView, { ref: scrollViewRef, style: { flex: 1 }, keyboardShouldPersistTaps: keyboardShouldPersistTaps ? "handled" : "never", showsVerticalScrollIndicator: false, onScroll: handleOnScroll, stickyHeaderIndices: stickyHeaderIndices },
186
+ renderHeader(),
187
+ React.createElement(View, { testID: "ATL-Overlay-Children" }, children))) : (React.createElement(BottomSheetView, { style: { flex: 1 } },
188
+ renderHeader(),
189
+ React.createElement(View, { testID: "ATL-Overlay-Children" }, children))),
190
+ React.createElement(View, { style: {
191
+ paddingBottom: isKeyboardVisible ? 0 : insets.bottom,
192
+ } }, 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
193
  renderHeader(),
180
194
  React.createElement(View, { testID: "ATL-Overlay-Children" }, children))) : (React.createElement(BottomSheetView, null,
181
195
  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
+ });