From 4ec1e85d8469897c8fd8c600a014ea30929b6a87 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Mon, 15 Dec 2025 07:05:31 +0000 Subject: [PATCH] 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 --- .../shortcutsListItems.module.css | 2 +- .../CampaignBanner/campaignBanner.module.css | 1 - .../components/CampaignBanner/index.tsx | 3 +- .../Carousel/CarouselNavigation.tsx | 6 +- .../components/Carousel/carousel.module.css | 2 +- .../CityMapCard/index.tsx | 4 +- .../DestinationSearch/index.tsx | 4 +- .../DestinationPage/HotelMapCard/index.tsx | 4 +- .../Map/DynamicMap/dynamicMap.module.css | 1 - .../DestinationPage/Map/DynamicMap/index.tsx | 10 +- .../HotelMapPage/hotelMapPage.module.css | 1 - .../MainMenuButton/menuButton.module.css | 2 +- .../MainMenu/MenuLink/menuLink.module.css | 2 +- .../components/HotelFilterAndSort/index.tsx | 2 +- .../ViewAndPrintReceipt/view.module.css | 2 +- .../MyStay/Rooms/MultiRoom/Room.tsx | 4 +- .../Rooms/SingleRoom/Details/Terms/Terms.tsx | 7 +- .../components/MeetingPackageWidget/index.tsx | 4 +- .../LevelProgressModal/index.tsx | 2 +- .../Profile/DeleteCreditCardConfirmation.tsx | 3 +- .../Form/PasswordInput/index.tsx | 5 +- .../components/BookingCodeFilter/index.tsx | 8 +- .../FormContent/BookingCode/index.tsx | 2 +- .../FormContent/RewardNight/index.tsx | 2 +- .../FormContent/formContent.module.css | 25 +- .../BookingWidgetForm/FormContent/index.tsx | 76 ++- .../GuestsRoomsPicker/Counter/index.tsx | 10 +- .../guests-rooms-picker.module.css | 2 +- .../MobileToggleButton/button.module.css | 4 +- .../ListingHotelCardDialog/index.tsx | 4 +- .../Filters/FilterAndSortModal/index.tsx | 4 +- .../selectHotelMapContent.module.css | 2 - .../MobileSummary/Content/index.tsx | 4 +- .../RoomsHeader/RoomPackageFilter/Modal.tsx | 4 +- .../RoomsHeader/RoomPackageFilter/index.tsx | 4 +- .../lib/components/BookingCodeChip/index.tsx | 9 +- .../lib/components/Button/Button.stories.tsx | 9 +- .../lib/components/Button/button.module.css | 58 ++- .../StandaloneHotelCardDialog/index.tsx | 4 +- .../IconButton/IconButton.stories.tsx | 445 +++++++++++++----- .../lib/components/IconButton/IconButton.tsx | 18 +- .../IconButton/iconButton.module.css | 245 +++++++--- .../lib/components/IconButton/types.ts | 10 - .../lib/components/IconButton/variants.ts | 96 +--- .../lib/components/InputNew/Input.tsx | 16 +- .../components/Lightbox/FullView/index.tsx | 11 +- .../lib/components/Lightbox/Gallery/index.tsx | 10 +- .../components/Map/InteractiveMap/index.tsx | 6 +- .../lib/components/Modal/index.tsx | 4 +- .../components/RateCard/Campaign/index.tsx | 10 +- .../lib/components/RateCard/Code/index.tsx | 12 +- .../lib/components/RateCard/Modal/index.tsx | 14 +- .../RateCard/NoRateAvailable/index.tsx | 2 +- .../lib/components/RateCard/Points/index.tsx | 8 +- .../lib/components/RateCard/Regular/index.tsx | 12 +- .../components/SidePeek/SelfControlled.tsx | 4 +- .../lib/components/SidePeek/index.tsx | 4 +- .../lib/components/Toasts/Toast.tsx | 11 +- packages/design-system/lib/normalize.css | 5 + 59 files changed, 741 insertions(+), 504 deletions(-) delete mode 100644 packages/design-system/lib/components/IconButton/types.ts diff --git a/apps/scandic-web/components/Blocks/ShortcutsList/ShortcutsListItems/shortcutsListItems.module.css b/apps/scandic-web/components/Blocks/ShortcutsList/ShortcutsListItems/shortcutsListItems.module.css index 545dd2dfc..1f8423db0 100644 --- a/apps/scandic-web/components/Blocks/ShortcutsList/ShortcutsListItems/shortcutsListItems.module.css +++ b/apps/scandic-web/components/Blocks/ShortcutsList/ShortcutsListItems/shortcutsListItems.module.css @@ -22,6 +22,6 @@ } .link:focus-visible { - outline: 2px auto -webkit-focus-ring-color; + outline: 2px auto var(--Border-Interactive-Focus); outline-offset: -4px; } diff --git a/apps/scandic-web/components/CampaignBanner/campaignBanner.module.css b/apps/scandic-web/components/CampaignBanner/campaignBanner.module.css index 4f678775c..c91e5a8f9 100644 --- a/apps/scandic-web/components/CampaignBanner/campaignBanner.module.css +++ b/apps/scandic-web/components/CampaignBanner/campaignBanner.module.css @@ -53,7 +53,6 @@ } .closeButton { - flex-shrink: 0; z-index: 1; } diff --git a/apps/scandic-web/components/CampaignBanner/index.tsx b/apps/scandic-web/components/CampaignBanner/index.tsx index 226a0b457..35fc5c72e 100644 --- a/apps/scandic-web/components/CampaignBanner/index.tsx +++ b/apps/scandic-web/components/CampaignBanner/index.tsx @@ -119,8 +119,7 @@ export default function CampaignBanner() {
- + diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Upcoming/ManageStay/Actions/ViewAndPrintReceipt/view.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Upcoming/ManageStay/Actions/ViewAndPrintReceipt/view.module.css index 498bff0cf..8b98fde59 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Upcoming/ManageStay/Actions/ViewAndPrintReceipt/view.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Upcoming/ManageStay/Actions/ViewAndPrintReceipt/view.module.css @@ -7,5 +7,5 @@ } .download:focus-visible { - outline: 2px solid -webkit-focus-ring-color; + outline: 2px solid var(--Border-Interactive-Focus); } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/Room.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/Room.tsx index 2c2ddfb75..096dfba66 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/Room.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/Room.tsx @@ -265,8 +265,8 @@ export default function Room({ booking, roomNr, user }: RoomProps) { subtitle={rateTerm.paymentTerm} trigger={ + } diff --git a/apps/scandic-web/components/MeetingPackageWidget/index.tsx b/apps/scandic-web/components/MeetingPackageWidget/index.tsx index f331a0a04..50986a301 100644 --- a/apps/scandic-web/components/MeetingPackageWidget/index.tsx +++ b/apps/scandic-web/components/MeetingPackageWidget/index.tsx @@ -89,8 +89,8 @@ export default function MeetingPackageWidget(props: MeetingPackageWidgetProps) { <>
+ {visibilityToggleable ? ( setIsPasswordVisible((value) => !value)} + variant="Muted" + emphasis + onPress={() => setIsPasswordVisible((value) => !value)} aria-label={ isPasswordVisible ? intl.formatMessage({ diff --git a/packages/booking-flow/lib/components/BookingCodeFilter/index.tsx b/packages/booking-flow/lib/components/BookingCodeFilter/index.tsx index c291e2df3..f58978017 100644 --- a/packages/booking-flow/lib/components/BookingCodeFilter/index.tsx +++ b/packages/booking-flow/lib/components/BookingCodeFilter/index.tsx @@ -134,13 +134,7 @@ export default function BookingCodeFilter() { })} - { - close() - }} - > + + +
- + + +
) : null}
@@ -214,38 +210,26 @@ export function BookingWidgetFormContentSkeleton() {
- + + +
diff --git a/packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/Counter/index.tsx b/packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/Counter/index.tsx index 73c71ae2c..3b8834f49 100644 --- a/packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/Counter/index.tsx +++ b/packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/Counter/index.tsx @@ -25,9 +25,8 @@ export default function Counter({
@@ -37,9 +36,8 @@ export default function Counter({ diff --git a/packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/guests-rooms-picker.module.css b/packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/guests-rooms-picker.module.css index e65aa60bd..d6b537910 100644 --- a/packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/guests-rooms-picker.module.css +++ b/packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/guests-rooms-picker.module.css @@ -102,7 +102,7 @@ .addRoomBtn:is(:focus, :focus-visible, :focus-within), .footer .hideOnMobile .addRoomBtn: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; } diff --git a/packages/booking-flow/lib/components/BookingWidget/MobileToggleButton/button.module.css b/packages/booking-flow/lib/components/BookingWidget/MobileToggleButton/button.module.css index eea5db500..17ef9ddec 100644 --- a/packages/booking-flow/lib/components/BookingWidget/MobileToggleButton/button.module.css +++ b/packages/booking-flow/lib/components/BookingWidget/MobileToggleButton/button.module.css @@ -52,10 +52,10 @@ background-color: var(--Base-Button-Primary-Fill-Normal); border-radius: 50%; display: flex; - height: 36px; + height: 40px; justify-content: center; justify-self: flex-end; - width: 36px; + width: 40px; } @media screen and (min-width: 768px) { diff --git a/packages/booking-flow/lib/components/ListingHotelCardDialog/index.tsx b/packages/booking-flow/lib/components/ListingHotelCardDialog/index.tsx index 2adbfd7c7..e731a09c0 100644 --- a/packages/booking-flow/lib/components/ListingHotelCardDialog/index.tsx +++ b/packages/booking-flow/lib/components/ListingHotelCardDialog/index.tsx @@ -67,8 +67,8 @@ export default function ListingHotelCardDialog({ return (
setIsOpen(false)} > diff --git a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/index.tsx b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/index.tsx index 2b8af938d..53d6338f1 100644 --- a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/index.tsx +++ b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/index.tsx @@ -99,9 +99,7 @@ export function RoomPackageFilter({ roomIndex }: { roomIndex: number }) { /> {filterLabel} deletePackage(pkg.code)} aria-label={intl.formatMessage( { diff --git a/packages/design-system/lib/components/BookingCodeChip/index.tsx b/packages/design-system/lib/components/BookingCodeChip/index.tsx index f7ebd610f..afc26a891 100644 --- a/packages/design-system/lib/components/BookingCodeChip/index.tsx +++ b/packages/design-system/lib/components/BookingCodeChip/index.tsx @@ -1,13 +1,13 @@ import { useIntl } from 'react-intl' import IconChip from '../IconChip' -import FilledDiscountIcon from '../Icons/Nucleo/Benefits/FilledDiscount' import { MaterialIcon } from '../Icons/MaterialIcon' +import FilledDiscountIcon from '../Icons/Nucleo/Benefits/FilledDiscount' import { Typography } from '../Typography' -import styles from './bookingCodeChip.module.css' import { cx } from 'class-variance-authority' import { IconButton } from '../IconButton' +import styles from './bookingCodeChip.module.css' type BaseBookingCodeChipProps = { alignCenter?: boolean @@ -102,9 +102,8 @@ export function BookingCodeChip({

{withCloseButton && ( = { summary: buttonConfig.defaultVariants.variant, }, type: { - summary: 'string', - detail: Object.keys(buttonConfig.variants.variant).join(' | '), + summary: Object.keys(buttonConfig.variants.variant).join(' | '), }, }, }, @@ -39,8 +38,7 @@ const meta: Meta = { options: Object.keys(buttonConfig.variants.color), table: { type: { - summary: 'string', - detail: Object.keys(buttonConfig.variants.color).join(' | '), + summary: Object.keys(buttonConfig.variants.color).join(' | '), }, defaultValue: { summary: buttonConfig.defaultVariants.color, @@ -52,8 +50,7 @@ const meta: Meta = { options: Object.keys(buttonConfig.variants.size), table: { type: { - summary: 'string', - detail: Object.keys(buttonConfig.variants.size).join(' | '), + summary: Object.keys(buttonConfig.variants.size).join(' | '), }, defaultValue: { summary: buttonConfig.defaultVariants.size, diff --git a/packages/design-system/lib/components/Button/button.module.css b/packages/design-system/lib/components/Button/button.module.css index 118dc4e5e..8fa39963d 100644 --- a/packages/design-system/lib/components/Button/button.module.css +++ b/packages/design-system/lib/components/Button/button.module.css @@ -21,23 +21,28 @@ &:focus-visible { outline: 2px solid var(--Border-Interactive-Focus); 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 { - padding: var(--Space-x2) var(--Space-x3); + padding: calc(var(--Space-x2) - 2px) var(--Space-x3); /* Adjust for 2px border */ } .size-medium { - padding: var(--Space-x15) var(--Space-x2); + padding: calc(var(--Space-x15) - 2px) var(--Space-x2); /* Adjust for 2px border */ } .size-small { - padding: 10px var(--Space-x2); + padding: var(--Space-x1) var(--Space-x2); /* Adjust for 2px border */ } .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] { background-color: var(--Component-Button-Brand-Primary-Fill-Disabled); border-color: var(--Component-Button-Brand-Primary-Border-Disabled); @@ -103,6 +97,14 @@ border-color: var(--Component-Button-Inverted-Border-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 { @@ -150,6 +152,14 @@ border-color: var(--Component-Button-Brand-Secondary-Border-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 { @@ -229,6 +239,10 @@ .variant-text.no-wrapping { padding: var(--Space-x025) 0; border-width: 0; + + &:focus-visible { + outline-offset: 4px; + } } .variant-text.color-inverted { @@ -246,6 +260,14 @@ &[data-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 { diff --git a/packages/design-system/lib/components/HotelCard/HotelDialogCard/StandaloneHotelCardDialog/index.tsx b/packages/design-system/lib/components/HotelCard/HotelDialogCard/StandaloneHotelCardDialog/index.tsx index 10dfb5d12..14556161d 100644 --- a/packages/design-system/lib/components/HotelCard/HotelDialogCard/StandaloneHotelCardDialog/index.tsx +++ b/packages/design-system/lib/components/HotelCard/HotelDialogCard/StandaloneHotelCardDialog/index.tsx @@ -76,8 +76,8 @@ export function StandaloneHotelCardDialog({ return (
= { disable: true, }, }, - theme: { + variant: { control: 'select', - options: Object.keys(config.variants.theme), + options: Object.keys(config.variants.variant), table: { defaultValue: { - summary: config.defaultVariants.theme, + summary: config.defaultVariants.variant, }, type: { - summary: 'string', - detail: Object.keys(config.variants.theme).join(' | '), + summary: Object.keys(config.variants.variant).join(' | '), }, }, }, - style: { + size: { control: 'select', - options: Object.keys(config.variants.style), + options: Object.keys(config.variants.size), table: { defaultValue: { - summary: config.defaultVariants.style, + summary: config.defaultVariants.size, }, type: { - summary: 'string', - detail: Object.keys(config.variants.style).join(' | '), + summary: Object.keys(config.variants.size).join(' | '), }, }, 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 = { backgrounds: { value: 'scandicPrimaryDark' }, } @@ -58,6 +102,36 @@ export default meta type Story = StoryObj +function renderAllSizesFn( + args: Story['args'], + iconName: MaterialIconProps['icon'] = 'search' +) { + return ( +
+ {buttonAndIconSizesMap.map(({ size, iconSize }) => ( +
+ + + + {size} +
+ ))} +
+ ) +} + export const Default: Story = { args: { onPress: fn(), @@ -69,11 +143,150 @@ export const Default: Story = { }, } -export const Primary: Story = { +export const Examples: Story = { + render: () => { + return ( +
+
+

Filled

+
+ {renderAllSizesFn({ ...Default.args, variant: 'Filled' })} +
+
+
+

Filled with emphasis

+
+ {renderAllSizesFn( + { + ...Default.args, + variant: 'Filled', + emphasis: true, + }, + 'arrow_forward' + )} +
+
+
+

Outlined

+
+ {renderAllSizesFn( + { + ...Default.args, + variant: 'Outlined', + }, + 'arrow_forward' + )} +
+
+
+

Elevated

+
+ {renderAllSizesFn( + { + ...Default.args, + variant: 'Elevated', + }, + 'arrow_forward' + )} +
+
+
+

Faded

+
+ {renderAllSizesFn( + { + ...Default.args, + variant: 'Faded', + }, + 'arrow_forward' + )} +
+
+
+

Muted

+
+ {renderAllSizesFn( + { + ...Default.args, + variant: 'Muted', + }, + 'arrow_forward' + )} +
+
+
+

Muted with emphasis

+
+ {renderAllSizesFn( + { + ...Default.args, + variant: 'Muted', + emphasis: true, + }, + 'arrow_forward' + )} +
+
+
+ ) + }, +} +export const Filled: Story = { args: { ...Default.args, - theme: 'Primary', - onPress: fn(), // Fresh spy instance + variant: 'Filled', }, play: async ({ canvas, userEvent, args }) => { await userEvent.click(canvas.getByRole('button')) @@ -81,11 +294,10 @@ export const Primary: Story = { }, } -export const PrimaryDisabled: Story = { +export const FilledDisabled: Story = { args: { - ...Primary.args, + ...Filled.args, isDisabled: true, - onPress: fn(), // Fresh spy instance for disabled test }, play: async ({ canvas, userEvent, args }) => { 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: ( + + ), + 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: { ...Default.args, children: ( ), - theme: 'Inverted', + variant: 'Outlined', }, play: async ({ canvas, userEvent, args }) => { await userEvent.click(canvas.getByRole('button')) @@ -107,11 +355,10 @@ export const Inverted: Story = { }, } -export const InvertedDisabled: Story = { +export const OutlinedDisabled: Story = { args: { - ...Inverted.args, + ...Outlined.args, isDisabled: true, - onPress: fn(), // Fresh spy instance for disabled test }, play: async ({ canvas, userEvent, args }) => { await userEvent.click(canvas.getByRole('button')) @@ -119,86 +366,13 @@ export const InvertedDisabled: Story = { }, } -export const InvertedElevated: 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: , - 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 = { +export const Elevated: Story = { args: { ...Default.args, - children: , - theme: 'Tertiary', - style: 'Elevated', + children: ( + + ), + variant: 'Elevated', }, play: async ({ canvas, userEvent, args }) => { await userEvent.click(canvas.getByRole('button')) @@ -206,11 +380,10 @@ export const TertiaryElevated: Story = { }, } -export const TertiaryDisabled: Story = { +export const ElevatedDisabled: Story = { args: { - ...TertiaryElevated.args, + ...Elevated.args, isDisabled: true, - onPress: fn(), // Fresh spy instance for disabled test }, play: async ({ canvas, userEvent, args }) => { 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: { ...Default.args, - children: , - theme: 'Black', + children: ( + + ), + variant: 'Faded', }, play: async ({ canvas, userEvent, args }) => { 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: { - ...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: ( + + ), + 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, - onPress: fn(), // Fresh spy instance for disabled test }, play: async ({ canvas, userEvent, args }) => { await userEvent.click(canvas.getByRole('button')) diff --git a/packages/design-system/lib/components/IconButton/IconButton.tsx b/packages/design-system/lib/components/IconButton/IconButton.tsx index a327ed1c1..555d0ca26 100644 --- a/packages/design-system/lib/components/IconButton/IconButton.tsx +++ b/packages/design-system/lib/components/IconButton/IconButton.tsx @@ -1,20 +1,24 @@ import { Button as ButtonRAC } from 'react-aria-components' +import { VariantProps } from 'class-variance-authority' +import { ComponentProps } from 'react' import { variants } from './variants' -import type { IconButtonProps } from './types' +interface IconButtonProps + extends ComponentProps, + VariantProps {} export function IconButton({ - theme, - style, + variant, + emphasis, + size, className, - wrapping, ...props }: IconButtonProps) { const classNames = variants({ - theme, - style, - wrapping, + variant, + emphasis, + size, className, }) diff --git a/packages/design-system/lib/components/IconButton/iconButton.module.css b/packages/design-system/lib/components/IconButton/iconButton.module.css index 302ee443b..64f97c1b3 100644 --- a/packages/design-system/lib/components/IconButton/iconButton.module.css +++ b/packages/design-system/lib/components/IconButton/iconButton.module.css @@ -1,12 +1,14 @@ .iconButton { position: relative; - border-radius: var(--Corner-radius-rounded); - border-width: 0; - cursor: pointer; - display: inline-flex; - align-items: center; + display: flex; + padding: 0; 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] { cursor: unset; @@ -15,13 +17,47 @@ &:focus-visible { outline: 2px solid var(--Border-Interactive-Focus); 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); 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) { &:hover:not([data-disabled]) { background: @@ -35,26 +71,46 @@ } } - /* This theme 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 */ - &:focus-visible::after { - content: ''; - position: absolute; - inset: -2px; - border: 2px solid var(--Border-Inverted); - border-radius: inherit; - pointer-events: none; - } + &.emphasis { + background-color: var(--Component-Button-Brand-Tertiary-Fill-Default); + color: var(--Component-Button-Brand-Tertiary-On-fill-Default); - &[data-disabled] { - background-color: var(--Component-Button-Brand-Primary-Fill-Disabled); - color: var(--Component-Button-Brand-Primary-On-fill-Disabled); + &[data-disabled] { + background-color: var(--Component-Button-Brand-Tertiary-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); - 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) { &:hover:not([data-disabled]) { @@ -68,90 +124,125 @@ } } - &[data-disabled] { - background-color: var(--Component-Button-Inverted-Fill-Disabled); - color: var(--Component-Button-Inverted-On-fill-Disabled); - } + &:focus-visible { + outline-offset: 0; - &.style-muted { - background-color: var(--Component-Button-Muted-Fill-Default); - 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); + &::before { + inset: -5px; } } } -.theme-tertiary { - background-color: var(--Component-Button-Brand-Tertiary-Fill-Default); - color: var(--Component-Button-Brand-Tertiary-On-fill-Default); +.variant-elevated { + background-color: var(--Component-Button-Inverted-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) { &: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-Inverted-Fill-Hover) 0%, + var(--Component-Button-Inverted-Fill-Hover) 100% ), - var(--Component-Button-Brand-Tertiary-Fill-Default); - color: var(--Component-Button-Brand-Tertiary-On-fill-Hover); + var(--Component-Button-Inverted-Fill-Default); } } - &[data-disabled] { - background-color: var(--Component-Button-Brand-Tertiary-Fill-Disabled); - color: var(--Component-Button-Brand-Tertiary-On-fill-Disabled); - } -} + &:focus-visible { + outline-offset: 0; -.theme-black { - color: var(--Component-Button-Muted-On-fill-Default); - - @media (hover: hover) { - &:hover:not([data-disabled]) { - color: var(--Component-Button-Muted-On-fill-Hover-Inverted); + &::before { + inset: -4px; } } - - &[data-disabled] { - color: var(--Component-Button-Muted-On-fill-Disabled); - } } -.style-elevated { - box-shadow: 0px 0px 8px 1px #0000001a; -} - -.style-faded { +.variant-faded { background-color: var(--Component-Button-Inverted-Fill-Faded); -} + color: var(--Icon-Interactive-Default); -.style-muted { - background-color: var(--Component-Button-Muted-Fill-Default); + &[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-Default); + color: var(--Component-Button-Brand-Primary-On-fill-Disabled); + } @media (hover: hover) { &: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] { - background-color: var(--Component-Button-Muted-Fill-Disabled-inverted); + &:focus-visible { + outline-offset: 0; + + &::before { + inset: -4px; + } } } -.no-wrapping { - padding: 0; +.variant-muted { + 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); + } + } + } } diff --git a/packages/design-system/lib/components/IconButton/types.ts b/packages/design-system/lib/components/IconButton/types.ts deleted file mode 100644 index 2c84912b8..000000000 --- a/packages/design-system/lib/components/IconButton/types.ts +++ /dev/null @@ -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, 'style'>, - VariantProps {} diff --git a/packages/design-system/lib/components/IconButton/variants.ts b/packages/design-system/lib/components/IconButton/variants.ts index c84eca8ad..a6c842b52 100644 --- a/packages/design-system/lib/components/IconButton/variants.ts +++ b/packages/design-system/lib/components/IconButton/variants.ts @@ -2,81 +2,31 @@ import { cva } from 'class-variance-authority' import styles from './iconButton.module.css' -const variantKeys = { - theme: { - Primary: 'Primary', - Tertiary: 'Tertiary', - Inverted: 'Inverted', - Black: 'Black', +export const config = { + variants: { + variant: { + Filled: styles['variant-filled'], + Outlined: styles['variant-outlined'], + 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: { - Normal: 'Normal', - Muted: 'Muted', - Elevated: 'Elevated', - Faded: 'Faded', + defaultVariants: { + variant: 'Filled', + size: 'lg', + emphasis: false, }, } 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) diff --git a/packages/design-system/lib/components/InputNew/Input.tsx b/packages/design-system/lib/components/InputNew/Input.tsx index 76aa70fcc..328d575e4 100644 --- a/packages/design-system/lib/components/InputNew/Input.tsx +++ b/packages/design-system/lib/components/InputNew/Input.tsx @@ -12,10 +12,10 @@ import { InputLabel } from '../InputLabel' 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 { MaterialIcon } from '../Icons/MaterialIcon' +import { Typography } from '../Typography' +import type { InputProps } from './types' import { clearInput, useInputHasValue } from './utils' const InputComponent = forwardRef(function AriaInputWithLabelComponent( @@ -108,8 +108,9 @@ const InputComponent = forwardRef(function AriaInputWithLabelComponent(
@@ -156,8 +157,9 @@ const InputComponent = forwardRef(function AriaInputWithLabelComponent(
diff --git a/packages/design-system/lib/components/Lightbox/FullView/index.tsx b/packages/design-system/lib/components/Lightbox/FullView/index.tsx index 06f41c66e..d98c053ff 100644 --- a/packages/design-system/lib/components/Lightbox/FullView/index.tsx +++ b/packages/design-system/lib/components/Lightbox/FullView/index.tsx @@ -87,8 +87,7 @@ export default function FullView({ return (
diff --git a/packages/design-system/lib/components/Lightbox/Gallery/index.tsx b/packages/design-system/lib/components/Lightbox/Gallery/index.tsx index b5839e64c..2d3049088 100644 --- a/packages/design-system/lib/components/Lightbox/Gallery/index.tsx +++ b/packages/design-system/lib/components/Lightbox/Gallery/index.tsx @@ -88,8 +88,8 @@ export default function Gallery({ return (

- + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} diff --git a/packages/design-system/lib/components/RateCard/Points/index.tsx b/packages/design-system/lib/components/RateCard/Points/index.tsx index e4e92bec1..7bf36d582 100644 --- a/packages/design-system/lib/components/RateCard/Points/index.tsx +++ b/packages/design-system/lib/components/RateCard/Points/index.tsx @@ -2,13 +2,13 @@ import { Typography } from '../../Typography' import { RatePointsOption, RateTermDetails } from '../types' import { RadioGroup } from 'react-aria-components' +import { useIntl } from 'react-intl' import { IconButton } from '../../IconButton' import { MaterialIcon } from '../../Icons/MaterialIcon' import { Radio } from '../../Radio' import Modal from '../Modal' import styles from '../rate-card.module.css' import { variants } from '../variants' -import { useIntl } from 'react-intl' interface PointsRateCardProps { rateTitle: string @@ -56,9 +56,9 @@ export default function PointsRateCard({ subtitle={paymentTerm} trigger={ ) : null} handleClose(true)} aria-label={intl.formatMessage({ id: 'common.close', diff --git a/packages/design-system/lib/components/SidePeek/index.tsx b/packages/design-system/lib/components/SidePeek/index.tsx index 38cd698a3..70fc4715e 100644 --- a/packages/design-system/lib/components/SidePeek/index.tsx +++ b/packages/design-system/lib/components/SidePeek/index.tsx @@ -100,8 +100,8 @@ export default function SidePeek({ ) : null} diff --git a/packages/design-system/lib/components/Toasts/Toast.tsx b/packages/design-system/lib/components/Toasts/Toast.tsx index d78ff1675..0d8fab4bb 100644 --- a/packages/design-system/lib/components/Toasts/Toast.tsx +++ b/packages/design-system/lib/components/Toasts/Toast.tsx @@ -1,12 +1,12 @@ import type { VariantProps } from 'class-variance-authority' -import { toastVariants } from './variants' 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 { IconButton } from '../IconButton' +import { Typography } from '../Typography' +import styles from './toasts.module.css' export type ToastsProps = VariantProps & { variant: NonNullable['variant']> @@ -39,12 +39,13 @@ export function Toast({ children, message, onClose, variant }: ToastsProps) { )} {onClose ? ( diff --git a/packages/design-system/lib/normalize.css b/packages/design-system/lib/normalize.css index 4a4c37a6b..85e2d9a68 100644 --- a/packages/design-system/lib/normalize.css +++ b/packages/design-system/lib/normalize.css @@ -19,3 +19,8 @@ ul { margin-block-start: 0; margin-block-end: 0; } + +*:focus-visible { + outline-color: var(--Border-Interactive-Focus); + outline-offset: 2px; +}