@jobber/components-native 0.95.4-improve-co-ca924fd.14 → 0.95.4
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/package.json +5 -3
- package/dist/src/ContentOverlay/ContentOverlay.js +107 -128
- package/dist/src/ContentOverlay/ContentOverlay.style.js +12 -8
- package/dist/src/ContentOverlay/UNSAFE_WrappedModalize.js +23 -0
- package/dist/src/ContentOverlay/index.js +0 -1
- package/dist/src/InputFieldWrapper/InputFieldWrapper.js +12 -2
- package/dist/src/InputText/InputText.js +2 -36
- package/dist/src/utils/meta/meta.json +0 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +5 -1
- package/dist/types/src/ContentOverlay/ContentOverlay.style.d.ts +10 -11
- package/dist/types/src/ContentOverlay/UNSAFE_WrappedModalize.d.ts +3 -0
- package/dist/types/src/ContentOverlay/index.d.ts +0 -1
- package/dist/types/src/ContentOverlay/types.d.ts +12 -5
- package/dist/types/src/InputFieldWrapper/InputFieldWrapper.d.ts +5 -1
- package/jestSetup.js +0 -2
- package/package.json +5 -3
- package/src/ContentOverlay/ContentOverlay.style.ts +12 -12
- package/src/ContentOverlay/ContentOverlay.test.tsx +79 -157
- package/src/ContentOverlay/ContentOverlay.tsx +210 -223
- package/src/ContentOverlay/UNSAFE_WrappedModalize.tsx +41 -0
- package/src/ContentOverlay/index.ts +0 -1
- package/src/ContentOverlay/types.ts +13 -5
- package/src/InputFieldWrapper/InputFieldWrapper.test.tsx +47 -0
- package/src/InputFieldWrapper/InputFieldWrapper.tsx +19 -1
- package/src/InputText/InputText.test.tsx +0 -122
- package/src/InputText/InputText.tsx +3 -52
- package/src/ThumbnailList/__snapshots__/ThumbnailList.test.tsx.snap +20 -0
- package/src/utils/meta/meta.json +0 -1
- package/dist/src/ContentOverlay/ContentOverlayProvider.js +0 -5
- package/dist/src/ContentOverlay/computeContentOverlayBehavior.js +0 -76
- package/dist/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.js +0 -25
- package/dist/types/src/ContentOverlay/ContentOverlayProvider.d.ts +0 -6
- package/dist/types/src/ContentOverlay/computeContentOverlayBehavior.d.ts +0 -32
- package/dist/types/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.d.ts +0 -7
- package/src/ContentOverlay/ContentOverlay.stories.tsx +0 -59
- package/src/ContentOverlay/ContentOverlayProvider.tsx +0 -12
- package/src/ContentOverlay/computeContentOverlayBehavior.test.ts +0 -276
- package/src/ContentOverlay/computeContentOverlayBehavior.ts +0 -119
- package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.test.ts +0 -81
- package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.ts +0 -36
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
3
|
-
import { Text as RNText, View } from "react-native";
|
|
3
|
+
import { PixelRatio, Platform, Text as RNText, View } from "react-native";
|
|
4
4
|
import type { FieldError } from "react-hook-form";
|
|
5
5
|
import type { IconNames } from "@jobber/design";
|
|
6
6
|
import { useStyles } from "./InputFieldWrapper.style";
|
|
@@ -112,6 +112,11 @@ export interface InputFieldWrapperProps {
|
|
|
112
112
|
* Change the type of loading indicator to spinner or glimmer.
|
|
113
113
|
*/
|
|
114
114
|
readonly loadingType?: "spinner" | "glimmer";
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether the input is a multiline input.
|
|
118
|
+
*/
|
|
119
|
+
readonly multiline?: boolean;
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
export const INPUT_FIELD_WRAPPER_GLIMMERS_TEST_ID =
|
|
@@ -138,6 +143,7 @@ export function InputFieldWrapper({
|
|
|
138
143
|
toolbarVisibility = "while-editing",
|
|
139
144
|
loading = false,
|
|
140
145
|
loadingType = "spinner",
|
|
146
|
+
multiline = false,
|
|
141
147
|
}: InputFieldWrapperProps) {
|
|
142
148
|
fieldAffixRequiredPropsCheck([prefix, suffix]);
|
|
143
149
|
const handleClear = onClear ?? noopClear;
|
|
@@ -163,6 +169,9 @@ export function InputFieldWrapper({
|
|
|
163
169
|
(Boolean(invalid) || error) && styles.inputInvalid,
|
|
164
170
|
disabled && styles.disabled,
|
|
165
171
|
styleOverride?.container,
|
|
172
|
+
shouldApplyScrollTrapWorkaround(multiline) && {
|
|
173
|
+
maxWidth: "90%",
|
|
174
|
+
},
|
|
166
175
|
]}
|
|
167
176
|
>
|
|
168
177
|
<View style={styles.field}>
|
|
@@ -283,6 +292,15 @@ export function InputFieldWrapper({
|
|
|
283
292
|
);
|
|
284
293
|
}
|
|
285
294
|
|
|
295
|
+
function shouldApplyScrollTrapWorkaround(isMultiline: boolean): boolean {
|
|
296
|
+
const isCustomFontScale = PixelRatio.getFontScale() !== 1;
|
|
297
|
+
|
|
298
|
+
// On iOS, when the OS font scale is not default, it causes multiline inputs to become scroll-trapped
|
|
299
|
+
// which prevents the user from scrolling the parent form view. For now, we're working around this
|
|
300
|
+
// by limiting the width of the input field, thus providing a gap so the user can scroll more easily.
|
|
301
|
+
return isMultiline && Platform.OS === "ios" && isCustomFontScale;
|
|
302
|
+
}
|
|
303
|
+
|
|
286
304
|
function getLabelVariation(
|
|
287
305
|
error?: FieldError,
|
|
288
306
|
invalid?: boolean | string,
|
|
@@ -836,126 +836,4 @@ describe("Transform", () => {
|
|
|
836
836
|
});
|
|
837
837
|
});
|
|
838
838
|
});
|
|
839
|
-
|
|
840
|
-
describe("Bottom Sheet keyboard handling integration", () => {
|
|
841
|
-
beforeEach(() => {
|
|
842
|
-
jest.clearAllMocks();
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
// eslint-disable-next-line max-statements
|
|
846
|
-
it("updates animatedKeyboardState on focus when inside ContentOverlay", () => {
|
|
847
|
-
const mockSet = jest.fn();
|
|
848
|
-
const mockGet = jest.fn(() => ({ target: undefined }));
|
|
849
|
-
const mockKeyboardState = {
|
|
850
|
-
value: { target: undefined },
|
|
851
|
-
set: mockSet,
|
|
852
|
-
get: mockGet,
|
|
853
|
-
};
|
|
854
|
-
|
|
855
|
-
const mockTextInputNodesRef = { current: new Set<number>() };
|
|
856
|
-
|
|
857
|
-
// Mock the useBottomSheetInternal hook to simulate being inside ContentOverlay
|
|
858
|
-
jest
|
|
859
|
-
.spyOn(require("@gorhom/bottom-sheet"), "useBottomSheetInternal")
|
|
860
|
-
.mockReturnValue({
|
|
861
|
-
animatedKeyboardState: mockKeyboardState,
|
|
862
|
-
textInputNodesRef: mockTextInputNodesRef,
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
const a11yLabel = "Test InputText";
|
|
866
|
-
const { getByLabelText } = render(
|
|
867
|
-
<InputText accessibilityLabel={a11yLabel} />,
|
|
868
|
-
);
|
|
869
|
-
|
|
870
|
-
const input = getByLabelText(a11yLabel);
|
|
871
|
-
|
|
872
|
-
fireEvent(input, "onFocus", {
|
|
873
|
-
nativeEvent: { target: 123 },
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
expect(mockSet).toHaveBeenCalledWith(expect.any(Function));
|
|
877
|
-
// Verify the set callback updates the state correctly
|
|
878
|
-
const setCallback = mockSet.mock.calls[0][0];
|
|
879
|
-
const newState = setCallback({ target: undefined });
|
|
880
|
-
expect(newState).toEqual({ target: 123 });
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
// eslint-disable-next-line max-statements
|
|
884
|
-
it("consumer onFocus and onBlur callbacks still fire when inside ContentOverlay", () => {
|
|
885
|
-
const focusCallback = jest.fn();
|
|
886
|
-
const blurCallback = jest.fn();
|
|
887
|
-
const mockSet = jest.fn();
|
|
888
|
-
const mockGet = jest.fn(() => ({ target: undefined }));
|
|
889
|
-
const mockKeyboardState = {
|
|
890
|
-
value: { target: undefined },
|
|
891
|
-
set: mockSet,
|
|
892
|
-
get: mockGet,
|
|
893
|
-
};
|
|
894
|
-
|
|
895
|
-
const mockTextInputNodesRef = { current: new Set<number>() };
|
|
896
|
-
|
|
897
|
-
jest
|
|
898
|
-
.spyOn(require("@gorhom/bottom-sheet"), "useBottomSheetInternal")
|
|
899
|
-
.mockReturnValue({
|
|
900
|
-
animatedKeyboardState: mockKeyboardState,
|
|
901
|
-
textInputNodesRef: mockTextInputNodesRef,
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
const a11yLabel = "Test InputText";
|
|
905
|
-
const { getByLabelText } = render(
|
|
906
|
-
<InputText
|
|
907
|
-
onFocus={focusCallback}
|
|
908
|
-
onBlur={blurCallback}
|
|
909
|
-
accessibilityLabel={a11yLabel}
|
|
910
|
-
/>,
|
|
911
|
-
);
|
|
912
|
-
|
|
913
|
-
const input = getByLabelText(a11yLabel);
|
|
914
|
-
|
|
915
|
-
fireEvent(input, "onFocus", {
|
|
916
|
-
nativeEvent: { target: 123 },
|
|
917
|
-
});
|
|
918
|
-
expect(focusCallback).toHaveBeenCalled();
|
|
919
|
-
|
|
920
|
-
fireEvent(input, "onBlur", {
|
|
921
|
-
nativeEvent: { target: 123 },
|
|
922
|
-
});
|
|
923
|
-
expect(blurCallback).toHaveBeenCalled();
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
it("handles value changes when inside ContentOverlay", () => {
|
|
927
|
-
const mockSet = jest.fn();
|
|
928
|
-
const mockGet = jest.fn(() => ({ target: undefined }));
|
|
929
|
-
const mockKeyboardState = {
|
|
930
|
-
value: { target: undefined },
|
|
931
|
-
set: mockSet,
|
|
932
|
-
get: mockGet,
|
|
933
|
-
};
|
|
934
|
-
|
|
935
|
-
const mockTextInputNodesRef = { current: new Set<number>() };
|
|
936
|
-
const changeCallback = jest.fn();
|
|
937
|
-
|
|
938
|
-
jest
|
|
939
|
-
.spyOn(require("@gorhom/bottom-sheet"), "useBottomSheetInternal")
|
|
940
|
-
.mockReturnValue({
|
|
941
|
-
animatedKeyboardState: mockKeyboardState,
|
|
942
|
-
textInputNodesRef: mockTextInputNodesRef,
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
const a11yLabel = "Test InputText";
|
|
946
|
-
const { getByLabelText } = render(
|
|
947
|
-
<InputText
|
|
948
|
-
onChangeText={changeCallback}
|
|
949
|
-
accessibilityLabel={a11yLabel}
|
|
950
|
-
/>,
|
|
951
|
-
);
|
|
952
|
-
|
|
953
|
-
const input = getByLabelText(a11yLabel);
|
|
954
|
-
|
|
955
|
-
fireEvent.changeText(input, "New value");
|
|
956
|
-
|
|
957
|
-
// Value changes should work normally
|
|
958
|
-
expect(changeCallback).toHaveBeenCalledWith("New value");
|
|
959
|
-
});
|
|
960
|
-
});
|
|
961
839
|
});
|
|
@@ -14,8 +14,7 @@ import type {
|
|
|
14
14
|
TextInputProps,
|
|
15
15
|
TextStyle,
|
|
16
16
|
} from "react-native";
|
|
17
|
-
import { Platform, TextInput
|
|
18
|
-
import { useBottomSheetInternal } from "@gorhom/bottom-sheet";
|
|
17
|
+
import { Platform, TextInput } from "react-native";
|
|
19
18
|
import type { RegisterOptions } from "react-hook-form";
|
|
20
19
|
import type { IconNames } from "@jobber/design";
|
|
21
20
|
import identity from "lodash/identity";
|
|
@@ -316,11 +315,6 @@ function InputTextInternal(
|
|
|
316
315
|
disabled,
|
|
317
316
|
});
|
|
318
317
|
|
|
319
|
-
// Bottom sheet keyboard handling - detect if we're inside a ContentOverlay
|
|
320
|
-
const bottomSheetContext = useBottomSheetInternal(true);
|
|
321
|
-
const animatedKeyboardState = bottomSheetContext?.animatedKeyboardState;
|
|
322
|
-
const textInputNodesRef = bottomSheetContext?.textInputNodesRef;
|
|
323
|
-
|
|
324
318
|
// Android doesn't have an accessibility label like iOS does. By adding
|
|
325
319
|
// it as a placeholder it readds it like a label. However we don't want to
|
|
326
320
|
// add a placeholder on iOS.
|
|
@@ -398,6 +392,7 @@ function InputTextInternal(
|
|
|
398
392
|
toolbarVisibility={toolbarVisibility}
|
|
399
393
|
loading={loading}
|
|
400
394
|
loadingType={loadingType}
|
|
395
|
+
multiline={multiline}
|
|
401
396
|
>
|
|
402
397
|
<TextInput
|
|
403
398
|
inputAccessoryViewID={inputAccessoryID || undefined}
|
|
@@ -444,13 +439,11 @@ function InputTextInternal(
|
|
|
444
439
|
secureTextEntry={secureTextEntry}
|
|
445
440
|
{...androidA11yProps}
|
|
446
441
|
onFocus={event => {
|
|
447
|
-
handleBottomSheetFocus(event);
|
|
448
442
|
_name && setFocusedInput(_name);
|
|
449
443
|
setFocused(true);
|
|
450
444
|
onFocus?.(event);
|
|
451
445
|
}}
|
|
452
446
|
onBlur={event => {
|
|
453
|
-
handleBottomSheetBlur(event);
|
|
454
447
|
_name && setFocusedInput("");
|
|
455
448
|
setFocused(false);
|
|
456
449
|
onBlur?.(event);
|
|
@@ -477,48 +470,6 @@ function InputTextInternal(
|
|
|
477
470
|
updateFormAndState(removedIOSCharValue);
|
|
478
471
|
}
|
|
479
472
|
|
|
480
|
-
function handleBottomSheetFocus(event?: FocusEvent) {
|
|
481
|
-
if (!animatedKeyboardState || !textInputNodesRef || !event?.nativeEvent) {
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
animatedKeyboardState.set(state => ({
|
|
486
|
-
...state,
|
|
487
|
-
target: event.nativeEvent.target,
|
|
488
|
-
}));
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function handleBottomSheetBlur(event?: FocusEvent) {
|
|
492
|
-
if (!animatedKeyboardState || !textInputNodesRef || !event?.nativeEvent) {
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
const keyboardState = animatedKeyboardState.get();
|
|
496
|
-
const currentlyFocusedInput = TextInput.State.currentlyFocusedInput();
|
|
497
|
-
const currentFocusedInput =
|
|
498
|
-
currentlyFocusedInput !== null
|
|
499
|
-
? findNodeHandle(
|
|
500
|
-
// @ts-expect-error - TextInput.State.currentlyFocusedInput() returns NativeMethods
|
|
501
|
-
// which is not directly assignable to findNodeHandle's expected type,
|
|
502
|
-
// but it works at runtime. This is a known type limitation in React Native.
|
|
503
|
-
currentlyFocusedInput,
|
|
504
|
-
)
|
|
505
|
-
: null;
|
|
506
|
-
|
|
507
|
-
// Only remove the target if it belongs to the current component
|
|
508
|
-
// and if the currently focused input is not in the targets set
|
|
509
|
-
const shouldRemoveCurrentTarget =
|
|
510
|
-
keyboardState.target === event.nativeEvent.target;
|
|
511
|
-
const shouldIgnoreBlurEvent =
|
|
512
|
-
currentFocusedInput && textInputNodesRef.current.has(currentFocusedInput);
|
|
513
|
-
|
|
514
|
-
if (shouldRemoveCurrentTarget && !shouldIgnoreBlurEvent) {
|
|
515
|
-
animatedKeyboardState.set(state => ({
|
|
516
|
-
...state,
|
|
517
|
-
target: undefined,
|
|
518
|
-
}));
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
473
|
function handleClear() {
|
|
523
474
|
handleChangeText("");
|
|
524
475
|
}
|
|
@@ -565,7 +516,7 @@ interface UseTextInputRefProps {
|
|
|
565
516
|
}
|
|
566
517
|
|
|
567
518
|
function useTextInputRef({ ref, onClear }: UseTextInputRefProps) {
|
|
568
|
-
const textInputRef = useRef<
|
|
519
|
+
const textInputRef = useRef<InputTextRef | null>(null);
|
|
569
520
|
|
|
570
521
|
useImperativeHandle(
|
|
571
522
|
ref,
|
|
@@ -152,6 +152,20 @@ exports[`renders a thumbnail component with attachments 1`] = `
|
|
|
152
152
|
}
|
|
153
153
|
>
|
|
154
154
|
<View
|
|
155
|
+
backdropComponent={[Function]}
|
|
156
|
+
backgroundStyle={
|
|
157
|
+
{
|
|
158
|
+
"borderTopLeftRadius": 24,
|
|
159
|
+
"borderTopRightRadius": 24,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
enablePanDownToClose={true}
|
|
163
|
+
handleStyle={
|
|
164
|
+
{
|
|
165
|
+
"display": "none",
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
keyboardBlurBehavior="restore"
|
|
155
169
|
style={
|
|
156
170
|
{
|
|
157
171
|
"display": "none",
|
|
@@ -160,6 +174,12 @@ exports[`renders a thumbnail component with attachments 1`] = `
|
|
|
160
174
|
testID="bottom-sheet-mock"
|
|
161
175
|
>
|
|
162
176
|
<View
|
|
177
|
+
style={
|
|
178
|
+
{
|
|
179
|
+
"paddingBottom": 8,
|
|
180
|
+
"paddingTop": 8,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
163
183
|
testID="bottom-sheet-view"
|
|
164
184
|
>
|
|
165
185
|
<View
|
package/src/utils/meta/meta.json
CHANGED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Computes the abstract behavior of ContentOverlay from its props and state.
|
|
3
|
-
*
|
|
4
|
-
* This pure function documents and centralizes the complex logic that determines:
|
|
5
|
-
* - Initial height mode (fullScreen vs contentHeight)
|
|
6
|
-
* - Whether the overlay is draggable
|
|
7
|
-
* - Whether the dismiss button should be shown
|
|
8
|
-
*
|
|
9
|
-
* The logic accounts for legacy behavior where:
|
|
10
|
-
* - `onBeforeExit` silently overrides `isDraggable` to false
|
|
11
|
-
* - Default props (neither fullScreen nor adjustToContentHeight) are treated
|
|
12
|
-
* as contentHeight for the new implementation
|
|
13
|
-
* - Dismiss button visibility depends on multiple factors including position state
|
|
14
|
-
*/
|
|
15
|
-
export function computeContentOverlayBehavior(config, state) {
|
|
16
|
-
const isDraggable = computeIsDraggable(config);
|
|
17
|
-
const initialHeight = computeInitialHeight(config, isDraggable);
|
|
18
|
-
const showDismiss = computeShowDismiss(config, state);
|
|
19
|
-
return {
|
|
20
|
-
initialHeight,
|
|
21
|
-
isDraggable,
|
|
22
|
-
showDismiss,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Order is important to maintain legacy behavior, despite the questionable logic.
|
|
27
|
-
* A non draggable overlay wants to be fullscreen, so as to have the dismiss button be visible.
|
|
28
|
-
* There is an invalid combination here with adjustToContentHeight and onBeforeExit which in turn overrides isDraggable to false.
|
|
29
|
-
* This requires an explicit showDismiss=true or else it will not be possible to dismiss the overlay.
|
|
30
|
-
*/
|
|
31
|
-
function computeInitialHeight(config, isDraggable) {
|
|
32
|
-
if (config.adjustToContentHeight) {
|
|
33
|
-
return "contentHeight";
|
|
34
|
-
}
|
|
35
|
-
if (config.fullScreen) {
|
|
36
|
-
return "fullScreen";
|
|
37
|
-
}
|
|
38
|
-
if (!isDraggable) {
|
|
39
|
-
return "fullScreen";
|
|
40
|
-
}
|
|
41
|
-
return "contentHeight";
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Draggability determination:
|
|
45
|
-
* - hasOnBeforeExit: true → false (silent override, regardless of isDraggable prop)
|
|
46
|
-
* - Otherwise → use isDraggable prop value
|
|
47
|
-
*
|
|
48
|
-
* This silent override exists because onBeforeExit needs to intercept close attempts,
|
|
49
|
-
* and dragging would bypass that interception.
|
|
50
|
-
*/
|
|
51
|
-
function computeIsDraggable(config) {
|
|
52
|
-
if (config.hasOnBeforeExit) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
return config.isDraggable;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Dismiss button visibility:
|
|
59
|
-
* The idea behind fullscreen having it is that there may be little room to tap the background to dismiss.
|
|
60
|
-
* While this logic is redundant with the position, it's a relic of the legacy behavior where position didn't update in time.
|
|
61
|
-
*/
|
|
62
|
-
function computeShowDismiss(config, state) {
|
|
63
|
-
if (config.showDismiss) {
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
if (state.isScreenReaderEnabled) {
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
if (config.fullScreen) {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
if (!config.adjustToContentHeight && state.position === 0) {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { useCallback, useRef } from "react";
|
|
2
|
-
import { BackHandler } from "react-native";
|
|
3
|
-
/**
|
|
4
|
-
* Hook that dismisses the bottom sheet on the hardware back button press if it is visible
|
|
5
|
-
* @param bottomSheetModalRef ref to the bottom sheet modal component
|
|
6
|
-
*/
|
|
7
|
-
export function useBottomSheetModalBackHandler(onCloseController) {
|
|
8
|
-
const backHandlerSubscriptionRef = useRef(null);
|
|
9
|
-
const handleSheetPositionChange = useCallback((index) => {
|
|
10
|
-
var _a;
|
|
11
|
-
const isBottomSheetModalVisible = index >= 0;
|
|
12
|
-
if (isBottomSheetModalVisible && !backHandlerSubscriptionRef.current) {
|
|
13
|
-
// Setup the back handler if the bottom sheet is right in front of the user
|
|
14
|
-
backHandlerSubscriptionRef.current = BackHandler.addEventListener("hardwareBackPress", () => {
|
|
15
|
-
onCloseController();
|
|
16
|
-
return true;
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
else if (!isBottomSheetModalVisible) {
|
|
20
|
-
(_a = backHandlerSubscriptionRef.current) === null || _a === void 0 ? void 0 : _a.remove();
|
|
21
|
-
backHandlerSubscriptionRef.current = null;
|
|
22
|
-
}
|
|
23
|
-
}, [onCloseController]);
|
|
24
|
-
return { handleSheetPositionChange };
|
|
25
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export interface ContentOverlayConfig {
|
|
2
|
-
fullScreen: boolean;
|
|
3
|
-
adjustToContentHeight: boolean;
|
|
4
|
-
isDraggable: boolean;
|
|
5
|
-
hasOnBeforeExit: boolean;
|
|
6
|
-
showDismiss: boolean;
|
|
7
|
-
}
|
|
8
|
-
export interface ContentOverlayState {
|
|
9
|
-
isScreenReaderEnabled: boolean;
|
|
10
|
-
position: number;
|
|
11
|
-
}
|
|
12
|
-
export type InitialHeight = "fullScreen" | "contentHeight";
|
|
13
|
-
export interface ContentOverlayBehavior {
|
|
14
|
-
initialHeight: InitialHeight;
|
|
15
|
-
isDraggable: boolean;
|
|
16
|
-
showDismiss: boolean;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Computes the abstract behavior of ContentOverlay from its props and state.
|
|
20
|
-
*
|
|
21
|
-
* This pure function documents and centralizes the complex logic that determines:
|
|
22
|
-
* - Initial height mode (fullScreen vs contentHeight)
|
|
23
|
-
* - Whether the overlay is draggable
|
|
24
|
-
* - Whether the dismiss button should be shown
|
|
25
|
-
*
|
|
26
|
-
* The logic accounts for legacy behavior where:
|
|
27
|
-
* - `onBeforeExit` silently overrides `isDraggable` to false
|
|
28
|
-
* - Default props (neither fullScreen nor adjustToContentHeight) are treated
|
|
29
|
-
* as contentHeight for the new implementation
|
|
30
|
-
* - Dismiss button visibility depends on multiple factors including position state
|
|
31
|
-
*/
|
|
32
|
-
export declare function computeContentOverlayBehavior(config: ContentOverlayConfig, state: ContentOverlayState): ContentOverlayBehavior;
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook that dismisses the bottom sheet on the hardware back button press if it is visible
|
|
3
|
-
* @param bottomSheetModalRef ref to the bottom sheet modal component
|
|
4
|
-
*/
|
|
5
|
-
export declare function useBottomSheetModalBackHandler(onCloseController: () => void): {
|
|
6
|
-
handleSheetPositionChange: (index: number) => void;
|
|
7
|
-
};
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import React, { useRef } from "react";
|
|
2
|
-
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
3
|
-
import { View } from "react-native";
|
|
4
|
-
import { SafeAreaProvider } from "react-native-safe-area-context";
|
|
5
|
-
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
|
6
|
-
import { Button, Heading, Text } from "@jobber/components-native";
|
|
7
|
-
import { ContentOverlay } from "./ContentOverlay";
|
|
8
|
-
import type { ContentOverlayRef } from "./types";
|
|
9
|
-
|
|
10
|
-
const meta = {
|
|
11
|
-
title: "Components/Overlays/ContentOverlay",
|
|
12
|
-
component: ContentOverlay,
|
|
13
|
-
} satisfies Meta<typeof ContentOverlay>;
|
|
14
|
-
|
|
15
|
-
export default meta;
|
|
16
|
-
type Story = StoryObj<typeof meta>;
|
|
17
|
-
|
|
18
|
-
const BasicTemplate = () => {
|
|
19
|
-
const contentOverlayRef = useRef<ContentOverlayRef>(null);
|
|
20
|
-
|
|
21
|
-
const openContentOverlay = () => {
|
|
22
|
-
contentOverlayRef.current?.open?.();
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const closeContentOverlay = () => {
|
|
26
|
-
contentOverlayRef.current?.close?.();
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<SafeAreaProvider>
|
|
31
|
-
<BottomSheetModalProvider>
|
|
32
|
-
<View style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
33
|
-
<Heading>Basic ContentOverlay</Heading>
|
|
34
|
-
<Text>
|
|
35
|
-
Note that due to the differences between React Native Web and React
|
|
36
|
-
Native, this does not render 100% properly
|
|
37
|
-
</Text>
|
|
38
|
-
<Button label="Open Content Overlay" onPress={openContentOverlay} />
|
|
39
|
-
<Button label="Close Content Overlay" onPress={closeContentOverlay} />
|
|
40
|
-
</View>
|
|
41
|
-
<ContentOverlay
|
|
42
|
-
ref={contentOverlayRef}
|
|
43
|
-
title="Content Overlay Title"
|
|
44
|
-
onClose={() => console.log("closed content overlay")}
|
|
45
|
-
onOpen={() => console.log("opened content overlay")}
|
|
46
|
-
>
|
|
47
|
-
<View style={{ padding: 16 }}>
|
|
48
|
-
<Text>This is the content inside the overlay.</Text>
|
|
49
|
-
</View>
|
|
50
|
-
</ContentOverlay>
|
|
51
|
-
</BottomSheetModalProvider>
|
|
52
|
-
</SafeAreaProvider>
|
|
53
|
-
);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export const Basic: Story = {
|
|
57
|
-
render: BasicTemplate,
|
|
58
|
-
args: {} as Story["args"],
|
|
59
|
-
};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
|
3
|
-
|
|
4
|
-
interface ContentOverlayProviderProps {
|
|
5
|
-
readonly children: React.ReactNode;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function ContentOverlayProvider({
|
|
9
|
-
children,
|
|
10
|
-
}: ContentOverlayProviderProps) {
|
|
11
|
-
return <BottomSheetModalProvider>{children}</BottomSheetModalProvider>;
|
|
12
|
-
}
|