@boostdev/design-system-react-native 0.2.6 → 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.
- package/CHANGELOG.md +77 -0
- package/dist/index.cjs +164 -158
- package/dist/index.d.cts +12 -2
- package/dist/index.d.ts +12 -2
- package/dist/index.js +164 -158
- package/package.json +1 -1
- package/src/components/interaction/Dialog/Dialog.mdx +3 -2
- package/src/components/interaction/Dialog/Dialog.spec.tsx +42 -11
- package/src/components/interaction/Dialog/Dialog.tsx +19 -12
- package/src/components/interaction/DropdownMenu/DropdownMenu.mdx +3 -1
- package/src/components/interaction/DropdownMenu/DropdownMenu.spec.tsx +73 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +79 -73
- package/src/components/interaction/Popover/Popover.mdx +3 -1
- package/src/components/interaction/Popover/Popover.spec.tsx +43 -3
- package/src/components/interaction/Popover/Popover.tsx +23 -13
- package/src/components/interaction/SideSheet/SideSheet.mdx +4 -3
- package/src/components/interaction/SideSheet/SideSheet.spec.tsx +8 -9
- package/src/components/interaction/SideSheet/SideSheet.tsx +9 -3
- package/src/components/ui/Alert/Alert.spec.tsx +32 -1
- package/src/components/ui/Alert/Alert.stories.tsx +45 -0
- package/src/components/ui/Alert/Alert.tsx +11 -2
- package/src/components/ui/NotificationBanner/NotificationBanner.spec.tsx +32 -1
- package/src/components/ui/NotificationBanner/NotificationBanner.stories.tsx +30 -0
- 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,
|
|
@@ -3782,46 +3786,48 @@ function Dialog({
|
|
|
3782
3786
|
transparent: true,
|
|
3783
3787
|
animationType: "fade",
|
|
3784
3788
|
onRequestClose: onClose,
|
|
3785
|
-
children: /* @__PURE__ */
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
}
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
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
|
+
] })
|
|
3825
3831
|
}
|
|
3826
3832
|
);
|
|
3827
3833
|
}
|
|
@@ -3947,8 +3953,9 @@ function SideSheet({
|
|
|
3947
3953
|
{
|
|
3948
3954
|
style: StyleSheet41.absoluteFill,
|
|
3949
3955
|
onPress: onClose,
|
|
3950
|
-
|
|
3951
|
-
|
|
3956
|
+
testID: "bds-sidesheet-backdrop",
|
|
3957
|
+
accessible: false,
|
|
3958
|
+
importantForAccessibility: "no"
|
|
3952
3959
|
}
|
|
3953
3960
|
)
|
|
3954
3961
|
}
|
|
@@ -3964,7 +3971,8 @@ function SideSheet({
|
|
|
3964
3971
|
},
|
|
3965
3972
|
style
|
|
3966
3973
|
],
|
|
3967
|
-
|
|
3974
|
+
accessibilityViewIsModal: true,
|
|
3975
|
+
onAccessibilityEscape: onClose,
|
|
3968
3976
|
children: [
|
|
3969
3977
|
/* @__PURE__ */ jsxs30(
|
|
3970
3978
|
View38,
|
|
@@ -4300,8 +4308,9 @@ function DropdownMenu({
|
|
|
4300
4308
|
{
|
|
4301
4309
|
style: StyleSheet43.absoluteFill,
|
|
4302
4310
|
onPress: () => setOpen(false),
|
|
4303
|
-
|
|
4304
|
-
|
|
4311
|
+
testID: "bds-dropdownmenu-backdrop",
|
|
4312
|
+
accessible: false,
|
|
4313
|
+
importantForAccessibility: "no"
|
|
4305
4314
|
}
|
|
4306
4315
|
)
|
|
4307
4316
|
}
|
|
@@ -4316,100 +4325,95 @@ function DropdownMenu({
|
|
|
4316
4325
|
transform: [{ translateY: slideAnim }]
|
|
4317
4326
|
}
|
|
4318
4327
|
],
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
{
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
hitSlop: 8,
|
|
4341
|
-
children: /* @__PURE__ */ jsx47(
|
|
4342
|
-
Text35,
|
|
4343
|
-
{
|
|
4344
|
-
style: [
|
|
4345
|
-
styles43.closeText,
|
|
4346
|
-
{ color: colors2.colorInteractive }
|
|
4347
|
-
],
|
|
4348
|
-
children: "Done"
|
|
4349
|
-
}
|
|
4350
|
-
)
|
|
4351
|
-
}
|
|
4352
|
-
)
|
|
4353
|
-
]
|
|
4354
|
-
}
|
|
4355
|
-
),
|
|
4356
|
-
/* @__PURE__ */ jsx47(
|
|
4357
|
-
FlatList2,
|
|
4358
|
-
{
|
|
4359
|
-
data: items,
|
|
4360
|
-
keyExtractor: (item) => item.id,
|
|
4361
|
-
renderItem: ({ item }) => /* @__PURE__ */ jsxs32(Fragment2, { children: [
|
|
4362
|
-
item.separator && /* @__PURE__ */ jsx47(
|
|
4363
|
-
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,
|
|
4364
4349
|
{
|
|
4365
4350
|
style: [
|
|
4366
|
-
styles43.
|
|
4367
|
-
{
|
|
4351
|
+
styles43.closeText,
|
|
4352
|
+
{ color: colors2.colorInteractive }
|
|
4368
4353
|
],
|
|
4369
|
-
|
|
4370
|
-
}
|
|
4371
|
-
),
|
|
4372
|
-
/* @__PURE__ */ jsxs32(
|
|
4373
|
-
Pressable25,
|
|
4374
|
-
{
|
|
4375
|
-
accessibilityRole: "menuitem",
|
|
4376
|
-
accessibilityLabel: item.label,
|
|
4377
|
-
accessibilityState: { disabled: item.disabled },
|
|
4378
|
-
disabled: item.disabled,
|
|
4379
|
-
onPress: () => handleItemPress(item),
|
|
4380
|
-
style: ({ pressed }) => [
|
|
4381
|
-
styles43.item,
|
|
4382
|
-
pressed && { backgroundColor: colors2.colorBgSubtle },
|
|
4383
|
-
item.disabled && { opacity: 0.4 }
|
|
4384
|
-
],
|
|
4385
|
-
children: [
|
|
4386
|
-
!!item.icon && /* @__PURE__ */ jsx47(
|
|
4387
|
-
View40,
|
|
4388
|
-
{
|
|
4389
|
-
importantForAccessibility: "no-hide-descendants",
|
|
4390
|
-
accessibilityElementsHidden: true,
|
|
4391
|
-
children: item.icon
|
|
4392
|
-
}
|
|
4393
|
-
),
|
|
4394
|
-
/* @__PURE__ */ jsx47(
|
|
4395
|
-
Text35,
|
|
4396
|
-
{
|
|
4397
|
-
style: [
|
|
4398
|
-
styles43.itemText,
|
|
4399
|
-
{ color: colors2.colorOnBg }
|
|
4400
|
-
],
|
|
4401
|
-
children: item.label
|
|
4402
|
-
}
|
|
4403
|
-
)
|
|
4404
|
-
]
|
|
4354
|
+
children: "Done"
|
|
4405
4355
|
}
|
|
4406
4356
|
)
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
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
|
+
] })
|
|
4413
4417
|
}
|
|
4414
4418
|
)
|
|
4415
4419
|
] })
|
|
@@ -4458,6 +4462,7 @@ function Popover({
|
|
|
4458
4462
|
}) {
|
|
4459
4463
|
const { colors: colors2 } = useTheme();
|
|
4460
4464
|
const [isOpen, setIsOpen] = useState15(false);
|
|
4465
|
+
const close = () => setIsOpen(false);
|
|
4461
4466
|
return /* @__PURE__ */ jsxs33(View41, { style: [styles44.wrapper, style], children: [
|
|
4462
4467
|
React2.cloneElement(children, {
|
|
4463
4468
|
onPress: () => setIsOpen(true),
|
|
@@ -4470,27 +4475,28 @@ function Popover({
|
|
|
4470
4475
|
visible: isOpen,
|
|
4471
4476
|
transparent: true,
|
|
4472
4477
|
animationType: "fade",
|
|
4473
|
-
onRequestClose:
|
|
4474
|
-
children: /* @__PURE__ */
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
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
|
+
] })
|
|
4494
4500
|
}
|
|
4495
4501
|
)
|
|
4496
4502
|
] });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boostdev/design-system-react-native",
|
|
3
|
-
"version": "0.
|
|
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
|
-
-
|
|
60
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
</
|
|
125
|
-
</
|
|
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
|
|
@@ -75,4 +75,77 @@ describe('DropdownMenu (native)', () => {
|
|
|
75
75
|
fireEvent.click(screen.getByRole('button', { name: 'My Menu' }));
|
|
76
76
|
expect(screen.getByText('My Menu')).toBeInTheDocument();
|
|
77
77
|
});
|
|
78
|
+
|
|
79
|
+
it('has an unlabelled, roleless backdrop that closes the menu on press', () => {
|
|
80
|
+
renderWithTheme(
|
|
81
|
+
<DropdownMenu trigger={<Trigger>Menu</Trigger>} items={items} title="Menu" />,
|
|
82
|
+
);
|
|
83
|
+
fireEvent.click(screen.getByRole('button', { name: 'Menu' }));
|
|
84
|
+
const backdrop = screen.getByTestId('bds-dropdownmenu-backdrop');
|
|
85
|
+
expect(backdrop).not.toHaveAttribute('role');
|
|
86
|
+
fireEvent.click(backdrop);
|
|
87
|
+
expect(screen.queryByText('Edit')).not.toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('has no full-screen "Close menu" button; "Done" closes the menu', () => {
|
|
91
|
+
renderWithTheme(
|
|
92
|
+
<DropdownMenu trigger={<Trigger>Menu</Trigger>} items={items} title="Menu" />,
|
|
93
|
+
);
|
|
94
|
+
fireEvent.click(screen.getByRole('button', { name: 'Menu' }));
|
|
95
|
+
expect(
|
|
96
|
+
screen.queryByRole('button', { name: 'Close menu' }),
|
|
97
|
+
).not.toBeInTheDocument();
|
|
98
|
+
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
|
99
|
+
expect(screen.queryByText('Edit')).not.toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('exposes each item as its own menuitem, pressable independently', () => {
|
|
103
|
+
const onEdit = vi.fn();
|
|
104
|
+
const onDelete = vi.fn();
|
|
105
|
+
const menuItems = [
|
|
106
|
+
{ id: 'edit', label: 'Edit', onPress: onEdit },
|
|
107
|
+
{ id: 'delete', label: 'Delete', onPress: onDelete },
|
|
108
|
+
];
|
|
109
|
+
renderWithTheme(
|
|
110
|
+
<DropdownMenu trigger={<Trigger>Actions</Trigger>} items={menuItems} title="Actions" />,
|
|
111
|
+
);
|
|
112
|
+
fireEvent.click(screen.getByRole('button', { name: 'Actions' }));
|
|
113
|
+
|
|
114
|
+
const menuitems = screen.getAllByRole('menuitem');
|
|
115
|
+
expect(menuitems).toHaveLength(2);
|
|
116
|
+
expect(menuitems.map((el) => el.textContent)).toEqual(['Edit', 'Delete']);
|
|
117
|
+
|
|
118
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Edit' }));
|
|
119
|
+
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(onDelete).not.toHaveBeenCalled();
|
|
121
|
+
expect(screen.queryByText('Delete')).not.toBeInTheDocument();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// On iOS a Pressable is `accessible` by default, so a no-op Pressable
|
|
125
|
+
// wrapped around the sheet's content used to make VoiceOver treat the
|
|
126
|
+
// whole sheet (all items included) as a single element. jsdom has no iOS
|
|
127
|
+
// grouping, but react-native-web's Pressable always renders with a
|
|
128
|
+
// `tabindex` attribute (0, or -1 disabled) on its own DOM node, wrapper
|
|
129
|
+
// or not. So a stray wrapper is detectable here as a `tabindex`-bearing
|
|
130
|
+
// ancestor between the menuitem and the sheet — the menuitem should have
|
|
131
|
+
// none.
|
|
132
|
+
it('renders each item with no tabindex-bearing wrapper between it and the sheet', () => {
|
|
133
|
+
renderWithTheme(
|
|
134
|
+
<DropdownMenu trigger={<Trigger>Menu</Trigger>} items={items} title="Menu" />,
|
|
135
|
+
);
|
|
136
|
+
fireEvent.click(screen.getByRole('button', { name: 'Menu' }));
|
|
137
|
+
const item = screen.getByRole('menuitem', { name: 'Edit' });
|
|
138
|
+
|
|
139
|
+
let node: HTMLElement | null = item.parentElement;
|
|
140
|
+
let tabIndexAncestor: HTMLElement | null = null;
|
|
141
|
+
while (node) {
|
|
142
|
+
if (node.hasAttribute('tabindex')) {
|
|
143
|
+
tabIndexAncestor = node;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
node = node.parentElement;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
expect(tabIndexAncestor).toBeNull();
|
|
150
|
+
});
|
|
78
151
|
});
|