Skip to content

Commit

Permalink
Add Alert (#653)
Browse files Browse the repository at this point in the history
  • Loading branch information
robphoenix authored Dec 13, 2024
1 parent 1770a51 commit 80bc685
Show file tree
Hide file tree
Showing 8 changed files with 472 additions and 0 deletions.
103 changes: 103 additions & 0 deletions packages/react/src/components/Alert/Alert.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
.uw-Alert {
flex-direction: row;
gap: var(--space-100);
padding: var(--space-200);
border-radius: 8px;
border-width: 1px;
border-style: solid;
border-color: var(--alert-border-color);
background-color: var(--alert-background-color);
color: var(--alert-text-color);

&:where(:focus-visible) {
outline: none;
border-radius: 4px;
box-shadow: 0 0 0 2px var(--alert-focus-color);
}

> :where(svg, [data-icon]) {
color: var(--alert-icon-color);
}

&:where([data-colorscheme='cyan']) {
--alert-text-color: var(--color-cyan900);
--alert-background-color: var(--color-cyan50);
--alert-icon-color: var(--color-cyan700);
--alert-border-color: var(--color-cyan500);
}

&:where([data-colorscheme='green']) {
--alert-text-color: var(--color-green900);
--alert-background-color: var(--color-green50);
--alert-icon-color: var(--color-green700);
--alert-border-color: var(--color-green500);
}

&:where([data-colorscheme='gold']) {
--alert-text-color: var(--color-gold900);
--alert-background-color: var(--color-gold50);
--alert-icon-color: var(--color-gold700);
--alert-border-color: var(--color-gold500);
}

&:where([data-colorscheme='red']) {
--alert-text-color: var(--color-red900);
--alert-background-color: var(--color-red50);
--alert-icon-color: var(--color-red700);
--alert-border-color: var(--color-red500);
}

.uw-IconButton {
height: auto;
width: auto;

&:where(:active) {
background-color: transparent;
}
}
}

.uw-AlertLink {
font-weight: var(--font-weight-semibold);
color: var(--alert-link-color);
text-decoration-color: var(--alert-link-color);

&:where(:visited) {
color: var(--alert-link-color);
text-decoration-color: var(--alert-link-color); /* TODO: do we need this? */
}

&:where([data-colorscheme='cyan'] &) {
--alert-link-color: var(--color-cyan700);
--alert-focus-color: var(--color-cyan700);
}

&:where([data-colorscheme='green'] &) {
--alert-link-color: var(--color-green700);
--alert-focus-color: var(--color-green700);
}

&:where([data-colorscheme='gold'] &) {
--alert-link-color: var(--color-gold700);
--alert-focus-color: var(--color-gold700);
}

&:where([data-colorscheme='red'] &) {
--alert-link-color: var(--color-red700);
--alert-focus-color: var(--color-red700);
}
}

.uw-AlertButton {
color: var(--alert-button-color);

@media (hover: hover) {
&:where(:hover) {
color: var(--alert-button-color-hover);
}
}
}

.uw-AlertTitle {
color: var(--alert-text-color);
}
34 changes: 34 additions & 0 deletions packages/react/src/components/Alert/Alert.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ComponentPropsWithout, RemovedProps } from '../../types/component-props';

export interface AlertProps extends ComponentPropsWithout<'div', RemovedProps> {
/**
* Sets the colour scheme.
* @default cyan
*/
colorScheme?: 'cyan' | 'green' | 'gold' | 'red';
/**
* Sets the function to be called when the alert is closed.
*/
onClose?: () => void;
/**
* Sets the direction of the alert content.
* @default column
*/
direction?: 'row' | 'column';
/**
* Sets the title of the alert.
*/
title?: string;
/**
* Sets the text of the alert.
*/
text?: React.ReactNode;
/**
* Sets the link text of the alert.
*/
linkText?: string;
/**
* Sets the link href of the alert.
*/
linkHref?: string;
}
130 changes: 130 additions & 0 deletions packages/react/src/components/Alert/Alert.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as React from 'react';

import type { Meta, StoryObj } from '@storybook/react';

import { Alert } from './Alert';
import { Flex } from '../Flex/Flex';
import { Button } from '../Button/Button';
import { AlertText } from './AlertText';
import { Strong } from '../Strong/Strong';

const colorSchemes = ['cyan', 'red', 'green', 'gold'] as const;

const meta: Meta<typeof Alert> = {
title: 'Stories / Alert',
component: Alert,
argTypes: {
children: { control: { type: 'text' } },
colorScheme: { options: colorSchemes, control: { type: 'radio' } },
direction: { options: ['column', 'row'], control: { type: 'radio' } },
},
args: {
title: 'Did you know?',
text: 'The quick brown fox jumps over the lazy dog.',
linkText: 'Learn more',
linkHref: '#',
},
};

export default meta;
type Story = StoryObj<typeof Alert>;

export const KitchenSink: Story = {
parameters: { controls: { hideNoControlsWarning: true } },
render: args => {
return (
<Flex direction="column" gap="800" width="800px" padding="400">
{colorSchemes.map(colorScheme => (
<Flex key={colorScheme} direction="column" gap="200">
<Alert {...args} colorScheme={colorScheme} direction="row" />
<Alert {...args} colorScheme={colorScheme} direction="column" />
<Alert
{...args}
colorScheme={colorScheme}
direction="row"
onClose={() => alert('closed')}
/>
<Alert
{...args}
colorScheme={colorScheme}
direction="column"
onClose={() => alert('closed')}
/>
<Alert {...args} colorScheme={colorScheme} direction="row" linkText={undefined} />
<Alert {...args} colorScheme={colorScheme} direction="column" linkText={undefined} />
</Flex>
))}
</Flex>
);
},
};

export const Workshop: Story = {};

export const ToggleAlert: Story = {
parameters: { layout: 'none' },
render: () => {
const [open, setOpen] = React.useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);

return (
<Flex direction="column" align="start" gap="200" width="fit-content" padding="400">
<Button size="small" variant="outline" onClick={handleOpen}>
Open alert
</Button>
{open ? (
<Alert direction="row" text="This is for your information." onClose={handleClose} />
) : null}
</Flex>
);
},
};

export const AlertColorSchemes: Story = {
name: 'Alert ColorSchemes',
render: () => {
return (
<Flex direction="column" gap="200" width="fit-content">
<Alert
colorScheme="cyan"
direction="row"
text="Cyan colour scheme for informational messages"
/>
<Alert
colorScheme="green"
direction="row"
text="Green colour scheme for positive or success messages"
/>
<Alert colorScheme="gold" direction="row" text="Gold colour scheme for warning messages" />
<Alert
colorScheme="red"
direction="row"
text="Red colour scheme for errors and higher warnings"
/>
</Flex>
);
},
};

export const AlertAdvancedUsage: Story = {
render: () => {
return (
<Flex align="start">
<Alert
colorScheme="red"
title="Delete account"
text={
<AlertText>
Are you <Strong>sure</Strong> you want to do this?
</AlertText>
}
linkHref="#"
linkText="Delete"
onClose={() => {}}
direction="row"
/>
</Flex>
);
},
};
115 changes: 115 additions & 0 deletions packages/react/src/components/Alert/Alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as React from 'react';

import clsx from 'clsx';

import {
ChevronRightMediumIcon,
CloseMediumIcon,
InformationMediumContainedIcon,
TickMediumContainedIcon,
WarningMediumContainedIcon,
} from '@utilitywarehouse/react-icons';

import { AlertProps } from './Alert.props';
import { AlertLink } from './AlertLink';
import { AlertText } from './AlertText';
import { AlertTitle } from './AlertTitle';
import { withGlobalPrefix } from '../../helpers/with-global-prefix';
import { ElementRef } from 'react';
import { DATA_ATTRIBUTES } from '../../helpers/data-attributes';
import { Flex } from '../Flex/Flex';
import { IconButton } from '../IconButton/IconButton';

const componentName = 'Alert';
const componentClassName = withGlobalPrefix(componentName);

type AlertElement = ElementRef<'div'>;

/**
* Provide feedback messages to users. Alerts are dynamic content that is
* injected into the page when it changes and should be used sparingly.
*/
export const Alert = React.forwardRef<AlertElement, AlertProps>(
(
{
className,
colorScheme = 'cyan',
direction = 'column',
children,
onClose,
title,
text,
linkText,
linkHref,
...props
},
ref
) => {
const icons = {
cyan: InformationMediumContainedIcon,
green: TickMediumContainedIcon,
gold: WarningMediumContainedIcon,
red: WarningMediumContainedIcon,
};
const AlertIcon = icons[colorScheme];
const dataAttributeProps = {
[DATA_ATTRIBUTES.colorscheme]: colorScheme,
'data-direction': direction,
};
return (
<Flex
align={direction === 'row' ? 'center' : 'start'}
ref={ref}
className={clsx(componentClassName, className)}
role="alert" // Adding role for dynamic alerts
aria-live="assertive" // Making it announced immediately
aria-atomic="true" // Ensuring the entire alert is read as a whole
{...dataAttributeProps}
{...props}
>
<AlertIcon />

<Flex
direction={direction}
gap="100"
style={{ flex: 1 }}
align={direction === 'row' ? 'center' : 'start'}
>
{children ?? (
<>
{title ? <AlertTitle>{title}</AlertTitle> : null}
{text ? <AlertText>{text}</AlertText> : null}
{linkText && linkHref ? <AlertLink href={linkHref}>{linkText}</AlertLink> : null}
</>
)}
</Flex>
{linkHref && !linkText ? (
<IconButton
variant="ghost"
colorScheme={colorScheme}
asChild
title="Alert action"
label="Alert action"
>
<a href={linkHref}>
<ChevronRightMediumIcon />
</a>
</IconButton>
) : null}
{onClose ? (
<IconButton
variant="ghost"
colorScheme={colorScheme}
onClick={onClose}
title="Close"
label="Close alert"
>
<CloseMediumIcon />
</IconButton>
) : null}
</Flex>
);
}
);

Alert.displayName = componentName;
Loading

0 comments on commit 80bc685

Please sign in to comment.