@jobber/components-native 0.36.0 → 0.38.0

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.
Files changed (43) hide show
  1. package/dist/src/ContentOverlay/ContentOverlay.js +144 -0
  2. package/dist/src/ContentOverlay/ContentOverlay.style.js +56 -0
  3. package/dist/src/ContentOverlay/hooks/useKeyboardVisibility.js +21 -0
  4. package/dist/src/ContentOverlay/hooks/useViewLayoutHeight.js +10 -0
  5. package/dist/src/ContentOverlay/index.js +1 -0
  6. package/dist/src/ContentOverlay/messages.js +8 -0
  7. package/dist/src/ContentOverlay/types.js +1 -0
  8. package/dist/src/Disclosure/Disclosure.js +50 -0
  9. package/dist/src/Disclosure/Disclosure.style.js +21 -0
  10. package/dist/src/Disclosure/constants.js +1 -0
  11. package/dist/src/Disclosure/index.js +1 -0
  12. package/dist/src/index.js +2 -0
  13. package/dist/tsconfig.tsbuildinfo +1 -1
  14. package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +6 -0
  15. package/dist/types/src/ContentOverlay/ContentOverlay.style.d.ts +60 -0
  16. package/dist/types/src/ContentOverlay/hooks/useKeyboardVisibility.d.ts +6 -0
  17. package/dist/types/src/ContentOverlay/hooks/useViewLayoutHeight.d.ts +6 -0
  18. package/dist/types/src/ContentOverlay/index.d.ts +2 -0
  19. package/dist/types/src/ContentOverlay/messages.d.ts +7 -0
  20. package/dist/types/src/ContentOverlay/types.d.ts +87 -0
  21. package/dist/types/src/Disclosure/Disclosure.d.ts +35 -0
  22. package/dist/types/src/Disclosure/Disclosure.style.d.ts +19 -0
  23. package/dist/types/src/Disclosure/constants.d.ts +1 -0
  24. package/dist/types/src/Disclosure/index.d.ts +1 -0
  25. package/dist/types/src/index.d.ts +2 -0
  26. package/package.json +2 -2
  27. package/src/ContentOverlay/ContentOverlay.style.ts +70 -0
  28. package/src/ContentOverlay/ContentOverlay.test.tsx +371 -0
  29. package/src/ContentOverlay/ContentOverlay.tsx +295 -0
  30. package/src/ContentOverlay/hooks/useKeyboardVisibility.test.ts +42 -0
  31. package/src/ContentOverlay/hooks/useKeyboardVisibility.ts +36 -0
  32. package/src/ContentOverlay/hooks/useViewLayoutHeight.test.ts +56 -0
  33. package/src/ContentOverlay/hooks/useViewLayoutHeight.ts +18 -0
  34. package/src/ContentOverlay/index.ts +2 -0
  35. package/src/ContentOverlay/messages.ts +9 -0
  36. package/src/ContentOverlay/types.ts +96 -0
  37. package/src/Disclosure/Disclosure.style.ts +22 -0
  38. package/src/Disclosure/Disclosure.test.tsx +71 -0
  39. package/src/Disclosure/Disclosure.tsx +162 -0
  40. package/src/Disclosure/__snapshots__/Disclosure.test.tsx.snap +488 -0
  41. package/src/Disclosure/constants.ts +1 -0
  42. package/src/Disclosure/index.ts +1 -0
  43. package/src/index.ts +2 -0
@@ -0,0 +1,371 @@
1
+ import React, { createRef } from "react";
2
+ import { fireEvent, render, waitFor } from "@testing-library/react-native";
3
+ import { AccessibilityInfo, View } from "react-native";
4
+ import { Host } from "react-native-portalize";
5
+ import { ReactTestInstance, act } from "react-test-renderer";
6
+ import { useIntl } from "react-intl";
7
+ import {
8
+ ContentOverlay,
9
+ ContentOverlayRef,
10
+ ModalBackgroundColor,
11
+ } from "./ContentOverlay";
12
+ import { messages } from "./messages";
13
+ import { tokens } from "../utils/design";
14
+ import { Button } from "../Button";
15
+ import { Content } from "../Content";
16
+ import { Text } from "../Text";
17
+
18
+ jest.unmock("../hooks/useIsScreenReaderEnabled");
19
+ function fireLayoutEvent(childrenContent: ReactTestInstance) {
20
+ fireEvent(childrenContent, "onLayout", {
21
+ nativeEvent: {
22
+ layout: {
23
+ height: 100,
24
+ },
25
+ },
26
+ });
27
+ }
28
+
29
+ interface testRendererOptions {
30
+ text: string;
31
+ title: string;
32
+ buttonLabel: string;
33
+ a11yLabel?: string;
34
+ fullScreen?: boolean;
35
+ showDismiss?: boolean;
36
+ modalBackgroundColor?: ModalBackgroundColor;
37
+ onCloseCallback?: () => void;
38
+ onOpenCallback?: () => void;
39
+ onBeforeExitCallback?: () => void;
40
+ }
41
+
42
+ function getDefaultOptions(): testRendererOptions {
43
+ return {
44
+ text: "I am the contentOverlay text",
45
+ title: "Title",
46
+ buttonLabel: "Open Content Overlay",
47
+ fullScreen: false,
48
+ showDismiss: false,
49
+ modalBackgroundColor: "surface",
50
+ onCloseCallback: () => {
51
+ return;
52
+ },
53
+ onOpenCallback: () => {
54
+ return;
55
+ },
56
+ };
57
+ }
58
+
59
+ function renderContentOverlay(
60
+ {
61
+ text,
62
+ title,
63
+ buttonLabel,
64
+ a11yLabel,
65
+ fullScreen,
66
+ showDismiss,
67
+ modalBackgroundColor,
68
+ onCloseCallback,
69
+ onOpenCallback,
70
+ onBeforeExitCallback,
71
+ } = getDefaultOptions(),
72
+ ) {
73
+ const contentOverlayRef = createRef<ContentOverlayRef>();
74
+
75
+ const renderResult = render(
76
+ <Host>
77
+ <View>
78
+ <Text>I am a bunch of text</Text>
79
+ <Button
80
+ label={buttonLabel}
81
+ onPress={() => {
82
+ contentOverlayRef?.current?.open?.();
83
+ }}
84
+ />
85
+ <ContentOverlay
86
+ ref={contentOverlayRef}
87
+ title={title}
88
+ onClose={onCloseCallback}
89
+ onOpen={onOpenCallback}
90
+ accessibilityLabel={a11yLabel}
91
+ fullScreen={fullScreen}
92
+ showDismiss={showDismiss}
93
+ modalBackgroundColor={modalBackgroundColor}
94
+ onBeforeExit={onBeforeExitCallback}
95
+ >
96
+ <Content>
97
+ <Text>{text}</Text>
98
+ </Content>
99
+ </ContentOverlay>
100
+ </View>
101
+ </Host>,
102
+ );
103
+
104
+ const childrenView = renderResult.getByTestId("ATL-Overlay-Children");
105
+ fireLayoutEvent(childrenView);
106
+ const headerComponent = renderResult.getByTestId("ATL-Overlay-Header");
107
+ fireLayoutEvent(headerComponent);
108
+
109
+ return renderResult;
110
+ }
111
+
112
+ function renderAndOpenContentOverlay(defaultOptions = getDefaultOptions()) {
113
+ const rendered = renderContentOverlay(defaultOptions);
114
+
115
+ act(() => {
116
+ fireEvent.press(rendered.getByLabelText(defaultOptions.buttonLabel));
117
+ });
118
+
119
+ return rendered;
120
+ }
121
+
122
+ describe("when open is called on the content overlay ref", () => {
123
+ it("should open the content overlay, exposing the content to the user", () => {
124
+ const options: testRendererOptions = {
125
+ ...getDefaultOptions(),
126
+ text: "I am text within the content overlay",
127
+ };
128
+ const contentOverlayScreen = renderAndOpenContentOverlay(options);
129
+
130
+ expect(contentOverlayScreen.getByText(options.text)).toBeDefined();
131
+ });
132
+ });
133
+
134
+ describe("when the close button is clicked on an open content overlay", () => {
135
+ it("should close the content overlay", async () => {
136
+ const options: testRendererOptions = {
137
+ ...getDefaultOptions(),
138
+ text: "I am text within the content overlay",
139
+ showDismiss: true,
140
+ };
141
+ const contentOverlayScreen = renderAndOpenContentOverlay(options);
142
+
143
+ act(() => {
144
+ fireEvent.press(
145
+ contentOverlayScreen.getByTestId("ATL-Overlay-CloseButton"),
146
+ );
147
+ });
148
+
149
+ await waitFor(() => {
150
+ expect(contentOverlayScreen.queryByText(options.text)).toBeNull();
151
+ });
152
+ });
153
+ });
154
+
155
+ describe("when the close button is clicked on an open content overlay with a defined onClose prop", () => {
156
+ it("should call the passed in onClose prop", async () => {
157
+ const options: testRendererOptions = {
158
+ ...getDefaultOptions(),
159
+ onCloseCallback: jest.fn(),
160
+ showDismiss: true,
161
+ };
162
+ const contentOverlayScreen = renderAndOpenContentOverlay(options);
163
+
164
+ act(() => {
165
+ fireEvent.press(
166
+ contentOverlayScreen.getByTestId("ATL-Overlay-CloseButton"),
167
+ );
168
+ });
169
+
170
+ await waitFor(() => {
171
+ expect(options.onCloseCallback).toHaveBeenCalled();
172
+ });
173
+ });
174
+ });
175
+
176
+ describe("when the content overlay is created with a defined onOpen prop", () => {
177
+ describe("when the content overlay is not opened", () => {
178
+ it("should not call the passed in onOpen prop", async () => {
179
+ const options: testRendererOptions = {
180
+ ...getDefaultOptions(),
181
+ onOpenCallback: jest.fn(),
182
+ };
183
+ renderContentOverlay(options);
184
+
185
+ await waitFor(() => {
186
+ expect(options.onOpenCallback).not.toHaveBeenCalled();
187
+ });
188
+ });
189
+ });
190
+
191
+ describe("when the content overlay is opened", () => {
192
+ it("should call the passed in onOpen prop", async () => {
193
+ const options: testRendererOptions = {
194
+ ...getDefaultOptions(),
195
+ onOpenCallback: jest.fn(),
196
+ };
197
+ renderAndOpenContentOverlay(options);
198
+
199
+ await waitFor(() => {
200
+ expect(options.onOpenCallback).toHaveBeenCalled();
201
+ });
202
+ });
203
+ });
204
+ });
205
+
206
+ describe("when title prop passed to content overlay", () => {
207
+ it("should set the header title", () => {
208
+ const options: testRendererOptions = {
209
+ ...getDefaultOptions(),
210
+ title: "Awesome Title",
211
+ };
212
+ const contentOverlayScreen = renderAndOpenContentOverlay(options);
213
+
214
+ expect(contentOverlayScreen.getByText(options.title)).toBeDefined();
215
+ });
216
+ });
217
+
218
+ describe("when accessibilityLabel prop passed to content overlay", () => {
219
+ it("should set the header accessibilityLabel", () => {
220
+ const options: testRendererOptions = {
221
+ ...getDefaultOptions(),
222
+ a11yLabel: "Awesome a11y Label",
223
+ showDismiss: true,
224
+ };
225
+ const contentOverlayScreen = renderAndOpenContentOverlay(options);
226
+
227
+ expect(
228
+ contentOverlayScreen.getByLabelText(options.a11yLabel || "ohno"),
229
+ ).toBeDefined();
230
+ });
231
+ });
232
+
233
+ describe("when accessibilityLabel prop NOT passed to content overlay", () => {
234
+ it("should use default accessibilityLabel", () => {
235
+ const { formatMessage } = useIntl();
236
+
237
+ const options: testRendererOptions = {
238
+ ...getDefaultOptions(),
239
+ title: "Awesome Title",
240
+ showDismiss: true,
241
+ };
242
+ const contentOverlayScreen = renderAndOpenContentOverlay(options);
243
+
244
+ const defaultCloseOverlayA11YLabel = formatMessage(
245
+ messages.closeOverlayA11YLabel,
246
+ {
247
+ title: options.title,
248
+ },
249
+ );
250
+ expect(
251
+ contentOverlayScreen.getAllByLabelText(defaultCloseOverlayA11YLabel),
252
+ ).toHaveLength(2);
253
+ });
254
+ });
255
+
256
+ describe("when there is a screen reader enabled", () => {
257
+ jest
258
+ .spyOn(AccessibilityInfo, "isScreenReaderEnabled")
259
+ .mockImplementation(() => Promise.resolve(true));
260
+
261
+ it("should show the dismiss button", async () => {
262
+ const options: testRendererOptions = {
263
+ ...getDefaultOptions(),
264
+ };
265
+ const contentOverlayScreen = renderAndOpenContentOverlay(options);
266
+
267
+ expect(
268
+ await contentOverlayScreen.findByTestId("ATL-Overlay-CloseButton"),
269
+ ).toBeDefined();
270
+ });
271
+ });
272
+
273
+ describe("when fullScreen is set to true", () => {
274
+ it("should show the dismiss button", () => {
275
+ const options: testRendererOptions = {
276
+ ...getDefaultOptions(),
277
+ fullScreen: true,
278
+ };
279
+ const contentOverlayScreen = renderAndOpenContentOverlay(options);
280
+ expect(
281
+ contentOverlayScreen.getByTestId("ATL-Overlay-CloseButton"),
282
+ ).toBeDefined();
283
+ });
284
+ });
285
+
286
+ describe("when showDismiss is set to true", () => {
287
+ it("should show the dismiss button", () => {
288
+ const options: testRendererOptions = {
289
+ ...getDefaultOptions(),
290
+ showDismiss: true,
291
+ };
292
+ const contentOverlayScreen = renderAndOpenContentOverlay(options);
293
+ expect(
294
+ contentOverlayScreen.getByTestId("ATL-Overlay-CloseButton"),
295
+ ).toBeDefined();
296
+ });
297
+ });
298
+
299
+ describe("when the close button is clicked on an open content overlay with a defined onBeforeExit", () => {
300
+ it("should call the callback method on exit", async () => {
301
+ const options: testRendererOptions = {
302
+ ...getDefaultOptions(),
303
+ onBeforeExitCallback: jest.fn(),
304
+ showDismiss: true,
305
+ };
306
+ const contentOverlayScreen = renderAndOpenContentOverlay(options);
307
+
308
+ act(() => {
309
+ fireEvent.press(
310
+ contentOverlayScreen.getByTestId("ATL-Overlay-CloseButton"),
311
+ );
312
+ });
313
+
314
+ await waitFor(() => {
315
+ expect(options.onBeforeExitCallback).toHaveBeenCalled();
316
+ });
317
+ });
318
+ });
319
+
320
+ describe("modalBackgroundColor prop", () => {
321
+ describe("when using the default surface value", () => {
322
+ it("renders the component with the color-surface color", () => {
323
+ const options: testRendererOptions = {
324
+ ...getDefaultOptions(),
325
+ };
326
+ const contentOverlayScreen = renderAndOpenContentOverlay(options);
327
+ const OverlayHeader = contentOverlayScreen.getByTestId(
328
+ "ATL-Overlay-Header",
329
+ ).children[0] as ReactTestInstance;
330
+ const OverlayHeaderStyles = OverlayHeader.props.style;
331
+
332
+ expect(OverlayHeaderStyles).toEqual(
333
+ expect.arrayContaining([
334
+ expect.objectContaining({
335
+ backgroundColor: tokens["color-surface"],
336
+ }),
337
+ ]),
338
+ );
339
+
340
+ expect(OverlayHeaderStyles).not.toEqual(
341
+ expect.arrayContaining([
342
+ expect.objectContaining({
343
+ backgroundColor: tokens["color-surface--background"],
344
+ }),
345
+ ]),
346
+ );
347
+ });
348
+ });
349
+
350
+ describe("when set to background", () => {
351
+ it("changes the backround color of the modal to color-surface--background", () => {
352
+ const options: testRendererOptions = {
353
+ ...getDefaultOptions(),
354
+ modalBackgroundColor: "background",
355
+ };
356
+ const contentOverlayScreen = renderAndOpenContentOverlay(options);
357
+ const OverlayHeader = contentOverlayScreen.getByTestId(
358
+ "ATL-Overlay-Header",
359
+ ).children[0] as ReactTestInstance;
360
+ const OverlayHeaderStyles = OverlayHeader.props.style;
361
+
362
+ expect(OverlayHeaderStyles).toEqual(
363
+ expect.arrayContaining([
364
+ expect.objectContaining({
365
+ backgroundColor: tokens["color-surface--background"],
366
+ }),
367
+ ]),
368
+ );
369
+ });
370
+ });
371
+ });
@@ -0,0 +1,295 @@
1
+ import React, {
2
+ Ref,
3
+ forwardRef,
4
+ useCallback,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+ import { Modalize } from "react-native-modalize";
11
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
12
+ import {
13
+ AccessibilityInfo,
14
+ NativeScrollEvent,
15
+ NativeSyntheticEvent,
16
+ Platform,
17
+ View,
18
+ findNodeHandle,
19
+ useWindowDimensions,
20
+ } from "react-native";
21
+ import { Portal } from "react-native-portalize";
22
+ import { useIntl } from "react-intl";
23
+ import { useKeyboardVisibility } from "./hooks/useKeyboardVisibility";
24
+ import { styles } from "./ContentOverlay.style";
25
+ import { messages } from "./messages";
26
+ import { useViewLayoutHeight } from "./hooks/useViewLayoutHeight";
27
+ import {
28
+ ContentOverlayProps,
29
+ ContentOverlayRef,
30
+ ModalBackgroundColor,
31
+ } from "./types";
32
+ import { useIsScreenReaderEnabled } from "../hooks";
33
+ import { IconButton } from "../IconButton";
34
+ import { tokens } from "../utils/design";
35
+ import { Heading } from "../Heading";
36
+
37
+ export const ContentOverlay = forwardRef(ContentOverlayPortal);
38
+ const ContentOverlayModal = forwardRef(ContentOverlayInternal);
39
+
40
+ // eslint-disable-next-line max-statements
41
+ function ContentOverlayInternal(
42
+ {
43
+ children,
44
+ title,
45
+ accessibilityLabel,
46
+ fullScreen = false,
47
+ showDismiss = false,
48
+ isDraggable = true,
49
+ adjustToContentHeight = false,
50
+ keyboardShouldPersistTaps = false,
51
+ keyboardAvoidingBehavior,
52
+ scrollEnabled = false,
53
+ modalBackgroundColor = "surface",
54
+ onClose,
55
+ onOpen,
56
+ onBeforeExit,
57
+ loading = false,
58
+ avoidKeyboardLikeIOS,
59
+ }: ContentOverlayProps,
60
+ ref: Ref<ContentOverlayRef>,
61
+ ): JSX.Element {
62
+ isDraggable = onBeforeExit ? false : isDraggable;
63
+ const isCloseableOnOverlayTap = onBeforeExit ? false : true;
64
+ const { formatMessage } = useIntl();
65
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
66
+ const insets = useSafeAreaInsets();
67
+ const [position, setPosition] = useState<"top" | "initial">("initial");
68
+ const isScreenReaderEnabled = useIsScreenReaderEnabled();
69
+ const isFullScreenOrTopPosition =
70
+ fullScreen || (!adjustToContentHeight && position === "top");
71
+ const shouldShowDismiss =
72
+ showDismiss || isScreenReaderEnabled || isFullScreenOrTopPosition;
73
+ const [showHeaderShadow, setShowHeaderShadow] = useState<boolean>(false);
74
+ const overlayHeader = useRef<View>();
75
+
76
+ const internalRef = useRef<Modalize>();
77
+ const [modalizeMethods, setModalizeMethods] = useState<ContentOverlayRef>();
78
+ const callbackInternalRef = useCallback((instance: Modalize) => {
79
+ if (instance && !internalRef.current) {
80
+ internalRef.current = instance;
81
+ setModalizeMethods(instance);
82
+ }
83
+ }, []);
84
+
85
+ const refMethods = useMemo(() => {
86
+ if (!modalizeMethods?.open || !modalizeMethods?.close) {
87
+ return {};
88
+ }
89
+ return {
90
+ open: modalizeMethods?.open,
91
+ close: modalizeMethods?.close,
92
+ };
93
+ }, [modalizeMethods]);
94
+
95
+ const { keyboardHeight } = useKeyboardVisibility();
96
+ useImperativeHandle(ref, () => refMethods, [refMethods]);
97
+
98
+ const {
99
+ handleLayout: handleChildrenLayout,
100
+ height: childrenHeight,
101
+ heightKnown: childrenHeightKnown,
102
+ } = useViewLayoutHeight();
103
+ const {
104
+ handleLayout: handleHeaderLayout,
105
+ height: headerHeight,
106
+ heightKnown: headerHeightKnown,
107
+ } = useViewLayoutHeight();
108
+
109
+ const snapPoint = useMemo(() => {
110
+ if (fullScreen || !isDraggable || adjustToContentHeight) {
111
+ return undefined;
112
+ }
113
+ const overlayHeight = headerHeight + childrenHeight;
114
+ if (overlayHeight >= windowHeight) {
115
+ return undefined;
116
+ }
117
+ return overlayHeight;
118
+ }, [
119
+ fullScreen,
120
+ isDraggable,
121
+ adjustToContentHeight,
122
+ headerHeight,
123
+ childrenHeight,
124
+ windowHeight,
125
+ ]);
126
+
127
+ const modalStyle = [
128
+ styles.modal,
129
+ windowWidth > 640 ? styles.modalForLargeScreens : undefined,
130
+ { backgroundColor: getModalBackgroundColor(modalBackgroundColor) },
131
+ keyboardHeight > 0 && { marginBottom: 0 },
132
+ ];
133
+
134
+ const renderedChildren = renderChildren();
135
+ const renderedHeader = renderHeader();
136
+
137
+ const onCloseController = () => {
138
+ if (!onBeforeExit) {
139
+ internalRef.current?.close();
140
+ return true;
141
+ } else {
142
+ onBeforeExit();
143
+ return false;
144
+ }
145
+ };
146
+
147
+ return (
148
+ <>
149
+ {headerHeightKnown && childrenHeightKnown && (
150
+ <Modalize
151
+ ref={callbackInternalRef}
152
+ overlayStyle={styles.overlay}
153
+ handleStyle={styles.handle}
154
+ handlePosition="inside"
155
+ modalStyle={modalStyle}
156
+ modalTopOffset={tokens["space-larger"]}
157
+ snapPoint={snapPoint}
158
+ closeSnapPointStraightEnabled={false}
159
+ withHandle={isDraggable}
160
+ panGestureEnabled={isDraggable}
161
+ adjustToContentHeight={adjustToContentHeight}
162
+ disableScrollIfPossible={!adjustToContentHeight} // workaround for scroll not working on Android when content fills the screen with adjustToContentHeight
163
+ onClose={onClose}
164
+ onOpen={onOpen}
165
+ keyboardAvoidingBehavior={keyboardAvoidingBehavior}
166
+ avoidKeyboardLikeIOS={avoidKeyboardLikeIOS}
167
+ childrenStyle={styles.childrenStyle}
168
+ onBackButtonPress={onCloseController}
169
+ closeOnOverlayTap={isCloseableOnOverlayTap}
170
+ onOpened={() => {
171
+ if (overlayHeader.current) {
172
+ const reactTag = findNodeHandle(overlayHeader.current);
173
+ if (reactTag) {
174
+ AccessibilityInfo.setAccessibilityFocus(reactTag);
175
+ }
176
+ }
177
+ }}
178
+ scrollViewProps={{
179
+ scrollEnabled,
180
+ showsVerticalScrollIndicator: false,
181
+ stickyHeaderIndices: Platform.OS === "android" ? [0] : undefined,
182
+ onScroll: handleOnScroll,
183
+ keyboardShouldPersistTaps: keyboardShouldPersistTaps
184
+ ? "handled"
185
+ : "never",
186
+ }}
187
+ HeaderComponent={Platform.OS === "ios" ? renderedHeader : undefined}
188
+ onPositionChange={setPosition}
189
+ >
190
+ {Platform.OS === "android" ? renderedHeader : undefined}
191
+ {renderedChildren}
192
+ </Modalize>
193
+ )}
194
+ {!childrenHeightKnown && (
195
+ <View style={[styles.hiddenContent, modalStyle]}>
196
+ {renderedChildren}
197
+ </View>
198
+ )}
199
+ {!headerHeightKnown && (
200
+ <View style={[styles.hiddenContent, modalStyle]}>{renderedHeader}</View>
201
+ )}
202
+ </>
203
+ );
204
+
205
+ function renderHeader() {
206
+ const closeOverlayA11YLabel = formatMessage(
207
+ messages.closeOverlayA11YLabel,
208
+ {
209
+ title: title,
210
+ },
211
+ );
212
+
213
+ const headerStyles = [
214
+ styles.header,
215
+ showHeaderShadow && styles.headerShadow,
216
+ { backgroundColor: getModalBackgroundColor(modalBackgroundColor) },
217
+ ];
218
+
219
+ return (
220
+ <View onLayout={handleHeaderLayout} testID="ATL-Overlay-Header">
221
+ <View style={headerStyles}>
222
+ <View
223
+ style={
224
+ showDismiss ? styles.titleWithDismiss : styles.titleWithoutDimiss
225
+ }
226
+ >
227
+ <Heading
228
+ level="subtitle"
229
+ variation={loading ? "subdued" : "heading"}
230
+ >
231
+ {title}
232
+ </Heading>
233
+ </View>
234
+ {shouldShowDismiss && (
235
+ <View
236
+ style={styles.dismissButton}
237
+ // @ts-expect-error tsc-ci
238
+ ref={overlayHeader}
239
+ accessibilityLabel={accessibilityLabel || closeOverlayA11YLabel}
240
+ accessible={true}
241
+ >
242
+ <IconButton
243
+ name="cross"
244
+ customColor={
245
+ loading ? tokens["color-disabled"] : tokens["color-heading"]
246
+ }
247
+ onPress={() => onCloseController()}
248
+ accessibilityLabel={closeOverlayA11YLabel}
249
+ testID="ATL-Overlay-CloseButton"
250
+ />
251
+ </View>
252
+ )}
253
+ </View>
254
+ </View>
255
+ );
256
+ }
257
+
258
+ function renderChildren() {
259
+ return (
260
+ <View
261
+ style={{ paddingBottom: insets.bottom }}
262
+ onLayout={handleChildrenLayout}
263
+ testID="ATL-Overlay-Children"
264
+ >
265
+ {children}
266
+ </View>
267
+ );
268
+ }
269
+
270
+ function handleOnScroll({
271
+ nativeEvent,
272
+ }: NativeSyntheticEvent<NativeScrollEvent>) {
273
+ setShowHeaderShadow(nativeEvent.contentOffset.y > 0);
274
+ }
275
+
276
+ function getModalBackgroundColor(variation: ModalBackgroundColor) {
277
+ switch (variation) {
278
+ case "surface":
279
+ return tokens["color-surface"];
280
+ case "background":
281
+ return tokens["color-surface--background"];
282
+ }
283
+ }
284
+ }
285
+
286
+ function ContentOverlayPortal(
287
+ modalProps: ContentOverlayProps,
288
+ ref: Ref<ContentOverlayRef>,
289
+ ) {
290
+ return (
291
+ <Portal>
292
+ <ContentOverlayModal ref={ref} {...modalProps} />
293
+ </Portal>
294
+ );
295
+ }
@@ -0,0 +1,42 @@
1
+ import { act, renderHook } from "@testing-library/react-hooks";
2
+ import { DeviceEventEmitter, KeyboardEvent } from "react-native";
3
+ import { useKeyboardVisibility } from "./useKeyboardVisibility";
4
+
5
+ const keyboardEvent: Partial<KeyboardEvent> = {
6
+ endCoordinates: { height: 350, screenX: 120, screenY: 120, width: 200 },
7
+ };
8
+
9
+ describe("when the user is typing", () => {
10
+ it("sets the isKeyboardVisible to true", () => {
11
+ const { result } = renderHook(() => useKeyboardVisibility());
12
+
13
+ act(() => {
14
+ DeviceEventEmitter.emit("keyboardDidShow", keyboardEvent);
15
+ });
16
+
17
+ expect(result.current.isKeyboardVisible).toBe(true);
18
+ });
19
+ it("the keyboardDidShow event emits the keyboard height", () => {
20
+ const { result } = renderHook(() => useKeyboardVisibility());
21
+
22
+ act(() => {
23
+ DeviceEventEmitter.emit("keyboardDidShow", keyboardEvent);
24
+ });
25
+
26
+ expect(result.current.keyboardHeight).toBe(
27
+ keyboardEvent.endCoordinates?.height,
28
+ );
29
+ });
30
+ });
31
+
32
+ describe("when the user not typing", () => {
33
+ it("sets the isKeyboardVisible to false", () => {
34
+ const { result } = renderHook(() => useKeyboardVisibility());
35
+
36
+ act(() => {
37
+ DeviceEventEmitter.emit("keyboardDidHide");
38
+ });
39
+
40
+ expect(result.current.isKeyboardVisible).toBe(false);
41
+ });
42
+ });