diff --git a/docs/pages/components/Notice.mdx b/docs/pages/components/Notice.mdx new file mode 100644 index 000000000..17dff9348 --- /dev/null +++ b/docs/pages/components/Notice.mdx @@ -0,0 +1,152 @@ +--- +title: Notice +description: A notification banner - similar to a Toast, but allows for more flexibile positioning and control. +source: https://github.com/dequelabs/cauldron/blob/develop/packages/react/src/components/Notice/Notice.tsx +--- + +import { useState } from 'react'; +import { Notice, Icon, Button } from '@deque/cauldron-react'; + +```js +import { Notice } from '@deque/cauldron-react'; +``` + +The `Notice` component is used to display a notification banner. It is similar to the [Toast](./Toast) component, +but can be used in more flexible ways. + +For example, the `Notice` component can be used to display a notification banner within a [Panel](./Panel) component +or anywhere else in the DOM without needing to be absolutely/statically positioned. + +## Examples + +There are two variants of the `Notice` component: `info` and `caution`. The `info` variant is used to display general +information, while the `caution` variant is used to display a warning. Depending on the variant, the background color +and icon will change (can be overwritten). + +### Info + +```jsx example + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam at porttitor + sem. Aliquam erat volutpat. Donec placerat nisl magna, et faucibus arcu + condimentum sed. + +``` + +### Caution + +```jsx example + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam at porttitor + sem. Aliquam erat volutpat. Donec placerat nisl magna, et faucibus arcu + condimentum sed. + +``` + +### Danger + +```jsx example + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam at porttitor + sem. Aliquam erat volutpat. Donec placerat nisl magna, et faucibus arcu + condimentum sed. + +``` + +### Thin + +Rendering a `Notice` that appears as a thin bar can be accomplished by only passing a `title` prop, or only passing a child. + +```jsx example + +``` + +### Notice Title + +```jsx example +Information}> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam at porttitor + sem. Aliquam erat volutpat. Donec placerat nisl magna, et faucibus arcu + condimentum sed. + +``` + +### Customizing Icon + +If you would like to customize the preset icon/type variants, the `icon` prop can be used with any [Cauldron +Icon Type](/components/icon). + +```jsx example + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam at porttitor + sem. Aliquam erat volutpat. Donec placerat nisl magna, et faucibus arcu + condimentum sed. + +``` + +### Dismissable + +```jsx example +function DismissableNotice() { + const [dismissed, setDismissed] = useState(false); + const handleDismiss = () => { + setDismissed(true); + setTimeout(() => setDismissed(false), 2000); + }; + + return dismissed ? null : ( + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam at + porttitor sem. Aliquam erat volutpat. Donec placerat nisl magna, et + faucibus arcu condimentum sed. +

+ +
+ ); +} +``` + +## Props + + + +## Related Components + +- [Toast](./Toast) diff --git a/docs/pages/components/Toast.mdx b/docs/pages/components/Toast.mdx index 1a3d484f6..fd6d6e797 100644 --- a/docs/pages/components/Toast.mdx +++ b/docs/pages/components/Toast.mdx @@ -4,41 +4,35 @@ description: A banner with text positioned at the top of the page. source: https://github.com/dequelabs/cauldron/tree/develop/packages/react/src/components/Toast/index.tsx --- -import { useState } from 'react' -import { Toast, Button } from '@deque/cauldron-react' +import { useState } from 'react'; +import { Toast, Button } from '@deque/cauldron-react'; ```js -import { Toast } from '@deque/cauldron-react' +import { Toast } from '@deque/cauldron-react'; ``` ## Examples - Only a single toast should be rendered at a given moment as additional toasts will stack on top of each other. + Only a single toast should be rendered at a given moment as additional toasts + will stack on top of each other. ### Confirmation ```jsx example function ConfirmationToastExample() { - const [show, setShow] = useState(false) + const [show, setShow] = useState(false); return ( <> - setShow(false)} - > + setShow(false)}> Your toast is ready! - - ) + ); } ``` @@ -46,24 +40,17 @@ function ConfirmationToastExample() { ```jsx example function CautionToastExample() { - const [show, setShow] = useState(false) + const [show, setShow] = useState(false); return ( <> - setShow(false)} - > + setShow(false)}> Your toast is getting toasty... - - ) + ); } ``` @@ -71,24 +58,20 @@ function CautionToastExample() { ```jsx example function ActionNeededToastExample() { - const [show, setShow] = useState(false) + const [show, setShow] = useState(false); return ( <> - + You burnt the toast! Check yourself before you wreck yourself... - + - - ) + ); } ``` @@ -96,24 +79,17 @@ function ActionNeededToastExample() { ```jsx example function InfoToastExample() { - const [show, setShow] = useState(false) + const [show, setShow] = useState(false); return ( <> - setShow(false)} - > + setShow(false)}> It is getting toasty in here! - - ) + ); } ``` @@ -121,24 +97,17 @@ function InfoToastExample() { ```jsx example function ErrorToastExample() { - const [show, setShow] = useState(false) + const [show, setShow] = useState(false); return ( <> - setShow(false)} - > + setShow(false)}> This toast tastes like toast! - - ) + ); } ``` @@ -146,31 +115,25 @@ function ErrorToastExample() { Non-dismissable toasts are shown inline to prevent clipping of content. - ```jsx example function NonDismissibleToastExample() { - const [show, setShow] = useState(true) + const [show, setShow] = useState(true); return ( <> - - This toast is not dismissible by normal methods. - But you can . + + This toast is not dismissible by normal methods. But you can{' '} + + . {!show && ( - )} - ) + ); } ``` @@ -189,13 +152,14 @@ function NonDismissibleToastExample() { name: 'show', type: 'boolean', defaultValue: 'false', - description: 'Whether or not to show the toast.', + description: 'Whether or not to show the toast.' }, { name: 'focus', type: 'boolean', defaultValue: 'true', - description: 'Whether or not to focus the toast. This will also set the toast to `role="alert"` when set to true.' + description: + 'Whether or not to focus the toast. This will also set the toast to `role="alert"` when set to true.' }, { name: 'onDismiss', @@ -221,3 +185,7 @@ function NonDismissibleToastExample() { } ]} /> + +## Related + +- [Notice](./Notice) diff --git a/packages/react/__tests__/src/components/Notice/index.js b/packages/react/__tests__/src/components/Notice/index.js new file mode 100644 index 000000000..a0e309b58 --- /dev/null +++ b/packages/react/__tests__/src/components/Notice/index.js @@ -0,0 +1,95 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import Notice from 'src/components/Notice'; +import axe from '../../../axe'; + +test('handles rendering without errors', done => { + const wrapper = mount(); + expect(wrapper.find('Notice').length).toBe(1); + done(); +}); + +test('should render with defaults when no props passed in', async () => { + const wrapper = mount(child); + + expect(wrapper.find('.Notice').length).toBe(1); +}); +test('should render the correct default icon for a given `type`', async () => { + const cautionNotice = mount(); + + expect(cautionNotice.find('Icon').prop('type')).toBe('caution'); +}); + +test('should return no axe violations', async () => { + const info = mount( + + bar + + ); + + const caution = mount( + + bar + + ); + + const danger = mount( + + bar + + ); + + const infoAxeResults = await axe(info.html()); + const cautionAxeResults = await axe(caution.html()); + const dangerAxeResults = await axe(danger.html()); + expect(infoAxeResults).toHaveNoViolations(); + expect(cautionAxeResults).toHaveNoViolations(); + expect(dangerAxeResults).toHaveNoViolations(); +}); + +test('should return correctly with props passed in', async () => { + const wrapper = mount( + + bar + + ); + + expect(wrapper.find('Notice').length).toBe(1); + expect(wrapper.prop('title')).toBe('foo'); + expect(wrapper.prop('type')).toBe('info'); + expect(wrapper.prop('children')).toBe('bar'); +}); + +test('should render with the correct icon when a valid icon `type` string is passed in', async () => { + const wrapper = mount(); + + expect(wrapper.find('Notice').length).toBe(1); + expect(wrapper.find('Icon').prop('type')).toBe('bolt'); + expect(wrapper.find('.Icon--bolt').length).toBe(1); +}); + +test('should render only a `title` when no children are passed in', async () => { + const wrapper = mount(); + + expect(wrapper.find('Notice').length).toBe(1); + expect(wrapper.find('.Notice__title').contains('foo')).toBeTruthy(); +}); + +test('`title` prop should allow for any valid ContentNode element', async () => { + const wrapper = mount( + foo}> + bar + + ); + + expect(wrapper.find('Notice').length).toBe(1); + expect(wrapper.contains(

foo

)).toBeTruthy(); +}); + +test('should allow a ref to be forwarded', async () => { + const ref = React.createRef(); + const wrapper = mount(); + + expect(wrapper.find('Notice').length).toBe(1); + expect(ref.current).toBeTruthy(); +}); diff --git a/packages/react/src/components/Notice/index.tsx b/packages/react/src/components/Notice/index.tsx new file mode 100644 index 000000000..fb47030c3 --- /dev/null +++ b/packages/react/src/components/Notice/index.tsx @@ -0,0 +1,59 @@ +import React, { forwardRef, ReactNode } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import Icon, { IconType } from '../Icon'; +import { ContentNode } from '../../types'; + +const iconTypeMap = { + caution: 'caution', + danger: 'caution', + info: 'info-circle' +}; + +export interface NoticeProps + extends Omit, 'title'> { + type?: keyof typeof iconTypeMap; + title: ContentNode; + icon?: IconType; + children?: ReactNode; +} + +const Notice = forwardRef( + ( + { type = 'info', title, icon, children, ...otherProps }: NoticeProps, + ref + ) => { + return ( +
+
+ + {title} +
+ {children &&
{children}
} +
+ ); + } +); + +Notice.displayName = 'Notice'; +Notice.propTypes = { + // @ts-expect-error + children: PropTypes.node, + type: PropTypes.oneOf(['caution', 'info', 'danger']), + // @ts-expect-error + title: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.element + ]), + // @ts-expect-error + icon: PropTypes.string +}; + +export default Notice; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 2c516cc1e..6c87ba59a 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -119,6 +119,7 @@ export { ColumnRight, ColumnList } from './components/TwoColumnPanel'; +export { default as Notice } from './components/Notice'; /** * Helpers / Utils diff --git a/packages/styles/index.css b/packages/styles/index.css index 4e5d12c07..4bf7cf0fc 100644 --- a/packages/styles/index.css +++ b/packages/styles/index.css @@ -40,3 +40,4 @@ @import './breadcrumb.css'; @import './two-column-panel.css'; @import './accordion.css'; +@import './notice.css'; diff --git a/packages/styles/notice.css b/packages/styles/notice.css new file mode 100644 index 000000000..0dba2d9fc --- /dev/null +++ b/packages/styles/notice.css @@ -0,0 +1,86 @@ +:root { + --notice-info-color: var(--accent-info-active); + --notice-caution-color: var(--accent-caution); + --notice-danger-color: var(--accent-warning-light); + --notice-text-color: var(--accent-dark); + --notice-title-text-color: var(--accent-dark); + --notice-title-font-weight: var(--font-weight-medium); + --notice-background-color: var(--notice-info-color); + --notice-border-color: var(--accent-dark); + --notice-link-hover-color: var(--accent-medium); + --notice-icon-size: 1.2em; +} + +.Notice--info { + --notice-background-color: var(--notice-info-color); +} + +.Notice--caution { + --notice-background-color: var(--notice-caution-color); +} + +.Notice--danger { + --notice-background-color: var(--notice-danger-color); +} + +.Notice { + display: block; + padding: var(--space-smaller) var(--space-small); + border: 1px solid var(--notice-border-color); + background-color: var(--notice-background-color); + color: var(--notice-text-color); + width: 100%; + font-size: var(--text-size-smaller); +} + +.Notice .Notice__title, +.Notice .Notice__title > :is(h1, h2, h3, h4, h5, h6) { + display: flex; + align-items: center; + font-size: var(--text-size-small); + font-weight: var(--notice-title-font-weight); + margin: 0; + padding: 0; + gap: var(--space-three-quarters); + color: var(--notice-title-text-color); +} + +.Notice .Notice__title + .Notice__content { + margin-top: var(--space-smallest); +} + +.Notice .Icon > svg { + height: var(--notice-icon-size); + width: var(--notice-icon-size); +} + +.Notice button.Link, +.Notice a.Link { + background: transparent; + border: 0; + color: currentColor; +} + +.Notice button.Link, +.Notice a.Link { + color: var(--accent-dark); + font-size: var(--text-size-small); + font-weight: var(--font-weight-light); + text-decoration: underline; +} + +.Notice button.Link:focus, +.Notice a.Link:focus { + outline: 2px solid; + color: var(--gray-90); +} + +.Notice button.Link:hover, +.Notice a.Link:hover { + color: var(--notice-link-hover-color); +} + +.Notice .Notice__content > p:first-of-type { + margin-top: 0; + margin-bottom: var(--space-smallest); +} diff --git a/packages/styles/toast.css b/packages/styles/toast.css index 92089be95..2d5591120 100644 --- a/packages/styles/toast.css +++ b/packages/styles/toast.css @@ -82,6 +82,11 @@ outline-offset: var(--space-quarter); } +.Toast__message button.Link:hover, +.Notice a.Link:hover { + color: var(--accent-medium); +} + .Toast:focus { outline: 0; } diff --git a/packages/styles/variables.css b/packages/styles/variables.css index df184f43c..0252bd6f3 100644 --- a/packages/styles/variables.css +++ b/packages/styles/variables.css @@ -34,6 +34,7 @@ --accent-warning-dark: #b88a00; --accent-info: #6cdaf2; --accent-info-light: #83e4fa; + --accent-info-active: #a7e9f7; --accent-primary: #3c7aae; --accent-primary-active: #316091; --accent-secondary: var(--gray-20);