@boostdev/design-system-react-native 0.2.5 → 0.3.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 (26) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/dist/index.cjs +176 -165
  3. package/dist/index.d.cts +12 -2
  4. package/dist/index.d.ts +12 -2
  5. package/dist/index.js +176 -165
  6. package/package.json +1 -1
  7. package/src/components/interaction/Dialog/Dialog.mdx +3 -2
  8. package/src/components/interaction/Dialog/Dialog.spec.tsx +42 -11
  9. package/src/components/interaction/Dialog/Dialog.tsx +19 -12
  10. package/src/components/interaction/DropdownMenu/DropdownMenu.mdx +3 -1
  11. package/src/components/interaction/DropdownMenu/DropdownMenu.spec.tsx +73 -0
  12. package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +79 -73
  13. package/src/components/interaction/Popover/Popover.mdx +3 -1
  14. package/src/components/interaction/Popover/Popover.spec.tsx +43 -3
  15. package/src/components/interaction/Popover/Popover.tsx +23 -13
  16. package/src/components/interaction/SideSheet/SideSheet.mdx +4 -3
  17. package/src/components/interaction/SideSheet/SideSheet.spec.tsx +8 -9
  18. package/src/components/interaction/SideSheet/SideSheet.tsx +9 -3
  19. package/src/components/ui/Alert/Alert.spec.tsx +32 -1
  20. package/src/components/ui/Alert/Alert.stories.tsx +45 -0
  21. package/src/components/ui/Alert/Alert.tsx +11 -2
  22. package/src/components/ui/Collapsible/Collapsible.spec.tsx +11 -0
  23. package/src/components/ui/Collapsible/Collapsible.tsx +13 -8
  24. package/src/components/ui/NotificationBanner/NotificationBanner.spec.tsx +32 -1
  25. package/src/components/ui/NotificationBanner/NotificationBanner.stories.tsx +30 -0
  26. package/src/components/ui/NotificationBanner/NotificationBanner.tsx +16 -3
package/dist/index.js CHANGED
@@ -435,6 +435,7 @@ function Alert({
435
435
  error: { bg: colors2.colorRedSubtle, text: colors2.colorOnBg, border: colors2.colorError }
436
436
  };
437
437
  const { bg, text, border: borderColor } = variantColors[variant];
438
+ const resolvedChildren = typeof children === "function" ? children({ color: text }) : children;
438
439
  return /* @__PURE__ */ jsxs2(
439
440
  View5,
440
441
  {
@@ -445,7 +446,7 @@ function Alert({
445
446
  icon && /* @__PURE__ */ jsx9(View5, { style: styles7.icon, children: icon }),
446
447
  /* @__PURE__ */ jsxs2(View5, { style: styles7.content, children: [
447
448
  title && /* @__PURE__ */ jsx9(Text5, { style: [styles7.title, { color: text }], children: title }),
448
- /* @__PURE__ */ jsx9(Text5, { style: [styles7.body, { color: text }], children })
449
+ typeof resolvedChildren === "string" || typeof resolvedChildren === "number" ? /* @__PURE__ */ jsx9(Text5, { style: [styles7.body, { color: text }], children: resolvedChildren }) : resolvedChildren
449
450
  ] }),
450
451
  onDismiss && /* @__PURE__ */ jsx9(
451
452
  Pressable,
@@ -474,8 +475,10 @@ var styles8 = StyleSheet8.create({
474
475
  paddingHorizontal: spacing.m,
475
476
  gap: spacing.xs
476
477
  },
478
+ body: {
479
+ flex: 1
480
+ },
477
481
  content: {
478
- flex: 1,
479
482
  fontSize: font.size.bodyS,
480
483
  fontFamily: font.family.body,
481
484
  lineHeight: lineHeight.bodyS
@@ -501,6 +504,7 @@ function NotificationBanner({
501
504
  error: { bg: colors2.colorRedSubtle, text: colors2.colorOnBg, border: colors2.colorError }
502
505
  };
503
506
  const { bg, text, border: borderColor } = variantColors[variant];
507
+ const resolvedChildren = typeof children === "function" ? children({ color: text }) : children;
504
508
  return /* @__PURE__ */ jsxs3(
505
509
  View6,
506
510
  {
@@ -508,7 +512,7 @@ function NotificationBanner({
508
512
  accessibilityLiveRegion: isUrgent ? "assertive" : "polite",
509
513
  style: [styles8.banner, { backgroundColor: bg, borderBottomWidth: 3, borderBottomColor: borderColor }, style],
510
514
  children: [
511
- /* @__PURE__ */ jsx10(Text6, { style: [styles8.content, { color: text }], children }),
515
+ /* @__PURE__ */ jsx10(View6, { style: styles8.body, children: typeof resolvedChildren === "string" || typeof resolvedChildren === "number" ? /* @__PURE__ */ jsx10(Text6, { style: [styles8.content, { color: text }], children: resolvedChildren }) : resolvedChildren }),
512
516
  action,
513
517
  onDismiss && /* @__PURE__ */ jsx10(
514
518
  Pressable2,
@@ -1059,7 +1063,12 @@ var styles15 = StyleSheet15.create({
1059
1063
  lineHeight: lineHeight.body
1060
1064
  },
1061
1065
  chevron: {
1062
- fontSize: font.size.body,
1066
+ // Border-box chevron (no glyph font dependency): a small square with two
1067
+ // adjacent borders, rotated to form a crisp "›" / "⌄" disclosure indicator.
1068
+ width: 8,
1069
+ height: 8,
1070
+ borderRightWidth: 2,
1071
+ borderBottomWidth: 2,
1063
1072
  marginStart: spacing.xs
1064
1073
  },
1065
1074
  content: {
@@ -1098,9 +1107,9 @@ function Collapsible({
1098
1107
  }
1099
1108
  onToggle?.(next);
1100
1109
  }, [isOpen, isControlled, onToggle, rotation]);
1101
- const rotateZ = rotation.interpolate({
1110
+ const rotate = rotation.interpolate({
1102
1111
  inputRange: [0, 1],
1103
- outputRange: ["0deg", "90deg"]
1112
+ outputRange: ["-45deg", "45deg"]
1104
1113
  });
1105
1114
  return /* @__PURE__ */ jsxs9(
1106
1115
  View12,
@@ -1119,12 +1128,12 @@ function Collapsible({
1119
1128
  children: [
1120
1129
  /* @__PURE__ */ jsx17(Text13, { style: [styles15.summaryText, { color: colors2.colorOnBg }], children: summary }),
1121
1130
  /* @__PURE__ */ jsx17(
1122
- Animated2.Text,
1131
+ Animated2.View,
1123
1132
  {
1124
- style: [styles15.chevron, { color: colors2.colorOnBgSubtle, transform: [{ rotateZ }] }],
1133
+ testID: "bds-collapsible-chevron",
1134
+ style: [styles15.chevron, { borderColor: colors2.colorOnBgSubtle, transform: [{ rotate }] }],
1125
1135
  importantForAccessibility: "no",
1126
- accessibilityElementsHidden: true,
1127
- children: "\u25B8"
1136
+ accessibilityElementsHidden: true
1128
1137
  }
1129
1138
  )
1130
1139
  ]
@@ -3777,46 +3786,48 @@ function Dialog({
3777
3786
  transparent: true,
3778
3787
  animationType: "fade",
3779
3788
  onRequestClose: onClose,
3780
- children: /* @__PURE__ */ jsx44(
3781
- Pressable22,
3782
- {
3783
- style: styles40.overlay,
3784
- onPress: onClose,
3785
- accessibilityRole: "button",
3786
- accessibilityLabel: "Close dialog",
3787
- children: /* @__PURE__ */ jsxs29(
3788
- Pressable22,
3789
- {
3790
- style: [styles40.panel, { backgroundColor: colors2.colorGreySubtle }, style],
3791
- onPress: () => {
3792
- },
3793
- accessibilityRole: "none",
3794
- children: [
3795
- /* @__PURE__ */ jsx44(
3796
- Pressable22,
3797
- {
3798
- style: styles40.closeButton,
3799
- onPress: onClose,
3800
- accessibilityRole: "button",
3801
- accessibilityLabel: "Close dialog",
3802
- hitSlop: 8,
3803
- children: /* @__PURE__ */ jsx44(
3804
- Text32,
3805
- {
3806
- style: [styles40.closeText, { color: colors2.colorOnBgSubtle }],
3807
- "aria-hidden": true,
3808
- children: "\u2715"
3809
- }
3810
- )
3811
- }
3812
- ),
3813
- !!title && /* @__PURE__ */ jsx44(View37, { style: styles40.header, children: /* @__PURE__ */ jsx44(Text32, { style: [styles40.title, { color: colors2.colorOnGreySubtle }], children: title }) }),
3814
- children
3815
- ]
3816
- }
3817
- )
3818
- }
3819
- )
3789
+ children: /* @__PURE__ */ jsxs29(View37, { style: styles40.overlay, children: [
3790
+ /* @__PURE__ */ jsx44(
3791
+ Pressable22,
3792
+ {
3793
+ testID: "bds-dialog-backdrop",
3794
+ style: StyleSheet40.absoluteFill,
3795
+ onPress: onClose,
3796
+ accessible: false,
3797
+ importantForAccessibility: "no"
3798
+ }
3799
+ ),
3800
+ /* @__PURE__ */ jsxs29(
3801
+ View37,
3802
+ {
3803
+ style: [styles40.panel, { backgroundColor: colors2.colorGreySubtle }, style],
3804
+ accessibilityViewIsModal: true,
3805
+ onAccessibilityEscape: onClose,
3806
+ children: [
3807
+ /* @__PURE__ */ jsx44(
3808
+ Pressable22,
3809
+ {
3810
+ style: styles40.closeButton,
3811
+ onPress: onClose,
3812
+ accessibilityRole: "button",
3813
+ accessibilityLabel: "Close dialog",
3814
+ hitSlop: 8,
3815
+ children: /* @__PURE__ */ jsx44(
3816
+ Text32,
3817
+ {
3818
+ style: [styles40.closeText, { color: colors2.colorOnBgSubtle }],
3819
+ "aria-hidden": true,
3820
+ children: "\u2715"
3821
+ }
3822
+ )
3823
+ }
3824
+ ),
3825
+ !!title && /* @__PURE__ */ jsx44(View37, { style: styles40.header, children: /* @__PURE__ */ jsx44(Text32, { style: [styles40.title, { color: colors2.colorOnGreySubtle }], children: title }) }),
3826
+ children
3827
+ ]
3828
+ }
3829
+ )
3830
+ ] })
3820
3831
  }
3821
3832
  );
3822
3833
  }
@@ -3942,8 +3953,9 @@ function SideSheet({
3942
3953
  {
3943
3954
  style: StyleSheet41.absoluteFill,
3944
3955
  onPress: onClose,
3945
- accessibilityRole: "button",
3946
- accessibilityLabel: "Close side sheet"
3956
+ testID: "bds-sidesheet-backdrop",
3957
+ accessible: false,
3958
+ importantForAccessibility: "no"
3947
3959
  }
3948
3960
  )
3949
3961
  }
@@ -3959,7 +3971,8 @@ function SideSheet({
3959
3971
  },
3960
3972
  style
3961
3973
  ],
3962
- accessibilityRole: "none",
3974
+ accessibilityViewIsModal: true,
3975
+ onAccessibilityEscape: onClose,
3963
3976
  children: [
3964
3977
  /* @__PURE__ */ jsxs30(
3965
3978
  View38,
@@ -4295,8 +4308,9 @@ function DropdownMenu({
4295
4308
  {
4296
4309
  style: StyleSheet43.absoluteFill,
4297
4310
  onPress: () => setOpen(false),
4298
- accessibilityRole: "button",
4299
- accessibilityLabel: "Close menu"
4311
+ testID: "bds-dropdownmenu-backdrop",
4312
+ accessible: false,
4313
+ importantForAccessibility: "no"
4300
4314
  }
4301
4315
  )
4302
4316
  }
@@ -4311,100 +4325,95 @@ function DropdownMenu({
4311
4325
  transform: [{ translateY: slideAnim }]
4312
4326
  }
4313
4327
  ],
4314
- children: /* @__PURE__ */ jsx47(
4315
- Pressable25,
4316
- {
4317
- onPress: () => {
4318
- },
4319
- children: /* @__PURE__ */ jsxs32(SafeAreaView2, { children: [
4320
- /* @__PURE__ */ jsxs32(
4321
- View40,
4322
- {
4323
- style: [
4324
- styles43.sheetHeader,
4325
- { borderBottomColor: colors2.colorBgSubtle }
4326
- ],
4327
- children: [
4328
- /* @__PURE__ */ jsx47(Text35, { style: [styles43.sheetTitle, { color: colors2.colorOnBg }], children: title }),
4329
- /* @__PURE__ */ jsx47(
4330
- Pressable25,
4331
- {
4332
- accessibilityRole: "button",
4333
- accessibilityLabel: "Close",
4334
- onPress: () => setOpen(false),
4335
- hitSlop: 8,
4336
- children: /* @__PURE__ */ jsx47(
4337
- Text35,
4338
- {
4339
- style: [
4340
- styles43.closeText,
4341
- { color: colors2.colorInteractive }
4342
- ],
4343
- children: "Done"
4344
- }
4345
- )
4346
- }
4347
- )
4348
- ]
4349
- }
4350
- ),
4351
- /* @__PURE__ */ jsx47(
4352
- FlatList2,
4353
- {
4354
- data: items,
4355
- keyExtractor: (item) => item.id,
4356
- renderItem: ({ item }) => /* @__PURE__ */ jsxs32(Fragment2, { children: [
4357
- item.separator && /* @__PURE__ */ jsx47(
4358
- View40,
4328
+ accessibilityViewIsModal: true,
4329
+ onAccessibilityEscape: () => setOpen(false),
4330
+ children: /* @__PURE__ */ jsxs32(SafeAreaView2, { children: [
4331
+ /* @__PURE__ */ jsxs32(
4332
+ View40,
4333
+ {
4334
+ style: [
4335
+ styles43.sheetHeader,
4336
+ { borderBottomColor: colors2.colorBgSubtle }
4337
+ ],
4338
+ children: [
4339
+ /* @__PURE__ */ jsx47(Text35, { style: [styles43.sheetTitle, { color: colors2.colorOnBg }], children: title }),
4340
+ /* @__PURE__ */ jsx47(
4341
+ Pressable25,
4342
+ {
4343
+ accessibilityRole: "button",
4344
+ accessibilityLabel: "Close",
4345
+ onPress: () => setOpen(false),
4346
+ hitSlop: 8,
4347
+ children: /* @__PURE__ */ jsx47(
4348
+ Text35,
4359
4349
  {
4360
4350
  style: [
4361
- styles43.separator,
4362
- { backgroundColor: colors2.colorBgSubtle }
4363
- ],
4364
- accessibilityRole: "none"
4365
- }
4366
- ),
4367
- /* @__PURE__ */ jsxs32(
4368
- Pressable25,
4369
- {
4370
- accessibilityRole: "menuitem",
4371
- accessibilityLabel: item.label,
4372
- accessibilityState: { disabled: item.disabled },
4373
- disabled: item.disabled,
4374
- onPress: () => handleItemPress(item),
4375
- style: ({ pressed }) => [
4376
- styles43.item,
4377
- pressed && { backgroundColor: colors2.colorBgSubtle },
4378
- item.disabled && { opacity: 0.4 }
4351
+ styles43.closeText,
4352
+ { color: colors2.colorInteractive }
4379
4353
  ],
4380
- children: [
4381
- !!item.icon && /* @__PURE__ */ jsx47(
4382
- View40,
4383
- {
4384
- importantForAccessibility: "no-hide-descendants",
4385
- accessibilityElementsHidden: true,
4386
- children: item.icon
4387
- }
4388
- ),
4389
- /* @__PURE__ */ jsx47(
4390
- Text35,
4391
- {
4392
- style: [
4393
- styles43.itemText,
4394
- { color: colors2.colorOnBg }
4395
- ],
4396
- children: item.label
4397
- }
4398
- )
4399
- ]
4354
+ children: "Done"
4400
4355
  }
4401
4356
  )
4402
- ] })
4403
- }
4404
- )
4405
- ] })
4406
- }
4407
- )
4357
+ }
4358
+ )
4359
+ ]
4360
+ }
4361
+ ),
4362
+ /* @__PURE__ */ jsx47(
4363
+ FlatList2,
4364
+ {
4365
+ data: items,
4366
+ keyExtractor: (item) => item.id,
4367
+ renderItem: ({ item }) => /* @__PURE__ */ jsxs32(Fragment2, { children: [
4368
+ item.separator && /* @__PURE__ */ jsx47(
4369
+ View40,
4370
+ {
4371
+ style: [
4372
+ styles43.separator,
4373
+ { backgroundColor: colors2.colorBgSubtle }
4374
+ ],
4375
+ accessibilityRole: "none"
4376
+ }
4377
+ ),
4378
+ /* @__PURE__ */ jsxs32(
4379
+ Pressable25,
4380
+ {
4381
+ accessibilityRole: "menuitem",
4382
+ accessibilityLabel: item.label,
4383
+ accessibilityState: { disabled: item.disabled },
4384
+ disabled: item.disabled,
4385
+ onPress: () => handleItemPress(item),
4386
+ style: ({ pressed }) => [
4387
+ styles43.item,
4388
+ pressed && { backgroundColor: colors2.colorBgSubtle },
4389
+ item.disabled && { opacity: 0.4 }
4390
+ ],
4391
+ children: [
4392
+ !!item.icon && /* @__PURE__ */ jsx47(
4393
+ View40,
4394
+ {
4395
+ importantForAccessibility: "no-hide-descendants",
4396
+ accessibilityElementsHidden: true,
4397
+ children: item.icon
4398
+ }
4399
+ ),
4400
+ /* @__PURE__ */ jsx47(
4401
+ Text35,
4402
+ {
4403
+ style: [
4404
+ styles43.itemText,
4405
+ { color: colors2.colorOnBg }
4406
+ ],
4407
+ children: item.label
4408
+ }
4409
+ )
4410
+ ]
4411
+ }
4412
+ )
4413
+ ] })
4414
+ }
4415
+ )
4416
+ ] })
4408
4417
  }
4409
4418
  )
4410
4419
  ] })
@@ -4453,6 +4462,7 @@ function Popover({
4453
4462
  }) {
4454
4463
  const { colors: colors2 } = useTheme();
4455
4464
  const [isOpen, setIsOpen] = useState15(false);
4465
+ const close = () => setIsOpen(false);
4456
4466
  return /* @__PURE__ */ jsxs33(View41, { style: [styles44.wrapper, style], children: [
4457
4467
  React2.cloneElement(children, {
4458
4468
  onPress: () => setIsOpen(true),
@@ -4465,27 +4475,28 @@ function Popover({
4465
4475
  visible: isOpen,
4466
4476
  transparent: true,
4467
4477
  animationType: "fade",
4468
- onRequestClose: () => setIsOpen(false),
4469
- children: /* @__PURE__ */ jsx48(
4470
- Pressable26,
4471
- {
4472
- style: styles44.overlay,
4473
- onPress: () => setIsOpen(false),
4474
- accessibilityRole: "button",
4475
- accessibilityLabel: "Close popover",
4476
- children: /* @__PURE__ */ jsx48(
4477
- Pressable26,
4478
- {
4479
- style: [styles44.panel, { backgroundColor: colors2.colorBg }],
4480
- onPress: () => {
4481
- },
4482
- accessibilityRole: "none",
4483
- accessibilityLabel,
4484
- children: content
4485
- }
4486
- )
4487
- }
4488
- )
4478
+ onRequestClose: close,
4479
+ children: /* @__PURE__ */ jsxs33(View41, { style: styles44.overlay, onAccessibilityEscape: close, children: [
4480
+ /* @__PURE__ */ jsx48(
4481
+ Pressable26,
4482
+ {
4483
+ testID: "bds-popover-backdrop",
4484
+ style: StyleSheet44.absoluteFill,
4485
+ onPress: close,
4486
+ onAccessibilityTap: close,
4487
+ accessibilityRole: "button",
4488
+ accessibilityLabel: "Close popover"
4489
+ }
4490
+ ),
4491
+ /* @__PURE__ */ jsx48(
4492
+ View41,
4493
+ {
4494
+ style: [styles44.panel, { backgroundColor: colors2.colorBg }],
4495
+ accessibilityLabel,
4496
+ children: content
4497
+ }
4498
+ )
4499
+ ] })
4489
4500
  }
4490
4501
  )
4491
4502
  ] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boostdev/design-system-react-native",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "BoostDev React Native component library: accessible, token-driven components built on @boostdev/design-system-foundation",
5
5
  "keywords": [
6
6
  "react-native",
@@ -56,6 +56,7 @@ export default function App() {
56
56
 
57
57
  ## Accessibility
58
58
 
59
- - Backdrop press and close button both call `onClose`
60
- - Close button has `accessibilityLabel="Close dialog"`
59
+ - The backdrop still closes on press, but is hidden from assistive tech (a leaf `Pressable` would otherwise group the whole dialog into one element on iOS); the close button, labelled `accessibilityLabel="Close dialog"`, is the accessible way to close
60
+ - The title, the content and any controls inside are separately reachable by VoiceOver and XCUITest, not grouped into one element
61
+ - VoiceOver's escape gesture also calls `onClose`
61
62
  - Android back button triggers `onRequestClose` which calls `onClose`
@@ -40,34 +40,65 @@ describe('Dialog (native)', () => {
40
40
  <span>Body</span>
41
41
  </Dialog>,
42
42
  );
43
- const closeButtons = screen.getAllByRole('button', { name: 'Close dialog' });
44
- expect(closeButtons.length).toBeGreaterThanOrEqual(1);
43
+ expect(screen.getByRole('button', { name: 'Close dialog' })).toBeInTheDocument();
45
44
  });
46
45
 
47
- it('calls onClose when close button is pressed', () => {
46
+ it('calls onClose when the close button is pressed', () => {
48
47
  const onClose = vi.fn();
49
48
  renderWithTheme(
50
49
  <Dialog isOpen onClose={onClose}>
51
50
  <span>Body</span>
52
51
  </Dialog>,
53
52
  );
54
- const closeButtons = screen.getAllByRole('button', { name: 'Close dialog' });
55
- // The inner close button (not the backdrop)
56
- const innerClose = closeButtons[closeButtons.length - 1];
57
- fireEvent.click(innerClose);
53
+ fireEvent.click(screen.getByRole('button', { name: 'Close dialog' }));
58
54
  expect(onClose).toHaveBeenCalledTimes(1);
59
55
  });
60
56
 
61
- it('calls onClose when backdrop is pressed', () => {
57
+ it('calls onClose when the backdrop is pressed', () => {
62
58
  const onClose = vi.fn();
63
59
  renderWithTheme(
64
60
  <Dialog isOpen onClose={onClose}>
65
61
  <span>Body</span>
66
62
  </Dialog>,
67
63
  );
68
- const closeButtons = screen.getAllByRole('button', { name: 'Close dialog' });
69
- // The first one is the backdrop
70
- fireEvent.click(closeButtons[0]);
64
+ fireEvent.click(screen.getByTestId('bds-dialog-backdrop'));
71
65
  expect(onClose).toHaveBeenCalledTimes(1);
72
66
  });
67
+
68
+ it('does not include the content inside the backdrop', () => {
69
+ renderWithTheme(
70
+ <Dialog isOpen onClose={() => {}}>
71
+ <span>Dialog body</span>
72
+ </Dialog>,
73
+ );
74
+ const backdrop = screen.getByTestId('bds-dialog-backdrop');
75
+ expect(backdrop).not.toContainElement(screen.getByText('Dialog body'));
76
+ expect(backdrop).not.toHaveAttribute('role');
77
+ });
78
+
79
+ it('does not call onClose when the content is pressed', () => {
80
+ const onClose = vi.fn();
81
+ renderWithTheme(
82
+ <Dialog isOpen onClose={onClose}>
83
+ <span>Dialog body</span>
84
+ </Dialog>,
85
+ );
86
+ fireEvent.click(screen.getByText('Dialog body'));
87
+ expect(onClose).not.toHaveBeenCalled();
88
+ });
89
+
90
+ it('keeps a child button reachable and separate from onClose', () => {
91
+ const onClose = vi.fn();
92
+ const onAction = vi.fn();
93
+ renderWithTheme(
94
+ <Dialog isOpen onClose={onClose}>
95
+ <button type="button" onClick={onAction}>
96
+ Confirm
97
+ </button>
98
+ </Dialog>,
99
+ );
100
+ fireEvent.click(screen.getByRole('button', { name: 'Confirm' }));
101
+ expect(onAction).toHaveBeenCalledTimes(1);
102
+ expect(onClose).not.toHaveBeenCalled();
103
+ });
73
104
  });
@@ -84,18 +84,25 @@ export function Dialog({
84
84
  animationType="fade"
85
85
  onRequestClose={onClose}
86
86
  >
87
- <Pressable
88
- style={styles.overlay}
89
- onPress={onClose}
90
- accessibilityRole="button"
91
- accessibilityLabel="Close dialog"
92
- >
87
+ <View style={styles.overlay}>
88
+ {/*
89
+ * A Pressable is accessible by default on iOS, which makes it a leaf:
90
+ * VoiceOver cannot reach anything nested inside it. As a sibling behind
91
+ * the panel (not its parent), the backdrop still closes on press for
92
+ * sighted/touch users but never swallows the panel's contents, so the
93
+ * close button below stays reachable as the accessible way to close.
94
+ */}
93
95
  <Pressable
96
+ testID="bds-dialog-backdrop"
97
+ style={StyleSheet.absoluteFill}
98
+ onPress={onClose}
99
+ accessible={false}
100
+ importantForAccessibility="no"
101
+ />
102
+ <View
94
103
  style={[styles.panel, { backgroundColor: colors.colorGreySubtle }, style]}
95
- onPress={() => {
96
- /* prevent close when pressing content */
97
- }}
98
- accessibilityRole="none"
104
+ accessibilityViewIsModal
105
+ onAccessibilityEscape={onClose}
99
106
  >
100
107
  <Pressable
101
108
  style={styles.closeButton}
@@ -121,8 +128,8 @@ export function Dialog({
121
128
  )}
122
129
 
123
130
  {children}
124
- </Pressable>
125
- </Pressable>
131
+ </View>
132
+ </View>
126
133
  </Modal>
127
134
  );
128
135
  }
@@ -69,6 +69,8 @@ export default function App() {
69
69
  ## Accessibility
70
70
 
71
71
  - Trigger has `accessibilityRole="button"` with `expanded` state
72
- - Menu items have `accessibilityRole="menuitem"`
72
+ - Menu items have `accessibilityRole="menuitem"` and are each reachable on their own — on iOS the sheet is no longer wrapped in a `Pressable`, which used to group the whole sheet (title, Done, every item) into a single VoiceOver element
73
73
  - Disabled items communicate their state to assistive technologies
74
+ - The backdrop still closes the menu on press, but is hidden from assistive tech (`accessible={false}`, `importantForAccessibility="no"`) so it doesn't duplicate the sheet's own close control
75
+ - "Done" is the accessible close control; VoiceOver's escape gesture (`onAccessibilityEscape`) also closes the menu
74
76
  - Android back button closes the menu