import { Children, ComponentProps, Fragment, ReactElement } from 'react'; import { TOKEN_SPACE_800 } from '@swipebox/morphe-design-tokens'; import styles from './BannerOverlay.css'; import CallToAction from './BannerOverlay/CalltoAction'; import Box from './Box'; import Button from './Button'; import ButtonLink from './ButtonLink'; import { useColorScheme } from './contexts/ColorSchemeProvider'; import { useDefaultLabelContext } from './contexts/DefaultLabelProvider'; import { useDeviceType } from './contexts/DeviceTypeProvider'; import Flex from './Flex'; import Heading from './Heading'; import InternalDismissButton from './sharedSubcomponents/InternalDismissButton'; import { IconThumbnail, ImageThumbnail, Message, } from './sharedSubcomponents/thumbnailSubcomponents'; import Text from './Text'; import useExperimentalTheme from './utils/useExperimentalTheme'; import { Indexable } from './zIndex'; const DEFAULT_COLORS = { lightModeBackground: 'default', darkModeBackground: 'elevationFloating', textColor: 'default', iconColor: 'white', } as const; type Props = { /** * Adds a dismiss button to BannerOverlay. See the [Dismissible variant](https://gestalt.pinterest.systems/web/banneroverlay#Dismissible) for more info. */ onDismiss?: () => void; /** * Main content of BannerOverlay. Content should be [localized](https://gestalt.pinterest.systems/web/banneroverlay#Localization). See the [Text variant](https://gestalt.pinterest.systems/web/banneroverlay#Text) to learn more. */ message: string | ReactElement; /** * Distance (in pixels) from the viewport edge (top will be used, if desktop, bottom will be used, if mobile). See the [Responsive section](https://gestalt.pinterest.systems/web/banneroverlay#Responsive) to learn more. */ offset?: { bottom: number; top: number; reverseOffset?: boolean; }; /** * Adds an optional primary button for user interaction. * Main action for users to take on BannerOverlay. If href is supplied, the action will serve as a link. * If no href is supplied, the action will be a button. * The accessibilityLabel should follow the Accessibility guidelines. * See the Primary action variant to learn more. */ primaryAction?: | { accessibilityLabel: string; href: string; label: string; onClick?: ComponentProps['onClick']; rel?: ComponentProps['rel']; role: 'link'; size?: ComponentProps['size']; target?: ComponentProps['target']; } | { accessibilityLabel: string; label: string; onClick: ComponentProps['onClick']; role?: 'button'; size?: ComponentProps['size']; }; /** * Adds an optional button for user interaction. * In this case, we use our ButtonGroup component. */ secondaryAction?: | { accessibilityLabel: string; href: string; label: string; onClick?: ComponentProps['onClick']; rel?: ComponentProps['rel']; role: 'link'; size?: ComponentProps['size']; target?: ComponentProps['target']; } | { accessibilityLabel: string; label: string; onClick: ComponentProps['onClick']; role?: 'button'; size?: ComponentProps['size']; }; /** * An optional thumbnail to display next to the text. */ thumbnail?: { image: ReactElement; icon?: never } | { image?: never; icon: ReactElement }; /** * Heading of BannerOverlay. Content should be [localized](https://gestalt.pinterest.systems/web/banneroverlay#Localization). See the [Text variant](https://gestalt.pinterest.systems/web/banneroverlay#Text) to learn more. */ title?: string; /** * zIndex of BannerOverlay. See the [zIndex guidelines](https://gestalt.pinterest.systems/web/banneroverlay#zIndex) to learn more. */ zIndex?: Indexable; }; /** * [BannerOverlays](https://gestalt.pinterest.systems/web/banneroverlay) displays short educational messages when users have performed actions that indicate some intent, such as a related pin tap or idea pin swipe. It is scrim-less, meaning users can scroll content underneath without having a wash layer on top of the content. * * BannerOverlay is a sticky component triggered by scroll, on tap or long-press, placed at the bottom of the screen. * ![BannerOverlay light mode](https://raw.githubusercontent.com/pinterest/gestalt/master/playwright/visual-test/BannerOverlay.spec.ts-snapshots/BannerOverlay-chromium-darwin.png) */ export default function BannerOverlay({ message, title, onDismiss, primaryAction, secondaryAction, offset = { top: 0, bottom: 0, reverseOffset: false }, thumbnail, zIndex, }: Props) { const { colorSchemeName } = useColorScheme(); const isDarkMode = colorSchemeName === 'darkMode'; const isMobileDevice = useDeviceType() === 'mobile'; const theme = useExperimentalTheme(); const { accessibilityDismissButtonLabel: accessibilityDismissButtonLabelDefault } = useDefaultLabelContext('BannerOverlay'); let messageTextElement: ReactElement | string | undefined; if (typeof message === 'string') { messageTextElement = message; } // If `message` is a Text component, we need to override any text colors within to ensure they all match const checkTextIsNode = () => { const messageIsTextNode = typeof message !== 'string' && // @ts-expect-error - TS2339 Children.only(message).type.displayName === 'Text'; if (messageIsTextNode) { const textColorOverrideStyles = isDarkMode ? styles.textColorOverrideDark : styles.textColorOverrideLight; messageTextElement = {message}; } return messageIsTextNode; }; const isMessageTextNode = checkTextIsNode(); const getPosition: () => { bottom: number | 'unset'; top: number | 'unset' } = () => { if (!offset.reverseOffset && isMobileDevice) return { bottom: offset.bottom, top: 'unset' }; if (!offset.reverseOffset && !isMobileDevice) return { bottom: 'unset', top: offset.top }; if (offset.reverseOffset && !isMobileDevice) return { bottom: offset.bottom, top: 'unset' }; if (offset.reverseOffset && isMobileDevice) return { bottom: 'unset', top: offset.top }; return { bottom: 0, top: 0 }; }; const checkDisplayName: (arg0: ReactElement, arg1: string) => boolean = ( thumbnailElement, displayName, ) => // @ts-expect-error - TS2339 Children.only(thumbnailElement).type.displayName === displayName; return ( {/* XS BREAKPOINT */} {thumbnail?.image && checkDisplayName(thumbnail.image, 'Image') ? ( ) : null} {thumbnail?.icon && checkDisplayName(thumbnail.icon, 'Icon') ? ( ) : null} {title && !theme.MAIN ? {title} : null} {title && theme.MAIN ? {title} : null} {onDismiss && ( )} {secondaryAction && ( {secondaryAction.role === 'link' ? ( ) : ( )} )} {primaryAction && ( {primaryAction.role === 'link' ? ( ) : ( )} )} {/* SM BREAKPOINT */} {thumbnail?.image && checkDisplayName(thumbnail.image, 'Image') ? ( ) : null} {thumbnail?.icon && checkDisplayName(thumbnail.icon, 'Icon') ? ( ) : null} {title && !theme.MAIN ? {title} : null} {title && theme.MAIN ? {title} : null} {secondaryAction && secondaryAction.role === 'link' ? ( ) : null} {secondaryAction && secondaryAction.role === 'button' ? ( ) : null} {primaryAction && primaryAction.role === 'link' ? ( ) : null} {primaryAction && primaryAction.role === 'button' ? ( ) : null} {!!onDismiss && ( )}{' '} ); } BannerOverlay.displayName = 'BannerOverlay';