Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/alert-dialog-responsive-actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] AlertDialog preserves its >640px centered layout and, at 640px and below, fills available width with token gutters, stacks the destructive action above Cancel, and wraps complete decision labels regardless of pointer type.

@rubycheung
60 changes: 38 additions & 22 deletions apps/storybook/stories/AlertDialog.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,32 +86,48 @@ export const Async: Story = {
};

/**
* Non-destructive confirmation with a primary action button.
* Desktop fine-pointer reference. Above 640px the dialog keeps the pre-existing
* 400px centered surface and horizontal Cancel/destructive action row.
*/
export const Informational: Story = {
render: () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button
label="Show notice"
variant="secondary"
onClick={() => setIsOpen(true)}
/>
<AlertDialog
isOpen={isOpen}
onOpenChange={setIsOpen}
title="Session expired"
description="Your session has expired. You will be redirected to the login page."
actionLabel="Sign in"
actionVariant="primary"
onAction={() => setIsOpen(false)}
/>
</>
);
export const DesktopFinePointer: Story = {
args: {
isOpen: true,
title: 'Delete item?',
description:
'This action cannot be undone. The item and all its data will be permanently removed.',
actionLabel: 'Delete',
onOpenChange: () => {},
onAction: () => {},
},
};

/**
* Narrow fine-pointer reference. At 640px and below, available width determines
* layout: the surface uses token gutters, destructive appears above Cancel, and
* labels wrap even when the pointer can hover.
*/
export const NarrowFinePointer: Story = {
args: {
isOpen: true,
title: 'Permanently delete this workspace?',
description:
'Everyone will lose access to its dashboards, saved queries, and sharing links. This cannot be undone.',
cancelLabel: 'Keep this workspace',
actionLabel: 'Permanently delete workspace',
onOpenChange: () => {},
onAction: () => {},
},
};

/**
* Mobile touch reference. A <=640px coarse-pointer/no-hover viewport uses the
* same narrow stacked layout as NarrowFinePointer; pointer type does not decide
* geometry.
*/
export const MobileTouch: Story = {
args: NarrowFinePointer.args,
};

/**
* Imperative API — no state management needed.
*/
Expand Down
144 changes: 114 additions & 30 deletions packages/core/src/AlertDialog/AlertDialog.doc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,75 @@ export const docs = {
],
usage: {
description:
'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.',
'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\nAt widths above 640px, AlertDialog keeps its requested width and horizontal Cancel/destructive actions. At 640px and below, the surface fills the viewport with token gutters, the destructive action appears above Cancel, and complete labels wrap. Geometry 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.',
bestPractices: [
{guidance: true, description: 'Make the action button label specific: "Delete project" is better than "OK" or "Confirm".'},
{guidance: true, description: 'Describe what will happen in the description so the user knows the consequences before confirming.'},
{guidance: true, description: 'Keep the cancel button first: it takes initial focus, so the least destructive choice is the one already selected when the dialog opens.'},
{guidance: false, description: 'Use AlertDialog for non-destructive actions; use a standard Dialog instead.'},
{guidance: false, description: 'Rely on color alone to signal danger; the action label itself should say what will happen.'},
{guidance: false, description: 'Close the dialog from onAction before the work finishes; hold it open with isActionLoading and call onOpenChange(false) when the action settles.'},
{
guidance: true,
description:
'Make the action button label specific: "Delete project" is better than "OK" or "Confirm".',
},
{
guidance: true,
description:
'Describe what will happen in the description so the user knows the consequences before confirming.',
},
{
guidance: true,
description:
'Keep the cancel button first: it takes initial focus, so the least destructive choice is the one already selected when the dialog opens.',
},
{
guidance: true,
description:
'Use complete action labels. At 640px and below, the destructive action appears above Cancel and both labels wrap instead of truncating, regardless of pointer type.',
},
{
guidance: false,
description:
'Use AlertDialog for non-destructive actions; use a standard Dialog instead.',
},
{
guidance: false,
description:
'Rely on color alone to signal danger; the action label itself should say what will happen.',
},
{
guidance: false,
description:
'Close the dialog from onAction before the work finishes; hold it open with isActionLoading and call onOpenChange(false) when the action settles.',
},
],
anatomy: [
{name: 'Title', required: true, description: 'The question being asked. Renders as a level-2 heading and labels the dialog via aria-labelledby.'},
{name: 'Description', required: true, description: 'What will happen if the user confirms. Linked to the dialog via aria-describedby.'},
{name: 'Cancel button', required: true, description: 'Ghost button that dismisses without acting. Takes initial focus, and Escape does the same thing.'},
{name: 'Action button', required: true, description: 'The confirming action. Destructive by default; shows a spinner while isActionLoading is set.'},
{name: 'Backdrop', required: true, description: 'Overlay behind the dialog that blocks page interaction. Clicking it does not dismiss.'},
{
name: 'Title',
required: true,
description:
'The question being asked. Renders as a level-2 heading and labels the dialog via aria-labelledby.',
},
{
name: 'Description',
required: true,
description:
'What will happen if the user confirms. Linked to the dialog via aria-describedby.',
},
{
name: 'Cancel button',
required: true,
description:
'Ghost button that dismisses without acting. Takes initial focus, and Escape does the same thing.',
},
{
name: 'Action button',
required: true,
description:
'The confirming action. Destructive by default; shows a spinner while isActionLoading is set.',
},
{
name: 'Backdrop',
required: true,
description:
'Overlay behind the dialog that blocks page interaction. Clicking it does not dismiss.',
},
],
},
// Intentionally a contained isInline preview, not playground.overlay: the
Expand All @@ -48,11 +102,13 @@ export const docs = {
isInline: true,
onOpenChange: undefined,
title: 'Delete item?',
description: 'This action cannot be undone. The item and all its data will be permanently removed.',
description:
'This action cannot be undone. The item and all its data will be permanently removed.',
actionLabel: 'Delete',
},
},
description: 'A modal dialog that asks the user to confirm a destructive action.',
description:
'A modal dialog that asks the user to confirm a destructive action.',
props: [
{
name: 'isOpen',
Expand Down Expand Up @@ -111,38 +167,66 @@ export const docs = {
name: 'width',
type: 'number | string',
default: '400',
description: 'Dialog width.',
description:
'Requested dialog width above 640px. At 640px and below, the dialog fills available width with token gutters regardless of pointer type.',
},
{
name: 'isInline',
type: 'boolean',
default: 'false',
description: '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".',
description:
'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".',
},
],
components: [
{name: 'useImperativeAlertDialog'},
],
components: [{name: 'useImperativeAlertDialog'}],
theming: {
targets: [
{className: 'astryx-alert-dialog'},
],
targets: [{className: 'astryx-alert-dialog'}],
},
};

/** @type {import('@astryxdesign/cli/authoring').ComponentTranslationDoc} */
export const docsDense = {
description: 'Confirms destructive/irreversible action before it happens (delete, revoke access, discard unsaved changes).',
description:
'Confirms destructive/irreversible action before it happens (delete, revoke access, discard unsaved changes).',
usage: {
description:
'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.',
'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. >640px keeps requested width + horizontal actions. <=640px fills w/ token gutters, puts destructive action above Cancel, and wraps labels regardless of pointer/hover capability. Body scrolls when height constrained. To show w/o managing open state, use useImperativeAlertDialog hook: call alert.show(options) + render alert.element in tree.',
bestPractices: [
{guidance: true, description: 'Make action button label specific: "Delete project" > "OK"/"Confirm".'},
{guidance: true, description: 'Describe consequences in description so user knows outcome before confirming.'},
{guidance: true, description: 'Keep cancel first: it takes initial focus, so least destructive choice is preselected.'},
{guidance: false, description: 'Use AlertDialog for non-destructive actions; use standard Dialog instead.'},
{guidance: false, description: 'Rely on color alone for danger; the action label should say what happens.'},
{guidance: false, description: 'Close from onAction before work finishes; hold open w/ isActionLoading, call onOpenChange(false) when it settles.'},
{
guidance: true,
description:
'Make action button label specific: "Delete project" > "OK"/"Confirm".',
},
{
guidance: true,
description:
'Describe consequences in description so user knows outcome before confirming.',
},
{
guidance: true,
description:
'Keep cancel first: it takes initial focus, so least destructive choice is preselected.',
},
{
guidance: true,
description:
'Use complete action labels; <=640px puts the destructive action above Cancel and allows wrapping regardless of pointer type.',
},
{
guidance: false,
description:
'Use AlertDialog for non-destructive actions; use standard Dialog instead.',
},
{
guidance: false,
description:
'Rely on color alone for danger; the action label should say what happens.',
},
{
guidance: false,
description:
'Close from onAction before work finishes; hold open w/ isActionLoading, call onOpenChange(false) when it settles.',
},
],
},
};
Loading
Loading