Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ninety-crabs-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/polaris': minor
---

Refactored `Toast`, `ContextualSaveBar`, and `Banner` components to use `Box`
61 changes: 33 additions & 28 deletions polaris-react/src/components/Banner/Banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -84,38 +85,39 @@ export const Banner = forwardRef<BannerHandles, BannerProps>(function Banner(
if (title) {
headingID = `${id}Heading`;
headingMarkup = (
<div className={styles.Heading} id={headingID}>
<Box className={styles.Heading} id={headingID}>
<Heading element="p">{title}</Heading>
</div>
</Box>
);
}

const spinnerMarkup = action?.loading ? (
<button
<Box
as="button"
disabled
aria-busy
ariaBusy
className={classNames(styles.Button, styles.loading)}
>
<span className={styles.Spinner}>
<Box as="span" className={styles.Spinner}>
<Spinner
size="small"
accessibilityLabel={i18n.translate(
'Polaris.Button.spinnerAccessibilityLabel',
)}
/>
</span>
</Box>
{action.content}
</button>
</Box>
) : null;

const primaryActionMarkup = action ? (
<div className={styles.PrimaryAction}>
<Box className={styles.PrimaryAction}>
{action.loading
? spinnerMarkup
: unstyledButtonFrom(action, {
className: styles.Button,
})}
</div>
</Box>
) : null;

const secondaryActionMarkup = secondaryAction ? (
Expand All @@ -124,12 +126,12 @@ export const Banner = forwardRef<BannerHandles, BannerProps>(function Banner(

const actionMarkup =
action || secondaryAction ? (
<div className={styles.Actions}>
<Box className={styles.Actions}>
<ButtonGroup>
{primaryActionMarkup}
{secondaryActionMarkup}
</ButtonGroup>
</div>
</Box>
) : null;

let contentMarkup: React.ReactNode = null;
Expand All @@ -138,50 +140,49 @@ export const Banner = forwardRef<BannerHandles, BannerProps>(function Banner(
if (children || actionMarkup) {
contentID = `${id}Content`;
contentMarkup = (
<div className={styles.Content} id={contentID}>
<Box className={styles.Content} id={contentID}>
{children}
{actionMarkup}
</div>
</Box>
);
}

const dismissButton = onDismiss && (
<div className={styles.Dismiss}>
<Box className={styles.Dismiss}>
<Button
plain
icon={CancelSmallMinor}
onClick={onDismiss}
accessibilityLabel="Dismiss notification"
/>
</div>
</Box>
);

return (
<BannerContext.Provider value>
<div
<Box
className={className}
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
ref={wrapperRef}
role={ariaRoleType}
aria-live={stopAnnouncements ? 'off' : 'polite'}
ariaRoleType={ariaRoleType}
ariaLive={stopAnnouncements ? 'off' : 'polite'}
ariaLabelledBy={headingID}
ariaDescribedBy={contentID}
onMouseUp={handleMouseUp}
onKeyUp={handleKeyUp}
onBlur={handleBlur}
aria-labelledby={headingID}
aria-describedby={contentID}
>
{dismissButton}

<div className={styles.Ribbon}>
<Box className={styles.Ribbon}>
<Icon source={iconName} color={iconColor} />
</div>
</Box>

<div className={styles.ContentWrapper}>
<Box className={styles.ContentWrapper}>
{headingMarkup}
{contentMarkup}
</div>
</div>
</Box>
</Box>
</BannerContext.Provider>
);
});
Expand All @@ -194,7 +195,9 @@ function SecondaryActionFrom({action}: {action: Action}) {
url={action.url}
external={action.external}
>
<span className={styles.Text}>{action.content}</span>
<Box as="span" className={styles.Text}>
{action.content}
</Box>
</UnstyledLink>
);
}
Expand All @@ -204,7 +207,9 @@ function SecondaryActionFrom({action}: {action: Action}) {
className={styles.SecondaryAction}
onClick={action.onAction}
>
<span className={styles.Text}>{action.content}</span>
<Box as="span" className={styles.Text}>
{action.content}
</Box>
</UnstyledButton>
);
}
Expand Down
17 changes: 9 additions & 8 deletions polaris-react/src/components/Banner/tests/Banner.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, {useEffect, useRef} from 'react';
import type {Element as ElementType} from '@shopify/react-testing';
import {
CirclePlusMinor,
CircleTickMajor,
Expand Down Expand Up @@ -262,32 +263,32 @@ describe('<Banner />', () => {
it('adds a keyFocused class to the banner on keyUp', () => {
const banner = mountWithApp(<Banner />);

const bannerDiv = banner.find('div', {
className: 'Banner withinPage',
});
const bannerDiv = banner.findWhere((el: any) =>
el.prop('className')?.includes('Banner withinPage'),
) as ElementType<any>;

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(<Banner />);

const bannerDiv = banner.find('div', {
className: 'Banner withinPage',
});
const bannerDiv = banner.findWhere((el: any) =>
el.prop('className')?.includes('Banner withinPage'),
) as ElementType<any>;

bannerDiv!.trigger('onMouseUp', {
currentTarget: bannerDiv!.domNode as HTMLDivElement,
});

expect(banner).toContainReactComponent('div', {
className: 'Banner withinPage',
className: expect.stringContaining('Banner withinPage'),
});
});
});
Expand Down
92 changes: 71 additions & 21 deletions polaris-react/src/components/Box/Box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand All @@ -20,18 +23,15 @@ type BackgroundColorTokenScale = Extract<
type DepthTokenGroup = typeof depth;
type DepthTokenName = keyof DepthTokenGroup;
type ShadowsTokenName = Exclude<DepthTokenName, `shadows-${string}`>;

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}`
Expand Down Expand Up @@ -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;
Expand All @@ -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 */
Expand All @@ -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<HTMLElement>): void;
/** Callback triggered on click */
onClick?(event: React.MouseEvent<HTMLElement>): void;
/** Callback triggered on key up */
onKeyUp?(event: React.KeyboardEvent<HTMLElement>): void;
/** Callback triggered on mouse up */
onMouseUp?(event: React.MouseEvent<HTMLElement>): void;
}

export const Box = forwardRef<HTMLElement, BoxProps>(
(
{
ariaBusy,
ariaDescribedBy,
ariaLabelledBy,
ariaLive,
ariaRoleType,
as = 'div',
background,
border,
Expand All @@ -138,7 +168,10 @@ export const Box = forwardRef<HTMLElement, BoxProps>(
borderRadiusBottomRight,
borderRadiusTopLeft,
borderRadiusTopRight,
className,
children,
disabled = false,
id,
margin,
marginBottom,
marginLeft,
Expand All @@ -150,6 +183,11 @@ export const Box = forwardRef<HTMLElement, BoxProps>(
paddingRight,
paddingTop,
shadow,
tabIndex,
onBlur,
onClick,
onKeyUp,
onMouseUp,
},
ref,
) => {
Expand Down Expand Up @@ -250,14 +288,26 @@ export const Box = forwardRef<HTMLElement, BoxProps>(
: 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,
);
Expand Down
Loading