@astryxdesign/core 0.5.0-canary.cac77bc → 0.5.0-canary.cb20a01
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/dist/AlertDialog/AlertDialog.d.ts +1 -1
- package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
- package/dist/AlertDialog/AlertDialog.js +45 -20
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +42 -13
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +55 -3
- package/dist/HoverCard/useHoverCard.d.ts +4 -1
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/locales/en.json +1 -1
- package/package.json +2 -2
- package/src/AlertDialog/AlertDialog.doc.mjs +114 -30
- package/src/AlertDialog/AlertDialog.test.tsx +211 -2
- package/src/AlertDialog/AlertDialog.tsx +62 -20
- package/src/DateInput/DateInputTouch.test.tsx +20 -12
- package/src/DateInput/TouchDateField.tsx +67 -13
- package/src/HoverCard/HoverCard.test.tsx +54 -1
- package/src/HoverCard/HoverCard.tsx +74 -2
- package/src/HoverCard/useHoverCard.tsx +4 -1
- package/src/Timestamp/Timestamp.test.tsx +18 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file AlertDialog.tsx
|
|
3
|
-
* @input Uses React, Dialog, Layout, Heading, Text, Button
|
|
3
|
+
* @input Uses React, StyleX, Dialog, Layout, Heading, Text, Button
|
|
4
4
|
* @output Exports AlertDialog component, AlertDialogProps type
|
|
5
5
|
* @position Core implementation; consumed by index.ts, tested by AlertDialog.test.tsx
|
|
6
6
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../src/AlertDialog/AlertDialog.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAA2B,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../src/AlertDialog/AlertDialog.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAA2B,MAAM,OAAO,CAAC;AAShD,OAAO,EAAS,KAAK,aAAa,EAAC,MAAM,WAAW,CAAC;AACrD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAkB5C,MAAM,WAAW,gBAAiB,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IACpE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAE3C;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAE9B;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;OAGG;IACH,QAAQ,EAAE,MAAM,OAAO,CAAC;IAExB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,GAAG,EACH,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,KAAK,EACL,WAAW,EACX,WAAW,EAAE,oBAAoB,EACjC,WAAW,EACX,aAA6B,EAC7B,eAAe,EACf,QAAQ,EACR,KAAW,EACX,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,gBAAgB,qBA8FlB;yBAhHe,WAAW"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file AlertDialog.tsx
|
|
7
|
-
* @input Uses React, Dialog, Layout, Heading, Text, Button
|
|
7
|
+
* @input Uses React, StyleX, Dialog, Layout, Heading, Text, Button
|
|
8
8
|
* @output Exports AlertDialog component, AlertDialogProps type
|
|
9
9
|
* @position Core implementation; consumed by index.ts, tested by AlertDialog.test.tsx
|
|
10
10
|
*
|
|
@@ -16,18 +16,32 @@
|
|
|
16
16
|
* - /packages/cli/assets/templates/blocks/components/AlertDialog/ (showcase blocks)
|
|
17
17
|
*/
|
|
18
18
|
import React, { useId, useCallback } from 'react';
|
|
19
|
+
import * as stylex from '@stylexjs/stylex';
|
|
19
20
|
import { Dialog } from "../Dialog/index.js";
|
|
20
21
|
import { Layout } from "../Layout/Layout.js";
|
|
21
22
|
import { LayoutContent } from "../Layout/LayoutContent.js";
|
|
22
23
|
import { LayoutFooter } from "../Layout/LayoutFooter.js";
|
|
23
|
-
import {
|
|
24
|
+
import { Stack } from "../Stack/index.js";
|
|
24
25
|
import { Heading } from "../Heading/Heading.js";
|
|
25
26
|
import { Text } from "../Text/Text.js";
|
|
26
27
|
import { Button } from "../Button/index.js";
|
|
27
28
|
import { mergeProps } from "../utils/index.js";
|
|
28
29
|
import { themeProps } from "../utils/themeProps.js";
|
|
29
30
|
import { useTranslator } from "../i18n/index.js";
|
|
31
|
+
import { useMediaQuery } from "../hooks/index.js";
|
|
30
32
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
33
|
+
const SMALL_SCREEN_QUERY = '(max-width: 640px)';
|
|
34
|
+
const styles = {
|
|
35
|
+
action: {
|
|
36
|
+
ks0D6T: "x193iq5w",
|
|
37
|
+
k7Eaqz: "xeuugli",
|
|
38
|
+
$$css: true
|
|
39
|
+
},
|
|
40
|
+
stackedAction: {
|
|
41
|
+
kzqmXN: "xh8yej3",
|
|
42
|
+
$$css: true
|
|
43
|
+
}
|
|
44
|
+
};
|
|
31
45
|
/**
|
|
32
46
|
* A confirmation dialog for destructive or irreversible actions.
|
|
33
47
|
*
|
|
@@ -77,9 +91,33 @@ export function AlertDialog({
|
|
|
77
91
|
const cancelLabel = cancelLabelFromProps ?? t('@astryx.alertDialog.cancel');
|
|
78
92
|
const titleId = useId();
|
|
79
93
|
const descriptionId = useId();
|
|
94
|
+
// Width stays delegated to Dialog: this query exists only so AlertDialog can
|
|
95
|
+
// keep narrow visual, DOM, and tab order aligned for its destructive/cancel
|
|
96
|
+
// semantics without coupling action order to pointer or hover capability.
|
|
97
|
+
const isSmallScreen = useMediaQuery(SMALL_SCREEN_QUERY);
|
|
80
98
|
const handleCancel = useCallback(() => {
|
|
81
99
|
onOpenChange(false);
|
|
82
100
|
}, [onOpenChange]);
|
|
101
|
+
const buttonActionStyle = [styles.action, isSmallScreen && styles.stackedAction];
|
|
102
|
+
const cancelButton = /*#__PURE__*/_jsx(Button, {
|
|
103
|
+
variant: "ghost",
|
|
104
|
+
label: cancelLabel,
|
|
105
|
+
onClick: handleCancel,
|
|
106
|
+
xstyle: buttonActionStyle
|
|
107
|
+
// Dialog focuses [data-autofocus] itself after showModal(), because
|
|
108
|
+
// React's autoFocus runs during commit while the dialog is invisible.
|
|
109
|
+
// Cancel is least destructive, so it remains the autofocus target even
|
|
110
|
+
// when small-screen visual order places the destructive action above it.
|
|
111
|
+
,
|
|
112
|
+
"data-autofocus": true
|
|
113
|
+
}, "cancel");
|
|
114
|
+
const actionButton = /*#__PURE__*/_jsx(Button, {
|
|
115
|
+
variant: actionVariant,
|
|
116
|
+
label: actionLabel,
|
|
117
|
+
onClick: onAction,
|
|
118
|
+
isLoading: isActionLoading,
|
|
119
|
+
xstyle: buttonActionStyle
|
|
120
|
+
}, "action");
|
|
83
121
|
return /*#__PURE__*/_jsx(Dialog, {
|
|
84
122
|
...rest,
|
|
85
123
|
ref: ref,
|
|
@@ -117,25 +155,12 @@ export function AlertDialog({
|
|
|
117
155
|
})]
|
|
118
156
|
}),
|
|
119
157
|
footer: /*#__PURE__*/_jsx(LayoutFooter, {
|
|
120
|
-
children: /*#__PURE__*/
|
|
158
|
+
children: /*#__PURE__*/_jsx(Stack, {
|
|
159
|
+
direction: isSmallScreen ? 'vertical' : 'horizontal',
|
|
121
160
|
gap: 2,
|
|
122
|
-
hAlign:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
label: cancelLabel,
|
|
126
|
-
onClick: handleCancel
|
|
127
|
-
// Dialog focuses [data-autofocus] itself after showModal(),
|
|
128
|
-
// because React's autoFocus runs during commit while the
|
|
129
|
-
// dialog is still invisible. Cancel is the least destructive
|
|
130
|
-
// choice, so it is the one that should be preselected.
|
|
131
|
-
,
|
|
132
|
-
"data-autofocus": true
|
|
133
|
-
}), /*#__PURE__*/_jsx(Button, {
|
|
134
|
-
variant: actionVariant,
|
|
135
|
-
label: actionLabel,
|
|
136
|
-
onClick: onAction,
|
|
137
|
-
isLoading: isActionLoading
|
|
138
|
-
})]
|
|
161
|
+
hAlign: isSmallScreen ? 'stretch' : 'end',
|
|
162
|
+
wrap: isSmallScreen ? 'nowrap' : 'wrap',
|
|
163
|
+
children: isSmallScreen ? [actionButton, cancelButton] : [cancelButton, actionButton]
|
|
139
164
|
})
|
|
140
165
|
})
|
|
141
166
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TouchDateField.d.ts","sourceRoot":"","sources":["../../src/DateInput/TouchDateField.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TouchDateField.d.ts","sourceRoot":"","sources":["../../src/DateInput/TouchDateField.tsx"],"names":[],"mappings":"AAgEA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,aAAa,CAAC;AAoehD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,UAAkB,EAClB,eAAe,EACf,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAiB,EACjB,GAAG,EACH,GAAG,EACH,eAAe,EACf,WAAW,EAAE,oBAAoB,EACjC,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,aAA0B,EAC1B,YAAY,EACZ,QAAgB,EAIhB,cAAc,EAAE,eAAe,EAC/B,YAAY,EAAE,gBAAoB,EAClC,MAAoB,EACpB,KAAK,EACL,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,IAAI,EACR,EAAE,cAAc,+BAmtBhB;yBApvBe,cAAc"}
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
* 3. The title is the escape hatch. Tap it and the same box becomes a month
|
|
39
39
|
* wheel and a year wheel — a flick each to reach 2019 instead of forty.
|
|
40
40
|
*
|
|
41
|
+
* Reset is chrome, so it sits in the header beside the arrows rather than in
|
|
42
|
+
* the footer: the footer is where the task ends, and an undo of equal weight
|
|
43
|
+
* beside Save is a mis-tap that throws away the date just chosen.
|
|
44
|
+
*
|
|
41
45
|
* SYNC: When modified, update these files to stay in sync:
|
|
42
46
|
* - /packages/core/src/DateInput/DateInput.tsx
|
|
43
47
|
* - /packages/core/src/DateInput/DateInput.doc.mjs
|
|
@@ -152,6 +156,10 @@ const styles = {
|
|
|
152
156
|
k1xSpc: "x3nfvp2",
|
|
153
157
|
$$css: true
|
|
154
158
|
},
|
|
159
|
+
resetButton: {
|
|
160
|
+
kAzted: "xygd9yz",
|
|
161
|
+
$$css: true
|
|
162
|
+
},
|
|
155
163
|
title: {
|
|
156
164
|
k1xSpc: "x78zum5",
|
|
157
165
|
kGNEyG: "x6s0dn4",
|
|
@@ -168,10 +176,13 @@ const styles = {
|
|
|
168
176
|
k63SB2: "x2mo6ok",
|
|
169
177
|
kkrTdU: "x1ypdohk x16khyan",
|
|
170
178
|
khDVqt: "xuxw1ft",
|
|
179
|
+
k7Eaqz: "xeuugli",
|
|
180
|
+
kVQacm: "xb3r6kr",
|
|
171
181
|
$$css: true
|
|
172
182
|
},
|
|
173
183
|
titleChevron: {
|
|
174
184
|
k1xSpc: "x3nfvp2",
|
|
185
|
+
kmuXW: "x2lah0s",
|
|
175
186
|
k1ekBW: "x11xpdln",
|
|
176
187
|
kIyJzY: "xuedmi6",
|
|
177
188
|
kAMwcw: "xlr8y92",
|
|
@@ -547,6 +558,9 @@ export function TouchDateField({
|
|
|
547
558
|
"data-title": "month-year",
|
|
548
559
|
...stylex.props(styles.title, focusOutlineStyles.focusVisible),
|
|
549
560
|
children: [/*#__PURE__*/_jsx("span", {
|
|
561
|
+
...{
|
|
562
|
+
className: "xeuugli xb3r6kr xlyipyv"
|
|
563
|
+
},
|
|
550
564
|
children: monthYearLabel
|
|
551
565
|
}), /*#__PURE__*/_jsx(Icon, {
|
|
552
566
|
icon: "chevronDown",
|
|
@@ -603,6 +617,27 @@ export function TouchDateField({
|
|
|
603
617
|
})
|
|
604
618
|
})
|
|
605
619
|
})]
|
|
620
|
+
}), /*#__PURE__*/_jsx("span", {
|
|
621
|
+
"data-action": "reset",
|
|
622
|
+
inert: isWheelOpen ? true : undefined,
|
|
623
|
+
...{
|
|
624
|
+
0: {
|
|
625
|
+
className: "x78zum5 x6s0dn4 x1jl3cmp xuedmi6 xcj1dhv xzg1mie"
|
|
626
|
+
},
|
|
627
|
+
1: {
|
|
628
|
+
className: "x78zum5 x6s0dn4 x1jl3cmp xuedmi6 xcj1dhv xzg1mie xlshs6z xg01cxk x47corl"
|
|
629
|
+
}
|
|
630
|
+
}[!!isWheelOpen << 0],
|
|
631
|
+
children: /*#__PURE__*/_jsx(Button
|
|
632
|
+
// ghost: a filled button up here would outrank the Save that
|
|
633
|
+
// finishes the task.
|
|
634
|
+
, {
|
|
635
|
+
variant: "ghost",
|
|
636
|
+
size: "sm",
|
|
637
|
+
xstyle: styles.resetButton,
|
|
638
|
+
label: t('@astryx.dateInput.resetPicking'),
|
|
639
|
+
onClick: handleResetInSheet
|
|
640
|
+
})
|
|
606
641
|
})]
|
|
607
642
|
}), /*#__PURE__*/_jsx("div", {
|
|
608
643
|
"aria-hidden": "true",
|
|
@@ -677,23 +712,17 @@ export function TouchDateField({
|
|
|
677
712
|
...{
|
|
678
713
|
className: "x1xye8es xrvj5dj x1y6fwsi"
|
|
679
714
|
},
|
|
680
|
-
children: [/*#__PURE__*/
|
|
715
|
+
children: [/*#__PURE__*/_jsx("div", {
|
|
681
716
|
inert: isWheelOpen ? true : undefined,
|
|
682
717
|
...{
|
|
683
718
|
0: {
|
|
684
|
-
className: "x15r89dc x78zum5
|
|
719
|
+
className: "x15r89dc x78zum5 x74b7sa xuedmi6 xzg1mie"
|
|
685
720
|
},
|
|
686
721
|
1: {
|
|
687
|
-
className: "x15r89dc x78zum5
|
|
722
|
+
className: "x15r89dc x78zum5 x74b7sa xuedmi6 xzg1mie xlshs6z x47corl"
|
|
688
723
|
}
|
|
689
724
|
}[!!isWheelOpen << 0],
|
|
690
|
-
children:
|
|
691
|
-
variant: "secondary",
|
|
692
|
-
size: "md",
|
|
693
|
-
width: "100%",
|
|
694
|
-
label: t('@astryx.dateInput.resetPicking'),
|
|
695
|
-
onClick: handleResetInSheet
|
|
696
|
-
}), /*#__PURE__*/_jsx(Button, {
|
|
725
|
+
children: /*#__PURE__*/_jsx(Button, {
|
|
697
726
|
variant: "primary"
|
|
698
727
|
// md, not sm: it is the action a thumb reaches for, so it gets
|
|
699
728
|
// the comfortable size rather than the compact one the header's
|
|
@@ -703,15 +732,15 @@ export function TouchDateField({
|
|
|
703
732
|
width: "100%",
|
|
704
733
|
label: t('@astryx.dateInput.savePicking'),
|
|
705
734
|
onClick: () => setIsSheetOpen(false)
|
|
706
|
-
})
|
|
735
|
+
})
|
|
707
736
|
}), /*#__PURE__*/_jsx("div", {
|
|
708
737
|
inert: isWheelOpen ? undefined : true,
|
|
709
738
|
...{
|
|
710
739
|
0: {
|
|
711
|
-
className: "x15r89dc x78zum5
|
|
740
|
+
className: "x15r89dc x78zum5 x10xzikg xc8icb0 x1jl3cmp xuedmi6 xcj1dhv xzg1mie"
|
|
712
741
|
},
|
|
713
742
|
1: {
|
|
714
|
-
className: "x15r89dc x78zum5
|
|
743
|
+
className: "x15r89dc x78zum5 x10xzikg xc8icb0 x1jl3cmp xuedmi6 xcj1dhv xzg1mie xlshs6z xg01cxk x47corl"
|
|
715
744
|
}
|
|
716
745
|
}[!!!isWheelOpen << 0],
|
|
717
746
|
children: /*#__PURE__*/_jsx(Button
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HoverCard.d.ts","sourceRoot":"","sources":["../../src/HoverCard/HoverCard.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAsB,KAAK,YAAY,EAAE,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG7E,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAC,cAAc,EAAE,cAAc,EAAC,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAG5C,YAAY,EACV,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"HoverCard.d.ts","sourceRoot":"","sources":["../../src/HoverCard/HoverCard.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAsB,KAAK,YAAY,EAAE,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG7E,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAC,cAAc,EAAE,cAAc,EAAC,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAG5C,YAAY,EACV,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AA6ExB,MAAM,WAAW,cAAe,SAAQ,IAAI,CAC1C,SAAS,EACT,QAAQ,GAAG,WAAW,GAAG,OAAO,CACjC;IACC;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,OAAO,EAAE,SAAS,CAAC;IAEnB;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEzC;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAEtC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAiBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,EACxB,QAAQ,EACR,OAAO,EACP,SAAmB,EACnB,SAAoB,EACpB,KAAW,EACX,SAAe,EACf,YAAqB,EACrB,YAAqB,EACrB,SAAgB,EAChB,KAAK,EACL,YAAY,EACZ,kBAA2B,EAC3B,MAAM,EACN,aAAa,EACb,MAAM,EACN,SAAS,EACT,KAAK,GACN,EAAE,cAAc,GAAG,YAAY,CAoL/B;yBAtMe,SAAS"}
|
|
@@ -21,6 +21,45 @@ import { useHoverCard } from "./useHoverCard.js";
|
|
|
21
21
|
import "../theme/tokens.stylex.js";
|
|
22
22
|
import { colorVars, spacingVars } from "../theme/tokens.stylex.js";
|
|
23
23
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
24
|
+
// `aria-haspopup` and `aria-controls` are global, so any trigger may carry
|
|
25
|
+
// them. `aria-expanded` is not. This is the ARIA 1.2 "Supported States and
|
|
26
|
+
// Properties" list for it — deliberately the narrow reading: axe accepts the
|
|
27
|
+
// attribute on rather more roles, since it also allows every subclass of these,
|
|
28
|
+
// so a role outside this set is not necessarily a violation. Widening it is
|
|
29
|
+
// safe; the set is a floor, not the spec's ceiling. The same rule is applied in
|
|
30
|
+
// Chat/useTriggerMenu.tsx, which only emits the combobox attributes once the
|
|
31
|
+
// element is actually a combobox.
|
|
32
|
+
const EXPANDABLE_ROLES = new Set(['application', 'button', 'checkbox', 'columnheader', 'combobox', 'gridcell', 'link', 'listbox', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'row', 'rowheader', 'switch', 'tab', 'treeitem']);
|
|
33
|
+
const BUTTON_INPUT_TYPES = new Set(['button', 'submit', 'reset', 'image']);
|
|
34
|
+
|
|
35
|
+
// Deliberately partial, and unlisted elements are read as role-less: dropping
|
|
36
|
+
// aria-expanded where it might have been legal costs an AT user a state they
|
|
37
|
+
// can still infer, whereas emitting it where it is illegal is a critical
|
|
38
|
+
// aria-allowed-attr defect.
|
|
39
|
+
function supportsAriaExpanded(el) {
|
|
40
|
+
const explicit = el.getAttribute('role')?.trim().split(/\s+/)[0];
|
|
41
|
+
if (explicit) {
|
|
42
|
+
return EXPANDABLE_ROLES.has(explicit);
|
|
43
|
+
}
|
|
44
|
+
switch (el.tagName) {
|
|
45
|
+
case 'BUTTON':
|
|
46
|
+
case 'SUMMARY':
|
|
47
|
+
return true;
|
|
48
|
+
case 'A':
|
|
49
|
+
case 'AREA':
|
|
50
|
+
return el.hasAttribute('href');
|
|
51
|
+
case 'INPUT':
|
|
52
|
+
// The button-flavoured input types map to role="button"; every other
|
|
53
|
+
// type maps to a textbox-family role, which does not take it.
|
|
54
|
+
return BUTTON_INPUT_TYPES.has(el.type?.toLowerCase());
|
|
55
|
+
case 'SELECT':
|
|
56
|
+
// combobox when it is a single-line picker, listbox otherwise; both
|
|
57
|
+
// support aria-expanded.
|
|
58
|
+
return true;
|
|
59
|
+
default:
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
24
63
|
/**
|
|
25
64
|
* Check if children are text-only (no React elements)
|
|
26
65
|
*/
|
|
@@ -121,12 +160,17 @@ export function HoverCard({
|
|
|
121
160
|
// trigger with the dialog's content (aria-describedby is for plain-text
|
|
122
161
|
// descriptions, not navigable regions). See #5049.
|
|
123
162
|
//
|
|
163
|
+
// aria-expanded only goes on a trigger whose role permits it. A role-less
|
|
164
|
+
// trigger (Timestamp's <time>/<span>) gets haspopup + controls, which are
|
|
165
|
+
// global, and no expanded state.
|
|
166
|
+
//
|
|
124
167
|
// Merge rather than overwrite: the trigger may already carry its own
|
|
125
168
|
// popup semantics (e.g. a menu button wrapped in a labelled HoverCard),
|
|
126
169
|
// so preserve the existing values and restore them on cleanup.
|
|
127
170
|
const existingHaspopup = firstChild.getAttribute('aria-haspopup');
|
|
128
171
|
const existingControls = firstChild.getAttribute('aria-controls');
|
|
129
172
|
const existingExpanded = firstChild.getAttribute('aria-expanded');
|
|
173
|
+
const canExpand = supportsAriaExpanded(firstChild);
|
|
130
174
|
firstChild.setAttribute('aria-haspopup', 'dialog');
|
|
131
175
|
// Only point aria-controls at the layer while it is open and in the DOM.
|
|
132
176
|
// While closed, useLayer leaves only an inert <template> marker, so the
|
|
@@ -139,7 +183,13 @@ export function HoverCard({
|
|
|
139
183
|
} else {
|
|
140
184
|
firstChild.removeAttribute('aria-controls');
|
|
141
185
|
}
|
|
142
|
-
|
|
186
|
+
if (canExpand) {
|
|
187
|
+
firstChild.setAttribute('aria-expanded', String(hoverCard.isOpen));
|
|
188
|
+
} else if (existingExpanded) {
|
|
189
|
+
firstChild.setAttribute('aria-expanded', existingExpanded);
|
|
190
|
+
} else {
|
|
191
|
+
firstChild.removeAttribute('aria-expanded');
|
|
192
|
+
}
|
|
143
193
|
return () => {
|
|
144
194
|
hoverCard.ref(null);
|
|
145
195
|
if (existingHaspopup) {
|
|
@@ -192,8 +242,10 @@ export function HoverCard({
|
|
|
192
242
|
ref: hoverCard.ref,
|
|
193
243
|
tabIndex: 0,
|
|
194
244
|
"aria-haspopup": label ? 'dialog' : undefined,
|
|
195
|
-
"aria-controls": label && hoverCard.isOpen ? hoverCard.id : undefined
|
|
196
|
-
|
|
245
|
+
"aria-controls": label && hoverCard.isOpen ? hoverCard.id : undefined
|
|
246
|
+
// No aria-expanded: this wrapper is a role-less <span>, and
|
|
247
|
+
// aria-expanded is invalid on it (see EXPANDABLE_ROLES above).
|
|
248
|
+
,
|
|
197
249
|
"aria-describedby": label ? undefined : hoverCard.describedBy,
|
|
198
250
|
...{
|
|
199
251
|
0: {
|
|
@@ -134,7 +134,10 @@ export interface HoverCardReturn {
|
|
|
134
134
|
describedBy: string;
|
|
135
135
|
/**
|
|
136
136
|
* Whether the hover card is currently open.
|
|
137
|
-
* Useful for driving `aria-expanded` on the trigger
|
|
137
|
+
* Useful for driving `aria-expanded` on the trigger — but only when the
|
|
138
|
+
* trigger's role permits it (`button`, `link`, `combobox`, …). On a role-less
|
|
139
|
+
* trigger `aria-expanded` is invalid; use `aria-haspopup`/`aria-controls`
|
|
140
|
+
* alone there.
|
|
138
141
|
*/
|
|
139
142
|
isOpen: boolean;
|
|
140
143
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useHoverCard.d.ts","sourceRoot":"","sources":["../../src/HoverCard/useHoverCard.tsx"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,OAAO,CAAC;AAEf,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AA6BlC;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEhE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,iBAAiB,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,GAAG,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAE9B;;;OAGG;IACH,WAAW,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEtC;;;;OAIG;IACH,cAAc,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEzC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB
|
|
1
|
+
{"version":3,"file":"useHoverCard.d.ts","sourceRoot":"","sources":["../../src/HoverCard/useHoverCard.tsx"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,OAAO,CAAC;AAEf,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AA6BlC;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEhE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,iBAAiB,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,GAAG,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAE9B;;;OAGG;IACH,WAAW,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEtC;;;;OAIG;IACH,cAAc,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEzC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;;OAMG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;;;OAOG;IACH,eAAe,EAAE,CACf,QAAQ,EAAE,SAAS,EACnB,KAAK,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,KAC5C,SAAS,CAAC;IAEf;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;IAEjB;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAyBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,gBAAqB,GAAG,eAAe,CAib5E"}
|
package/locales/en.json
CHANGED
|
@@ -725,7 +725,7 @@
|
|
|
725
725
|
},
|
|
726
726
|
"@astryx.dateInput.resetPicking": {
|
|
727
727
|
"defaultMessage": "Reset",
|
|
728
|
-
"description": "
|
|
728
|
+
"description": "Ghost button in the mobile date picker's header, at the trailing corner beside the month arrows. Removes the chosen date AND returns the calendar to the current month — it undoes the picking done so far and puts the picker back to how it opens, so prefer a word covering both, not one that only means emptying a field. Not cancel: the picker stays open afterwards and nothing is dismissed. Keep it short; it shares one line with the month title and two arrows."
|
|
729
729
|
},
|
|
730
730
|
"@astryx.dateInput.savePicking": {
|
|
731
731
|
"defaultMessage": "Save",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.5.0-canary.
|
|
3
|
+
"version": "0.5.0-canary.cb20a01",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -661,7 +661,7 @@
|
|
|
661
661
|
"react-dom": ">=19.0.0"
|
|
662
662
|
},
|
|
663
663
|
"devDependencies": {
|
|
664
|
-
"@astryxdesign/cli": "0.5.0-canary.
|
|
664
|
+
"@astryxdesign/cli": "0.5.0-canary.cb20a01",
|
|
665
665
|
"@babel/cli": "^8.0.4",
|
|
666
666
|
"@babel/core": "^7.29.7",
|
|
667
667
|
"@babel/preset-react": "^8.0.1",
|
|
@@ -21,21 +21,75 @@ export const docs = {
|
|
|
21
21
|
],
|
|
22
22
|
usage: {
|
|
23
23
|
description:
|
|
24
|
-
'AlertDialog asks the user to confirm a destructive or irreversible action before it happens. Use it for things like deleting content, revoking access, or discarding unsaved changes.\n\nIt implements the WAI-ARIA APG [Alert Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/): `role="alertdialog"`, a title linked by `aria-labelledby`, a consequence description linked by `aria-describedby`, focus moved into the dialog on open and returned to the trigger on close, and no dismissal by clicking outside. Escape cancels.\n\nFor cases where you want to show an alert without managing open state, use the `useImperativeAlertDialog` hook: call `alert.show(options)` and render `alert.element` in your tree.',
|
|
24
|
+
'AlertDialog asks the user to confirm a destructive or irreversible action before it happens. Use it for things like deleting content, revoking access, or discarding unsaved changes.\n\nIt implements the WAI-ARIA APG [Alert Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/): `role="alertdialog"`, a title linked by `aria-labelledby`, a consequence description linked by `aria-describedby`, focus moved into the dialog on open and returned to the trigger on close, and no dismissal by clicking outside. Escape cancels.\n\nAlertDialog passes its requested width through to Dialog, which clamps the surface to the container and dynamic viewport with token gutters. Generic Dialog footers should wrap, but Dialog does not own action semantics or order; consumer composition controls that. AlertDialog owns its confirmation semantics: above 640px, actions render horizontally and may move onto another row; at 640px and below, the destructive action appears above Cancel and both buttons fill the footer width. Button labels retain their standard single-line behavior. The breakpoint follows available width, not pointer or hover capability. The body scrolls when block space is constrained.\n\nFor cases where you want to show an alert without managing open state, use the `useImperativeAlertDialog` hook: call `alert.show(options)` and render `alert.element` in your tree.',
|
|
25
25
|
bestPractices: [
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
26
|
+
{
|
|
27
|
+
guidance: true,
|
|
28
|
+
description:
|
|
29
|
+
'Make the action button label specific: "Delete project" is better than "OK" or "Confirm".',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
guidance: true,
|
|
33
|
+
description:
|
|
34
|
+
'Describe what will happen in the description so the user knows the consequences before confirming.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
guidance: true,
|
|
38
|
+
description:
|
|
39
|
+
'Keep the cancel button as the least-destructive focus target. On narrow screens the destructive action is visually and structurally above Cancel, but Cancel still receives initial focus.',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
guidance: true,
|
|
43
|
+
description:
|
|
44
|
+
'Use concise, specific action labels. Above 640px, complete buttons may move onto another row; at 640px and below, the destructive action appears above Cancel and both buttons fill the footer width.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
guidance: false,
|
|
48
|
+
description:
|
|
49
|
+
'Use AlertDialog for non-destructive actions; use a standard Dialog instead.',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
guidance: false,
|
|
53
|
+
description:
|
|
54
|
+
'Rely on color alone to signal danger; the action label itself should say what will happen.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
guidance: false,
|
|
58
|
+
description:
|
|
59
|
+
'Close the dialog from onAction before the work finishes; hold it open with isActionLoading and call onOpenChange(false) when the action settles.',
|
|
60
|
+
},
|
|
32
61
|
],
|
|
33
62
|
anatomy: [
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
63
|
+
{
|
|
64
|
+
name: 'Title',
|
|
65
|
+
required: true,
|
|
66
|
+
description:
|
|
67
|
+
'The question being asked. Renders as a level-2 heading and labels the dialog via aria-labelledby.',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'Description',
|
|
71
|
+
required: true,
|
|
72
|
+
description:
|
|
73
|
+
'What will happen if the user confirms. Linked to the dialog via aria-describedby.',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'Cancel button',
|
|
77
|
+
required: true,
|
|
78
|
+
description:
|
|
79
|
+
'Ghost button that dismisses without acting. Takes initial focus, and Escape does the same thing.',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'Action button',
|
|
83
|
+
required: true,
|
|
84
|
+
description:
|
|
85
|
+
'The confirming action. Destructive by default; shows a spinner while isActionLoading is set.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'Backdrop',
|
|
89
|
+
required: true,
|
|
90
|
+
description:
|
|
91
|
+
'Overlay behind the dialog that blocks page interaction. Clicking it does not dismiss.',
|
|
92
|
+
},
|
|
39
93
|
],
|
|
40
94
|
},
|
|
41
95
|
// Intentionally a contained isInline preview, not playground.overlay: the
|
|
@@ -48,11 +102,13 @@ export const docs = {
|
|
|
48
102
|
isInline: true,
|
|
49
103
|
onOpenChange: undefined,
|
|
50
104
|
title: 'Delete item?',
|
|
51
|
-
description:
|
|
105
|
+
description:
|
|
106
|
+
'This action cannot be undone. The item and all its data will be permanently removed.',
|
|
52
107
|
actionLabel: 'Delete',
|
|
53
108
|
},
|
|
54
109
|
},
|
|
55
|
-
description:
|
|
110
|
+
description:
|
|
111
|
+
'A modal dialog that asks the user to confirm a destructive action.',
|
|
56
112
|
props: [
|
|
57
113
|
{
|
|
58
114
|
name: 'isOpen',
|
|
@@ -111,38 +167,66 @@ export const docs = {
|
|
|
111
167
|
name: 'width',
|
|
112
168
|
type: 'number | string',
|
|
113
169
|
default: '400',
|
|
114
|
-
description:
|
|
170
|
+
description:
|
|
171
|
+
'Requested dialog width. Dialog preserves this preferred width and clamps it to the container and dynamic viewport with token gutters.',
|
|
115
172
|
},
|
|
116
173
|
{
|
|
117
174
|
name: 'isInline',
|
|
118
175
|
type: 'boolean',
|
|
119
176
|
default: 'false',
|
|
120
|
-
description:
|
|
177
|
+
description:
|
|
178
|
+
'Renders alert dialog content inline without modal behavior. For documentation previews and showcases only. Not being a modal, the inline path renders role="group" instead of role="alertdialog".',
|
|
121
179
|
},
|
|
122
180
|
],
|
|
123
|
-
components: [
|
|
124
|
-
{name: 'useImperativeAlertDialog'},
|
|
125
|
-
],
|
|
181
|
+
components: [{name: 'useImperativeAlertDialog'}],
|
|
126
182
|
theming: {
|
|
127
|
-
targets: [
|
|
128
|
-
{className: 'astryx-alert-dialog'},
|
|
129
|
-
],
|
|
183
|
+
targets: [{className: 'astryx-alert-dialog'}],
|
|
130
184
|
},
|
|
131
185
|
};
|
|
132
186
|
|
|
133
187
|
/** @type {import('@astryxdesign/cli/authoring').ComponentTranslationDoc} */
|
|
134
188
|
export const docsDense = {
|
|
135
|
-
description:
|
|
189
|
+
description:
|
|
190
|
+
'Confirms destructive/irreversible action before it happens (delete, revoke access, discard unsaved changes).',
|
|
136
191
|
usage: {
|
|
137
192
|
description:
|
|
138
|
-
'AlertDialog confirms destructive/irreversible action (delete, revoke access, discard changes). Implements WAI-ARIA APG Alert Dialog pattern (https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/): role="alertdialog", aria-labelledby title, aria-describedby description, focus into dialog on open + back to trigger on close, no outside-click dismissal, Escape cancels. To show w/o managing open state, use useImperativeAlertDialog hook: call alert.show(options) + render alert.element in tree.',
|
|
193
|
+
'AlertDialog confirms destructive/irreversible action (delete, revoke access, discard changes). Implements WAI-ARIA APG Alert Dialog pattern (https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/): role="alertdialog", aria-labelledby title, aria-describedby description, focus into dialog on open + back to trigger on close, no outside-click dismissal, Escape cancels. Dialog preserves requested width + clamps it to container/dynamic viewport gutters. Generic Dialog footers wrap while consumers own action order. AlertDialog owns confirmation action layout: >640px horizontal buttons may move to another row; <=640px puts the destructive action above Cancel and makes both buttons full width regardless of pointer/hover capability. Button labels remain single-line. Body scrolls when height constrained. To show w/o managing open state, use useImperativeAlertDialog hook: call alert.show(options) + render alert.element in tree.',
|
|
139
194
|
bestPractices: [
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
{
|
|
195
|
+
{
|
|
196
|
+
guidance: true,
|
|
197
|
+
description:
|
|
198
|
+
'Make action button label specific: "Delete project" > "OK"/"Confirm".',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
guidance: true,
|
|
202
|
+
description:
|
|
203
|
+
'Describe consequences in description so user knows outcome before confirming.',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
guidance: true,
|
|
207
|
+
description:
|
|
208
|
+
'Keep cancel as initial focus: least destructive choice is preselected even when narrow visual/DOM order places it after the destructive action.',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
guidance: true,
|
|
212
|
+
description:
|
|
213
|
+
'Use concise action labels; >640px complete buttons may move to another row, and <=640px puts the destructive action above Cancel with both buttons full width regardless of pointer type.',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
guidance: false,
|
|
217
|
+
description:
|
|
218
|
+
'Use AlertDialog for non-destructive actions; use standard Dialog instead.',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
guidance: false,
|
|
222
|
+
description:
|
|
223
|
+
'Rely on color alone for danger; the action label should say what happens.',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
guidance: false,
|
|
227
|
+
description:
|
|
228
|
+
'Close from onAction before work finishes; hold open w/ isActionLoading, call onOpenChange(false) when it settles.',
|
|
229
|
+
},
|
|
146
230
|
],
|
|
147
231
|
},
|
|
148
232
|
};
|