@boostdev/design-system-react-native 0.2.6 → 0.4.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 +94 -0
- package/dist/index.cjs +166 -159
- package/dist/index.d.cts +13 -3
- package/dist/index.d.ts +13 -3
- package/dist/index.js +166 -159
- 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/interaction/form/SegmentedControl/SegmentedControl.mdx +17 -3
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +15 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.stories.tsx +9 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +2 -1
- 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,
|
|
@@ -2840,7 +2844,8 @@ var TRACK_PADDING = spacing.xxxs;
|
|
|
2840
2844
|
var sizeTokens2 = {
|
|
2841
2845
|
small: { minHeight: 32, fontSize: font.size.bodyS, px: spacing.s, py: spacing.xxs },
|
|
2842
2846
|
medium: { minHeight: 44, fontSize: font.size.body, px: spacing.m, py: spacing.xs },
|
|
2843
|
-
large: { minHeight: 48, fontSize: font.size.body, px: spacing.m, py: spacing.s }
|
|
2847
|
+
large: { minHeight: 48, fontSize: font.size.body, px: spacing.m, py: spacing.s },
|
|
2848
|
+
xlarge: { minHeight: 56, fontSize: font.size.body, px: spacing.m, py: spacing.s }
|
|
2844
2849
|
};
|
|
2845
2850
|
var thumbShadow = {
|
|
2846
2851
|
shadowColor: "#000",
|
|
@@ -3782,46 +3787,48 @@ function Dialog({
|
|
|
3782
3787
|
transparent: true,
|
|
3783
3788
|
animationType: "fade",
|
|
3784
3789
|
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
|
-
|
|
3790
|
+
children: /* @__PURE__ */ jsxs29(View37, { style: styles40.overlay, children: [
|
|
3791
|
+
/* @__PURE__ */ jsx44(
|
|
3792
|
+
Pressable22,
|
|
3793
|
+
{
|
|
3794
|
+
testID: "bds-dialog-backdrop",
|
|
3795
|
+
style: StyleSheet40.absoluteFill,
|
|
3796
|
+
onPress: onClose,
|
|
3797
|
+
accessible: false,
|
|
3798
|
+
importantForAccessibility: "no"
|
|
3799
|
+
}
|
|
3800
|
+
),
|
|
3801
|
+
/* @__PURE__ */ jsxs29(
|
|
3802
|
+
View37,
|
|
3803
|
+
{
|
|
3804
|
+
style: [styles40.panel, { backgroundColor: colors2.colorGreySubtle }, style],
|
|
3805
|
+
accessibilityViewIsModal: true,
|
|
3806
|
+
onAccessibilityEscape: onClose,
|
|
3807
|
+
children: [
|
|
3808
|
+
/* @__PURE__ */ jsx44(
|
|
3809
|
+
Pressable22,
|
|
3810
|
+
{
|
|
3811
|
+
style: styles40.closeButton,
|
|
3812
|
+
onPress: onClose,
|
|
3813
|
+
accessibilityRole: "button",
|
|
3814
|
+
accessibilityLabel: "Close dialog",
|
|
3815
|
+
hitSlop: 8,
|
|
3816
|
+
children: /* @__PURE__ */ jsx44(
|
|
3817
|
+
Text32,
|
|
3818
|
+
{
|
|
3819
|
+
style: [styles40.closeText, { color: colors2.colorOnBgSubtle }],
|
|
3820
|
+
"aria-hidden": true,
|
|
3821
|
+
children: "\u2715"
|
|
3822
|
+
}
|
|
3823
|
+
)
|
|
3824
|
+
}
|
|
3825
|
+
),
|
|
3826
|
+
!!title && /* @__PURE__ */ jsx44(View37, { style: styles40.header, children: /* @__PURE__ */ jsx44(Text32, { style: [styles40.title, { color: colors2.colorOnGreySubtle }], children: title }) }),
|
|
3827
|
+
children
|
|
3828
|
+
]
|
|
3829
|
+
}
|
|
3830
|
+
)
|
|
3831
|
+
] })
|
|
3825
3832
|
}
|
|
3826
3833
|
);
|
|
3827
3834
|
}
|
|
@@ -3947,8 +3954,9 @@ function SideSheet({
|
|
|
3947
3954
|
{
|
|
3948
3955
|
style: StyleSheet41.absoluteFill,
|
|
3949
3956
|
onPress: onClose,
|
|
3950
|
-
|
|
3951
|
-
|
|
3957
|
+
testID: "bds-sidesheet-backdrop",
|
|
3958
|
+
accessible: false,
|
|
3959
|
+
importantForAccessibility: "no"
|
|
3952
3960
|
}
|
|
3953
3961
|
)
|
|
3954
3962
|
}
|
|
@@ -3964,7 +3972,8 @@ function SideSheet({
|
|
|
3964
3972
|
},
|
|
3965
3973
|
style
|
|
3966
3974
|
],
|
|
3967
|
-
|
|
3975
|
+
accessibilityViewIsModal: true,
|
|
3976
|
+
onAccessibilityEscape: onClose,
|
|
3968
3977
|
children: [
|
|
3969
3978
|
/* @__PURE__ */ jsxs30(
|
|
3970
3979
|
View38,
|
|
@@ -4300,8 +4309,9 @@ function DropdownMenu({
|
|
|
4300
4309
|
{
|
|
4301
4310
|
style: StyleSheet43.absoluteFill,
|
|
4302
4311
|
onPress: () => setOpen(false),
|
|
4303
|
-
|
|
4304
|
-
|
|
4312
|
+
testID: "bds-dropdownmenu-backdrop",
|
|
4313
|
+
accessible: false,
|
|
4314
|
+
importantForAccessibility: "no"
|
|
4305
4315
|
}
|
|
4306
4316
|
)
|
|
4307
4317
|
}
|
|
@@ -4316,100 +4326,95 @@ function DropdownMenu({
|
|
|
4316
4326
|
transform: [{ translateY: slideAnim }]
|
|
4317
4327
|
}
|
|
4318
4328
|
],
|
|
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,
|
|
4329
|
+
accessibilityViewIsModal: true,
|
|
4330
|
+
onAccessibilityEscape: () => setOpen(false),
|
|
4331
|
+
children: /* @__PURE__ */ jsxs32(SafeAreaView2, { children: [
|
|
4332
|
+
/* @__PURE__ */ jsxs32(
|
|
4333
|
+
View40,
|
|
4334
|
+
{
|
|
4335
|
+
style: [
|
|
4336
|
+
styles43.sheetHeader,
|
|
4337
|
+
{ borderBottomColor: colors2.colorBgSubtle }
|
|
4338
|
+
],
|
|
4339
|
+
children: [
|
|
4340
|
+
/* @__PURE__ */ jsx47(Text35, { style: [styles43.sheetTitle, { color: colors2.colorOnBg }], children: title }),
|
|
4341
|
+
/* @__PURE__ */ jsx47(
|
|
4342
|
+
Pressable25,
|
|
4343
|
+
{
|
|
4344
|
+
accessibilityRole: "button",
|
|
4345
|
+
accessibilityLabel: "Close",
|
|
4346
|
+
onPress: () => setOpen(false),
|
|
4347
|
+
hitSlop: 8,
|
|
4348
|
+
children: /* @__PURE__ */ jsx47(
|
|
4349
|
+
Text35,
|
|
4364
4350
|
{
|
|
4365
4351
|
style: [
|
|
4366
|
-
styles43.
|
|
4367
|
-
{
|
|
4352
|
+
styles43.closeText,
|
|
4353
|
+
{ color: colors2.colorInteractive }
|
|
4368
4354
|
],
|
|
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
|
-
]
|
|
4355
|
+
children: "Done"
|
|
4405
4356
|
}
|
|
4406
4357
|
)
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4358
|
+
}
|
|
4359
|
+
)
|
|
4360
|
+
]
|
|
4361
|
+
}
|
|
4362
|
+
),
|
|
4363
|
+
/* @__PURE__ */ jsx47(
|
|
4364
|
+
FlatList2,
|
|
4365
|
+
{
|
|
4366
|
+
data: items,
|
|
4367
|
+
keyExtractor: (item) => item.id,
|
|
4368
|
+
renderItem: ({ item }) => /* @__PURE__ */ jsxs32(Fragment2, { children: [
|
|
4369
|
+
item.separator && /* @__PURE__ */ jsx47(
|
|
4370
|
+
View40,
|
|
4371
|
+
{
|
|
4372
|
+
style: [
|
|
4373
|
+
styles43.separator,
|
|
4374
|
+
{ backgroundColor: colors2.colorBgSubtle }
|
|
4375
|
+
],
|
|
4376
|
+
accessibilityRole: "none"
|
|
4377
|
+
}
|
|
4378
|
+
),
|
|
4379
|
+
/* @__PURE__ */ jsxs32(
|
|
4380
|
+
Pressable25,
|
|
4381
|
+
{
|
|
4382
|
+
accessibilityRole: "menuitem",
|
|
4383
|
+
accessibilityLabel: item.label,
|
|
4384
|
+
accessibilityState: { disabled: item.disabled },
|
|
4385
|
+
disabled: item.disabled,
|
|
4386
|
+
onPress: () => handleItemPress(item),
|
|
4387
|
+
style: ({ pressed }) => [
|
|
4388
|
+
styles43.item,
|
|
4389
|
+
pressed && { backgroundColor: colors2.colorBgSubtle },
|
|
4390
|
+
item.disabled && { opacity: 0.4 }
|
|
4391
|
+
],
|
|
4392
|
+
children: [
|
|
4393
|
+
!!item.icon && /* @__PURE__ */ jsx47(
|
|
4394
|
+
View40,
|
|
4395
|
+
{
|
|
4396
|
+
importantForAccessibility: "no-hide-descendants",
|
|
4397
|
+
accessibilityElementsHidden: true,
|
|
4398
|
+
children: item.icon
|
|
4399
|
+
}
|
|
4400
|
+
),
|
|
4401
|
+
/* @__PURE__ */ jsx47(
|
|
4402
|
+
Text35,
|
|
4403
|
+
{
|
|
4404
|
+
style: [
|
|
4405
|
+
styles43.itemText,
|
|
4406
|
+
{ color: colors2.colorOnBg }
|
|
4407
|
+
],
|
|
4408
|
+
children: item.label
|
|
4409
|
+
}
|
|
4410
|
+
)
|
|
4411
|
+
]
|
|
4412
|
+
}
|
|
4413
|
+
)
|
|
4414
|
+
] })
|
|
4415
|
+
}
|
|
4416
|
+
)
|
|
4417
|
+
] })
|
|
4413
4418
|
}
|
|
4414
4419
|
)
|
|
4415
4420
|
] })
|
|
@@ -4458,6 +4463,7 @@ function Popover({
|
|
|
4458
4463
|
}) {
|
|
4459
4464
|
const { colors: colors2 } = useTheme();
|
|
4460
4465
|
const [isOpen, setIsOpen] = useState15(false);
|
|
4466
|
+
const close = () => setIsOpen(false);
|
|
4461
4467
|
return /* @__PURE__ */ jsxs33(View41, { style: [styles44.wrapper, style], children: [
|
|
4462
4468
|
React2.cloneElement(children, {
|
|
4463
4469
|
onPress: () => setIsOpen(true),
|
|
@@ -4470,27 +4476,28 @@ function Popover({
|
|
|
4470
4476
|
visible: isOpen,
|
|
4471
4477
|
transparent: true,
|
|
4472
4478
|
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
|
-
|
|
4479
|
+
onRequestClose: close,
|
|
4480
|
+
children: /* @__PURE__ */ jsxs33(View41, { style: styles44.overlay, onAccessibilityEscape: close, children: [
|
|
4481
|
+
/* @__PURE__ */ jsx48(
|
|
4482
|
+
Pressable26,
|
|
4483
|
+
{
|
|
4484
|
+
testID: "bds-popover-backdrop",
|
|
4485
|
+
style: StyleSheet44.absoluteFill,
|
|
4486
|
+
onPress: close,
|
|
4487
|
+
onAccessibilityTap: close,
|
|
4488
|
+
accessibilityRole: "button",
|
|
4489
|
+
accessibilityLabel: "Close popover"
|
|
4490
|
+
}
|
|
4491
|
+
),
|
|
4492
|
+
/* @__PURE__ */ jsx48(
|
|
4493
|
+
View41,
|
|
4494
|
+
{
|
|
4495
|
+
style: [styles44.panel, { backgroundColor: colors2.colorBg }],
|
|
4496
|
+
accessibilityLabel,
|
|
4497
|
+
children: content
|
|
4498
|
+
}
|
|
4499
|
+
)
|
|
4500
|
+
] })
|
|
4494
4501
|
}
|
|
4495
4502
|
)
|
|
4496
4503
|
] });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boostdev/design-system-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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
|
});
|