|
1 | 1 | import React, { |
2 | 2 | forwardRef, |
3 | 3 | useContext, |
4 | | - useImperativeHandle, |
5 | 4 | useRef, |
6 | 5 | useState, |
| 6 | + useEffect, |
| 7 | + useCallback, |
7 | 8 | } from 'react'; |
| 9 | +import type {PropsWithChildren} from 'react'; |
| 10 | +import type {ColorTextAlias} from '@shopify/polaris-tokens'; |
| 11 | +import {CancelMinor} from '@shopify/polaris-icons'; |
8 | 12 |
|
9 | | -import {classNames} from '../../utilities/css'; |
10 | | -import {BannerContext} from '../../utilities/banner-context'; |
11 | 13 | 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'; |
12 | 23 | import type {IconProps} from '../Icon'; |
| 24 | +import {BannerContext} from '../../utilities/banner-context'; |
13 | 25 | 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'; |
14 | 30 |
|
15 | 31 | import styles from './Banner.scss'; |
16 | | -import {BannerExperimental} from './components'; |
| 32 | +import type {BannerHandles} from './utilities'; |
| 33 | +import {bannerAttributes, useBannerFocus} from './utilities'; |
17 | 34 |
|
18 | 35 | export type BannerStatus = 'success' | 'info' | 'warning' | 'critical'; |
19 | 36 |
|
@@ -67,48 +84,249 @@ export const Banner = forwardRef<BannerHandles, BannerProps>(function Banner( |
67 | 84 | onKeyUp={handleKeyUp} |
68 | 85 | onBlur={handleBlur} |
69 | 86 | > |
70 | | - <BannerExperimental {...props} /> |
| 87 | + <BannerLayout {...props} /> |
71 | 88 | </div> |
72 | 89 | </BannerContext.Provider> |
73 | 90 | ); |
74 | 91 | }); |
75 | 92 |
|
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; |
78 | 100 | } |
79 | 101 |
|
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 | + ]; |
94 | 122 |
|
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, |
99 | 168 | }; |
100 | 169 |
|
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 | + } |
106 | 177 |
|
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 | + ); |
114 | 332 | } |
0 commit comments