Merged in chore/SW-3246-move-alert-to-design-system (pull request #2698)
chore(SW-3246): Moved Alert component into design system * chore(SW-3246): Moved Alert component into design system * chore(SW-3246): Optimsed code and imports * chore(SW-3246): Moved type AlertTypeEnum and other to common package Approved-by: Anton Gunnarsson
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import { MaterialIcon } from '../../Icons/MaterialIcon'
|
||||
import { JsonToHtml } from '../../JsonToHtml/JsonToHtml'
|
||||
import { Button } from '../../Button'
|
||||
import SidePeek from '../../SidePeek'
|
||||
|
||||
import styles from './sidepeek.module.css'
|
||||
|
||||
import type { AlertSidepeekProps } from './sidepeek'
|
||||
|
||||
export default function AlertSidepeek({
|
||||
ctaText,
|
||||
sidePeekContent,
|
||||
}: AlertSidepeekProps) {
|
||||
const intl = useIntl()
|
||||
const [sidePeekIsOpen, setSidePeekIsOpen] = useState(false)
|
||||
const { heading, content } = sidePeekContent
|
||||
|
||||
return (
|
||||
<div className={styles.alertSidepeek}>
|
||||
<Button
|
||||
onPress={() => setSidePeekIsOpen(true)}
|
||||
variant="Text"
|
||||
color="Primary"
|
||||
size="Small"
|
||||
wrapping
|
||||
typography="Body/Supporting text (caption)/smBold"
|
||||
>
|
||||
{ctaText}
|
||||
<MaterialIcon icon="chevron_right" size={20} color="CurrentColor" />
|
||||
</Button>
|
||||
|
||||
<SidePeek
|
||||
title={heading}
|
||||
isOpen={sidePeekIsOpen}
|
||||
handleClose={() => setSidePeekIsOpen(false)}
|
||||
closeLabel={intl.formatMessage({
|
||||
defaultMessage: 'Close',
|
||||
})}
|
||||
>
|
||||
<JsonToHtml
|
||||
nodes={content.json.children}
|
||||
embeds={content.embedded_itemsConnection.edges}
|
||||
/>
|
||||
</SidePeek>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.alertSidepeek {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { SidepeekContent } from '@scandic-hotels/common/constants/alert'
|
||||
|
||||
export interface AlertSidepeekProps {
|
||||
ctaText: string
|
||||
sidePeekContent: NonNullable<SidepeekContent>
|
||||
}
|
||||
115
packages/design-system/lib/components/Alert/alert.module.css
Normal file
115
packages/design-system/lib/components/Alert/alert.module.css
Normal file
@@ -0,0 +1,115 @@
|
||||
.alert {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
max-width: var(--max-width-page);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.innerContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x2) 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.textWrapper {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.alert .closeButton {
|
||||
padding: var(--Spacing-x-one-and-half);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Intent: inline */
|
||||
.inline {
|
||||
border-radius: var(--Corner-radius-lg);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
}
|
||||
.inline .innerContent {
|
||||
padding-right: var(--Spacing-x3);
|
||||
}
|
||||
.inline .iconWrapper {
|
||||
padding: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
.inline.alarm .iconWrapper {
|
||||
background-color: var(--Surface-Feedback-Error-Accent);
|
||||
}
|
||||
.inline.warning .iconWrapper {
|
||||
background-color: var(--Surface-Feedback-Warning-Accent);
|
||||
}
|
||||
.inline.info .iconWrapper {
|
||||
background-color: var(--Surface-Feedback-Information-Accent);
|
||||
}
|
||||
.inline.success .iconWrapper {
|
||||
background-color: var(--Surface-Feedback-Succes-Accent);
|
||||
}
|
||||
.inline .icon,
|
||||
.inline .icon * {
|
||||
fill: var(--Base-Surface-Primary-light-Normal);
|
||||
}
|
||||
|
||||
/* Intent: banner */
|
||||
.banner {
|
||||
border-left-width: 6px;
|
||||
border-left-style: solid;
|
||||
}
|
||||
.banner.alarm {
|
||||
border-left-color: var(--Surface-Feedback-Error-Accent);
|
||||
background-color: var(--Scandic-Red-00);
|
||||
}
|
||||
.banner.warning {
|
||||
border-left-color: var(--Surface-Feedback-Warning-Accent);
|
||||
background-color: var(--Scandic-Yellow-00);
|
||||
}
|
||||
.banner.info {
|
||||
border-left-color: var(--Surface-Feedback-Information-Accent);
|
||||
background-color: var(--Scandic-Blue-00);
|
||||
}
|
||||
.banner.success {
|
||||
border-left-color: var(--Surface-Feedback-Succes-Accent);
|
||||
background-color: var(--Scandic-Green-00);
|
||||
}
|
||||
.banner.alarm .icon,
|
||||
.banner.alarm .icon * {
|
||||
fill: var(--Surface-Feedback-Error-Accent);
|
||||
}
|
||||
.banner.warning .icon,
|
||||
.banner.warning .icon * {
|
||||
fill: var(--Surface-Feedback-Warning-Accent);
|
||||
}
|
||||
.banner.info .icon,
|
||||
.banner.info .icon * {
|
||||
fill: var(--Surface-Feedback-Information-Accent);
|
||||
}
|
||||
|
||||
.banner.success .icon,
|
||||
.banner.sucess .icon * {
|
||||
fill: var(--Surface-Feedback-Succes-Accent);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.innerContent {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
}
|
||||
30
packages/design-system/lib/components/Alert/alert.ts
Normal file
30
packages/design-system/lib/components/Alert/alert.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type {
|
||||
AlertTypeEnum,
|
||||
SidepeekContent,
|
||||
} from '@scandic-hotels/common/constants/alert'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import type { AriaRole } from 'react'
|
||||
|
||||
import type { alertVariants } from './variants'
|
||||
|
||||
export interface AlertProps extends VariantProps<typeof alertVariants> {
|
||||
className?: string
|
||||
type: AlertTypeEnum
|
||||
heading?: string | null
|
||||
text?: string | null
|
||||
phoneContact?: {
|
||||
displayText: string
|
||||
phoneNumber?: string
|
||||
footnote?: string | null
|
||||
} | null
|
||||
sidepeekContent?: SidepeekContent | null
|
||||
sidepeekCtaText?: string | null
|
||||
link?: {
|
||||
url: string
|
||||
title: string
|
||||
keepSearchParams?: boolean
|
||||
} | null
|
||||
close?: () => void
|
||||
ariaRole?: AriaRole
|
||||
ariaLive?: 'off' | 'assertive' | 'polite'
|
||||
}
|
||||
105
packages/design-system/lib/components/Alert/index.tsx
Normal file
105
packages/design-system/lib/components/Alert/index.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '../Button'
|
||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
import Link from '../Link'
|
||||
import { Typography } from '../Typography'
|
||||
|
||||
import AlertSidepeek from './Sidepeek'
|
||||
import { IconByAlertType } from './utils'
|
||||
import { alertVariants } from './variants'
|
||||
|
||||
import styles from './alert.module.css'
|
||||
|
||||
import type { AlertProps } from './alert'
|
||||
|
||||
export function Alert({
|
||||
className,
|
||||
variant,
|
||||
type,
|
||||
heading,
|
||||
text,
|
||||
link,
|
||||
close,
|
||||
phoneContact,
|
||||
sidepeekCtaText,
|
||||
sidepeekContent,
|
||||
ariaLive,
|
||||
ariaRole,
|
||||
}: AlertProps) {
|
||||
const classNames = alertVariants({
|
||||
className,
|
||||
variant,
|
||||
type,
|
||||
})
|
||||
|
||||
if (!text && !heading) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<section className={classNames} role={ariaRole} aria-live={ariaLive}>
|
||||
<div className={styles.content}>
|
||||
<span className={styles.iconWrapper}>
|
||||
<IconByAlertType
|
||||
alertType={type}
|
||||
className={styles.icon}
|
||||
variant={variant}
|
||||
/>
|
||||
</span>
|
||||
<div className={styles.innerContent}>
|
||||
<div className={styles.textWrapper}>
|
||||
{heading ? (
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<h2>{heading}</h2>
|
||||
</Typography>
|
||||
) : null}
|
||||
{text ? (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{text}
|
||||
{phoneContact?.phoneNumber ? (
|
||||
<>
|
||||
<span> {phoneContact.displayText} </span>
|
||||
<Link
|
||||
href={`tel:${phoneContact.phoneNumber.replace(/ /g, '')}`}
|
||||
>
|
||||
{phoneContact.phoneNumber}
|
||||
</Link>
|
||||
{phoneContact.footnote ? (
|
||||
<>
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<span>. ({phoneContact.footnote})</span>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</p>
|
||||
</Typography>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{link ? (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
href={link.url}
|
||||
keepSearchParams={link.keepSearchParams}
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
) : null}
|
||||
{!link && sidepeekCtaText && sidepeekContent ? (
|
||||
<AlertSidepeek
|
||||
ctaText={sidepeekCtaText}
|
||||
sidePeekContent={sidepeekContent}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{close ? (
|
||||
<Button onPress={close} variant="Text" className={styles.closeButton}>
|
||||
<MaterialIcon icon="close" color="CurrentColor" />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
66
packages/design-system/lib/components/Alert/utils.tsx
Normal file
66
packages/design-system/lib/components/Alert/utils.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
MaterialIcon,
|
||||
type MaterialIconSetIconProps,
|
||||
} from '../Icons/MaterialIcon'
|
||||
import { AlertTypeEnum } from '@scandic-hotels/common/constants/alert'
|
||||
|
||||
import type { JSX } from 'react'
|
||||
|
||||
import type { AlertProps } from './alert'
|
||||
|
||||
interface IconByAlertProps {
|
||||
alertType: AlertTypeEnum
|
||||
variant?: AlertProps['variant']
|
||||
}
|
||||
|
||||
export function IconByAlertType({
|
||||
alertType,
|
||||
variant = 'inline',
|
||||
...props
|
||||
}: IconByAlertProps & MaterialIconSetIconProps): JSX.Element {
|
||||
switch (alertType) {
|
||||
case AlertTypeEnum.Alarm:
|
||||
return (
|
||||
<MaterialIcon
|
||||
color={variant === 'inline' ? 'Icon/Inverted' : 'Icon/Feedback/Error'}
|
||||
isFilled
|
||||
icon="error"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
case AlertTypeEnum.Warning:
|
||||
return (
|
||||
<MaterialIcon
|
||||
icon="warning"
|
||||
color={
|
||||
variant === 'inline' ? 'Icon/Inverted' : 'Icon/Feedback/Warning'
|
||||
}
|
||||
isFilled
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
case AlertTypeEnum.Success:
|
||||
return (
|
||||
<MaterialIcon
|
||||
icon="check_circle"
|
||||
color={
|
||||
variant === 'inline' ? 'Icon/Inverted' : 'Icon/Feedback/Success'
|
||||
}
|
||||
isFilled
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
case AlertTypeEnum.Info:
|
||||
default:
|
||||
return (
|
||||
<MaterialIcon
|
||||
color={
|
||||
variant === 'inline' ? 'Icon/Inverted' : 'Icon/Feedback/Information'
|
||||
}
|
||||
isFilled
|
||||
icon="info"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
24
packages/design-system/lib/components/Alert/variants.ts
Normal file
24
packages/design-system/lib/components/Alert/variants.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
import { AlertTypeEnum } from '@scandic-hotels/common/constants/alert'
|
||||
|
||||
import styles from './alert.module.css'
|
||||
|
||||
export const alertVariants = cva(styles.alert, {
|
||||
variants: {
|
||||
variant: {
|
||||
inline: styles.inline,
|
||||
banner: styles.banner,
|
||||
},
|
||||
type: {
|
||||
[AlertTypeEnum.Info]: styles.info,
|
||||
[AlertTypeEnum.Warning]: styles.warning,
|
||||
[AlertTypeEnum.Alarm]: styles.alarm,
|
||||
[AlertTypeEnum.Success]: styles.success,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'inline',
|
||||
type: AlertTypeEnum.Info,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user