Skip to content

Commit

Permalink
Merge BannerExperimental with Banner
Browse files Browse the repository at this point in the history
  • Loading branch information
sophschneider committed Aug 15, 2023
1 parent d8a78e2 commit c3b7e2d
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 354 deletions.
43 changes: 43 additions & 0 deletions polaris-react/src/components/Banner/Banner.scss
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,46 @@
margin-top: var(--p-space-4);
}
}

// stylelint-disable -- duplicate selectors to bump specificity for button svg color override
// https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity#increasing_specificity_by_duplicating_selector

@mixin recolor-icon($fill-color: null) {
svg,
path {
fill: $fill-color;
}
}

.icon-on-color.icon-on-color.icon-on-color {
@include recolor-icon(var(--p-color-icon-on-color));
}

.icon-success-strong-experimental.icon-success-strong-experimental.icon-success-strong-experimental {
@include recolor-icon(var(--p-color-icon-success-strong-experimental));
}

.text-warning-strong.text-warning-strong.text-warning-strong {
@include recolor-icon(var(--p-color-text-warning-strong));
}

.icon-warning-strong-experimental.icon-warning-strong-experimental.icon-warning-strong-experimental {
@include recolor-icon(var(--p-color-icon-warning-strong-experimental));
}

.icon-critical-strong-experimental.icon-critical-strong-experimental.icon-critical-strong-experimental {
@include recolor-icon(var(--p-color-icon-critical-strong-experimental));
}

.text-info-strong.text-info-strong.text-info-strong {
@include recolor-icon(var(--p-color-text-info-strong));
}

.icon-info-strong-experimental.icon-info-strong-experimental.icon-info-strong-experimental {
@include recolor-icon(var(--p-color-icon-info-strong-experimental));
}

.icon-subdued.icon-subdued.icon-subdued {
@include recolor-icon(var(--p-color-icon-subdued));
}
// stylelint-enable
13 changes: 13 additions & 0 deletions polaris-react/src/components/Banner/Banner.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,19 @@ export function All() {
</Text>
}
/>
<Text as="h2" variant="headingMd">
No title with hidden icon
</Text>
<AllBanners
title={undefined}
hideIcon
children={
<Text as="p">
Changing the phone number for this customer will unsubscribe them
from SMS marketing text messages until they provide consent.
</Text>
}
/>
<Text as="h2" variant="headingMd">
Only title
</Text>
Expand Down
292 changes: 255 additions & 37 deletions polaris-react/src/components/Banner/Banner.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
import React, {
forwardRef,
useContext,
useImperativeHandle,
useRef,
useState,
useEffect,
useCallback,
} from 'react';
import type {PropsWithChildren} from 'react';
import type {ColorTextAlias} from '@shopify/polaris-tokens';
import {CancelMinor} from '@shopify/polaris-icons';

import {classNames} from '../../utilities/css';
import {BannerContext} from '../../utilities/banner-context';
import type {Action, DisableableAction, LoadableAction} from '../../types';
import {Text} from '../Text';
import {VerticalStack} from '../VerticalStack';
import type {HorizontalStackProps} from '../HorizontalStack';
import {HorizontalStack} from '../HorizontalStack';
import type {BoxProps} from '../Box';
import {Box} from '../Box';
import {Button} from '../Button';
import {ButtonGroup} from '../ButtonGroup';
import {Icon} from '../Icon';
import type {IconProps} from '../Icon';
import {BannerContext} from '../../utilities/banner-context';
import {WithinContentContext} from '../../utilities/within-content-context';
import {classNames} from '../../utilities/css';
import {useBreakpoints} from '../../utilities/breakpoints';
import {useI18n} from '../../utilities/i18n';
import {useEventListener} from '../../utilities/use-event-listener';

import styles from './Banner.scss';
import {BannerExperimental} from './components';
import type {BannerHandles} from './utilities';
import {bannerAttributes, useBannerFocus} from './utilities';

export type BannerStatus = 'success' | 'info' | 'warning' | 'critical';

Expand Down Expand Up @@ -67,48 +84,249 @@ export const Banner = forwardRef<BannerHandles, BannerProps>(function Banner(
onKeyUp={handleKeyUp}
onBlur={handleBlur}
>
<BannerExperimental {...props} />
<BannerLayout {...props} />
</div>
</BannerContext.Provider>
);
});

export interface BannerHandles {
focus(): void;
interface BannerLayoutProps {
backgroundColor: BoxProps['background'];
textColor: ColorTextAlias;
bannerTitle: React.ReactNode;
bannerIcon: React.ReactNode;
actionButtons: React.ReactNode;
dismissButton: React.ReactNode;
}

function useBannerFocus(bannerRef: React.Ref<BannerHandles>) {
const wrapperRef = useRef<HTMLDivElement>(null);
const [shouldShowFocus, setShouldShowFocus] = useState(false);

useImperativeHandle(
bannerRef,
() => ({
focus: () => {
wrapperRef.current?.focus();
setShouldShowFocus(true);
},
}),
[],
);
export function BannerLayout({
status = 'info',
icon,
hideIcon,
onDismiss,
action,
secondaryAction,
title,
children,
}: BannerProps) {
const i18n = useI18n();
const withinContentContainer = useContext(WithinContentContext);
const isInlineIconBanner = !title && !withinContentContainer;
const bannerStatus = Object.keys(bannerAttributes).includes(status)
? status
: 'info';
const bannerColors =
bannerAttributes[bannerStatus][
withinContentContainer ? 'withinContentContainer' : 'withinPage'
];

const handleKeyUp = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.target === wrapperRef.current) {
setShouldShowFocus(true);
}
const sharedBannerProps: BannerLayoutProps = {
backgroundColor: bannerColors.background,
textColor: bannerColors.text,
bannerTitle: title ? (
<Text as="h2" variant="headingSm" breakWord>
{title}
</Text>
) : null,
bannerIcon: hideIcon ? null : (
<span className={styles[bannerColors.icon]}>
<Icon source={icon ?? bannerAttributes[bannerStatus].icon} />
</span>
),
actionButtons:
action || secondaryAction ? (
<ButtonGroup>
{action && (
<Button onClick={action.onAction} {...action}>
{action.content}
</Button>
)}
{secondaryAction && (
<Button onClick={secondaryAction.onAction} {...secondaryAction}>
{secondaryAction.content}
</Button>
)}
</ButtonGroup>
) : null,
dismissButton: onDismiss ? (
<Button
plain
primary
icon={
<span
className={
styles[isInlineIconBanner ? 'icon-subdued' : bannerColors.icon]
}
>
<Icon source={CancelMinor} />
</span>
}
onClick={onDismiss}
accessibilityLabel={i18n.translate('Polaris.Banner.dismissButton')}
/>
) : null,
};

const handleBlur = () => setShouldShowFocus(false);
const handleMouseUp = (event: React.MouseEvent<HTMLDivElement>) => {
event.currentTarget.blur();
setShouldShowFocus(false);
};
if (withinContentContainer) {
return (
<WithinContentContainerBanner {...sharedBannerProps}>
{children}
</WithinContentContainerBanner>
);
}

return {
wrapperRef,
handleKeyUp,
handleBlur,
handleMouseUp,
shouldShowFocus,
};
if (isInlineIconBanner) {
return (
<InlineIconBanner {...sharedBannerProps}>{children}</InlineIconBanner>
);
}

return <DefaultBanner {...sharedBannerProps}>{children}</DefaultBanner>;
}

export function DefaultBanner({
backgroundColor,
textColor,
bannerTitle,
bannerIcon,
actionButtons,
dismissButton,
children,
}: PropsWithChildren<BannerLayoutProps>) {
const {smUp} = useBreakpoints();
const hasContent = children || actionButtons;

return (
<Box width="100%">
<VerticalStack align="space-between">
<Box
background={backgroundColor}
color={textColor}
borderRadiusStartStart={smUp ? '3' : undefined}
borderRadiusStartEnd={smUp ? '3' : undefined}
borderRadiusEndStart={!hasContent && smUp ? '3' : undefined}
borderRadiusEndEnd={!hasContent && smUp ? '3' : undefined}
padding="3"
>
<HorizontalStack
align="space-between"
blockAlign="center"
gap="2"
wrap={false}
>
<HorizontalStack gap="1" wrap={false}>
{bannerIcon}
{bannerTitle}
</HorizontalStack>
{dismissButton}
</HorizontalStack>
</Box>
{hasContent && (
<Box padding={{xs: '3', md: '4'}} paddingBlockStart="3">
<VerticalStack gap="2">
<div>{children}</div>
{actionButtons}
</VerticalStack>
</Box>
)}
</VerticalStack>
</Box>
);
}

export function InlineIconBanner({
backgroundColor,
bannerIcon,
actionButtons,
dismissButton,
children,
}: PropsWithChildren<Omit<BannerLayoutProps, 'textColor' | 'bannerTitle'>>) {
const [blockAlign, setBlockAlign] =
useState<HorizontalStackProps['blockAlign']>('center');
const contentNode = useRef<HTMLDivElement>(null);
const iconNode = useRef<HTMLDivElement>(null);

const handleResize = useCallback(() => {
const contentHeight = contentNode.current?.offsetHeight;
const iconBoxHeight = iconNode.current?.offsetHeight;

if (!contentHeight || !iconBoxHeight) return;

contentHeight > iconBoxHeight
? setBlockAlign('start')
: setBlockAlign('center');
}, []);

useEffect(() => handleResize(), [handleResize]);
useEventListener('resize', handleResize);

return (
<Box width="100%" padding="3" borderRadius="3">
<HorizontalStack
align="space-between"
blockAlign={blockAlign}
wrap={false}
>
<Box width="100%">
<HorizontalStack gap="2" wrap={false} blockAlign={blockAlign}>
{bannerIcon ? (
<div ref={iconNode}>
<Box background={backgroundColor} borderRadius="2" padding="1">
{bannerIcon}
</Box>
</div>
) : null}
<Box ref={contentNode} width="100%">
<VerticalStack gap="2">
<div>{children}</div>
{actionButtons}
</VerticalStack>
</Box>
</HorizontalStack>
</Box>
{dismissButton}
</HorizontalStack>
</Box>
);
}

export function WithinContentContainerBanner({
backgroundColor,
textColor,
bannerTitle,
bannerIcon,
actionButtons,
dismissButton,
children,
}: PropsWithChildren<BannerLayoutProps>) {
return (
<Box
width="100%"
background={backgroundColor}
padding="2"
borderRadius="2"
color={textColor}
>
<HorizontalStack
align="space-between"
blockAlign="start"
wrap={false}
gap="2"
>
<HorizontalStack gap="1_5-experimental" wrap={false}>
{bannerIcon}
<Box width="100%">
<VerticalStack gap="2">
<VerticalStack gap="05">
{bannerTitle}
<div>{children}</div>
</VerticalStack>
{actionButtons}
</VerticalStack>
</Box>
</HorizontalStack>
{dismissButton}
</HorizontalStack>
</Box>
);
}
Loading

0 comments on commit c3b7e2d

Please sign in to comment.