@terreno/ui 0.11.1 → 0.11.3

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/src/Common.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  import type {CountryCode} from "libphonenumber-js";
2
2
  import type React from "react";
3
3
  import type {ReactElement, ReactNode} from "react";
4
- import type {ListRenderItemInfo, StyleProp, TextStyle, ViewStyle} from "react-native";
4
+ import type {
5
+ ImageStyle,
6
+ ListRenderItemInfo,
7
+ StyleProp,
8
+ TextInput,
9
+ TextStyle,
10
+ ViewStyle,
11
+ } from "react-native";
5
12
  import type {DimensionValue} from "react-native/Libraries/StyleSheet/StyleSheetTypes";
6
13
  import type {Styles} from "react-native-google-places-autocomplete";
7
14
  import type {SvgProps} from "react-native-svg";
@@ -456,6 +463,7 @@ export interface BoxPropsBase {
456
463
  lgColumn?: UnsignedUpTo12;
457
464
  dangerouslySetInlineStyle?: {
458
465
  __style: {
466
+ // noExplicitAny: escape hatch for arbitrary inline style values
459
467
  [key: string]: any;
460
468
  };
461
469
  };
@@ -528,7 +536,7 @@ export interface BoxPropsBase {
528
536
 
529
537
  onClick?: () => void | Promise<void>;
530
538
  className?: string;
531
- style?: any;
539
+ style?: StyleProp<ViewStyle>;
532
540
  onHoverStart?: () => void | Promise<void>;
533
541
  onHoverEnd?: () => void | Promise<void>;
534
542
  scroll?: boolean;
@@ -554,7 +562,7 @@ export type BoxProps =
554
562
  export type BoxColor = SurfaceColor | "transparent";
555
563
 
556
564
  export interface ErrorBoundaryProps {
557
- onError?: (error: Error, stack: any) => void;
565
+ onError?: (error: Error, stack: string) => void;
558
566
  children?: ReactNode;
559
567
  }
560
568
 
@@ -652,7 +660,7 @@ export interface TextFieldProps extends BaseFieldProps, HelperTextProps, ErrorTe
652
660
  multiline?: boolean;
653
661
  rows?: number;
654
662
 
655
- inputRef?: any;
663
+ inputRef?: (ref: TextInput | null) => void;
656
664
  trimOnBlur?: boolean;
657
665
 
658
666
  aiSuggestion?: AiSuggestionProps;
@@ -782,7 +790,7 @@ export interface ImageProps {
782
790
  size?: string;
783
791
  srcSet?: string;
784
792
  fullWidth?: boolean;
785
- style?: any;
793
+ style?: ImageStyle;
786
794
  }
787
795
 
788
796
  export interface BackButtonInterface {
@@ -850,14 +858,17 @@ export interface SplitPageProps {
850
858
  loading?: boolean;
851
859
  color?: SurfaceColor;
852
860
  keyboardOffset?: number;
861
+ // noExplicitAny: ListRenderItemInfo generic type depends on the consumer's data shape
853
862
  renderListViewItem: (itemInfo: ListRenderItemInfo<any>) => ReactElement | null;
854
863
  renderListViewHeader?: () => ReactElement | null;
855
864
  renderContent?: (index?: number) => ReactElement | ReactElement[] | null;
865
+ // noExplicitAny: list data type varies by consumer's data model
856
866
  listViewData: any[];
857
- listViewExtraData?: any;
867
+ listViewExtraData?: unknown;
858
868
  listViewWidth?: number;
859
869
  listViewMaxWidth?: number;
860
870
  renderChild?: () => ReactChild;
871
+ // noExplicitAny: callback value type varies by consumer's data model
861
872
  onSelectionChange?: (value?: any) => void | Promise<void>;
862
873
  }
863
874
 
@@ -896,7 +907,7 @@ export interface AddressInterface {
896
907
  export interface TransformValueOptions {
897
908
  func?: (value: string) => string;
898
909
  options?: {
899
- [key: string]: any;
910
+ [key: string]: unknown;
900
911
  };
901
912
  }
902
913
 
@@ -1745,6 +1756,7 @@ export interface HeightActionSheetProps {
1745
1756
 
1746
1757
  export interface HyperlinkProps {
1747
1758
  linkDefault?: boolean;
1759
+ // noExplicitAny: type comes from external linkify-it library which lacks an exported type
1748
1760
  linkify?: any;
1749
1761
  linkStyle?: StyleProp<any>;
1750
1762
  linkText?: string | ((url: string) => string);
@@ -1901,10 +1913,12 @@ export interface ModalProps {
1901
1913
  /**
1902
1914
  * The function to call when the primary button is clicked.
1903
1915
  */
1916
+ // noExplicitAny: callback value type varies by consumer context
1904
1917
  primaryButtonOnClick?: (value?: any) => void | Promise<void>;
1905
1918
  /**
1906
1919
  * The function to call when the secondary button is clicked.
1907
1920
  */
1921
+ // noExplicitAny: callback value type varies by consumer context
1908
1922
  secondaryButtonOnClick?: (value?: any) => void | Promise<void>;
1909
1923
  }
1910
1924
 
@@ -1917,6 +1931,7 @@ export interface NumberPickerActionSheetProps {
1917
1931
  }
1918
1932
 
1919
1933
  export interface PageProps {
1934
+ // noExplicitAny: React Navigation type varies by navigation stack configuration
1920
1935
  navigation?: any;
1921
1936
  scroll?: boolean;
1922
1937
  loading?: boolean;
@@ -1930,11 +1945,11 @@ export interface PageProps {
1930
1945
  color?: SurfaceColor;
1931
1946
  maxWidth?: number | string;
1932
1947
  keyboardOffset?: number;
1933
- footer?: any;
1948
+ footer?: ReactNode;
1934
1949
  rightButton?: string;
1935
1950
  rightButtonOnClick?: () => void;
1936
- children?: any;
1937
- onError?: (error: Error, stack: any) => void;
1951
+ children?: ReactChildren;
1952
+ onError?: (error: Error, stack: string) => void;
1938
1953
  }
1939
1954
 
1940
1955
  export interface ProgressBarProps {
@@ -1957,7 +1972,7 @@ export interface RadioFieldProps {
1957
1972
  export interface SignatureFieldProps {
1958
1973
  disabled?: boolean; // default "default"
1959
1974
  value?: string;
1960
- onChange: (value: any) => void;
1975
+ onChange: (value: string) => void;
1961
1976
  title?: string; // default "Signature"
1962
1977
  onStart?: () => void;
1963
1978
  onEnd?: () => void;
@@ -2356,16 +2371,19 @@ export type TapToEditProps =
2356
2371
 
2357
2372
  export interface BaseTapToEditProps extends Omit<FieldProps, "onChange" | "value"> {
2358
2373
  title: string;
2374
+ // noExplicitAny: value type varies across TapToEdit field types (text, number, date, etc.)
2359
2375
  value: any;
2360
2376
 
2361
2377
  /**
2362
2378
  * Not required if not editable.
2363
2379
  */
2380
+ // noExplicitAny: value type varies across TapToEdit field types
2364
2381
  setValue?: (value: any) => void;
2365
2382
 
2366
2383
  /**
2367
2384
  * Not required if not editable.
2368
2385
  */
2386
+ // noExplicitAny: value type varies across TapToEdit field types
2369
2387
  onSave?: (value: any) => void | Promise<void>;
2370
2388
 
2371
2389
  /**
@@ -2378,6 +2396,7 @@ export interface BaseTapToEditProps extends Omit<FieldProps, "onChange" | "value
2378
2396
  * Enable edit mode from outside the component.
2379
2397
  */
2380
2398
  isEditing?: boolean;
2399
+ // noExplicitAny: input value type varies across TapToEdit field types
2381
2400
  transform?: (value: any) => string;
2382
2401
  /**
2383
2402
  * Show a confirmation modal before saving the value.
@@ -2433,7 +2452,7 @@ export interface APIError {
2433
2452
  source?: string;
2434
2453
  pointer?: string;
2435
2454
  parameter?: string;
2436
- meta?: {[id: string]: any};
2455
+ meta?: {[id: string]: unknown};
2437
2456
  };
2438
2457
  }
2439
2458
 
@@ -2466,6 +2485,7 @@ export interface ModelFields {
2466
2485
 
2467
2486
  export interface OpenAPISpec {
2468
2487
  paths: {
2488
+ // noExplicitAny: OpenAPI path items are deeply accessed with chained property lookups
2469
2489
  [key: string]: any;
2470
2490
  };
2471
2491
  }
@@ -2498,7 +2518,8 @@ export interface ModelAdminFieldConfig {
2498
2518
 
2499
2519
  // The props for a custom column component for ModelAdmin.
2500
2520
  export interface ModelAdminCustomComponentProps extends Omit<FieldProps, "name"> {
2501
- doc: any; // The rest of the document.
2521
+ // noExplicitAny: document shape varies by model used with ModelAdmin
2522
+ doc: any;
2502
2523
  fieldKey: string; // Dot notation representation of the field.
2503
2524
  // user: User;
2504
2525
  editing: boolean; // Allow for inline editing of the field.
@@ -1,6 +1,7 @@
1
- import {describe, expect, it, mock} from "bun:test";
1
+ import {afterEach, describe, expect, it, mock} from "bun:test";
2
2
  import {fireEvent} from "@testing-library/react-native";
3
3
 
4
+ import {isMobileDevice} from "./MediaQuery";
4
5
  import {Modal} from "./Modal";
5
6
  import {Text} from "./Text";
6
7
  import {renderWithTheme} from "./test-utils";
@@ -355,3 +356,47 @@ describe("Modal", () => {
355
356
  expect(stopPropagation).not.toHaveBeenCalled();
356
357
  });
357
358
  });
359
+
360
+ describe("Modal mobile branch", () => {
361
+ afterEach(() => {
362
+ (isMobileDevice as ReturnType<typeof mock>).mockImplementation(() => false);
363
+ });
364
+
365
+ it("renders ActionSheet when isMobileDevice is true", () => {
366
+ (isMobileDevice as ReturnType<typeof mock>).mockImplementation(() => true);
367
+ const {toJSON} = renderWithTheme(
368
+ <Modal onDismiss={() => {}} title="Mobile Modal" visible>
369
+ <Text>Mobile Content</Text>
370
+ </Modal>
371
+ );
372
+ expect(toJSON()).toBeTruthy();
373
+ });
374
+
375
+ it("renders ActionSheet with title and buttons on mobile", () => {
376
+ (isMobileDevice as ReturnType<typeof mock>).mockImplementation(() => true);
377
+ const {toJSON} = renderWithTheme(
378
+ <Modal
379
+ onDismiss={() => {}}
380
+ primaryButtonOnClick={() => {}}
381
+ primaryButtonText="Save"
382
+ secondaryButtonOnClick={() => {}}
383
+ secondaryButtonText="Cancel"
384
+ title="Mobile Actions"
385
+ visible
386
+ >
387
+ <Text>Content</Text>
388
+ </Modal>
389
+ );
390
+ expect(toJSON()).toBeTruthy();
391
+ });
392
+
393
+ it("renders ActionSheet with persistOnBackgroundClick enabled", () => {
394
+ (isMobileDevice as ReturnType<typeof mock>).mockImplementation(() => true);
395
+ const {toJSON} = renderWithTheme(
396
+ <Modal onDismiss={() => {}} persistOnBackgroundClick title="Persistent Mobile" visible>
397
+ <Text>Content</Text>
398
+ </Modal>
399
+ );
400
+ expect(toJSON()).toBeTruthy();
401
+ });
402
+ });
@@ -40,6 +40,7 @@ import {
40
40
 
41
41
  import {Icon} from "./Icon";
42
42
  import {useTheme} from "./Theme";
43
+ import {useWebDropdownAnchor, WebDropdownMenu, type WebDropdownMenuOption} from "./WebDropdownMenu";
43
44
 
44
45
  export const defaultStyles = StyleSheet.create({
45
46
  chevron: {
@@ -129,6 +130,15 @@ export function RNPickerSelect({
129
130
  const [doneDepressed, setDoneDepressed] = useState<boolean>(false);
130
131
  const {theme} = useTheme();
131
132
 
133
+ // Web-only: anchor the custom dropdown menu to the trigger element so that
134
+ // Safari/Firefox/Chrome all render the same styled menu instead of the
135
+ // browser's native <select> UI.
136
+ const {
137
+ anchor: webAnchor,
138
+ measure: measureWebAnchor,
139
+ triggerRef: webTriggerRef,
140
+ } = useWebDropdownAnchor();
141
+
132
142
  // On web, blur the active element before the picker modal opens to prevent
133
143
  // "aria-hidden on a focused element" warnings from React Native Web.
134
144
  useEffect(() => {
@@ -520,10 +530,61 @@ export function RNPickerSelect({
520
530
  );
521
531
  };
522
532
 
523
- // TODO: Create custom React component for web in order to apply library style rules
533
+ // Custom web dropdown. Rendering the native <Picker> on web delegates
534
+ // styling to each browser (Safari in particular looks very different from
535
+ // Chrome/Firefox). Instead, we render a styled trigger + popup menu so the
536
+ // dropdown looks identical across browsers and matches the Terreno design.
537
+ const openWebMenu = (): void => {
538
+ if (disabled) {
539
+ return;
540
+ }
541
+ measureWebAnchor(() => {
542
+ setShowPicker(true);
543
+ if (onOpen) {
544
+ onOpen();
545
+ }
546
+ });
547
+ };
548
+
549
+ const closeWebMenu = (): void => {
550
+ setShowPicker(false);
551
+ if (onClose) {
552
+ onClose();
553
+ }
554
+ };
555
+
556
+ // Build the dropdown option list AND track each option's original index in
557
+ // `options` so `onValueChange` receives the same index that the native
558
+ // Picker would have reported (needed when a placeholder is present).
559
+ const {menuOptions: webMenuOptions, originalIndexes: webMenuOptionIndexes} = useMemo<{
560
+ menuOptions: WebDropdownMenuOption[];
561
+ originalIndexes: number[];
562
+ }>(() => {
563
+ const menuOptions: WebDropdownMenuOption[] = [];
564
+ const originalIndexes: number[] = [];
565
+ for (let i = 0; i < options.length; i++) {
566
+ const item = options[i];
567
+ if (!item || typeof item !== "object" || typeof item.label !== "string") {
568
+ continue;
569
+ }
570
+ menuOptions.push({
571
+ color: item.color,
572
+ key: item.key,
573
+ label: item.label,
574
+ value: String(item.value ?? ""),
575
+ });
576
+ originalIndexes.push(i);
577
+ }
578
+ return {menuOptions, originalIndexes};
579
+ }, [options]);
580
+
524
581
  const renderWeb = () => {
582
+ const displayLabel = selectedItem?.inputLabel ?? selectedItem?.label ?? "";
583
+ const selectedOriginalIdx = getSelectedItem(itemKey, value).idx;
584
+ const webSelectedIndex = webMenuOptionIndexes.indexOf(selectedOriginalIdx);
525
585
  return (
526
586
  <View
587
+ ref={webTriggerRef}
527
588
  style={[
528
589
  defaultStyles.viewContainer,
529
590
  {
@@ -535,31 +596,55 @@ export function RNPickerSelect({
535
596
  },
536
597
  ]}
537
598
  >
538
- <Picker
539
- enabled={!disabled}
540
- onValueChange={onValueChangeEvent}
541
- selectedValue={selectedItem?.value}
542
- style={[
543
- {
544
- backgroundColor: theme.surface.base,
545
- borderColor: "black",
546
- borderRadius: 4,
547
- borderWidth: 0,
548
- height: "100%",
549
- paddingHorizontal: 8,
550
- paddingVertical: 8,
551
- width: "100%",
552
- },
553
- disabled && {
554
- backgroundColor: theme.surface.neutralLight,
555
- color: theme.text.secondaryLight,
556
- opacity: 1,
557
- },
558
- ]}
599
+ <Pressable
600
+ aria-role="button"
601
+ disabled={disabled}
602
+ onPress={openWebMenu}
603
+ style={{
604
+ alignItems: "center",
605
+ flexDirection: "row",
606
+ justifyContent: "space-between",
607
+ minHeight: 40,
608
+ paddingHorizontal: 8,
609
+ width: "100%",
610
+ }}
559
611
  testID="web_picker"
612
+ {...touchableWrapperProps}
560
613
  >
561
- {renderPickerItems()}
562
- </Picker>
614
+ <Text
615
+ numberOfLines={1}
616
+ style={{
617
+ color: disabled ? theme.text.secondaryLight : theme.text.primary,
618
+ flex: 1,
619
+ paddingRight: 8,
620
+ }}
621
+ testID="text_input"
622
+ >
623
+ {displayLabel}
624
+ </Text>
625
+ <Icon
626
+ color={disabled ? "secondaryLight" : "primary"}
627
+ iconName={showPicker ? "angle-up" : "angle-down"}
628
+ size="sm"
629
+ />
630
+ </Pressable>
631
+ <WebDropdownMenu
632
+ anchor={webAnchor}
633
+ onClose={closeWebMenu}
634
+ onSelect={(_val, idx) => {
635
+ const originalIndex = webMenuOptionIndexes[idx] ?? idx;
636
+ // Pass the original (non-stringified) value through so lodash
637
+ // `isEqual` matching in `getSelectedItem` works for number /
638
+ // object values.
639
+ const originalValue = options[originalIndex]?.value;
640
+ onValueChangeEvent(originalValue, originalIndex);
641
+ closeWebMenu();
642
+ }}
643
+ options={webMenuOptions}
644
+ selectedIndex={webSelectedIndex >= 0 ? webSelectedIndex : undefined}
645
+ testIDPrefix="web_dropdown"
646
+ visible={showPicker}
647
+ />
563
648
  </View>
564
649
  );
565
650
  };
@@ -6,6 +6,7 @@ import {Modal, Platform, Text, TouchableOpacity, View} from "react-native";
6
6
  import type {FieldOption, SelectBadgeProps, SurfaceTheme, TextTheme} from "./Common";
7
7
  import {Icon} from "./Icon";
8
8
  import {useTheme} from "./Theme";
9
+ import {useWebDropdownAnchor, WebDropdownMenu, type WebDropdownMenuOption} from "./WebDropdownMenu";
9
10
 
10
11
  export const SelectBadge = ({
11
12
  value,
@@ -24,6 +25,15 @@ export const SelectBadge = ({
24
25
  // Assures the badge display value persists when user scrolls through options
25
26
  const [iosDisplayValue, setIosDisplayValue] = useState<string | undefined>(value);
26
27
 
28
+ // Web-only: anchor the custom dropdown menu to the trigger element so that
29
+ // Safari/Firefox/Chrome all render the same styled menu instead of the
30
+ // browser's native <select> UI.
31
+ const {
32
+ anchor: webAnchor,
33
+ measure: measureWebAnchor,
34
+ triggerRef: webTriggerRef,
35
+ } = useWebDropdownAnchor();
36
+
27
37
  const secondaryBorderColors = {
28
38
  custom: "#AAAAAA",
29
39
  error: "#F39E9E",
@@ -190,14 +200,13 @@ export const SelectBadge = ({
190
200
  selectedValue={findSelectedItem(value)?.value ?? undefined}
191
201
  style={[
192
202
  {
203
+ backgroundColor: "transparent",
193
204
  color: "transparent",
194
205
  height: "100%",
195
206
  opacity: 0,
196
207
  position: "absolute",
197
208
  width: "100%",
198
209
  },
199
- // Android headless picker: transparent overlay to capture touches without visible UI.
200
- Platform.OS !== "web" && {backgroundColor: "transparent"},
201
210
  ]}
202
211
  >
203
212
  {renderPickerItems()}
@@ -205,14 +214,59 @@ export const SelectBadge = ({
205
214
  );
206
215
  }, [disabled, findSelectedItem, value, handleOnChange, renderPickerItems]);
207
216
 
217
+ // Custom web dropdown. Rendering the native <Picker> on web delegates
218
+ // styling to each browser (Safari in particular looks very different from
219
+ // Chrome/Firefox). Instead, we render a styled popup menu anchored to the
220
+ // badge so the dropdown looks identical across browsers.
221
+ const webMenuOptions = useMemo<WebDropdownMenuOption[]>(
222
+ () => options.map((item) => ({key: item.key, label: item.label, value: item.value})),
223
+ [options]
224
+ );
225
+
226
+ const renderWebPicker = useCallback(() => {
227
+ return (
228
+ <WebDropdownMenu
229
+ anchor={webAnchor}
230
+ minWidth={160}
231
+ onClose={() => setShowPicker(false)}
232
+ onSelect={(val) => handleOnChange(val)}
233
+ options={webMenuOptions}
234
+ optionTextStyle={{fontFamily: "text", fontSize: 12}}
235
+ selectedValue={value}
236
+ testIDPrefix="web_badge"
237
+ visible={showPicker}
238
+ width={undefined}
239
+ />
240
+ );
241
+ }, [showPicker, webAnchor, webMenuOptions, value, handleOnChange]);
242
+
243
+ const openWebMenu = (): void => {
244
+ if (disabled) {
245
+ return;
246
+ }
247
+ measureWebAnchor(() => {
248
+ setShowPicker(true);
249
+ });
250
+ };
251
+
208
252
  return (
209
- <View style={{alignItems: "flex-start", opacity: disabled ? 0.5 : 1}}>
253
+ <View ref={webTriggerRef} style={{alignItems: "flex-start", opacity: disabled ? 0.5 : 1}}>
210
254
  <TouchableOpacity
211
255
  accessibilityHint="Opens the options picker"
212
256
  accessibilityLabel="Open select badge options"
213
257
  aria-role="button"
214
258
  disabled={disabled}
215
- onPress={() => setShowPicker(!showPicker)}
259
+ onPress={() => {
260
+ if (Platform.OS === "web") {
261
+ if (showPicker) {
262
+ setShowPicker(false);
263
+ } else {
264
+ openWebMenu();
265
+ }
266
+ return;
267
+ }
268
+ setShowPicker(!showPicker);
269
+ }}
216
270
  >
217
271
  <View
218
272
  style={{
@@ -274,7 +328,11 @@ export const SelectBadge = ({
274
328
  </View>
275
329
  </View>
276
330
  </TouchableOpacity>
277
- {Platform.OS === "ios" ? renderIosPicker() : renderPicker()}
331
+ {Platform.OS === "ios"
332
+ ? renderIosPicker()
333
+ : Platform.OS === "web"
334
+ ? renderWebPicker()
335
+ : renderPicker()}
278
336
  </View>
279
337
  );
280
338
  };
@@ -0,0 +1,75 @@
1
+ import {afterAll, beforeAll, describe, expect, it, mock} from "bun:test";
2
+ import {act, fireEvent} from "@testing-library/react-native";
3
+ import {Platform} from "react-native";
4
+
5
+ import {SelectBadge} from "./SelectBadge";
6
+ import {renderWithTheme} from "./test-utils";
7
+
8
+ // Force Platform.OS to "web" for this file so SelectBadge takes the web
9
+ // rendering branch (custom WebDropdownMenu instead of the native iOS Picker).
10
+ const originalOS = Platform.OS;
11
+ beforeAll(() => {
12
+ Object.defineProperty(Platform, "OS", {configurable: true, value: "web"});
13
+ });
14
+ afterAll(() => {
15
+ Object.defineProperty(Platform, "OS", {configurable: true, value: originalOS});
16
+ });
17
+
18
+ describe("SelectBadge (web)", () => {
19
+ const options = [
20
+ {label: "Option A", value: "a"},
21
+ {label: "Option B", value: "b"},
22
+ {label: "Option C", value: "c"},
23
+ ];
24
+
25
+ it("renders the styled web dropdown menu (not the native picker) when opened", () => {
26
+ const {getByLabelText, getByTestId} = renderWithTheme(
27
+ <SelectBadge onChange={() => {}} options={options} value="a" />
28
+ );
29
+ expect(getByTestId("web_badge_modal").props.visible).toBe(false);
30
+ act(() => {
31
+ fireEvent.press(getByLabelText("Open select badge options"));
32
+ });
33
+ expect(getByTestId("web_badge_modal").props.visible).toBe(true);
34
+ expect(getByTestId("web_badge_menu")).toBeTruthy();
35
+ });
36
+
37
+ it("invokes onChange with the selected value when an option is pressed", () => {
38
+ const handleChange = mock((_val: string) => {});
39
+ const {getByLabelText, getByTestId} = renderWithTheme(
40
+ <SelectBadge onChange={handleChange} options={options} value="a" />
41
+ );
42
+ act(() => {
43
+ fireEvent.press(getByLabelText("Open select badge options"));
44
+ });
45
+ act(() => {
46
+ fireEvent.press(getByTestId("web_badge_option_b"));
47
+ });
48
+ expect(handleChange).toHaveBeenCalledTimes(1);
49
+ expect(handleChange).toHaveBeenCalledWith("b");
50
+ });
51
+
52
+ it("closes the menu when the backdrop is pressed", () => {
53
+ const {getByLabelText, getByTestId} = renderWithTheme(
54
+ <SelectBadge onChange={() => {}} options={options} value="a" />
55
+ );
56
+ act(() => {
57
+ fireEvent.press(getByLabelText("Open select badge options"));
58
+ });
59
+ expect(getByTestId("web_badge_modal").props.visible).toBe(true);
60
+ act(() => {
61
+ fireEvent.press(getByTestId("web_badge_backdrop"));
62
+ });
63
+ expect(getByTestId("web_badge_modal").props.visible).toBe(false);
64
+ });
65
+
66
+ it("does not open the dropdown when disabled", () => {
67
+ const {getByLabelText, getByTestId} = renderWithTheme(
68
+ <SelectBadge disabled onChange={() => {}} options={options} value="a" />
69
+ );
70
+ act(() => {
71
+ fireEvent.press(getByLabelText("Open select badge options"));
72
+ });
73
+ expect(getByTestId("web_badge_modal").props.visible).toBe(false);
74
+ });
75
+ });