diff --git a/.changeset/ninety-crabs-thank.md b/.changeset/ninety-crabs-thank.md new file mode 100644 index 00000000000..42cbd62984f --- /dev/null +++ b/.changeset/ninety-crabs-thank.md @@ -0,0 +1,5 @@ +--- +'@shopify/polaris': minor +--- + +Refactored `Toast`, `ContextualSaveBar`, and `Banner` components to use `Box` diff --git a/polaris-react/src/components/Banner/Banner.tsx b/polaris-react/src/components/Banner/Banner.tsx index eb3dc1875d2..50c816d2e05 100644 --- a/polaris-react/src/components/Banner/Banner.tsx +++ b/polaris-react/src/components/Banner/Banner.tsx @@ -18,6 +18,7 @@ import {BannerContext} from '../../utilities/banner-context'; import {useUniqueId} from '../../utilities/unique-id'; import {useI18n} from '../../utilities/i18n'; import type {Action, DisableableAction, LoadableAction} from '../../types'; +import {Box} from '../Box'; import {Button} from '../Button'; import {Heading} from '../Heading'; import {ButtonGroup} from '../ButtonGroup'; @@ -84,38 +85,39 @@ export const Banner = forwardRef(function Banner( if (title) { headingID = `${id}Heading`; headingMarkup = ( -
+ {title} -
+ ); } const spinnerMarkup = action?.loading ? ( - + ) : null; const primaryActionMarkup = action ? ( -
+ {action.loading ? spinnerMarkup : unstyledButtonFrom(action, { className: styles.Button, })} -
+ ) : null; const secondaryActionMarkup = secondaryAction ? ( @@ -124,12 +126,12 @@ export const Banner = forwardRef(function Banner( const actionMarkup = action || secondaryAction ? ( -
+ {primaryActionMarkup} {secondaryActionMarkup} -
+ ) : null; let contentMarkup: React.ReactNode = null; @@ -138,50 +140,49 @@ export const Banner = forwardRef(function Banner( if (children || actionMarkup) { contentID = `${id}Content`; contentMarkup = ( -
+ {children} {actionMarkup} -
+ ); } const dismissButton = onDismiss && ( -
+
+ ); return ( -
{dismissButton} -
+ -
+ -
+ {headingMarkup} {contentMarkup} -
-
+ +
); }); @@ -194,7 +195,9 @@ function SecondaryActionFrom({action}: {action: Action}) { url={action.url} external={action.external} > - {action.content} + + {action.content} + ); } @@ -204,7 +207,9 @@ function SecondaryActionFrom({action}: {action: Action}) { className={styles.SecondaryAction} onClick={action.onAction} > - {action.content} + + {action.content} + ); } diff --git a/polaris-react/src/components/Banner/tests/Banner.test.tsx b/polaris-react/src/components/Banner/tests/Banner.test.tsx index f6cf3e3042b..1c4cd748fb6 100644 --- a/polaris-react/src/components/Banner/tests/Banner.test.tsx +++ b/polaris-react/src/components/Banner/tests/Banner.test.tsx @@ -1,4 +1,5 @@ import React, {useEffect, useRef} from 'react'; +import type {Element as ElementType} from '@shopify/react-testing'; import { CirclePlusMinor, CircleTickMajor, @@ -262,32 +263,32 @@ describe('', () => { it('adds a keyFocused class to the banner on keyUp', () => { const banner = mountWithApp(); - const bannerDiv = banner.find('div', { - className: 'Banner withinPage', - }); + const bannerDiv = banner.findWhere((el: any) => + el.prop('className')?.includes('Banner withinPage'), + ) as ElementType; bannerDiv!.trigger('onKeyUp', { target: bannerDiv!.domNode as HTMLDivElement, }); expect(banner).toContainReactComponent('div', { - className: 'Banner keyFocused withinPage', + className: expect.stringContaining('Banner keyFocused withinPage'), }); }); it('does not add a keyFocused class onMouseUp', () => { const banner = mountWithApp(); - const bannerDiv = banner.find('div', { - className: 'Banner withinPage', - }); + const bannerDiv = banner.findWhere((el: any) => + el.prop('className')?.includes('Banner withinPage'), + ) as ElementType; bannerDiv!.trigger('onMouseUp', { currentTarget: bannerDiv!.domNode as HTMLDivElement, }); expect(banner).toContainReactComponent('div', { - className: 'Banner withinPage', + className: expect.stringContaining('Banner withinPage'), }); }); }); diff --git a/polaris-react/src/components/Box/Box.tsx b/polaris-react/src/components/Box/Box.tsx index f4a022f5c6c..325373218bd 100644 --- a/polaris-react/src/components/Box/Box.tsx +++ b/polaris-react/src/components/Box/Box.tsx @@ -5,6 +5,9 @@ import {classNames, sanitizeCustomProperties} from '../../utilities/css'; import styles from './Box.scss'; +type Element = 'div' | 'span' | 'button'; + +// TODO: Bring logic to extract token values into `polaris-tokens` type ColorsTokenGroup = typeof colors; type ColorsTokenName = keyof ColorsTokenGroup; type BackgroundColorTokenScale = Extract< @@ -20,18 +23,15 @@ type BackgroundColorTokenScale = Extract< type DepthTokenGroup = typeof depth; type DepthTokenName = keyof DepthTokenGroup; type ShadowsTokenName = Exclude; - type DepthTokenScale = ShadowsTokenName extends `shadow-${infer Scale}` ? Scale : never; type ShapeTokenGroup = typeof shape; type ShapeTokenName = keyof ShapeTokenGroup; - type BorderShapeTokenScale = ShapeTokenName extends `border-${infer Scale}` ? Scale : never; - type BorderTokenScale = Exclude< BorderShapeTokenScale, `radius-${string}` | `width-${string}` @@ -60,8 +60,6 @@ interface BorderRadius { type SpacingTokenGroup = typeof spacing; type SpacingTokenName = keyof SpacingTokenGroup; - -// TODO: Bring this logic into tokens type SpacingTokenScale = SpacingTokenName extends `space-${infer Scale}` ? Scale : never; @@ -74,31 +72,48 @@ interface Spacing { } export interface BoxProps { - as?: 'div' | 'span'; - /** Background color of the Box */ + /** Used to indicate the element is being modified */ + ariaBusy?: boolean; + /** Used to identify the ID of the element that describes Box */ + ariaDescribedBy?: string; + /** Used to identify the Id of element used as aria-labelledby */ + ariaLabelledBy?: string; + /** Used to indicate the element will be updated */ + ariaLive?: string; + /** Used to describe the semantic element type */ + ariaRoleType?: string; + /** HTML Element type */ + as?: Element; + /** Background color */ background?: BackgroundColorTokenScale; - /** Border styling of the Box */ + /** Border styling */ border?: BorderTokenScale; - /** Bottom border styling of the Box */ + /** Bottom border styling */ borderBottom?: BorderTokenScale; - /** Left border styling of the Box */ + /** Left border styling */ borderLeft?: BorderTokenScale; - /** Right border styling of the Box */ + /** Right border styling */ borderRight?: BorderTokenScale; - /** Top border styling of the Box */ + /** Top border styling */ borderTop?: BorderTokenScale; - /** Border radius of the Box */ + /** Border radius styling */ borderRadius?: BorderRadiusTokenScale; - /** Bottom left border radius of the Box */ + /** Bottom left border radius styling */ borderRadiusBottomLeft?: BorderRadiusTokenScale; - /** Bottom right border radius of the Box */ + /** Bottom right border radius styling */ borderRadiusBottomRight?: BorderRadiusTokenScale; - /** Top left border radius of the Box */ + /** Top left border radius styling */ borderRadiusTopLeft?: BorderRadiusTokenScale; - /** Top right border radius of the Box */ + /** Top right border radius styling */ borderRadiusTopRight?: BorderRadiusTokenScale; /** Inner content of the Box */ children: ReactNode; + /** A custom class name to apply styles to the Box */ + className?: string; + /** Set disabled state on the Box */ + disabled?: boolean; + /** A unique identifier */ + id?: string; /** Spacing outside of the Box */ margin?: SpacingTokenScale; /** Bottom spacing outside of the Box */ @@ -119,13 +134,28 @@ export interface BoxProps { paddingRight?: SpacingTokenScale; /** Top spacing inside of the Box */ paddingTop?: SpacingTokenScale; - /** Shadow on the Box */ + /** Shadow styling */ shadow?: DepthTokenScale; + /** Used to indicate the element is focusable in sequential order */ + tabIndex?: number; + /** Callback triggered when focus is removed */ + onBlur?(event: React.FocusEvent): void; + /** Callback triggered on click */ + onClick?(event: React.MouseEvent): void; + /** Callback triggered on key up */ + onKeyUp?(event: React.KeyboardEvent): void; + /** Callback triggered on mouse up */ + onMouseUp?(event: React.MouseEvent): void; } export const Box = forwardRef( ( { + ariaBusy, + ariaDescribedBy, + ariaLabelledBy, + ariaLive, + ariaRoleType, as = 'div', background, border, @@ -138,7 +168,10 @@ export const Box = forwardRef( borderRadiusBottomRight, borderRadiusTopLeft, borderRadiusTopRight, + className, children, + disabled = false, + id, margin, marginBottom, marginLeft, @@ -150,6 +183,11 @@ export const Box = forwardRef( paddingRight, paddingTop, shadow, + tabIndex, + onBlur, + onClick, + onKeyUp, + onMouseUp, }, ref, ) => { @@ -250,14 +288,26 @@ export const Box = forwardRef( : undefined), } as React.CSSProperties; - const className = classNames(styles.Box); + const boxClassName = classNames(styles.Box, className && className); return createElement( as, { - className, - style: sanitizeCustomProperties(style), + className: boxClassName, + 'aria-busy': ariaBusy, + 'aria-describedby': ariaDescribedBy, + 'aria-labelledby': ariaLabelledBy, + 'aria-live': ariaLive, + role: ariaRoleType, + disabled, + id, ref, + style: sanitizeCustomProperties(style), + tabIndex, + ...(onBlur ? {onBlur} : undefined), + ...(onClick ? {onClick} : undefined), + ...(onKeyUp ? {onKeyUp} : undefined), + ...(onMouseUp ? {onMouseUp} : undefined), }, children, ); diff --git a/polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.tsx b/polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.tsx index dfde3ab2e02..76e7a0d9b9a 100644 --- a/polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.tsx +++ b/polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.tsx @@ -1,5 +1,6 @@ import React, {useCallback} from 'react'; +import {Box} from '../../../Box'; import {Button} from '../../../Button'; import {Image} from '../../../Image'; import {Stack} from '../../../Stack'; @@ -94,15 +95,14 @@ export function ContextualSaveBar({ ); + const logoClassName = classNames(styles.LogoContainer, width); const logoMarkup = alignContentFlush || contextControl ? null : ( -
- {imageMarkup} -
+ {imageMarkup} ); const contextControlMarkup = contextControl ? ( -
{contextControl}
+ {contextControl} ) : null; const contentsClassName = classNames( @@ -112,20 +112,20 @@ export function ContextualSaveBar({ return ( <> -
+ {contextControlMarkup} {logoMarkup} -
+

{message}

-
+ {secondaryMenu} {discardActionMarkup} {saveActionMarkup} -
-
-
+ + + {discardConfirmationModalMarkup} ); diff --git a/polaris-react/src/components/Frame/components/Toast/Toast.tsx b/polaris-react/src/components/Frame/components/Toast/Toast.tsx index cd18403d5bd..8101b1b284f 100644 --- a/polaris-react/src/components/Frame/components/Toast/Toast.tsx +++ b/polaris-react/src/components/Frame/components/Toast/Toast.tsx @@ -2,6 +2,7 @@ import React, {useEffect} from 'react'; import {MobileCancelMajor} from '@shopify/polaris-icons'; import {classNames} from '../../../../utilities/css'; +import {Box} from '../../../Box'; import {Key} from '../../../../types'; import {Button} from '../../../Button'; import {Icon} from '../../../Icon'; @@ -47,27 +48,27 @@ export function Toast({ }, [action, duration, onDismiss]); const dismissMarkup = ( - + ); const actionMarkup = action ? ( -
+ -
+ ) : null; const className = classNames(styles.Toast, error && styles.error); return ( -
+ {content} {actionMarkup} {dismissMarkup} -
+ ); } diff --git a/polaris-react/src/components/Frame/components/Toast/tests/Toast.test.tsx b/polaris-react/src/components/Frame/components/Toast/tests/Toast.test.tsx index c8aefb19387..edfcfa7a4fd 100644 --- a/polaris-react/src/components/Frame/components/Toast/tests/Toast.test.tsx +++ b/polaris-react/src/components/Frame/components/Toast/tests/Toast.test.tsx @@ -33,7 +33,7 @@ describe('', () => { const message = mountWithApp(); expect(message).toContainReactComponent('div', { - className: 'Toast error', + className: 'Box Toast error', }); }); diff --git a/polaris-react/src/index.ts b/polaris-react/src/index.ts index 893d2403325..3f5f305ec2c 100644 --- a/polaris-react/src/index.ts +++ b/polaris-react/src/index.ts @@ -72,9 +72,6 @@ export type { BannerHandles, } from './components/Banner'; -export {Box} from './components/Box'; -export type {BoxProps} from './components/Box'; - export {Breadcrumbs} from './components/Breadcrumbs'; export type {BreadcrumbsProps} from './components/Breadcrumbs';