Skip to content

Commit

Permalink
feat(alert-dialog): add disableDestructive prop (#3402)
Browse files Browse the repository at this point in the history
* feat(alert-dialog): add disableDestructive prop

* docs(delete-pattern): add live examples
  • Loading branch information
nkrantz committed Aug 16, 2023
1 parent 4aba74d commit 48c8d82
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 49 deletions.
6 changes: 6 additions & 0 deletions .changeset/little-beans-laugh.md
@@ -0,0 +1,6 @@
---
'@twilio-paste/alert-dialog': minor
'@twilio-paste/core': minor
---

[Alert Dialog] Add prop `onConfirmDisabled` to allow for the confirm button on destructive Alert Dialogs to be conditionally disabled. This interaction is primarily for use in the High Severity Delete Pattern.
Expand Up @@ -3,7 +3,11 @@ import {render, screen} from '@testing-library/react';
import type {RenderOptions} from '@testing-library/react';
import {Theme} from '@twilio-paste/theme';

import {AlertDialogWithTwoActions, DestructiveAlertDialog} from '../stories/index.stories';
import {
AlertDialogWithTwoActions,
DestructiveAlertDialog,
DisabledButtonDestructiveAlertDialog,
} from '../stories/index.stories';

const ThemeWrapper: RenderOptions['wrapper'] = ({children}) => (
<Theme.Provider theme="default">{children}</Theme.Provider>
Expand All @@ -29,6 +33,12 @@ describe('Alert Dialog', () => {
expect(button).toHaveStyleRule('background-color', 'rgb(214, 31, 31)');
});

it('Should have a disabled destructive button style when the onConfirmDisabled prop is included', () => {
render(<DisabledButtonDestructiveAlertDialog dialogIsOpen />, {wrapper: ThemeWrapper});
const button = screen.getByRole('button', {name: 'Delete'});
expect(button).toHaveStyleRule('background-color', 'rgb(225, 227, 234)');
});

it('Should have a heading the same as the heading prop', () => {
render(<AlertDialogWithTwoActions />);
expect(screen.getByRole('heading')).toHaveTextContent('Submit application');
Expand All @@ -48,6 +58,11 @@ describe('Alert Dialog', () => {
);
});

it('Should have correct attributes when button is disabled', () => {
render(<DisabledButtonDestructiveAlertDialog dialogIsOpen />, {wrapper: ThemeWrapper});
expect(screen.getByRole('button', {name: 'Delete'})).toHaveAttribute('disabled');
});

it('Should have the initial focus land on the first focusable item', () => {
render(<AlertDialogWithTwoActions />);
expect(document.activeElement).toEqual(screen.getAllByRole('button')[0]);
Expand Down
Expand Up @@ -12,11 +12,21 @@ export interface AlertDialogFooterProps extends HTMLPasteProps<'div'>, Pick<BoxP
onConfirmLabel: string;
onDismiss: () => void;
onDismissLabel: string;
onConfirmDisabled?: boolean;
}

export const AlertDialogFooter = React.forwardRef<HTMLDivElement, AlertDialogFooterProps>(
(
{destructive, element = 'ALERT_DIALOG_FOOTER', onConfirm, onConfirmLabel, onDismiss, onDismissLabel, ...props},
{
destructive,
element = 'ALERT_DIALOG_FOOTER',
onConfirm,
onConfirmLabel,
onDismiss,
onDismissLabel,
onConfirmDisabled = false,
...props
},
ref
) => {
const primaryVariant = destructive ? 'destructive' : 'primary';
Expand All @@ -38,7 +48,7 @@ export const AlertDialogFooter = React.forwardRef<HTMLDivElement, AlertDialogFoo
<Button variant="secondary" onClick={onDismiss}>
{onDismissLabel}
</Button>
<Button variant={primaryVariant} onClick={onConfirm}>
<Button variant={primaryVariant} onClick={onConfirm} disabled={destructive && onConfirmDisabled}>
{onConfirmLabel}
</Button>
</Stack>
Expand Down
3 changes: 3 additions & 0 deletions packages/paste-core/components/alert-dialog/src/index.tsx
Expand Up @@ -34,6 +34,7 @@ export interface AlertDialogProps extends HTMLPasteProps<'div'>, Pick<BoxProps,
onConfirmLabel: string;
onDismiss: () => void;
onDismissLabel: string;
onConfirmDisabled?: boolean;
}

export const AlertDialog = React.forwardRef<HTMLDivElement, AlertDialogProps>(
Expand All @@ -48,6 +49,7 @@ export const AlertDialog = React.forwardRef<HTMLDivElement, AlertDialogProps>(
onConfirmLabel,
onDismiss,
onDismissLabel,
onConfirmDisabled,
...props
},
ref
Expand Down Expand Up @@ -86,6 +88,7 @@ export const AlertDialog = React.forwardRef<HTMLDivElement, AlertDialogProps>(
onDismissLabel={onDismissLabel}
onConfirm={onConfirm}
onConfirmLabel={onConfirmLabel}
onConfirmDisabled={onConfirmDisabled}
/>
</Box>
</ModalDialogOverlay>
Expand Down
Expand Up @@ -7,6 +7,10 @@ import {Modal, ModalBody, ModalFooter, ModalFooterActions, ModalHeader, ModalHea
import {Paragraph} from '@twilio-paste/paragraph';
import {CustomizationProvider} from '@twilio-paste/customization';
import {useTheme} from '@twilio-paste/theme';
import {Box} from '@twilio-paste/box';
import {Input} from '@twilio-paste/input';
import {Label} from '@twilio-paste/label';
import {HelpText} from '@twilio-paste/help-text';

import {AlertDialog} from '../src';
import {AlertDialogHeader} from '../src/AlertDialogHeader';
Expand Down Expand Up @@ -83,6 +87,74 @@ DestructiveAlertDialogStory.parameters = {
},
};

export const DisabledButtonDestructiveAlertDialog = ({dialogIsOpen = false}): JSX.Element => {
const [isOpen, setIsOpen] = React.useState(dialogIsOpen);
const [inputString, setInputString] = React.useState('');
const [inputHasError, setInputHasError] = React.useState(false);
const [isDisabled, setIsDisabled] = React.useState(true);
const handleOpen = (): void => {
if (inputString !== '') setIsDisabled(false);
setIsOpen(true);
};
const handleDismiss = (): void => {
setIsDisabled(true);
setIsOpen(false);
setInputHasError(false);
};
const handleConfirm = (): void => {
if (inputString === 'Toyota TCB Automobile (Gevelsberg)') {
setIsOpen(false);
setInputString('');
setInputHasError(false);
setIsDisabled(true);
} else {
setInputHasError(true);
}
};
const handleChange = (e): void => {
setInputString(e.target.value);
if (e.target.value !== '') setIsDisabled(false);
else setIsDisabled(true);
};
return (
<>
<Button variant="destructive" onClick={handleOpen}>
Delete object
</Button>
<AlertDialog
heading="Delete regulatory bundle"
isOpen={isOpen}
onConfirm={handleConfirm}
onConfirmLabel="Delete"
onDismiss={handleDismiss}
onDismissLabel="Cancel"
destructive
onConfirmDisabled={isDisabled}
>
You&apos;re about to delete &ldquo;Toyota TCB Automobile (Gevelsberg)&ldquo; and all data associated with it.
This regulatory bundle will be deleted immediately. You can&apos;t undo this action.
<Box display="flex" flexDirection="column" rowGap="space30" marginY="space50">
<Label htmlFor="delete-input" required>
Regulatory bundle name
</Label>
<Input
type="text"
required
id="delete-input"
aria-describedby="delete-help-text"
onChange={(e) => handleChange(e)}
hasError={inputHasError}
value={inputString}
/>
<HelpText id="delete-help-text" variant={inputHasError ? 'error' : 'default'}>
To confirm this deletion, please input the name of this regulatory bundle.
</HelpText>
</Box>
</AlertDialog>
</>
);
};

export const OpenAlertDialogFromButton = (): JSX.Element => {
const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = (): void => setIsOpen(true);
Expand Down
139 changes: 139 additions & 0 deletions packages/paste-website/src/component-examples/DeletePatternExamples.ts
@@ -0,0 +1,139 @@
export const LowSeverityExample = `
const LowSeverityDelete = () => {
return (
<Button variant="destructive_link">Remove</Button>
)
}
render(
<LowSeverityDelete />
)
`.trim();

export const MediumSeverityExample = `
const MediumSeverityDelete = () => {
const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false);
return (
<>
<Button variant="destructive" onClick={handleOpen}>Delete</Button>
<AlertDialog
heading="Delete from regulatory bundle?"
isOpen={isOpen}
onConfirm={handleClose}
onConfirmLabel="Delete"
onDismiss={handleClose}
onDismissLabel="Cancel"
destructive
>
You're about to delete "Plan A Productions, LLC" from this regulatory bundle. This does not impact any other regulatory bundles.
</AlertDialog>
</>
)
}
render(
<MediumSeverityDelete />
)
`.trim();

export const HighSeverityExample = `
const HighSeverityDelete = () => {
const [isDisabled, setIsDisabled] = React.useState(true);
const [isOpen, setIsOpen] = React.useState(false);
const [inputString, setInputString] = React.useState('');
const [inputHasError, setInputHasError] = React.useState(false);
const handleOpen = () => {
if (inputString !== '') setIsDisabled(false);
setIsOpen(true);
};
const handleDismiss = () => {
setIsDisabled(true)
setIsOpen(false)
};
const handleConfirm = () => {
if (inputString === 'Toyota TCB Automobile (Gevelsberg)') {
setIsOpen(false)
setInputString('')
setInputHasError(false)
}
else {
setInputHasError(true)
}
};
const handleChange = (e) => {
setInputString(e.target.value)
if (e.target.value !== '') setIsDisabled(false);
else setIsDisabled(true);
};
return (
<>
<Button variant="destructive" onClick={handleOpen}>Delete</Button>
<AlertDialog
heading="Delete regulatory bundle?"
isOpen={isOpen}
onConfirm={handleConfirm}
onConfirmLabel="Delete"
onDismiss={handleDismiss}
onDismissLabel="Cancel"
destructive
onConfirmDisabled={isDisabled}
>
You're about to delete "Toyota TCB Automobile (Gevelsberg)" and all associated data. The bundle will be deleted immediately. You cannot undo this action.
<Box display="flex" flexDirection="column" rowGap="space30" marginY="space50">
<Label htmlFor="delete-input" required>
Regulatory bundle name
</Label>
<Input type="text" id="delete-input" required aria-describedby="delete-help-text" onChange={(e) => handleChange(e)} hasError={inputHasError} value={inputString}/>
<HelpText id="delete-help-text" variant={inputHasError ? 'error' : 'default'}>
Enter the name of the bundle being deleted. Entries are case-sensitive.
</HelpText>
</Box>
</AlertDialog>
</>
)
}
render(
<HighSeverityDelete />
)
`.trim();

export const PostDeletionExample = `
const MediumSeverityDelete = () => {
const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = () => setIsOpen(true);
const handleDismiss = () => setIsOpen(false);
const toaster = useToaster();
const handleConfirm = () => {
setIsOpen(false)
toaster.push({
message: '"Plan A Productions, LLC" was deleted. To undo deletion, return to the regulatory bundle overview.',
variant: 'success',
})
}
return (
<>
<Button variant="destructive" onClick={handleOpen}>Delete</Button>
<AlertDialog
heading="Delete from regulatory bundle?"
isOpen={isOpen}
onConfirm={handleConfirm}
onConfirmLabel="Delete"
onDismiss={handleDismiss}
onDismissLabel="Cancel"
destructive
>
You're about to delete "Plan A Productions, LLC" from this regulatory bundle. This does not impact any other regulatory bundles.
</AlertDialog>
<Toaster left={['space40', 'unset', 'unset']} {...toaster} />
</>
)
}
render(
<MediumSeverityDelete />
)
`.trim();
21 changes: 11 additions & 10 deletions packages/paste-website/src/pages/components/alert-dialog/index.mdx
Expand Up @@ -158,16 +158,17 @@ const AlertDialogExample = () => {
#### Props
| Prop | Type | Description | Default |
| -------------- | -------------------- | ----------------------------------------------------------------------------------------- | -------------- |
| children | `ReactNode` | | null |
| isOpen | `boolean` | | null |
| destructive? | `boolean` | | null |
| onConfirmLabel | `string` | | null |
| onConfirm | `Function() => void` | | null |
| onDismissLabel | `string` | | null |
| onDismiss | `Function() => void` | | null |
| element? | `string` | Overrides the default element name to apply unique styles with the Customization Provider | 'ALERT_DIALOG' |
| Prop | Type | Description | Default |
| ------------------ | -------------------- | ------------------------------------------------------------------------------------------------- | -------------- |
| children | `ReactNode` | | null |
| isOpen | `boolean` | | null |
| destructive? | `boolean` | | null |
| onConfirmDisabled? | `boolean` | Disables the confirm button on destructive Alert Dialogs for high severity deletion confirmations | null |
| onConfirmLabel | `string` | | null |
| onConfirm | `Function() => void` | | null |
| onDismissLabel | `string` | | null |
| onDismiss | `Function() => void` | | null |
| element? | `string` | Overrides the default element name to apply unique styles with the Customization Provider | 'ALERT_DIALOG' |
<ChangelogRevealer>
<Changelog />
Expand Down

0 comments on commit 48c8d82

Please sign in to comment.