Feat/BOOK-293 button adjustments

* feat(BOOK-293): Adjusted padding of the buttons to match Figma design
* feat(BOOK-293): Updated variants for IconButton
* feat(BOOK-113): Updated focus indicators on buttons and added default focus ring color
* feat(BOOK-293): Replaced buttons inside booking widget

Approved-by: Christel Westerberg
This commit is contained in:
Erik Tiekstra
2025-12-15 07:05:31 +00:00
parent c153e0db50
commit 4ec1e85d84
59 changed files with 741 additions and 504 deletions

View File

@@ -22,6 +22,6 @@
} }
.link:focus-visible { .link:focus-visible {
outline: 2px auto -webkit-focus-ring-color; outline: 2px auto var(--Border-Interactive-Focus);
outline-offset: -4px; outline-offset: -4px;
} }

View File

@@ -53,7 +53,6 @@
} }
.closeButton { .closeButton {
flex-shrink: 0;
z-index: 1; z-index: 1;
} }

View File

@@ -119,8 +119,7 @@ export default function CampaignBanner() {
</InnerContent> </InnerContent>
<IconButton <IconButton
className={styles.closeButton} className={styles.closeButton}
theme="Inverted" variant="Muted"
style="Muted"
onPress={handleClose} onPress={handleClose}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: "campaignBanner.dismissBanner", id: "campaignBanner.dismissBanner",

View File

@@ -22,8 +22,7 @@ export function CarouselPrevious({ className }: CarouselButtonProps) {
return ( return (
<span className={cx(styles.buttonWrapper, styles.previous, className)}> <span className={cx(styles.buttonWrapper, styles.previous, className)}>
<IconButton <IconButton
theme="Inverted" variant="Elevated"
style="Elevated"
onPress={scrollPrev} onPress={scrollPrev}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: "carousel.previousSlide", id: "carousel.previousSlide",
@@ -46,8 +45,7 @@ export function CarouselNext({ className }: CarouselButtonProps) {
return ( return (
<span className={cx(styles.buttonWrapper, styles.next, className)}> <span className={cx(styles.buttonWrapper, styles.next, className)}>
<IconButton <IconButton
theme="Inverted" variant="Elevated"
style="Elevated"
onPress={scrollNext} onPress={scrollNext}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: "carousel.nextSlide", id: "carousel.nextSlide",

View File

@@ -18,7 +18,7 @@
} }
.item:focus-visible { .item:focus-visible {
outline: 2px auto -webkit-focus-ring-color; outline: 2px auto var(--Border-Interactive-Focus);
outline-offset: 1px; outline-offset: 1px;
} }
.buttonWrapper { .buttonWrapper {

View File

@@ -46,8 +46,8 @@ export default function CityMapCard({
return ( return (
<article className={cx(styles.cityMapCard, className)}> <article className={cx(styles.cityMapCard, className)}>
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
className={styles.closeButton} className={styles.closeButton}
onPress={handleClose} onPress={handleClose}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({

View File

@@ -54,8 +54,8 @@ export function DestinationSearch() {
<> <>
<IconButton <IconButton
onPress={close} onPress={close}
theme="Black" variant="Muted"
style="Muted" emphasis
className={styles.close} className={styles.close}
> >
<MaterialIcon <MaterialIcon

View File

@@ -49,8 +49,8 @@ export default function HotelMapCard({
<article className={className}> <article className={className}>
<div className={styles.wrapper}> <div className={styles.wrapper}>
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
className={styles.closeButton} className={styles.closeButton}
onPress={handleClose} onPress={handleClose}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({

View File

@@ -63,7 +63,6 @@
.seeAsListButton { .seeAsListButton {
display: flex !important; display: flex !important;
pointer-events: initial; pointer-events: initial;
box-shadow: var(--button-box-shadow);
} }
/* Overriding Google maps infoWindow styles */ /* Overriding Google maps infoWindow styles */

View File

@@ -146,10 +146,9 @@ export default function DynamicMap({
)} )}
<div className={styles.zoomButtons}> <div className={styles.zoomButtons}>
<IconButton <IconButton
theme="Inverted" variant="Elevated"
style="Elevated"
className={styles.zoomButton} className={styles.zoomButton}
onClick={zoomIn} onPress={zoomIn}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: "map.zoomIn", id: "map.zoomIn",
defaultMessage: "Zoom in", defaultMessage: "Zoom in",
@@ -159,10 +158,9 @@ export default function DynamicMap({
<MaterialIcon icon="add" color="CurrentColor" size={24} /> <MaterialIcon icon="add" color="CurrentColor" size={24} />
</IconButton> </IconButton>
<IconButton <IconButton
theme="Inverted" variant="Elevated"
style="Elevated"
className={styles.zoomButton} className={styles.zoomButton}
onClick={zoomOut} onPress={zoomOut}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: "map.zoomOut", id: "map.zoomOut",
defaultMessage: "Zoom out", defaultMessage: "Zoom out",

View File

@@ -10,6 +10,5 @@
} }
.closeButton { .closeButton {
box-shadow: var(--button-box-shadow);
pointer-events: initial; pointer-events: initial;
} }

View File

@@ -11,7 +11,7 @@
padding: var(--Space-x05) 0; padding: var(--Space-x05) 0;
&:focus-visible { &:focus-visible {
outline: 2px auto -webkit-focus-ring-color; outline: 2px auto var(--Border-Interactive-Focus);
outline-offset: 1px; outline-offset: 1px;
} }

View File

@@ -13,7 +13,7 @@
} }
&:focus-visible { &:focus-visible {
outline: 2px auto -webkit-focus-ring-color; outline: 2px auto var(--Border-Interactive-Focus);
outline-offset: 1px; outline-offset: 1px;
} }

View File

@@ -129,7 +129,7 @@ export default function HotelFilterAndSort() {
})} })}
</h3> </h3>
</Typography> </Typography>
<IconButton onPress={close} theme="Black"> <IconButton onPress={close} variant="Muted" emphasis>
<MaterialIcon icon="close" color="CurrentColor" /> <MaterialIcon icon="close" color="CurrentColor" />
</IconButton> </IconButton>
</header> </header>

View File

@@ -7,5 +7,5 @@
} }
.download:focus-visible { .download:focus-visible {
outline: 2px solid -webkit-focus-ring-color; outline: 2px solid var(--Border-Interactive-Focus);
} }

View File

@@ -265,8 +265,8 @@ export default function Room({ booking, roomNr, user }: RoomProps) {
subtitle={rateTerm.paymentTerm} subtitle={rateTerm.paymentTerm}
trigger={ trigger={
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
className={styles.termsInfoIcon} className={styles.termsInfoIcon}
> >
<MaterialIcon <MaterialIcon

View File

@@ -50,7 +50,12 @@ export default function Terms() {
title={rateTerm.title} title={rateTerm.title}
subtitle={rateTerm.paymentTerm} subtitle={rateTerm.paymentTerm}
trigger={ trigger={
<IconButton theme="Black" style="Muted" className={styles.button}> <IconButton
variant="Muted"
emphasis
size="sm"
className={styles.button}
>
<MaterialIcon icon="info" color="Icon/Default" size={20} /> <MaterialIcon icon="info" color="Icon/Default" size={20} />
</IconButton> </IconButton>
} }

View File

@@ -89,8 +89,8 @@ export default function MeetingPackageWidget(props: MeetingPackageWidgetProps) {
<> <>
<div className={styles.closeButtonWrapper}> <div className={styles.closeButtonWrapper}>
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
onPress={close} onPress={close}
className={styles.closeButton} className={styles.closeButton}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({

View File

@@ -41,7 +41,7 @@ export default function LevelProgressModal({
<Modal <Modal
className={styles.dialog} className={styles.dialog}
trigger={ trigger={
<IconButton theme="Black"> <IconButton variant="Muted" emphasis>
<MaterialIcon <MaterialIcon
className={styles.infoButton} className={styles.infoButton}
icon="info" icon="info"

View File

@@ -71,7 +71,8 @@ export default function DeleteCreditCardConfirmation({
})} })}
trigger={ trigger={
<IconButton <IconButton
theme="Black" variant="Muted"
emphasis
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: "profile.creditCard.deleteCard", id: "profile.creditCard.deleteCard",
defaultMessage: "Delete card", defaultMessage: "Delete card",

View File

@@ -98,8 +98,9 @@ export default function PasswordInput({
/> />
{visibilityToggleable ? ( {visibilityToggleable ? (
<IconButton <IconButton
theme="Black" variant="Muted"
onClick={() => setIsPasswordVisible((value) => !value)} emphasis
onPress={() => setIsPasswordVisible((value) => !value)}
aria-label={ aria-label={
isPasswordVisible isPasswordVisible
? intl.formatMessage({ ? intl.formatMessage({

View File

@@ -134,13 +134,7 @@ export default function BookingCodeFilter() {
})} })}
</h3> </h3>
</Typography> </Typography>
<IconButton <IconButton variant="Muted" emphasis onPress={close}>
theme="Black"
style="Muted"
onPress={() => {
close()
}}
>
<MaterialIcon <MaterialIcon
icon="close" icon="close"
size={24} size={24}

View File

@@ -230,7 +230,7 @@ function CodeRulesModal() {
return ( return (
<Modal <Modal
trigger={ trigger={
<IconButton theme="Black" wrapping> <IconButton variant="Muted" size="sm" emphasis>
<MaterialIcon <MaterialIcon
icon="info" icon="info"
color="Icon/Interactive/Placeholder" color="Icon/Interactive/Placeholder"

View File

@@ -101,7 +101,7 @@ export default function RewardNight() {
</Typography> </Typography>
<Modal <Modal
trigger={ trigger={
<IconButton theme="Black" wrapping> <IconButton variant="Muted" emphasis size="sm">
<MaterialIcon <MaterialIcon
icon="info" icon="info"
size={20} size={20}

View File

@@ -7,9 +7,6 @@
width: 24px; width: 24px;
height: 24px; height: 24px;
} }
.icon {
display: none;
}
.where, .where,
.rooms, .rooms,
@@ -26,6 +23,10 @@
display: none; display: none;
} }
.submitButton {
min-width: 118px;
}
.label { .label {
color: var(--Text-Accent-Primary); color: var(--Text-Accent-Primary);
} }
@@ -58,16 +59,11 @@
padding: var(--Space-x1) var(--Space-x15); padding: var(--Space-x1) var(--Space-x15);
} }
.button {
align-self: flex-end;
justify-content: center;
width: 100%;
}
.rooms { .rooms {
height: 60px; height: 60px;
} }
} }
.voucherContainer { .voucherContainer {
height: fit-content; height: fit-content;
} }
@@ -125,11 +121,6 @@
position: relative; position: relative;
} }
.button {
justify-content: center;
width: 118px;
}
.showOnMobile { .showOnMobile {
display: none; display: none;
} }
@@ -160,12 +151,6 @@
width: 48px; width: 48px;
height: 48px; height: 48px;
} }
.buttonText {
display: none;
}
.icon {
display: flex;
}
.voucherRow { .voucherRow {
display: flex; display: flex;

View File

@@ -9,6 +9,7 @@ import { useIntl } from "react-intl"
import { hotelreservation } from "@scandic-hotels/common/constants/routes/hotelReservation" import { hotelreservation } from "@scandic-hotels/common/constants/routes/hotelReservation"
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { Button } from "@scandic-hotels/design-system/Button" import { Button } from "@scandic-hotels/design-system/Button"
import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer" import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
@@ -112,11 +113,15 @@ export default function FormContent({
</div> </div>
</div> </div>
<div className={cx(styles.buttonContainer, styles.showOnTablet)}> <div className={cx(styles.buttonContainer, styles.showOnTablet)}>
<Button className={styles.button} form={formId} type="submit"> <IconButton
<span className={styles.icon}> size="xl"
<MaterialIcon icon="search" color="Icon/Inverted" size={28} /> variant="Filled"
</span> form={formId}
</Button> type="submit"
isDisabled={isSearching}
>
<MaterialIcon icon="search" color="CurrentColor" size={28} />
</IconButton>
</div> </div>
<div <div
className={cx( className={cx(
@@ -139,32 +144,23 @@ export default function FormContent({
</div> </div>
) : null} ) : null}
<Button <Button
className={styles.button} className={styles.submitButton}
form={formId} form={formId}
variant="Primary" variant="Primary"
size="Medium" size="Medium"
type="submit" type="submit"
isDisabled={isSearching} isDisabled={isSearching}
typography="Body/Supporting text (caption)/smBold"
> >
<Typography {isDirty && isBookingFlow
variant="Body/Supporting text (caption)/smBold" ? intl.formatMessage({
className={styles.buttonText} id: "bookingWidget.button.update",
> defaultMessage: "Update",
<span> })
{isDirty && isBookingFlow : intl.formatMessage({
? intl.formatMessage({ id: "bookingWidget.button.search",
id: "bookingWidget.button.update", defaultMessage: "Search",
defaultMessage: "Update", })}
})
: intl.formatMessage({
id: "bookingWidget.button.search",
defaultMessage: "Search",
})}
</span>
</Typography>
<span className={styles.icon}>
<MaterialIcon icon="search" color="Icon/Inverted" size={28} />
</span>
</Button> </Button>
</div> </div>
</div> </div>
@@ -214,38 +210,26 @@ export function BookingWidgetFormContentSkeleton() {
</div> </div>
</div> </div>
<div className={cx(styles.buttonContainer, styles.showOnTablet)}> <div className={cx(styles.buttonContainer, styles.showOnTablet)}>
<Button className={styles.button} type="submit" isDisabled> <IconButton variant="Filled" size="xl" type="submit" isDisabled>
<span className={styles.icon}> <MaterialIcon icon="search" color="CurrentColor" size={28} />
<MaterialIcon icon="search" color="Icon/Inverted" size={28} /> </IconButton>
</span>
</Button>
</div> </div>
<div className={cx(styles.voucherContainer, styles.voucherRow)}> <div className={cx(styles.voucherContainer, styles.voucherRow)}>
<VoucherSkeleton /> <VoucherSkeleton />
</div> </div>
<div className={cx(styles.buttonContainer, styles.hideOnTablet)}> <div className={cx(styles.buttonContainer, styles.hideOnTablet)}>
<Button <Button
className={styles.button} className={styles.submitButton}
variant="Primary" variant="Primary"
size="Medium" size="Medium"
type="submit" type="submit"
isDisabled isDisabled
typography="Body/Supporting text (caption)/smBold"
> >
<Typography {intl.formatMessage({
variant="Body/Supporting text (caption)/smBold" id: "bookingWidget.button.search",
className={styles.buttonText} defaultMessage: "Search",
> })}
<span>
{intl.formatMessage({
id: "bookingWidget.button.search",
defaultMessage: "Search",
})}
</span>
</Typography>
<span className={styles.icon}>
<MaterialIcon icon="search" color="Icon/Inverted" size={28} />
</span>
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -25,9 +25,8 @@ export default function Counter({
<div className={styles.counterContainer}> <div className={styles.counterContainer}>
<IconButton <IconButton
className={styles.counterBtn} className={styles.counterBtn}
onClick={handleOnDecrease} onPress={handleOnDecrease}
theme="Inverted" variant="Elevated"
style="Elevated"
isDisabled={disableDecrease} isDisabled={disableDecrease}
> >
<MaterialIcon icon="remove" color="CurrentColor" /> <MaterialIcon icon="remove" color="CurrentColor" />
@@ -37,9 +36,8 @@ export default function Counter({
</Typography> </Typography>
<IconButton <IconButton
className={styles.counterBtn} className={styles.counterBtn}
onClick={handleOnIncrease} onPress={handleOnIncrease}
theme="Inverted" variant="Elevated"
style="Elevated"
isDisabled={disableIncrease} isDisabled={disableIncrease}
> >
<MaterialIcon icon="add" color="CurrentColor" /> <MaterialIcon icon="add" color="CurrentColor" />

View File

@@ -102,7 +102,7 @@
.addRoomBtn:is(:focus, :focus-visible, :focus-within), .addRoomBtn:is(:focus, :focus-visible, :focus-within),
.footer .hideOnMobile .addRoomBtn:is(:focus, :focus-visible, :focus-within), .footer .hideOnMobile .addRoomBtn:is(:focus, :focus-visible, :focus-within),
.roomActionsButton:is(:focus, :focus-visible, :focus-within) { .roomActionsButton:is(:focus, :focus-visible, :focus-within) {
outline: -webkit-focus-ring-color auto 1px; outline: var(--Border-Interactive-Focus) auto 1px;
text-decoration: none; text-decoration: none;
} }

View File

@@ -52,10 +52,10 @@
background-color: var(--Base-Button-Primary-Fill-Normal); background-color: var(--Base-Button-Primary-Fill-Normal);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
height: 36px; height: 40px;
justify-content: center; justify-content: center;
justify-self: flex-end; justify-self: flex-end;
width: 36px; width: 40px;
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {

View File

@@ -67,8 +67,8 @@ export default function ListingHotelCardDialog({
return ( return (
<div className={styles.container}> <div className={styles.container}>
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
className={styles.closeButton} className={styles.closeButton}
onPress={handleClose} onPress={handleClose}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({

View File

@@ -178,8 +178,8 @@ export default function FilterAndSortModal({
</p> </p>
</Typography> </Typography>
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
onPress={close} onPress={close}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: "common.close", id: "common.close",

View File

@@ -1,7 +1,5 @@
.container .closeButton { .container .closeButton {
pointer-events: initial; pointer-events: initial;
box-shadow: var(--button-box-shadow);
gap: var(--Space-x05);
display: none; display: none;
} }

View File

@@ -89,8 +89,8 @@ export default function SummaryContent({
<IconButton <IconButton
className={styles.closeButton} className={styles.closeButton}
onPress={toggleSummaryOpen} onPress={toggleSummaryOpen}
theme="Black" variant="Muted"
style="Muted" emphasis
> >
<MaterialIcon <MaterialIcon
icon="keyboard_arrow_down" icon="keyboard_arrow_down"

View File

@@ -61,8 +61,8 @@ export function RoomPackageFilterModal({
</h3> </h3>
</Typography> </Typography>
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
onPress={() => setIsOpen(false)} onPress={() => setIsOpen(false)}
> >
<MaterialIcon icon="close" size={24} color="CurrentColor" /> <MaterialIcon icon="close" size={24} color="CurrentColor" />

View File

@@ -99,9 +99,7 @@ export function RoomPackageFilter({ roomIndex }: { roomIndex: number }) {
/> />
{filterLabel} {filterLabel}
<IconButton <IconButton
style="Muted" variant="Muted"
theme="Inverted"
wrapping
onPress={() => deletePackage(pkg.code)} onPress={() => deletePackage(pkg.code)}
aria-label={intl.formatMessage( aria-label={intl.formatMessage(
{ {

View File

@@ -1,13 +1,13 @@
import { useIntl } from 'react-intl' import { useIntl } from 'react-intl'
import IconChip from '../IconChip' import IconChip from '../IconChip'
import FilledDiscountIcon from '../Icons/Nucleo/Benefits/FilledDiscount'
import { MaterialIcon } from '../Icons/MaterialIcon' import { MaterialIcon } from '../Icons/MaterialIcon'
import FilledDiscountIcon from '../Icons/Nucleo/Benefits/FilledDiscount'
import { Typography } from '../Typography' import { Typography } from '../Typography'
import styles from './bookingCodeChip.module.css'
import { cx } from 'class-variance-authority' import { cx } from 'class-variance-authority'
import { IconButton } from '../IconButton' import { IconButton } from '../IconButton'
import styles from './bookingCodeChip.module.css'
type BaseBookingCodeChipProps = { type BaseBookingCodeChipProps = {
alignCenter?: boolean alignCenter?: boolean
@@ -102,9 +102,8 @@ export function BookingCodeChip({
</p> </p>
{withCloseButton && ( {withCloseButton && (
<IconButton <IconButton
style="Muted" variant="Muted"
theme="Inverted" size="sm"
wrapping
className={styles.removeButton} className={styles.removeButton}
onPress={onClose} onPress={onClose}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({

View File

@@ -29,8 +29,7 @@ const meta: Meta<typeof Button> = {
summary: buttonConfig.defaultVariants.variant, summary: buttonConfig.defaultVariants.variant,
}, },
type: { type: {
summary: 'string', summary: Object.keys(buttonConfig.variants.variant).join(' | '),
detail: Object.keys(buttonConfig.variants.variant).join(' | '),
}, },
}, },
}, },
@@ -39,8 +38,7 @@ const meta: Meta<typeof Button> = {
options: Object.keys(buttonConfig.variants.color), options: Object.keys(buttonConfig.variants.color),
table: { table: {
type: { type: {
summary: 'string', summary: Object.keys(buttonConfig.variants.color).join(' | '),
detail: Object.keys(buttonConfig.variants.color).join(' | '),
}, },
defaultValue: { defaultValue: {
summary: buttonConfig.defaultVariants.color, summary: buttonConfig.defaultVariants.color,
@@ -52,8 +50,7 @@ const meta: Meta<typeof Button> = {
options: Object.keys(buttonConfig.variants.size), options: Object.keys(buttonConfig.variants.size),
table: { table: {
type: { type: {
summary: 'string', summary: Object.keys(buttonConfig.variants.size).join(' | '),
detail: Object.keys(buttonConfig.variants.size).join(' | '),
}, },
defaultValue: { defaultValue: {
summary: buttonConfig.defaultVariants.size, summary: buttonConfig.defaultVariants.size,

View File

@@ -21,23 +21,28 @@
&:focus-visible { &:focus-visible {
outline: 2px solid var(--Border-Interactive-Focus); outline: 2px solid var(--Border-Interactive-Focus);
outline-offset: 2px; outline-offset: 2px;
&::before {
content: '';
position: absolute;
inset: -4px;
border: 2px solid var(--Border-Inverted);
border-radius: inherit;
pointer-events: none;
}
} }
} }
.color-inverted:focus-visible {
outline-color: var(--Border-Inverted);
}
.size-large { .size-large {
padding: var(--Space-x2) var(--Space-x3); padding: calc(var(--Space-x2) - 2px) var(--Space-x3); /* Adjust for 2px border */
} }
.size-medium { .size-medium {
padding: var(--Space-x15) var(--Space-x2); padding: calc(var(--Space-x15) - 2px) var(--Space-x2); /* Adjust for 2px border */
} }
.size-small { .size-small {
padding: 10px var(--Space-x2); padding: var(--Space-x1) var(--Space-x2); /* Adjust for 2px border */
} }
.variant-primary { .variant-primary {
@@ -60,17 +65,6 @@
} }
} }
/* This variant is able to be on top of dark background colors,
so we need to create an illusion that it also has an inverted border on focus */
&:not(.color-inverted):focus-visible::before {
content: '';
position: absolute;
inset: -4px;
border: 2px solid var(--Border-Inverted);
border-radius: inherit;
pointer-events: none;
}
&[data-disabled] { &[data-disabled] {
background-color: var(--Component-Button-Brand-Primary-Fill-Disabled); background-color: var(--Component-Button-Brand-Primary-Fill-Disabled);
border-color: var(--Component-Button-Brand-Primary-Border-Disabled); border-color: var(--Component-Button-Brand-Primary-Border-Disabled);
@@ -103,6 +97,14 @@
border-color: var(--Component-Button-Inverted-Border-Disabled); border-color: var(--Component-Button-Inverted-Border-Disabled);
color: var(--Component-Button-Inverted-On-fill-Disabled); color: var(--Component-Button-Inverted-On-fill-Disabled);
} }
&:focus-visible {
outline-color: var(--Border-Inverted);
&::before {
border-color: var(--Border-Interactive-Focus);
}
}
} }
.variant-secondary { .variant-secondary {
@@ -150,6 +152,14 @@
border-color: var(--Component-Button-Brand-Secondary-Border-Disabled); border-color: var(--Component-Button-Brand-Secondary-Border-Disabled);
color: var(--Component-Button-Brand-Secondary-On-fill-Disabled); color: var(--Component-Button-Brand-Secondary-On-fill-Disabled);
} }
&:focus-visible {
outline-color: var(--Border-Inverted);
&::before {
border-color: var(--Border-Interactive-Focus);
}
}
} }
.variant-tertiary { .variant-tertiary {
@@ -229,6 +239,10 @@
.variant-text.no-wrapping { .variant-text.no-wrapping {
padding: var(--Space-x025) 0; padding: var(--Space-x025) 0;
border-width: 0; border-width: 0;
&:focus-visible {
outline-offset: 4px;
}
} }
.variant-text.color-inverted { .variant-text.color-inverted {
@@ -246,6 +260,14 @@
&[data-disabled] { &[data-disabled] {
color: var(--Component-Button-Brand-Secondary-On-fill-Disabled); color: var(--Component-Button-Brand-Secondary-On-fill-Disabled);
} }
&:focus-visible {
outline-color: var(--Border-Inverted);
&::before {
border-color: var(--Border-Interactive-Focus);
}
}
} }
.spinnerWrapper { .spinnerWrapper {

View File

@@ -76,8 +76,8 @@ export function StandaloneHotelCardDialog({
return ( return (
<div className={styles.container}> <div className={styles.container}>
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
className={styles.closeButton} className={styles.closeButton}
onPress={handleClose} onPress={handleClose}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({

View File

@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { expect, fn } from 'storybook/test' import { expect, fn } from 'storybook/test'
import { MaterialIcon } from '../Icons/MaterialIcon' import { MaterialIcon, MaterialIconProps } from '../Icons/MaterialIcon'
import { IconButton } from './IconButton' import { IconButton } from './IconButton'
import { config } from './variants' import { config } from './variants'
@@ -20,37 +20,81 @@ const meta: Meta<typeof IconButton> = {
disable: true, disable: true,
}, },
}, },
theme: { variant: {
control: 'select', control: 'select',
options: Object.keys(config.variants.theme), options: Object.keys(config.variants.variant),
table: { table: {
defaultValue: { defaultValue: {
summary: config.defaultVariants.theme, summary: config.defaultVariants.variant,
}, },
type: { type: {
summary: 'string', summary: Object.keys(config.variants.variant).join(' | '),
detail: Object.keys(config.variants.theme).join(' | '),
}, },
}, },
}, },
style: { size: {
control: 'select', control: 'select',
options: Object.keys(config.variants.style), options: Object.keys(config.variants.size),
table: { table: {
defaultValue: { defaultValue: {
summary: config.defaultVariants.style, summary: config.defaultVariants.size,
}, },
type: { type: {
summary: 'string', summary: Object.keys(config.variants.size).join(' | '),
detail: Object.keys(config.variants.style).join(' | '),
}, },
}, },
description: description:
'The style variant is only applied on certain variants. The examples below shows the possible combinations of variants and style variants.', 'The size of the `IconButton`. Please note that you control the size of the icon inside the button separately. Please check the examples below for recommended icon sizes for each button size.',
},
emphasis: {
control: 'boolean',
options: Object.keys(config.variants.emphasis),
table: {
defaultValue: {
summary: config.defaultVariants.emphasis.toString(),
},
type: {
summary: 'boolean',
},
},
}, },
}, },
} }
const buttonAndIconSizesMap = Object.keys(config.variants.size).map<{
size: keyof typeof config.variants.size
iconSize: number
}>((key) => {
const typedKey = key as keyof typeof config.variants.size
switch (typedKey) {
case 'sm':
return {
size: typedKey,
iconSize: 16,
}
case 'md':
return {
size: typedKey,
iconSize: 20,
}
case 'lg':
return {
size: typedKey,
iconSize: 24,
}
case 'xl':
return {
size: typedKey,
iconSize: 28,
}
default:
return {
size: typedKey,
iconSize: 24,
}
}
})
const globalStoryPropsInverted = { const globalStoryPropsInverted = {
backgrounds: { value: 'scandicPrimaryDark' }, backgrounds: { value: 'scandicPrimaryDark' },
} }
@@ -58,6 +102,36 @@ export default meta
type Story = StoryObj<typeof IconButton> type Story = StoryObj<typeof IconButton>
function renderAllSizesFn(
args: Story['args'],
iconName: MaterialIconProps['icon'] = 'search'
) {
return (
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
{buttonAndIconSizesMap.map(({ size, iconSize }) => (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '8px',
}}
key={size}
>
<IconButton {...args} size={size} key={size}>
<MaterialIcon
icon={iconName}
size={iconSize}
color="CurrentColor"
/>
</IconButton>
<span>{size}</span>
</div>
))}
</div>
)
}
export const Default: Story = { export const Default: Story = {
args: { args: {
onPress: fn(), onPress: fn(),
@@ -69,11 +143,150 @@ export const Default: Story = {
}, },
} }
export const Primary: Story = { export const Examples: Story = {
render: () => {
return (
<div style={{ display: 'grid', gap: '16px', justifyContent: 'center' }}>
<div
style={{
padding: '16px',
borderRadius: '8px',
border: '1px solid #4D001B',
}}
>
<h3 style={{ marginBottom: '8px' }}>Filled</h3>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{renderAllSizesFn({ ...Default.args, variant: 'Filled' })}
</div>
</div>
<div
style={{
padding: '16px',
borderRadius: '8px',
border: '1px solid #4D001B',
}}
>
<h3 style={{ marginBottom: '8px' }}>Filled with emphasis</h3>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{renderAllSizesFn(
{
...Default.args,
variant: 'Filled',
emphasis: true,
},
'arrow_forward'
)}
</div>
</div>
<div
style={{
padding: '16px',
borderRadius: '8px',
border: '1px solid #4D001B',
}}
>
<h3 style={{ marginBottom: '8px' }}>Outlined</h3>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{renderAllSizesFn(
{
...Default.args,
variant: 'Outlined',
},
'arrow_forward'
)}
</div>
</div>
<div
style={{
padding: '16px',
borderRadius: '8px',
border: '1px solid #4D001B',
}}
>
<h3 style={{ marginBottom: '8px' }}>Elevated</h3>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{renderAllSizesFn(
{
...Default.args,
variant: 'Elevated',
},
'arrow_forward'
)}
</div>
</div>
<div
style={{
backgroundColor: '#4D001B',
color: 'white',
padding: '16px',
borderRadius: '8px',
border: '1px solid #4D001B',
}}
>
<h3 style={{ marginBottom: '8px' }}>Faded</h3>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{renderAllSizesFn(
{
...Default.args,
variant: 'Faded',
},
'arrow_forward'
)}
</div>
</div>
<div
style={{
backgroundColor: '#4D001B',
color: 'white',
padding: '16px',
borderRadius: '8px',
border: '1px solid #4D001B',
}}
>
<h3 style={{ marginBottom: '8px' }}>Muted</h3>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '8px',
}}
>
{renderAllSizesFn(
{
...Default.args,
variant: 'Muted',
},
'arrow_forward'
)}
</div>
</div>
<div
style={{
padding: '16px',
borderRadius: '8px',
border: '1px solid #4D001B',
}}
>
<h3 style={{ marginBottom: '8px' }}>Muted with emphasis</h3>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{renderAllSizesFn(
{
...Default.args,
variant: 'Muted',
emphasis: true,
},
'arrow_forward'
)}
</div>
</div>
</div>
)
},
}
export const Filled: Story = {
args: { args: {
...Default.args, ...Default.args,
theme: 'Primary', variant: 'Filled',
onPress: fn(), // Fresh spy instance
}, },
play: async ({ canvas, userEvent, args }) => { play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button')) await userEvent.click(canvas.getByRole('button'))
@@ -81,11 +294,10 @@ export const Primary: Story = {
}, },
} }
export const PrimaryDisabled: Story = { export const FilledDisabled: Story = {
args: { args: {
...Primary.args, ...Filled.args,
isDisabled: true, isDisabled: true,
onPress: fn(), // Fresh spy instance for disabled test
}, },
play: async ({ canvas, userEvent, args }) => { play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button')) await userEvent.click(canvas.getByRole('button'))
@@ -93,13 +305,49 @@ export const PrimaryDisabled: Story = {
}, },
} }
export const Inverted: Story = { export const FilledOnDarkBackground: Story = {
globals: globalStoryPropsInverted,
args: {
...Filled.args,
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button'))
expect(args.onPress).toHaveBeenCalledTimes(1)
},
}
export const FilledWithEmphasis: Story = {
args: {
...Filled.args,
children: (
<MaterialIcon icon="arrow_forward" size={24} color="CurrentColor" />
),
emphasis: true,
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button'))
expect(args.onPress).toHaveBeenCalledTimes(1)
},
}
export const FilledWithEmphasisDisabled: Story = {
args: {
...FilledWithEmphasis.args,
isDisabled: true,
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button'))
expect(args.onPress).toHaveBeenCalledTimes(0)
},
}
export const Outlined: Story = {
args: { args: {
...Default.args, ...Default.args,
children: ( children: (
<MaterialIcon icon="arrow_forward" size={24} color="CurrentColor" /> <MaterialIcon icon="arrow_forward" size={24} color="CurrentColor" />
), ),
theme: 'Inverted', variant: 'Outlined',
}, },
play: async ({ canvas, userEvent, args }) => { play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button')) await userEvent.click(canvas.getByRole('button'))
@@ -107,11 +355,10 @@ export const Inverted: Story = {
}, },
} }
export const InvertedDisabled: Story = { export const OutlinedDisabled: Story = {
args: { args: {
...Inverted.args, ...Outlined.args,
isDisabled: true, isDisabled: true,
onPress: fn(), // Fresh spy instance for disabled test
}, },
play: async ({ canvas, userEvent, args }) => { play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button')) await userEvent.click(canvas.getByRole('button'))
@@ -119,86 +366,13 @@ export const InvertedDisabled: Story = {
}, },
} }
export const InvertedElevated: Story = { export const Elevated: Story = {
args: {
...Inverted.args,
style: 'Elevated',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button'))
expect(args.onPress).toHaveBeenCalledTimes(1)
},
}
export const InvertedElevatedDisabled: Story = {
args: {
...InvertedElevated.args,
isDisabled: true,
onPress: fn(), // Fresh spy instance for disabled test
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button'))
expect(args.onPress).toHaveBeenCalledTimes(0)
},
}
export const InvertedMuted: Story = {
globals: globalStoryPropsInverted,
args: {
...Inverted.args,
children: <MaterialIcon icon="close" size={24} color="CurrentColor" />,
style: 'Muted',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button'))
expect(args.onPress).toHaveBeenCalledTimes(1)
},
}
export const InvertedMutedDisabled: Story = {
globals: globalStoryPropsInverted,
args: {
...InvertedMuted.args,
isDisabled: true,
onPress: fn(), // Fresh spy instance for disabled test
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button'))
expect(args.onPress).toHaveBeenCalledTimes(0)
},
}
export const InvertedFaded: Story = {
args: {
...Inverted.args,
style: 'Faded',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button'))
expect(args.onPress).toHaveBeenCalledTimes(1)
},
}
export const InvertedFadedDisabled: Story = {
args: {
...InvertedFaded.args,
isDisabled: true,
onPress: fn(), // Fresh spy instance for disabled test
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button'))
expect(args.onPress).toHaveBeenCalledTimes(0)
},
}
export const TertiaryElevated: Story = {
args: { args: {
...Default.args, ...Default.args,
children: <MaterialIcon icon="arrow_back" size={24} color="CurrentColor" />, children: (
theme: 'Tertiary', <MaterialIcon icon="arrow_forward" size={24} color="CurrentColor" />
style: 'Elevated', ),
variant: 'Elevated',
}, },
play: async ({ canvas, userEvent, args }) => { play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button')) await userEvent.click(canvas.getByRole('button'))
@@ -206,11 +380,10 @@ export const TertiaryElevated: Story = {
}, },
} }
export const TertiaryDisabled: Story = { export const ElevatedDisabled: Story = {
args: { args: {
...TertiaryElevated.args, ...Elevated.args,
isDisabled: true, isDisabled: true,
onPress: fn(), // Fresh spy instance for disabled test
}, },
play: async ({ canvas, userEvent, args }) => { play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button')) await userEvent.click(canvas.getByRole('button'))
@@ -218,11 +391,14 @@ export const TertiaryDisabled: Story = {
}, },
} }
export const BlackMuted: Story = { export const Faded: Story = {
globals: globalStoryPropsInverted,
args: { args: {
...Default.args, ...Default.args,
children: <MaterialIcon icon="close" size={24} color="CurrentColor" />, children: (
theme: 'Black', <MaterialIcon icon="arrow_forward" size={24} color="CurrentColor" />
),
variant: 'Faded',
}, },
play: async ({ canvas, userEvent, args }) => { play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button')) await userEvent.click(canvas.getByRole('button'))
@@ -230,11 +406,60 @@ export const BlackMuted: Story = {
}, },
} }
export const BlackMutedDisabled: Story = { export const FadedDisabled: Story = {
globals: globalStoryPropsInverted,
args: { args: {
...BlackMuted.args, ...Faded.args,
isDisabled: true,
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button'))
expect(args.onPress).toHaveBeenCalledTimes(0)
},
}
export const Muted: Story = {
globals: globalStoryPropsInverted,
args: {
...Default.args,
children: (
<MaterialIcon icon="arrow_forward" size={24} color="CurrentColor" />
),
variant: 'Muted',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button'))
expect(args.onPress).toHaveBeenCalledTimes(1)
},
}
export const MutedDisabled: Story = {
globals: globalStoryPropsInverted,
args: {
...Muted.args,
isDisabled: true,
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button'))
expect(args.onPress).toHaveBeenCalledTimes(0)
},
}
export const MutedWithEmphasis: Story = {
args: {
...Muted.args,
emphasis: true,
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button'))
expect(args.onPress).toHaveBeenCalledTimes(1)
},
}
export const MutedWithEmphasisDisabled: Story = {
args: {
...MutedWithEmphasis.args,
isDisabled: true, isDisabled: true,
onPress: fn(), // Fresh spy instance for disabled test
}, },
play: async ({ canvas, userEvent, args }) => { play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole('button')) await userEvent.click(canvas.getByRole('button'))

View File

@@ -1,20 +1,24 @@
import { Button as ButtonRAC } from 'react-aria-components' import { Button as ButtonRAC } from 'react-aria-components'
import { VariantProps } from 'class-variance-authority'
import { ComponentProps } from 'react'
import { variants } from './variants' import { variants } from './variants'
import type { IconButtonProps } from './types' interface IconButtonProps
extends ComponentProps<typeof ButtonRAC>,
VariantProps<typeof variants> {}
export function IconButton({ export function IconButton({
theme, variant,
style, emphasis,
size,
className, className,
wrapping,
...props ...props
}: IconButtonProps) { }: IconButtonProps) {
const classNames = variants({ const classNames = variants({
theme, variant,
style, emphasis,
wrapping, size,
className, className,
}) })

View File

@@ -1,12 +1,14 @@
.iconButton { .iconButton {
position: relative; position: relative;
border-radius: var(--Corner-radius-rounded); display: flex;
border-width: 0; padding: 0;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center; justify-content: center;
padding: 10px; align-items: center;
flex-shrink: 0;
border-width: 0;
background-color: transparent;
cursor: pointer;
border-radius: var(--Corner-radius-rounded);
&[data-disabled] { &[data-disabled] {
cursor: unset; cursor: unset;
@@ -15,13 +17,47 @@
&:focus-visible { &:focus-visible {
outline: 2px solid var(--Border-Interactive-Focus); outline: 2px solid var(--Border-Interactive-Focus);
outline-offset: 2px; outline-offset: 2px;
&::before {
content: '';
position: absolute;
inset: -2px;
border: 2px solid var(--Border-Inverted);
border-radius: inherit;
pointer-events: none;
}
} }
} }
.theme-primary { .size-sm {
width: 24px;
height: 24px;
}
.size-md {
width: 32px;
height: 32px;
}
.size-lg {
width: 40px;
height: 40px;
}
.size-xl {
width: 48px;
height: 48px;
}
.variant-filled {
background-color: var(--Component-Button-Brand-Primary-Fill-Default); background-color: var(--Component-Button-Brand-Primary-Fill-Default);
color: var(--Component-Button-Brand-Primary-On-fill-Default); color: var(--Component-Button-Brand-Primary-On-fill-Default);
&[data-disabled] {
background-color: var(--Component-Button-Brand-Primary-Fill-Disabled);
color: var(--Component-Button-Brand-Primary-On-fill-Disabled);
}
@media (hover: hover) { @media (hover: hover) {
&:hover:not([data-disabled]) { &:hover:not([data-disabled]) {
background: background:
@@ -35,26 +71,46 @@
} }
} }
/* This theme is able to be on top of dark background colors, &.emphasis {
so we need to create an illusion that it also has an inverted border on focus */ background-color: var(--Component-Button-Brand-Tertiary-Fill-Default);
&:focus-visible::after { color: var(--Component-Button-Brand-Tertiary-On-fill-Default);
content: '';
position: absolute;
inset: -2px;
border: 2px solid var(--Border-Inverted);
border-radius: inherit;
pointer-events: none;
}
&[data-disabled] { &[data-disabled] {
background-color: var(--Component-Button-Brand-Primary-Fill-Disabled); background-color: var(--Component-Button-Brand-Tertiary-Fill-Disabled);
color: var(--Component-Button-Brand-Primary-On-fill-Disabled); color: var(--Component-Button-Brand-Tertiary-On-fill-Disabled);
}
@media (hover: hover) {
&:hover:not([data-disabled]) {
background:
linear-gradient(
0deg,
var(--Component-Button-Brand-Tertiary-Fill-Hover) 0%,
var(--Component-Button-Brand-Tertiary-Fill-Hover) 100%
),
var(--Component-Button-Brand-Tertiary-Fill-Default);
color: var(--Component-Button-Brand-Tertiary-On-fill-Hover);
}
}
} }
} }
.theme-inverted { .variant-outlined {
border: 1px solid var(--Border-Default);
background-color: var(--Component-Button-Inverted-Fill-Default); background-color: var(--Component-Button-Inverted-Fill-Default);
color: var(--Component-Button-Inverted-On-fill-Default); color: var(--Icon-Interactive-Default);
&[data-disabled] {
border-color: var(--Border-Interactive-Disabled);
background:
linear-gradient(
0deg,
var(--Component-Button-Inverted-Fill-Disabled) 0%,
var(--Component-Button-Inverted-Fill-Disabled) 100%
),
var(--Component-Button-Inverted-Fill-Faded);
color: var(--Component-Button-Brand-Primary-On-fill-Disabled);
}
@media (hover: hover) { @media (hover: hover) {
&:hover:not([data-disabled]) { &:hover:not([data-disabled]) {
@@ -68,90 +124,125 @@
} }
} }
&[data-disabled] { &:focus-visible {
background-color: var(--Component-Button-Inverted-Fill-Disabled); outline-offset: 0;
color: var(--Component-Button-Inverted-On-fill-Disabled);
}
&.style-muted { &::before {
background-color: var(--Component-Button-Muted-Fill-Default); inset: -5px;
color: var(--Component-Button-Muted-On-fill-Inverted);
@media (hover: hover) {
&:hover:not(:disabled) {
background-color: var(--Component-Button-Muted-Fill-Hover);
}
}
&:focus-visible {
outline-color: var(--Border-Inverted);
}
&[data-disabled] {
color: var(--Component-Button-Muted-On-fill-Disabled);
} }
} }
} }
.theme-tertiary { .variant-elevated {
background-color: var(--Component-Button-Brand-Tertiary-Fill-Default); background-color: var(--Component-Button-Inverted-Fill-Default);
color: var(--Component-Button-Brand-Tertiary-On-fill-Default); color: var(--Icon-Interactive-Default);
box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1);
&[data-disabled] {
background:
linear-gradient(
0deg,
var(--Component-Button-Inverted-Fill-Disabled) 0%,
var(--Component-Button-Inverted-Fill-Disabled) 100%
),
var(--Component-Button-Inverted-Fill-Faded);
box-shadow: none;
color: var(--Component-Button-Brand-Primary-On-fill-Disabled);
}
@media (hover: hover) { @media (hover: hover) {
&:hover:not([data-disabled]) { &:hover:not([data-disabled]) {
background: background:
linear-gradient( linear-gradient(
0deg, 0deg,
var(--Component-Button-Brand-Tertiary-Fill-Hover) 0%, var(--Component-Button-Inverted-Fill-Hover) 0%,
var(--Component-Button-Brand-Tertiary-Fill-Hover) 100% var(--Component-Button-Inverted-Fill-Hover) 100%
), ),
var(--Component-Button-Brand-Tertiary-Fill-Default); var(--Component-Button-Inverted-Fill-Default);
color: var(--Component-Button-Brand-Tertiary-On-fill-Hover);
} }
} }
&[data-disabled] { &:focus-visible {
background-color: var(--Component-Button-Brand-Tertiary-Fill-Disabled); outline-offset: 0;
color: var(--Component-Button-Brand-Tertiary-On-fill-Disabled);
}
}
.theme-black { &::before {
color: var(--Component-Button-Muted-On-fill-Default); inset: -4px;
@media (hover: hover) {
&:hover:not([data-disabled]) {
color: var(--Component-Button-Muted-On-fill-Hover-Inverted);
} }
} }
&[data-disabled] {
color: var(--Component-Button-Muted-On-fill-Disabled);
}
} }
.style-elevated { .variant-faded {
box-shadow: 0px 0px 8px 1px #0000001a;
}
.style-faded {
background-color: var(--Component-Button-Inverted-Fill-Faded); background-color: var(--Component-Button-Inverted-Fill-Faded);
} color: var(--Icon-Interactive-Default);
.style-muted { &[data-disabled] {
background-color: var(--Component-Button-Muted-Fill-Default); background:
linear-gradient(
0deg,
var(--Component-Button-Inverted-Fill-Disabled) 0%,
var(--Component-Button-Inverted-Fill-Disabled) 100%
),
var(--Component-Button-Inverted-Fill-Default);
color: var(--Component-Button-Brand-Primary-On-fill-Disabled);
}
@media (hover: hover) { @media (hover: hover) {
&:hover:not([data-disabled]) { &:hover:not([data-disabled]) {
background-color: var(--Component-Button-Muted-Fill-Hover-inverted); background:
linear-gradient(
0deg,
var(--Component-Button-Inverted-Fill-Hover) 0%,
var(--Component-Button-Inverted-Fill-Hover) 100%
),
var(--Component-Button-Inverted-Fill-Default);
} }
} }
&[data-disabled] { &:focus-visible {
background-color: var(--Component-Button-Muted-Fill-Disabled-inverted); outline-offset: 0;
&::before {
inset: -4px;
}
} }
} }
.no-wrapping { .variant-muted {
padding: 0; background-color: var(--Component-Button-Muted-Fill-Default);
color: var(--Icon-Inverted);
&[data-disabled] {
background-color: var(--Component-Button-Muted-Fill-Disabled);
color: var(--Component-Button-Brand-Primary-On-fill-Disabled);
}
@media (hover: hover) {
&:hover:not([data-disabled]) {
background-color: var(--Component-Button-Muted-Fill-Hover);
}
}
&:focus-visible {
outline-offset: 0;
&::before {
inset: -4px;
}
}
&.emphasis {
color: var(--Component-Button-Muted-On-fill-Default);
&[data-disabled] {
background-color: var(--Component-Button-Muted-Fill-Disabled-inverted);
color: var(--Component-Button-Muted-On-fill-Disabled);
}
@media (hover: hover) {
&:hover:not([data-disabled]) {
background-color: var(--Component-Button-Muted-Fill-Hover-inverted);
color: var(--Component-Button-Muted-On-fill-Hover-Inverted);
}
}
}
} }

View File

@@ -1,10 +0,0 @@
import { Button } from 'react-aria-components'
import type { VariantProps } from 'class-variance-authority'
import type { ComponentProps } from 'react'
import type { variants } from './variants'
export interface IconButtonProps
extends Omit<ComponentProps<typeof Button>, 'style'>,
VariantProps<typeof variants> {}

View File

@@ -2,81 +2,31 @@ import { cva } from 'class-variance-authority'
import styles from './iconButton.module.css' import styles from './iconButton.module.css'
const variantKeys = { export const config = {
theme: { variants: {
Primary: 'Primary', variant: {
Tertiary: 'Tertiary', Filled: styles['variant-filled'],
Inverted: 'Inverted', Outlined: styles['variant-outlined'],
Black: 'Black', Elevated: styles['variant-elevated'],
Faded: styles['variant-faded'],
Muted: styles['variant-muted'],
},
emphasis: {
true: styles['emphasis'],
false: undefined,
},
size: {
xl: styles['size-xl'],
lg: styles['size-lg'],
md: styles['size-md'],
sm: styles['size-sm'],
},
}, },
style: { defaultVariants: {
Normal: 'Normal', variant: 'Filled',
Muted: 'Muted', size: 'lg',
Elevated: 'Elevated', emphasis: false,
Faded: 'Faded',
}, },
} as const } as const
export const config = {
variants: {
theme: {
[variantKeys.theme.Primary]: styles['theme-primary'],
[variantKeys.theme.Tertiary]: styles['theme-tertiary'],
[variantKeys.theme.Inverted]: styles['theme-inverted'],
[variantKeys.theme.Black]: styles['theme-black'],
},
// Some variants cannot be used in combination with certain style variants.
// The style variant will be applied using the compoundVariants.
style: {
[variantKeys.style.Normal]: '',
[variantKeys.style.Muted]: '',
[variantKeys.style.Elevated]: '',
[variantKeys.style.Faded]: '',
},
wrapping: {
true: styles['no-wrapping'],
false: undefined,
},
},
compoundVariants: [
// Primary should only use Normal
{ theme: variantKeys.theme.Primary, className: styles['style-normal'] },
// Tertiary should only use Elevated
{
theme: variantKeys.theme.Tertiary,
className: styles['style-elevated'],
},
// Black should only use Muted
{ theme: variantKeys.theme.Black, className: styles['style-muted'] },
// Inverted can use any style variant
{
theme: variantKeys.theme.Inverted,
style: variantKeys.style.Normal,
className: styles['style-normal'],
},
{
theme: variantKeys.theme.Inverted,
style: variantKeys.style.Muted,
className: styles['style-muted'],
},
{
theme: variantKeys.theme.Inverted,
style: variantKeys.style.Elevated,
className: styles['style-elevated'],
},
{
theme: variantKeys.theme.Inverted,
style: variantKeys.style.Faded,
className: styles['style-faded'],
},
],
defaultVariants: {
theme: variantKeys.theme.Primary,
style: variantKeys.style.Normal,
},
}
export const variants = cva(styles.iconButton, config) export const variants = cva(styles.iconButton, config)

View File

@@ -12,10 +12,10 @@ import { InputLabel } from '../InputLabel'
import styles from './input.module.css' import styles from './input.module.css'
import type { InputProps } from './types'
import { Typography } from '../Typography'
import { MaterialIcon } from '../Icons/MaterialIcon'
import { IconButton } from '../IconButton' import { IconButton } from '../IconButton'
import { MaterialIcon } from '../Icons/MaterialIcon'
import { Typography } from '../Typography'
import type { InputProps } from './types'
import { clearInput, useInputHasValue } from './utils' import { clearInput, useInputHasValue } from './utils'
const InputComponent = forwardRef(function AriaInputWithLabelComponent( const InputComponent = forwardRef(function AriaInputWithLabelComponent(
@@ -108,8 +108,9 @@ const InputComponent = forwardRef(function AriaInputWithLabelComponent(
<div className={styles.rightIconContainer}> <div className={styles.rightIconContainer}>
<IconButton <IconButton
className={styles.rightIconButton} className={styles.rightIconButton}
theme="Black" variant="Muted"
onClick={onClearContent} emphasis
onPress={onClearContent}
// eslint-disable-next-line formatjs/no-literal-string-in-jsx // eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Clear content" aria-label="Clear content"
> >
@@ -156,8 +157,9 @@ const InputComponent = forwardRef(function AriaInputWithLabelComponent(
<div className={styles.rightIconContainer}> <div className={styles.rightIconContainer}>
<IconButton <IconButton
className={styles.rightIconButton} className={styles.rightIconButton}
theme="Black" variant="Muted"
onClick={onClearContent} emphasis
onPress={onClearContent}
// eslint-disable-next-line formatjs/no-literal-string-in-jsx // eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Clear content" aria-label="Clear content"
> >

View File

@@ -87,8 +87,7 @@ export default function FullView({
return ( return (
<div className={styles.fullView}> <div className={styles.fullView}>
<IconButton <IconButton
theme="Inverted" variant="Muted"
style="Muted"
className={styles.closeButton} className={styles.closeButton}
onPress={onClose} onPress={onClose}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
@@ -139,16 +138,16 @@ export default function FullView({
</div> </div>
<IconButton <IconButton
theme="Inverted" variant="Muted"
className={`${styles.navigationButton} ${styles.prev}`} className={`${styles.navigationButton} ${styles.prev}`}
onClick={handlePrev} onPress={handlePrev}
> >
<MaterialIcon icon="arrow_back" color="CurrentColor" /> <MaterialIcon icon="arrow_back" color="CurrentColor" />
</IconButton> </IconButton>
<IconButton <IconButton
theme="Inverted" variant="Muted"
className={`${styles.navigationButton} ${styles.next}`} className={`${styles.navigationButton} ${styles.next}`}
onClick={handleNext} onPress={handleNext}
> >
<MaterialIcon icon="arrow_forward" color="CurrentColor" /> <MaterialIcon icon="arrow_forward" color="CurrentColor" />
</IconButton> </IconButton>

View File

@@ -88,8 +88,8 @@ export default function Gallery({
return ( return (
<div className={styles.gallery}> <div className={styles.gallery}>
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
className={styles.closeButton} className={styles.closeButton}
onPress={onClose} onPress={onClose}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
@@ -149,8 +149,7 @@ export default function Gallery({
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
<IconButton <IconButton
theme="Inverted" variant="Elevated"
style="Elevated"
className={cx(styles.navigationButton, styles.previous)} className={cx(styles.navigationButton, styles.previous)}
onPress={handlePrev} onPress={handlePrev}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
@@ -161,8 +160,7 @@ export default function Gallery({
<MaterialIcon icon="arrow_back" color="CurrentColor" /> <MaterialIcon icon="arrow_back" color="CurrentColor" />
</IconButton> </IconButton>
<IconButton <IconButton
theme="Inverted" variant="Elevated"
style="Elevated"
className={cx(styles.navigationButton, styles.next)} className={cx(styles.navigationButton, styles.next)}
onPress={handleNext} onPress={handleNext}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({

View File

@@ -142,8 +142,7 @@ export function InteractiveMap({
{closeButton} {closeButton}
<div className={styles.zoomButtons}> <div className={styles.zoomButtons}>
<IconButton <IconButton
theme="Inverted" variant="Elevated"
style="Elevated"
className={styles.zoomButton} className={styles.zoomButton}
onClick={zoomOut} onClick={zoomOut}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
@@ -156,8 +155,7 @@ export function InteractiveMap({
</IconButton> </IconButton>
<IconButton <IconButton
theme="Inverted" variant="Elevated"
style="Elevated"
className={styles.zoomButton} className={styles.zoomButton}
onClick={zoomIn} onClick={zoomIn}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({

View File

@@ -124,8 +124,8 @@ function InnerModal({
id: 'common.close', id: 'common.close',
defaultMessage: 'Close', defaultMessage: 'Close',
})} })}
theme="Black" variant="Muted"
style="Muted" emphasis
> >
<MaterialIcon <MaterialIcon
icon="close" icon="close"

View File

@@ -3,13 +3,13 @@ import { cx } from 'class-variance-authority'
import { Typography } from '../../Typography' import { Typography } from '../../Typography'
import { Rate, RateTermDetails } from '../types' import { Rate, RateTermDetails } from '../types'
import { Button as ButtonRAC } from 'react-aria-components'
import { useIntl } from 'react-intl'
import { IconButton } from '../../IconButton' import { IconButton } from '../../IconButton'
import { MaterialIcon } from '../../Icons/MaterialIcon' import { MaterialIcon } from '../../Icons/MaterialIcon'
import Modal from '../Modal' import Modal from '../Modal'
import styles from '../rate-card.module.css' import styles from '../rate-card.module.css'
import { variants } from '../variants' import { variants } from '../variants'
import { Button as ButtonRAC } from 'react-aria-components'
import { useIntl } from 'react-intl'
interface CampaignRateCardProps { interface CampaignRateCardProps {
id: string id: string
@@ -79,9 +79,9 @@ export default function CampaignRateCard({
subtitle={paymentTerm} subtitle={paymentTerm}
trigger={ trigger={
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
wrapping size="sm"
className={styles.triggerButton} className={styles.triggerButton}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: 'selectRate.rateCard.openReservationPolicy', id: 'selectRate.rateCard.openReservationPolicy',

View File

@@ -1,14 +1,14 @@
import { Rate, RateTermDetails } from '../types' import { Rate, RateTermDetails } from '../types'
import { cx } from 'class-variance-authority'
import { Button as ButtonRAC } from 'react-aria-components'
import { useIntl } from 'react-intl'
import { IconButton } from '../../IconButton' import { IconButton } from '../../IconButton'
import { MaterialIcon } from '../../Icons/MaterialIcon' import { MaterialIcon } from '../../Icons/MaterialIcon'
import { Typography } from '../../Typography' import { Typography } from '../../Typography'
import Modal from '../Modal' import Modal from '../Modal'
import styles from '../rate-card.module.css' import styles from '../rate-card.module.css'
import { variants } from '../variants' import { variants } from '../variants'
import { Button as ButtonRAC } from 'react-aria-components'
import { cx } from 'class-variance-authority'
import { useIntl } from 'react-intl'
interface CodeRateCardProps { interface CodeRateCardProps {
id: string id: string
@@ -69,9 +69,9 @@ export default function CodeRateCard({
subtitle={paymentTerm} subtitle={paymentTerm}
trigger={ trigger={
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
wrapping size="sm"
className={styles.triggerButton} className={styles.triggerButton}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: 'selectRate.rateCard.openReservationPolicy', id: 'selectRate.rateCard.openReservationPolicy',

View File

@@ -3,9 +3,9 @@
import { AnimatePresence, motion } from 'motion/react' import { AnimatePresence, motion } from 'motion/react'
import { type PropsWithChildren, useEffect, useState } from 'react' import { type PropsWithChildren, useEffect, useState } from 'react'
import { import {
Modal as AriaModal,
Dialog, Dialog,
DialogTrigger, DialogTrigger,
Modal as AriaModal,
ModalOverlay, ModalOverlay,
} from 'react-aria-components' } from 'react-aria-components'
@@ -17,11 +17,11 @@ import {
} from './modal' } from './modal'
import { fade, slideInOut } from './motionVariants' import { fade, slideInOut } from './motionVariants'
import styles from './modal.module.css'
import { Typography } from '../../Typography'
import { MaterialIcon } from '../../Icons/MaterialIcon'
import { IconButton } from '../../IconButton'
import { useIntl } from 'react-intl' import { useIntl } from 'react-intl'
import { IconButton } from '../../IconButton'
import { MaterialIcon } from '../../Icons/MaterialIcon'
import { Typography } from '../../Typography'
import styles from './modal.module.css'
const MotionOverlay = motion.create(ModalOverlay) const MotionOverlay = motion.create(ModalOverlay)
const MotionModal = motion.create(AriaModal) const MotionModal = motion.create(AriaModal)
@@ -91,8 +91,8 @@ function InnerModal({
)} )}
</div> </div>
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
onPress={close} onPress={close}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: 'common.close', id: 'common.close',

View File

@@ -34,7 +34,7 @@ export default function NoRateAvailableCard({
<header> <header>
<Typography variant="Tag/sm"> <Typography variant="Tag/sm">
<h3 className={`${styles.title} ${styles.textDisabled}`}> <h3 className={`${styles.title} ${styles.textDisabled}`}>
<IconButton theme="Black" style="Muted"> <IconButton variant="Muted" emphasis size="sm">
<MaterialIcon icon="info" size={20} color="Icon/Default" /> <MaterialIcon icon="info" size={20} color="Icon/Default" />
</IconButton> </IconButton>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}

View File

@@ -2,13 +2,13 @@ import { Typography } from '../../Typography'
import { RatePointsOption, RateTermDetails } from '../types' import { RatePointsOption, RateTermDetails } from '../types'
import { RadioGroup } from 'react-aria-components' import { RadioGroup } from 'react-aria-components'
import { useIntl } from 'react-intl'
import { IconButton } from '../../IconButton' import { IconButton } from '../../IconButton'
import { MaterialIcon } from '../../Icons/MaterialIcon' import { MaterialIcon } from '../../Icons/MaterialIcon'
import { Radio } from '../../Radio' import { Radio } from '../../Radio'
import Modal from '../Modal' import Modal from '../Modal'
import styles from '../rate-card.module.css' import styles from '../rate-card.module.css'
import { variants } from '../variants' import { variants } from '../variants'
import { useIntl } from 'react-intl'
interface PointsRateCardProps { interface PointsRateCardProps {
rateTitle: string rateTitle: string
@@ -56,9 +56,9 @@ export default function PointsRateCard({
subtitle={paymentTerm} subtitle={paymentTerm}
trigger={ trigger={
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
wrapping size="sm"
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: 'selectRate.rateCard.openReservationPolicy', id: 'selectRate.rateCard.openReservationPolicy',
defaultMessage: 'Open reservation policy', defaultMessage: 'Open reservation policy',

View File

@@ -1,14 +1,14 @@
import { Rate, RateTermDetails } from '../types' import { Rate, RateTermDetails } from '../types'
import { cx } from 'class-variance-authority'
import { Button as ButtonRAC } from 'react-aria-components'
import { useIntl } from 'react-intl'
import { IconButton } from '../../IconButton' import { IconButton } from '../../IconButton'
import { MaterialIcon } from '../../Icons/MaterialIcon' import { MaterialIcon } from '../../Icons/MaterialIcon'
import { Typography } from '../../Typography' import { Typography } from '../../Typography'
import Modal from '../Modal' import Modal from '../Modal'
import styles from '../rate-card.module.css' import styles from '../rate-card.module.css'
import { variants } from '../variants' import { variants } from '../variants'
import { Button as ButtonRAC } from 'react-aria-components'
import { cx } from 'class-variance-authority'
import { useIntl } from 'react-intl'
interface RegularRateCardProps { interface RegularRateCardProps {
id: string id: string
@@ -64,9 +64,9 @@ export default function RegularRateCard({
subtitle={paymentTerm} subtitle={paymentTerm}
trigger={ trigger={
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
wrapping size="sm"
className={styles.triggerButton} className={styles.triggerButton}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: 'selectRate.rateCard.openReservationPolicy', id: 'selectRate.rateCard.openReservationPolicy',

View File

@@ -65,8 +65,8 @@ export default function SidePeekSelfControlled({
</Typography> </Typography>
) : null} ) : null}
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
onPress={() => handleClose(true)} onPress={() => handleClose(true)}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: 'common.close', id: 'common.close',

View File

@@ -100,8 +100,8 @@ export default function SidePeek({
</Typography> </Typography>
) : null} ) : null}
<IconButton <IconButton
theme="Black" variant="Muted"
style="Muted" emphasis
aria-label={closeLabel} aria-label={closeLabel}
onPress={onClose} onPress={onClose}
> >

View File

@@ -1,12 +1,12 @@
import type { VariantProps } from 'class-variance-authority' import type { VariantProps } from 'class-variance-authority'
import { toastVariants } from './variants'
import { MaterialIcon, MaterialIconSetIconProps } from '../Icons/MaterialIcon' import { MaterialIcon, MaterialIconSetIconProps } from '../Icons/MaterialIcon'
import { toastVariants } from './variants'
import styles from './toasts.module.css'
import { Typography } from '../Typography'
import { useIntl } from 'react-intl' import { useIntl } from 'react-intl'
import { IconButton } from '../IconButton' import { IconButton } from '../IconButton'
import { Typography } from '../Typography'
import styles from './toasts.module.css'
export type ToastsProps = VariantProps<typeof toastVariants> & { export type ToastsProps = VariantProps<typeof toastVariants> & {
variant: NonNullable<VariantProps<typeof toastVariants>['variant']> variant: NonNullable<VariantProps<typeof toastVariants>['variant']>
@@ -39,12 +39,13 @@ export function Toast({ children, message, onClose, variant }: ToastsProps) {
)} )}
{onClose ? ( {onClose ? (
<IconButton <IconButton
onClick={onClose} onPress={onClose}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id: 'toast.dismissNotification', id: 'toast.dismissNotification',
defaultMessage: 'Dismiss notification', defaultMessage: 'Dismiss notification',
})} })}
theme="Black" variant="Muted"
emphasis
> >
<MaterialIcon icon="close" /> <MaterialIcon icon="close" />
</IconButton> </IconButton>

View File

@@ -19,3 +19,8 @@ ul {
margin-block-start: 0; margin-block-start: 0;
margin-block-end: 0; margin-block-end: 0;
} }
*:focus-visible {
outline-color: var(--Border-Interactive-Focus);
outline-offset: 2px;
}