Skip to content

Commit 0ee47ff

Browse files
committed
Merge BannerExperimental with Banner
1 parent 25e31a2 commit 0ee47ff

File tree

9 files changed

+348
-356
lines changed

9 files changed

+348
-356
lines changed

polaris-react/src/components/Banner/Banner.scss

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,46 @@
4646
margin-top: var(--p-space-4);
4747
}
4848
}
49+
50+
// stylelint-disable -- Duplicate selectors to bump specificity for button svg color override
51+
// https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity#increasing_specificity_by_duplicating_selector
52+
53+
@mixin recolor-icon($fill-color: null) {
54+
svg,
55+
path {
56+
fill: $fill-color;
57+
}
58+
}
59+
60+
.icon-on-color.icon-on-color.icon-on-color {
61+
@include recolor-icon(var(--p-color-icon-on-color));
62+
}
63+
64+
.icon-success-strong-experimental.icon-success-strong-experimental.icon-success-strong-experimental {
65+
@include recolor-icon(var(--p-color-icon-success-strong-experimental));
66+
}
67+
68+
.text-warning-strong.text-warning-strong.text-warning-strong {
69+
@include recolor-icon(var(--p-color-text-warning-strong));
70+
}
71+
72+
.icon-warning-strong-experimental.icon-warning-strong-experimental.icon-warning-strong-experimental {
73+
@include recolor-icon(var(--p-color-icon-warning-strong-experimental));
74+
}
75+
76+
.icon-critical-strong-experimental.icon-critical-strong-experimental.icon-critical-strong-experimental {
77+
@include recolor-icon(var(--p-color-icon-critical-strong-experimental));
78+
}
79+
80+
.text-info-strong.text-info-strong.text-info-strong {
81+
@include recolor-icon(var(--p-color-text-info-strong));
82+
}
83+
84+
.icon-info-strong-experimental.icon-info-strong-experimental.icon-info-strong-experimental {
85+
@include recolor-icon(var(--p-color-icon-info-strong-experimental));
86+
}
87+
88+
.icon-subdued.icon-subdued.icon-subdued {
89+
@include recolor-icon(var(--p-color-icon-subdued));
90+
}
91+
// stylelint-enable

polaris-react/src/components/Banner/Banner.tsx

Lines changed: 255 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
import React, {
22
forwardRef,
33
useContext,
4-
useImperativeHandle,
54
useRef,
65
useState,
6+
useEffect,
7+
useCallback,
78
} from 'react';
9+
import type {PropsWithChildren} from 'react';
10+
import type {ColorTextAlias} from '@shopify/polaris-tokens';
11+
import {CancelMinor} from '@shopify/polaris-icons';
812

9-
import {classNames} from '../../utilities/css';
10-
import {BannerContext} from '../../utilities/banner-context';
1113
import type {Action, DisableableAction, LoadableAction} from '../../types';
14+
import {Text} from '../Text';
15+
import {VerticalStack} from '../VerticalStack';
16+
import type {HorizontalStackProps} from '../HorizontalStack';
17+
import {HorizontalStack} from '../HorizontalStack';
18+
import type {BoxProps} from '../Box';
19+
import {Box} from '../Box';
20+
import {Button} from '../Button';
21+
import {ButtonGroup} from '../ButtonGroup';
22+
import {Icon} from '../Icon';
1223
import type {IconProps} from '../Icon';
24+
import {BannerContext} from '../../utilities/banner-context';
1325
import {WithinContentContext} from '../../utilities/within-content-context';
26+
import {classNames} from '../../utilities/css';
27+
import {useBreakpoints} from '../../utilities/breakpoints';
28+
import {useI18n} from '../../utilities/i18n';
29+
import {useEventListener} from '../../utilities/use-event-listener';
1430

1531
import styles from './Banner.scss';
16-
import {BannerExperimental} from './components';
32+
import type {BannerHandles} from './utilities';
33+
import {bannerAttributes, useBannerFocus} from './utilities';
1734

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

@@ -67,48 +84,249 @@ export const Banner = forwardRef<BannerHandles, BannerProps>(function Banner(
6784
onKeyUp={handleKeyUp}
6885
onBlur={handleBlur}
6986
>
70-
<BannerExperimental {...props} />
87+
<BannerLayout {...props} />
7188
</div>
7289
</BannerContext.Provider>
7390
);
7491
});
7592

76-
export interface BannerHandles {
77-
focus(): void;
93+
interface BannerLayoutProps {
94+
backgroundColor: BoxProps['background'];
95+
textColor: ColorTextAlias;
96+
bannerTitle: React.ReactNode;
97+
bannerIcon: React.ReactNode;
98+
actionButtons: React.ReactNode;
99+
dismissButton: React.ReactNode;
78100
}
79101

80-
function useBannerFocus(bannerRef: React.Ref<BannerHandles>) {
81-
const wrapperRef = useRef<HTMLDivElement>(null);
82-
const [shouldShowFocus, setShouldShowFocus] = useState(false);
83-
84-
useImperativeHandle(
85-
bannerRef,
86-
() => ({
87-
focus: () => {
88-
wrapperRef.current?.focus();
89-
setShouldShowFocus(true);
90-
},
91-
}),
92-
[],
93-
);
102+
export function BannerLayout({
103+
status = 'info',
104+
icon,
105+
hideIcon,
106+
onDismiss,
107+
action,
108+
secondaryAction,
109+
title,
110+
children,
111+
}: BannerProps) {
112+
const i18n = useI18n();
113+
const withinContentContainer = useContext(WithinContentContext);
114+
const isInlineIconBanner = !title && !withinContentContainer;
115+
const bannerStatus = Object.keys(bannerAttributes).includes(status)
116+
? status
117+
: 'info';
118+
const bannerColors =
119+
bannerAttributes[bannerStatus][
120+
withinContentContainer ? 'withinContentContainer' : 'withinPage'
121+
];
94122

95-
const handleKeyUp = (event: React.KeyboardEvent<HTMLDivElement>) => {
96-
if (event.target === wrapperRef.current) {
97-
setShouldShowFocus(true);
98-
}
123+
const sharedBannerProps: BannerLayoutProps = {
124+
backgroundColor: bannerColors.background,
125+
textColor: bannerColors.text,
126+
bannerTitle: title ? (
127+
<Text as="h2" variant="headingSm" breakWord>
128+
{title}
129+
</Text>
130+
) : null,
131+
bannerIcon: hideIcon ? null : (
132+
<span className={styles[bannerColors.icon]}>
133+
<Icon source={icon ?? bannerAttributes[bannerStatus].icon} />
134+
</span>
135+
),
136+
actionButtons:
137+
action || secondaryAction ? (
138+
<ButtonGroup>
139+
{action && (
140+
<Button onClick={action.onAction} {...action}>
141+
{action.content}
142+
</Button>
143+
)}
144+
{secondaryAction && (
145+
<Button onClick={secondaryAction.onAction} {...secondaryAction}>
146+
{secondaryAction.content}
147+
</Button>
148+
)}
149+
</ButtonGroup>
150+
) : null,
151+
dismissButton: onDismiss ? (
152+
<Button
153+
plain
154+
primary
155+
icon={
156+
<span
157+
className={
158+
styles[isInlineIconBanner ? 'icon-subdued' : bannerColors.icon]
159+
}
160+
>
161+
<Icon source={CancelMinor} />
162+
</span>
163+
}
164+
onClick={onDismiss}
165+
accessibilityLabel={i18n.translate('Polaris.Banner.dismissButton')}
166+
/>
167+
) : null,
99168
};
100169

101-
const handleBlur = () => setShouldShowFocus(false);
102-
const handleMouseUp = (event: React.MouseEvent<HTMLDivElement>) => {
103-
event.currentTarget.blur();
104-
setShouldShowFocus(false);
105-
};
170+
if (withinContentContainer) {
171+
return (
172+
<WithinContentContainerBanner {...sharedBannerProps}>
173+
{children}
174+
</WithinContentContainerBanner>
175+
);
176+
}
106177

107-
return {
108-
wrapperRef,
109-
handleKeyUp,
110-
handleBlur,
111-
handleMouseUp,
112-
shouldShowFocus,
113-
};
178+
if (isInlineIconBanner) {
179+
return (
180+
<InlineIconBanner {...sharedBannerProps}>{children}</InlineIconBanner>
181+
);
182+
}
183+
184+
return <DefaultBanner {...sharedBannerProps}>{children}</DefaultBanner>;
185+
}
186+
187+
export function DefaultBanner({
188+
backgroundColor,
189+
textColor,
190+
bannerTitle,
191+
bannerIcon,
192+
actionButtons,
193+
dismissButton,
194+
children,
195+
}: PropsWithChildren<BannerLayoutProps>) {
196+
const {smUp} = useBreakpoints();
197+
const hasContent = children || actionButtons;
198+
199+
return (
200+
<Box width="100%">
201+
<VerticalStack align="space-between">
202+
<Box
203+
background={backgroundColor}
204+
color={textColor}
205+
borderRadiusStartStart={smUp ? '3' : undefined}
206+
borderRadiusStartEnd={smUp ? '3' : undefined}
207+
borderRadiusEndStart={!hasContent && smUp ? '3' : undefined}
208+
borderRadiusEndEnd={!hasContent && smUp ? '3' : undefined}
209+
padding="3"
210+
>
211+
<HorizontalStack
212+
align="space-between"
213+
blockAlign="center"
214+
gap="2"
215+
wrap={false}
216+
>
217+
<HorizontalStack gap="1" wrap={false}>
218+
{bannerIcon}
219+
{bannerTitle}
220+
</HorizontalStack>
221+
{dismissButton}
222+
</HorizontalStack>
223+
</Box>
224+
{hasContent && (
225+
<Box padding={{xs: '3', md: '4'}} paddingBlockStart="3">
226+
<VerticalStack gap="2">
227+
<div>{children}</div>
228+
{actionButtons}
229+
</VerticalStack>
230+
</Box>
231+
)}
232+
</VerticalStack>
233+
</Box>
234+
);
235+
}
236+
237+
export function InlineIconBanner({
238+
backgroundColor,
239+
bannerIcon,
240+
actionButtons,
241+
dismissButton,
242+
children,
243+
}: PropsWithChildren<Omit<BannerLayoutProps, 'textColor' | 'bannerTitle'>>) {
244+
const [blockAlign, setBlockAlign] =
245+
useState<HorizontalStackProps['blockAlign']>('center');
246+
const contentNode = useRef<HTMLDivElement>(null);
247+
const iconNode = useRef<HTMLDivElement>(null);
248+
249+
const handleResize = useCallback(() => {
250+
const contentHeight = contentNode.current?.offsetHeight;
251+
const iconBoxHeight = iconNode.current?.offsetHeight;
252+
253+
if (!contentHeight || !iconBoxHeight) return;
254+
255+
contentHeight > iconBoxHeight
256+
? setBlockAlign('start')
257+
: setBlockAlign('center');
258+
}, []);
259+
260+
useEffect(() => handleResize(), [handleResize]);
261+
useEventListener('resize', handleResize);
262+
263+
return (
264+
<Box width="100%" padding="3" borderRadius="3">
265+
<HorizontalStack
266+
align="space-between"
267+
blockAlign={blockAlign}
268+
wrap={false}
269+
>
270+
<Box width="100%">
271+
<HorizontalStack gap="2" wrap={false} blockAlign={blockAlign}>
272+
{bannerIcon ? (
273+
<div ref={iconNode}>
274+
<Box background={backgroundColor} borderRadius="2" padding="1">
275+
{bannerIcon}
276+
</Box>
277+
</div>
278+
) : null}
279+
<Box ref={contentNode} width="100%">
280+
<VerticalStack gap="2">
281+
<div>{children}</div>
282+
{actionButtons}
283+
</VerticalStack>
284+
</Box>
285+
</HorizontalStack>
286+
</Box>
287+
{dismissButton}
288+
</HorizontalStack>
289+
</Box>
290+
);
291+
}
292+
293+
export function WithinContentContainerBanner({
294+
backgroundColor,
295+
textColor,
296+
bannerTitle,
297+
bannerIcon,
298+
actionButtons,
299+
dismissButton,
300+
children,
301+
}: PropsWithChildren<BannerLayoutProps>) {
302+
return (
303+
<Box
304+
width="100%"
305+
background={backgroundColor}
306+
padding="2"
307+
borderRadius="2"
308+
color={textColor}
309+
>
310+
<HorizontalStack
311+
align="space-between"
312+
blockAlign="start"
313+
wrap={false}
314+
gap="2"
315+
>
316+
<HorizontalStack gap="1_5-experimental" wrap={false}>
317+
{bannerIcon}
318+
<Box width="100%">
319+
<VerticalStack gap="2">
320+
<VerticalStack gap="05">
321+
{bannerTitle}
322+
<div>{children}</div>
323+
</VerticalStack>
324+
{actionButtons}
325+
</VerticalStack>
326+
</Box>
327+
</HorizontalStack>
328+
{dismissButton}
329+
</HorizontalStack>
330+
</Box>
331+
);
114332
}

0 commit comments

Comments
 (0)