From 80bc6851cde276b74323f75dd170b79717c12e8e Mon Sep 17 00:00:00 2001 From: Rob Phoenix <9257284+robphoenix@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:24:21 +0000 Subject: [PATCH] Add Alert (#653) --- packages/react/src/components/Alert/Alert.css | 103 ++++++++++++++ .../react/src/components/Alert/Alert.props.ts | 34 +++++ .../src/components/Alert/Alert.stories.tsx | 130 ++++++++++++++++++ packages/react/src/components/Alert/Alert.tsx | 115 ++++++++++++++++ .../react/src/components/Alert/AlertLink.tsx | 25 ++++ .../react/src/components/Alert/AlertText.tsx | 31 +++++ .../react/src/components/Alert/AlertTitle.tsx | 33 +++++ packages/react/src/components/index.css | 1 + 8 files changed, 472 insertions(+) create mode 100644 packages/react/src/components/Alert/Alert.css create mode 100644 packages/react/src/components/Alert/Alert.props.ts create mode 100644 packages/react/src/components/Alert/Alert.stories.tsx create mode 100644 packages/react/src/components/Alert/Alert.tsx create mode 100644 packages/react/src/components/Alert/AlertLink.tsx create mode 100644 packages/react/src/components/Alert/AlertText.tsx create mode 100644 packages/react/src/components/Alert/AlertTitle.tsx diff --git a/packages/react/src/components/Alert/Alert.css b/packages/react/src/components/Alert/Alert.css new file mode 100644 index 000000000..012fc3b05 --- /dev/null +++ b/packages/react/src/components/Alert/Alert.css @@ -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); +} diff --git a/packages/react/src/components/Alert/Alert.props.ts b/packages/react/src/components/Alert/Alert.props.ts new file mode 100644 index 000000000..7f392d1ec --- /dev/null +++ b/packages/react/src/components/Alert/Alert.props.ts @@ -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; +} diff --git a/packages/react/src/components/Alert/Alert.stories.tsx b/packages/react/src/components/Alert/Alert.stories.tsx new file mode 100644 index 000000000..4d02c4255 --- /dev/null +++ b/packages/react/src/components/Alert/Alert.stories.tsx @@ -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 = { + 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; + +export const KitchenSink: Story = { + parameters: { controls: { hideNoControlsWarning: true } }, + render: args => { + return ( + + {colorSchemes.map(colorScheme => ( + + + + alert('closed')} + /> + alert('closed')} + /> + + + + ))} + + ); + }, +}; + +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 ( + + + {open ? ( + + ) : null} + + ); + }, +}; + +export const AlertColorSchemes: Story = { + name: 'Alert ColorSchemes', + render: () => { + return ( + + + + + + + ); + }, +}; + +export const AlertAdvancedUsage: Story = { + render: () => { + return ( + + + Are you sure you want to do this? + + } + linkHref="#" + linkText="Delete" + onClose={() => {}} + direction="row" + /> + + ); + }, +}; diff --git a/packages/react/src/components/Alert/Alert.tsx b/packages/react/src/components/Alert/Alert.tsx new file mode 100644 index 000000000..6be0ce401 --- /dev/null +++ b/packages/react/src/components/Alert/Alert.tsx @@ -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( + ( + { + 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 ( + + + + + {children ?? ( + <> + {title ? {title} : null} + {text ? {text} : null} + {linkText && linkHref ? {linkText} : null} + + )} + + {linkHref && !linkText ? ( + + + + + + ) : null} + {onClose ? ( + + + + ) : null} + + ); + } +); + +Alert.displayName = componentName; diff --git a/packages/react/src/components/Alert/AlertLink.tsx b/packages/react/src/components/Alert/AlertLink.tsx new file mode 100644 index 000000000..25667d487 --- /dev/null +++ b/packages/react/src/components/Alert/AlertLink.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import clsx from 'clsx'; + +import { withGlobalPrefix } from '../../helpers/with-global-prefix'; +import type { ElementRef } from 'react'; +import type { TextLinkProps } from '../TextLink/TextLink.props'; +import { TextLink } from '../TextLink/TextLink'; + +const componentName = 'AlertLink'; +const componentClassName = withGlobalPrefix(componentName); + +type AlertLinkElement = ElementRef<'a'>; +type AlertLinkProps = TextLinkProps; + +/** + * An `AlertLink` is a component that is used to display the link of an `Alert`. + */ +export const AlertLink = React.forwardRef( + ({ className, ...props }, ref) => ( + + ) +); + +AlertLink.displayName = componentName; diff --git a/packages/react/src/components/Alert/AlertText.tsx b/packages/react/src/components/Alert/AlertText.tsx new file mode 100644 index 000000000..475f44af5 --- /dev/null +++ b/packages/react/src/components/Alert/AlertText.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; + +import clsx from 'clsx'; +import { withGlobalPrefix } from '../../helpers/with-global-prefix'; +import type { ElementRef } from 'react'; +import type { BodyTextProps } from '../BodyText/BodyText.props'; +import { BodyText } from '../BodyText/BodyText'; + +const componentName = 'AlertText'; +const componentClassName = withGlobalPrefix(componentName); + +type AlertTextElement = ElementRef<'h2'>; +type AlertTextProps = BodyTextProps; + +/** + * An `AlertText` is a component that is used to display the text of an `Alert`. + */ +export const AlertText = React.forwardRef( + ({ className, ...props }, ref) => ( + + ) +); + +AlertText.displayName = componentName; diff --git a/packages/react/src/components/Alert/AlertTitle.tsx b/packages/react/src/components/Alert/AlertTitle.tsx new file mode 100644 index 000000000..df1eb4606 --- /dev/null +++ b/packages/react/src/components/Alert/AlertTitle.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; + +import clsx from 'clsx'; +import { withGlobalPrefix } from '../../helpers/with-global-prefix'; +import type { ElementRef } from 'react'; +import type { BodyTextProps } from '../BodyText/BodyText.props'; +import { BodyText } from '../BodyText/BodyText'; + +const componentName = 'AlertTitle'; +const componentClassName = withGlobalPrefix(componentName); + +type AlertTitleElement = ElementRef<'h2'>; +type AlertTitleProps = BodyTextProps; + +/** + * An `AlertTitle` is a component that is used to display the title of an `Alert`. + */ +export const AlertTitle = React.forwardRef( + ({ className, children, ...props }, ref) => ( + +

{children}

+
+ ) +); + +AlertTitle.displayName = componentName; diff --git a/packages/react/src/components/index.css b/packages/react/src/components/index.css index 8077ea69e..860ea3678 100644 --- a/packages/react/src/components/index.css +++ b/packages/react/src/components/index.css @@ -24,3 +24,4 @@ @import url('./CheckboxGroup/CheckboxGroup.css'); @import url('./CheckboxTile/CheckboxTile.css'); @import url('./CheckboxGridGroup/CheckboxGridGroup.css'); +@import url('./Alert/Alert.css');