From 8b32abbefc81dd79f7136904afb9762c5c6b25e6 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Fri, 2 May 2025 06:27:30 +0000 Subject: [PATCH] Fix/SW-1563 accessibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(SW-1563): Added new IconButton component to the design system and removed Icon variant inside the Button component * fix(SW-1563): Added buttons around clickable images and changed to design system components * fix(SW-1563): Renamed variants to match Figma * fix(SW-1563): Renamed AriaButton to ButtonRAC Approved-by: Michael Zetterberg Approved-by: Matilda Landström --- .../CityMap/HotelListItem/index.tsx | 6 +- .../DestinationPage/HotelMapCard/index.tsx | 10 +- .../HotelMapPage/Sidebar/index.tsx | 10 +- .../HotelPage/PreviewImages/index.tsx | 62 +++- .../PreviewImages/previewImages.module.css | 15 +- .../EnterDetails/Summary/UI/index.tsx | 8 +- .../RateSummary/MobileSummary/Summary.tsx | 5 +- .../RoomsHeader/RoomPackageFilter/Modal.tsx | 10 +- .../RoomsHeader/RoomPackageFilter/index.tsx | 6 +- .../Lightbox/FullView/fullView.module.css | 126 +++++++ .../{FullView.tsx => FullView/index.tsx} | 64 ++-- .../Lightbox/Gallery/gallery.module.css | 160 ++++++++ .../{Gallery.tsx => Gallery/index.tsx} | 147 ++++---- .../components/Lightbox/Lightbox.module.css | 348 ------------------ .../scandic-web/components/Lightbox/index.tsx | 2 +- .../components/Lightbox/lightbox.module.css | 57 +++ .../lib/components/Button/Button.stories.tsx | 25 +- .../lib/components/Button/Button.tsx | 2 - .../lib/components/Button/button.module.css | 9 +- .../lib/components/Button/variants.ts | 1 - .../IconButton/IconButton.stories.tsx | 141 +++++++ .../lib/components/IconButton/IconButton.tsx | 20 + .../IconButton/iconButton.module.css | 102 +++++ .../lib/components/IconButton/index.tsx | 1 + .../lib/components/IconButton/types.ts | 10 + .../lib/components/IconButton/variants.ts | 78 ++++ .../components/RateCard/Campaign/index.tsx | 6 +- .../lib/components/RateCard/Code/index.tsx | 6 +- .../RateCard/NoRateAvailable/index.tsx | 6 +- .../lib/components/RateCard/Points/index.tsx | 6 +- .../lib/components/RateCard/Regular/index.tsx | 6 +- packages/design-system/package.json | 1 + 32 files changed, 909 insertions(+), 547 deletions(-) create mode 100644 apps/scandic-web/components/Lightbox/FullView/fullView.module.css rename apps/scandic-web/components/Lightbox/{FullView.tsx => FullView/index.tsx} (65%) create mode 100644 apps/scandic-web/components/Lightbox/Gallery/gallery.module.css rename apps/scandic-web/components/Lightbox/{Gallery.tsx => Gallery/index.tsx} (58%) delete mode 100644 apps/scandic-web/components/Lightbox/Lightbox.module.css create mode 100644 apps/scandic-web/components/Lightbox/lightbox.module.css create mode 100644 packages/design-system/lib/components/IconButton/IconButton.stories.tsx create mode 100644 packages/design-system/lib/components/IconButton/IconButton.tsx create mode 100644 packages/design-system/lib/components/IconButton/iconButton.module.css create mode 100644 packages/design-system/lib/components/IconButton/index.tsx create mode 100644 packages/design-system/lib/components/IconButton/types.ts create mode 100644 packages/design-system/lib/components/IconButton/variants.ts diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/index.tsx index a4715ed85..14e21d550 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/index.tsx @@ -1,7 +1,7 @@ "use client" import { useCallback, useEffect, useRef } from "react" -import { Button as AriaButton } from "react-aria-components" +import { Button as ButtonRAC } from "react-aria-components" import { useIntl } from "react-intl" import HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon" @@ -94,12 +94,12 @@ export default function HotelListItem(data: DestinationPagesHotelData) {
- setActiveMarker(hotel.id)} > {address} - +

diff --git a/apps/scandic-web/components/ContentType/DestinationPage/HotelMapCard/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/HotelMapCard/index.tsx index 69f96c5dd..d3241d9c0 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/HotelMapCard/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/HotelMapCard/index.tsx @@ -2,7 +2,7 @@ import { useState } from "react" import { useIntl } from "react-intl" -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 { Typography } from "@scandic-hotels/design-system/Typography" @@ -48,9 +48,9 @@ export default function HotelMapCard({ return (

- + {image ? ( - {isFullScreenSidebar ? viewAsMapMsg : viewAsListMsg} - +
@@ -168,7 +168,7 @@ export default function Sidebar({ {pois.map((poi) => (
  • - - +
  • ))} diff --git a/apps/scandic-web/components/ContentType/HotelPage/PreviewImages/index.tsx b/apps/scandic-web/components/ContentType/HotelPage/PreviewImages/index.tsx index 2e462d5e7..f4a362632 100644 --- a/apps/scandic-web/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/apps/scandic-web/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -1,11 +1,13 @@ "use client" import { useState } from "react" +import { Button as ButtonRAC } from "react-aria-components" import { useIntl } from "react-intl" +import { Button } from "@scandic-hotels/design-system/Button" + import Image from "@/components/Image" import Lightbox from "@/components/Lightbox/" -import Button from "@/components/TempDesignSystem/Button" import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" import styles from "./previewImages.module.css" @@ -17,31 +19,52 @@ export default function PreviewImages({ hotelName, }: PreviewImagesProps) { const intl = useIntl() - const [lightboxIsOpen, setLightboxIsOpen] = useState(false) + const [lightboxState, setLightboxState] = useState({ + activeIndex: 0, + isOpen: false, + }) const lightboxImages = mapApiImagesToGalleryImages(images) return (
    - {images.slice(0, 3).map((image, index) => ( - {image.metaData.altText} setLightboxIsOpen(true)} - className={styles.image} - /> + {lightboxImages.slice(0, 3).map((image, index) => ( + + setLightboxState({ + activeIndex: index, + isOpen: true, + }) + } + > + {image.alt} + ))} {images.length > 1 && ( <> + {rooms.map(({ room }, idx) => { diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx index 3490af59c..07f258ccc 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx @@ -3,6 +3,7 @@ import { Fragment } from "react" import { useIntl } from "react-intl" 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 { dt } from "@/lib/dt" @@ -88,13 +89,13 @@ export default function Summary({ {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} {dt(booking.toDate).locale(lang).format("ddd, D MMM")} ({nights}) - + {rooms.map((room, idx) => { diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/Modal.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/Modal.tsx index e048de418..8119b0c39 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/Modal.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/Modal.tsx @@ -7,8 +7,8 @@ import { } from "react-aria-components" import { useIntl } from "react-intl" -import { Button } from "@scandic-hotels/design-system/Button" import { ChipButton } from "@scandic-hotels/design-system/ChipButton" +import { IconButton } from "@scandic-hotels/design-system/IconButton" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { Typography } from "@scandic-hotels/design-system/Typography" @@ -39,9 +39,13 @@ export default function RoomPackageFilterModal() { {intl.formatMessage({ defaultMessage: "Special needs" })} - +
    setIsOpen(false)} /> diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/index.tsx index d16c67147..cc4095c9f 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/index.tsx @@ -1,5 +1,5 @@ "use client" -import { Button as AriaButton } from "react-aria-components" +import { Button as ButtonRAC } from "react-aria-components" import { useMediaQuery } from "usehooks-ts" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" @@ -69,12 +69,12 @@ export default function RoomPackageFilter() { color="CurrentColor" /> {pkg.description} - deleteSelectedPackage(pkg.code)} className={styles.removeButton} > - + ))} diff --git a/apps/scandic-web/components/Lightbox/FullView/fullView.module.css b/apps/scandic-web/components/Lightbox/FullView/fullView.module.css new file mode 100644 index 000000000..1985516e1 --- /dev/null +++ b/apps/scandic-web/components/Lightbox/FullView/fullView.module.css @@ -0,0 +1,126 @@ +.fullViewContainer { + background-color: var(--UI-Text-High-contrast); + height: 100%; + padding: var(--Spacing-x3) var(--Spacing-x2); + position: relative; + align-items: center; + justify-items: center; + display: grid; + grid-template-rows: auto 1fr auto; + grid-template-columns: 1fr; + place-content: center; + gap: var(--Spacing-x5); +} + +.closeButton { + position: absolute; + top: var(--Space-x2); + right: var(--Space-x2); + z-index: 1; +} + +.header { + display: flex; + justify-content: center; + width: 100%; +} + +.imageCount { + background-color: var(--Overlay-90); + padding: var(--Space-x025) var(--Space-x05); + border-radius: var(--Corner-radius-Small); + color: var(--Text-Inverted); +} + +.imageContainer { + position: relative; + width: 100%; + height: 100%; + max-height: 25rem; + margin-bottom: var(--Spacing-x5); +} + +.imageWrapper { + position: absolute; + width: 100%; + height: 100%; +} + +.image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.footer { + color: var(--Text-Inverted); + position: absolute; + bottom: calc(-1 * var(--Spacing-x5)); +} + +@media screen and (max-width: 767px) { + .navigationButton { + display: none; + } +} + +@media screen and (min-width: 768px) and (max-width: 1366px) { + .fullViewContainer { + padding: var(--Spacing-x5); + } + + .imageContainer { + height: 100%; + max-height: 560px; + } +} + +@media screen and (min-width: 768px) { + .closeButton { + position: fixed; + top: var(--Spacing-x-one-and-half); + right: var(--Spacing-x-half); + } + + .fullViewContainer { + margin-top: 0; + padding: var(--Spacing-x5); + grid-template-rows: auto 1fr auto; + width: 100%; + height: 100%; + } + + .imageContainer { + width: 70%; + max-width: 1454px; + max-height: 700px; + } + + .navigationButton { + position: absolute; + top: 50%; + transform: translateY(-50%); + background-color: var(--Component-Button-Inverted-Fill-Default); + color: var(--Component-Button-Inverted-On-fill-Default); + border-radius: var(--Corner-radius-rounded); + padding: 10px; + cursor: pointer; + border-width: 0; + display: flex; + z-index: 1; + box-shadow: 0px 0px 8px 1px #0000001a; + + &:hover { + background-color: var(--Component-Button-Inverted-Fill-Hover); + color: var(--Component-Button-Inverted-On-fill-Hover); + } + } + + .fullViewNextButton { + right: var(--Spacing-x5); + } + + .fullViewPrevButton { + left: var(--Spacing-x5); + } +} diff --git a/apps/scandic-web/components/Lightbox/FullView.tsx b/apps/scandic-web/components/Lightbox/FullView/index.tsx similarity index 65% rename from apps/scandic-web/components/Lightbox/FullView.tsx rename to apps/scandic-web/components/Lightbox/FullView/index.tsx index 00cc3c44f..768522bce 100644 --- a/apps/scandic-web/components/Lightbox/FullView.tsx +++ b/apps/scandic-web/components/Lightbox/FullView/index.tsx @@ -2,15 +2,15 @@ import { AnimatePresence, motion } from "framer-motion" import { useState } from "react" +import { useIntl } from "react-intl" +import { IconButton } from "@scandic-hotels/design-system/IconButton" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" import Image from "@/components/Image" -import Button from "@/components/TempDesignSystem/Button" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import styles from "./Lightbox.module.css" +import styles from "./fullView.module.css" import type { FullViewProps } from "@/types/components/lightbox/lightbox" @@ -23,6 +23,7 @@ export default function FullView({ totalImages, hideLabel, }: FullViewProps) { + const intl = useIntl() const [animateLeft, setAnimateLeft] = useState(true) function handleSwipe(offset: number) { @@ -54,29 +55,26 @@ export default function FullView({ return (
    - -
    - - + + +
    + + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} {`${currentIndex + 1} / ${totalImages}`} - - + +
    -
    +
    handleSwipe(info.offset.x)} > @@ -95,14 +93,14 @@ export default function FullView({ fill sizes="(min-width: 1500px) 1500px, 100vw" src={image.src} - style={{ objectFit: "cover" }} + className={styles.image} /> -
    - {image.caption && !hideLabel && ( - {image.caption} - )} -
    + {image.caption && !hideLabel ? ( + +

    {image.caption}

    +
    + ) : null}
    @@ -112,8 +110,8 @@ export default function FullView({ onClick={handlePrev} > @@ -121,7 +119,7 @@ export default function FullView({ className={`${styles.navigationButton} ${styles.fullViewNextButton}`} onClick={handleNext} > - +
    ) diff --git a/apps/scandic-web/components/Lightbox/Gallery/gallery.module.css b/apps/scandic-web/components/Lightbox/Gallery/gallery.module.css new file mode 100644 index 000000000..b8212ca5c --- /dev/null +++ b/apps/scandic-web/components/Lightbox/Gallery/gallery.module.css @@ -0,0 +1,160 @@ +.galleryContainer { + display: grid; + gap: var(--Space-x2); + padding: var(--Space-x2); + height: 100%; + overflow-y: auto; + background-color: var(--Base-Background-Primary-Normal); +} + +.mobileGallery { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--Space-x1); + padding-bottom: var(--Space-x3); +} + +.thumbnailContainer { + position: relative; + height: 242px; +} + +.fullWidthImage { + grid-column: 1 / -1; + height: 240px; +} + +.imageButton { + position: relative; + height: 100%; + width: 100%; + padding: 0; + border-width: 0; + background-color: transparent; + cursor: pointer; + border-radius: var(--Corner-radius-Medium); + overflow: hidden; + z-index: 0; + + &:focus-visible { + outline-offset: -2px; /* Adjust the outline offset as wrappers uses overflow-hidden */ + } +} + +.image { + transition: opacity 0.3s ease-in-out; + object-fit: cover; + z-index: -1; +} + +@media screen and (max-width: 767px) { + .desktopCloseIcon, + .desktopGallery { + display: none; + } + + .closeButton { + justify-self: start; + } +} + +@media screen and (min-width: 768px) { + .mobileGallery, + .mobileCloseIcon { + display: none; + } + + .galleryContainer { + padding: var(--Spacing-x5) var(--Spacing-x6); + } + + .closeButton { + position: absolute; + top: var(--Space-x2); + right: var(--Space-x2); + z-index: 1; + } + + .desktopGallery { + display: grid; + grid-template-rows: 28px 1fr 7.8125rem; + row-gap: var(--Spacing-x-one-and-half); + background-color: var(--Base-Background-Primary-Normal); + height: 100%; + position: relative; + overflow: hidden; + } + + .galleryHeader { + display: flex; + align-items: center; + } + + .imageCaption { + background-color: var(--Base-Surface-Subtle-Normal); + padding: var(--Spacing-x-half) var(--Spacing-x1); + border-radius: var(--Corner-radius-Small); + color: var(--Text-Secondary); + } + + .mainImageWrapper { + position: relative; + width: 100%; + } + + .mainImageContainer { + width: 100%; + height: 100%; + will-change: transform; + position: absolute; + } + + .desktopThumbnailGrid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: var(--Spacing-x1); + max-height: 7.8125rem; + overflow: hidden; + } + + .thumbnailContainer { + height: 125px; + } + + .fullWidthImage { + grid-column: auto; + height: auto; + } + + .thumbnailContainer img { + border-radius: var(--Corner-radius-Small); + } + + .navigationButton { + position: absolute; + top: 50%; + transform: translateY(-50%); + background-color: var(--Component-Button-Inverted-Fill-Default); + color: var(--Component-Button-Inverted-On-fill-Default); + border-radius: var(--Corner-radius-rounded); + padding: 10px; + cursor: pointer; + border-width: 0; + display: flex; + z-index: 1; + box-shadow: 0px 0px 8px 1px #0000001a; + + &:hover { + background-color: var(--Component-Button-Inverted-Fill-Hover); + color: var(--Component-Button-Inverted-On-fill-Hover); + } + } + + .galleryPrevButton { + left: var(--Spacing-x2); + } + + .galleryNextButton { + right: var(--Spacing-x2); + } +} diff --git a/apps/scandic-web/components/Lightbox/Gallery.tsx b/apps/scandic-web/components/Lightbox/Gallery/index.tsx similarity index 58% rename from apps/scandic-web/components/Lightbox/Gallery.tsx rename to apps/scandic-web/components/Lightbox/Gallery/index.tsx index 5d8868376..9368951bf 100644 --- a/apps/scandic-web/components/Lightbox/Gallery.tsx +++ b/apps/scandic-web/components/Lightbox/Gallery/index.tsx @@ -1,15 +1,16 @@ "use client" import { AnimatePresence, motion } from "framer-motion" import { useState } from "react" +import { Button as ButtonRAC } from "react-aria-components" import { useIntl } from "react-intl" +import { IconButton } from "@scandic-hotels/design-system/IconButton" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" import Image from "@/components/Image" -import Button from "@/components/TempDesignSystem/Button" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import styles from "./Lightbox.module.css" +import styles from "./gallery.module.css" import type { GalleryProps } from "@/types/components/lightbox/lightbox" @@ -61,36 +62,38 @@ export default function Gallery({ return (
    - + + {/* Desktop Gallery */}
    -
    - {mainImage.caption && !hideLabel && ( -
    - {mainImage.caption} -
    - )} -
    + +

    + {mainImage.caption && !hideLabel && ( + {mainImage.caption} + )} +

    +
    - {mainImage.alt} + + {mainImage.alt} + - + - +
    @@ -139,19 +142,26 @@ export default function Gallery({ onSelectImage(image)} initial={{ opacity: 0, x: 50 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -50 }} transition={{ duration: 0.2, delay: index * 0.05 }} > - {image.alt} + onSelectImage(image)} + aria-label={intl.formatMessage({ + defaultMessage: "Open image", + })} + > + {image.alt} + ))} @@ -160,31 +170,32 @@ export default function Gallery({ {/* Mobile Gallery */}
    -
    -
    - {images.map((image, index) => ( - { - onSelectImage(image) - onImageClick() - }} - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ duration: 0.3, delay: index * 0.05 }} - > - {image.alt} - - ))} -
    -
    + {images.map((image, index) => ( + + { + onSelectImage(image) + onImageClick() + }} + > + {image.alt} + + + ))}
    ) diff --git a/apps/scandic-web/components/Lightbox/Lightbox.module.css b/apps/scandic-web/components/Lightbox/Lightbox.module.css deleted file mode 100644 index 22f98fe5c..000000000 --- a/apps/scandic-web/components/Lightbox/Lightbox.module.css +++ /dev/null @@ -1,348 +0,0 @@ -@keyframes darken-background { - from { - background-color: rgba(0, 0, 0, 0); - } - - to { - background-color: rgba(0, 0, 0, 0.5); - } -} - -.mobileGallery { - height: 100%; - position: relative; - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.closeButton { - justify-content: flex-start; - width: fit-content; -} -.closeButton .desktopCloseIcon { - display: none; -} - -.mobileGalleryContent { - display: block; -} - -.fullViewCloseButton { - position: absolute; - top: var(--Spacing-x-one-and-half); - right: var(--Spacing-x-half); - z-index: 1; -} - -.fullViewCloseButton:hover .fullViewCloseIcon { - background-color: var(--UI-Text-Medium-contrast); - border-radius: 50%; -} - -.leftTransformIcon { - transform: scaleX(-1); -} - -.content { - width: 100%; - height: 100%; - border-radius: 0; - position: fixed; - top: 50%; - left: 50%; - z-index: var(--lightbox-z-index); -} - -.overlay { - position: fixed; - inset: 0; - background-color: rgba(0, 0, 0, 0.5); - z-index: var(--lightbox-z-index); -} - -.overlay[data-entering] { - animation: darken-background 0.2s; -} - -.overlay[data-exiting] { - animation: darken-background 0.2s reverse; -} - -.galleryContainer { - background-color: var(--Base-Background-Primary-Normal); - padding: var(--Spacing-x2); - height: 100%; - display: flex; - flex-direction: column; - position: relative; - overflow-y: auto; -} - -.galleryHeader { - display: flex; - justify-content: space-between; - align-items: center; - height: 1.71875rem; -} - -.desktopGallery, -.desktopThumbnailGrid, -.navigationButton { - display: none; -} - -.imageCaption { - background-color: var(--Base-Surface-Subtle-Normal); - padding: var(--Spacing-x-half) var(--Spacing-x1); - border-radius: var(--Corner-radius-Small); -} - -.mainImageWrapper { - position: relative; - width: 100%; -} - -.mainImageContainer { - width: 100%; - height: 100%; - will-change: transform; - overflow: hidden; - position: absolute; -} - -.mainImageContainer img, -.thumbnailContainer img { - border-radius: var(--Corner-radius-Small); - cursor: pointer; - transition: opacity 0.3s ease-in-out; -} - -.thumbnailGrid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--Spacing-x1); - max-height: none; - padding: var(--Spacing-x3) 0; -} - -.thumbnailContainer { - position: relative; - height: 242px; -} - -.fullWidthImage { - grid-column: 1 / -1; - height: 240px; -} - -.thumbnailContainer img { - border-radius: var(--Corner-radius-Medium); -} - -.fullViewContainer { - background-color: var(--UI-Text-High-contrast); - height: 100%; - padding: var(--Spacing-x2); - position: relative; - align-items: center; - justify-items: center; - display: grid; - grid-template-rows: auto 1fr auto; - grid-template-columns: 1fr; - place-content: center; - gap: var(--Spacing-x5); -} - -.fullViewHeader { - display: flex; - justify-content: center; - width: 100%; -} - -.fullViewImageContainer { - position: relative; - width: 100%; - height: 100%; - max-height: 25rem; - margin-bottom: var(--Spacing-x5); -} - -.fullViewImage { - position: absolute; - width: 100%; - height: 100%; - border-radius: var(--Corner-radius-Medium); -} - -.fullViewImageContainer img { - border-radius: var(--Corner-radius-Medium); - width: 100%; - height: 100%; -} - -.fullViewFooter { - position: absolute; - bottom: calc(-1 * var(--Spacing-x5)); -} - -.imagePosition { - background-color: var(--UI-Grey-90); - padding: var(--Spacing-x-quarter) var(--Spacing-x-half); - border-radius: var(--Corner-radius-Small); -} - -.portraitImage { - max-width: 548px; -} - -.image { - object-fit: cover; -} -@media (min-width: 768px) and (max-width: 1366px) { - .fullViewContainer { - padding: var(--Spacing-x5); - } - - .fullViewImageContainer { - height: 100%; - max-height: 35rem; - } -} - -@media (min-width: 768px) { - .mobileGallery, - .thumbnailGrid { - display: none; - } - - .content { - position: fixed; - top: 50%; - left: 50%; - overflow: hidden; - } - - .content:not(.fullViewContent) { - border-radius: var(--Corner-radius-Large); - } - - .galleryContent { - width: 1090px; - width: min(var(--max-width-page), 1090px); - height: min(725px, 85dvh); - } - - .fullViewContent { - width: 100vw; - height: 100vh; - } - - .galleryContainer { - padding: var(--Spacing-x5) var(--Spacing-x6); - } - - .desktopGallery { - display: grid; - grid-template-rows: 1.71875rem 1fr 7.8125rem; - row-gap: var(--Spacing-x-one-and-half); - background-color: var(--Base-Background-Primary-Normal); - height: 100%; - position: relative; - overflow: hidden; - } - - .closeButton { - display: block; - position: absolute; - top: var(--Spacing-x-one-and-half); - right: var(--Spacing-x1); - z-index: 1; - } - - .closeButton .mobileCloseIcon { - display: none; - } - .closeButton .desktopCloseIcon { - display: block; - } - - .closeButton:hover .desktopCloseIcon { - background-color: var(--Base-Surface-Primary-light-Hover-alt); - border-radius: 50%; - } - - .desktopThumbnailGrid { - display: grid; - grid-template-columns: repeat(5, 1fr); - gap: var(--Spacing-x1); - max-height: 7.8125rem; - overflow: hidden; - } - - .thumbnailContainer { - height: 125px; - } - - .fullViewCloseButton { - position: fixed; - top: var(--Spacing-x-one-and-half); - right: var(--Spacing-x-half); - } - - .fullWidthImage { - grid-column: auto; - height: auto; - } - - .thumbnailContainer img { - border-radius: var(--Corner-radius-Small); - } - - .fullViewContainer { - margin-top: 0; - padding: var(--Spacing-x5); - grid-template-rows: auto 1fr auto; - width: 100%; - height: 100%; - } - - .fullViewImageContainer { - width: 70%; - max-width: 90.875rem; - max-height: 43.75rem; - } - - .navigationButton { - position: absolute; - top: 50%; - transform: translateY(-50%); - background-color: var(--Base-Button-Inverted-Fill-Normal); - border-radius: 50%; - padding: var(--Spacing-x1); - cursor: pointer; - border: none; - display: flex; - z-index: 1; - } - - .galleryPrevButton { - left: var(--Spacing-x2); - } - - .galleryNextButton { - right: var(--Spacing-x2); - } - - .fullViewNextButton { - right: var(--Spacing-x5); - } - - .fullViewPrevButton { - left: var(--Spacing-x5); - } - - .fullViewFooter { - text-align: left; - } -} diff --git a/apps/scandic-web/components/Lightbox/index.tsx b/apps/scandic-web/components/Lightbox/index.tsx index 3520dcbd4..1459ac2d8 100644 --- a/apps/scandic-web/components/Lightbox/index.tsx +++ b/apps/scandic-web/components/Lightbox/index.tsx @@ -6,7 +6,7 @@ import { Dialog, Modal, ModalOverlay } from "react-aria-components" import FullView from "./FullView" import Gallery from "./Gallery" -import styles from "./Lightbox.module.css" +import styles from "./lightbox.module.css" import type { LightboxProps } from "@/types/components/lightbox/lightbox" diff --git a/apps/scandic-web/components/Lightbox/lightbox.module.css b/apps/scandic-web/components/Lightbox/lightbox.module.css new file mode 100644 index 000000000..049733103 --- /dev/null +++ b/apps/scandic-web/components/Lightbox/lightbox.module.css @@ -0,0 +1,57 @@ +.overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: var(--lightbox-z-index); + + &[data-entering] { + animation: darken-background 0.2s; + } + + &[data-exiting] { + animation: darken-background 0.2s reverse; + } +} + +.content { + width: 100%; + height: 100%; + border-radius: 0; + position: fixed; + top: 50%; + left: 50%; + z-index: var(--lightbox-z-index); +} + +@media screen and (min-width: 768px) { + .content { + position: fixed; + top: 50%; + left: 50%; + overflow: hidden; + + &:not(.fullViewContent) { + border-radius: var(--Corner-radius-Large); + } + + &.fullViewContent { + width: 100vw; + height: 100vh; + } + + &.galleryContent { + width: min(var(--max-width-page), 1090px); + height: min(725px, 85dvh); + } + } +} + +@keyframes darken-background { + from { + background-color: rgba(0, 0, 0, 0); + } + + to { + background-color: rgba(0, 0, 0, 0.5); + } +} diff --git a/packages/design-system/lib/components/Button/Button.stories.tsx b/packages/design-system/lib/components/Button/Button.stories.tsx index 7311bb957..3b6a40cfe 100644 --- a/packages/design-system/lib/components/Button/Button.stories.tsx +++ b/packages/design-system/lib/components/Button/Button.stories.tsx @@ -36,8 +36,7 @@ const meta: Meta = { control: 'select', options: Object.keys(buttonConfig.variants.size), type: 'string', - description: - 'The size of the button. Defaults to `Large`. This variant does not apply to the `Icon` variant.', + description: 'The size of the button. Defaults to `Large`.', }, wrapping: { control: 'radio', @@ -351,25 +350,3 @@ export const TextWithIconInverted: Story = { color: 'Inverted', }, } - -export const Icon: Story = { - args: { - onPress: fn(), - children: , - variant: 'Icon', - }, -} - -export const IconWithColor: Story = { - args: { - onPress: fn(), - children: ( - - ), - variant: 'Icon', - }, -} diff --git a/packages/design-system/lib/components/Button/Button.tsx b/packages/design-system/lib/components/Button/Button.tsx index c5b7268c2..02c7a7938 100644 --- a/packages/design-system/lib/components/Button/Button.tsx +++ b/packages/design-system/lib/components/Button/Button.tsx @@ -1,5 +1,3 @@ -'use client' - import { Button as ButtonRAC } from 'react-aria-components' import { variants } from './variants' diff --git a/packages/design-system/lib/components/Button/button.module.css b/packages/design-system/lib/components/Button/button.module.css index e6a39a091..12227961e 100644 --- a/packages/design-system/lib/components/Button/button.module.css +++ b/packages/design-system/lib/components/Button/button.module.css @@ -6,6 +6,7 @@ display: flex; align-items: center; justify-content: center; + gap: var(--Space-x05); &:disabled { cursor: unset; @@ -166,11 +167,3 @@ .variant-text.color-inverted:disabled { color: var(--Component-Button-Brand-Secondary-On-fill-Disabled); } - -.variant-icon { - background-color: transparent; - border-color: transparent; - color: inherit; - padding: 0; - margin: 0; -} diff --git a/packages/design-system/lib/components/Button/variants.ts b/packages/design-system/lib/components/Button/variants.ts index 18dd4e6a4..2e5e59ead 100644 --- a/packages/design-system/lib/components/Button/variants.ts +++ b/packages/design-system/lib/components/Button/variants.ts @@ -16,7 +16,6 @@ export const config = { Tertiary: styles['variant-tertiary'], Inverted: styles['variant-inverted'], Text: styles['variant-text'], - Icon: styles['variant-icon'], }, color: { Primary: styles['color-primary'], diff --git a/packages/design-system/lib/components/IconButton/IconButton.stories.tsx b/packages/design-system/lib/components/IconButton/IconButton.stories.tsx new file mode 100644 index 000000000..383786abc --- /dev/null +++ b/packages/design-system/lib/components/IconButton/IconButton.stories.tsx @@ -0,0 +1,141 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { fn } from '@storybook/test' + +import { MaterialIcon } from '../Icons/MaterialIcon' +import { IconButton } from './IconButton' +import { config } from './variants' + +const meta: Meta = { + title: 'Components/IconButton', + component: IconButton, + argTypes: { + onPress: { + table: { + disable: true, + }, + }, + theme: { + control: 'select', + options: Object.keys(config.variants.theme), + default: 'Primary', + }, + style: { + control: 'select', + options: Object.keys(config.variants.style), + default: 'Normal', + type: 'string', + description: `The style variant is only applied on certain variants. The examples below shows the possible combinations of variants and style variants.`, + }, + }, +} + +export default meta + +type Story = StoryObj + +export const PrimaryDefault: Story = { + args: { + onPress: fn(), + children: , + theme: 'Primary', + }, +} + +export const PrimaryDisabled: Story = { + args: { + ...PrimaryDefault.args, + isDisabled: true, + }, +} + +export const InvertedDefault: Story = { + args: { + onPress: fn(), + children: ( + + ), + theme: 'Inverted', + }, +} + +export const InvertedDisabled: Story = { + args: { + ...InvertedDefault.args, + isDisabled: true, + }, +} + +export const InvertedElevated: Story = { + args: { + ...InvertedDefault.args, + style: 'Elevated', + }, +} + +export const InvertedElevatedDisabled: Story = { + args: { + ...InvertedElevated.args, + isDisabled: true, + }, +} + +export const InvertedMuted: Story = { + args: { + ...InvertedDefault.args, + children: , + style: 'Muted', + }, +} + +export const InvertedMutedDisabled: Story = { + args: { + ...InvertedMuted.args, + isDisabled: true, + }, +} + +export const InvertedFaded: Story = { + args: { + ...InvertedDefault.args, + style: 'Faded', + }, +} + +export const InvertedFadedDisabled: Story = { + args: { + ...InvertedFaded.args, + isDisabled: true, + }, +} + +export const TertiaryElevated: Story = { + args: { + onPress: fn(), + children: , + theme: 'Tertiary', + style: 'Elevated', + }, +} + +export const TertiaryDisabled: Story = { + args: { + ...TertiaryElevated.args, + isDisabled: true, + }, +} + +export const BlackMuted: Story = { + args: { + onPress: fn(), + children: , + theme: 'Black', + }, +} + +export const BlackMutedDisabled: Story = { + args: { + ...BlackMuted.args, + isDisabled: true, + }, +} diff --git a/packages/design-system/lib/components/IconButton/IconButton.tsx b/packages/design-system/lib/components/IconButton/IconButton.tsx new file mode 100644 index 000000000..a94ffb08d --- /dev/null +++ b/packages/design-system/lib/components/IconButton/IconButton.tsx @@ -0,0 +1,20 @@ +import { Button as ButtonRAC } from 'react-aria-components' + +import { variants } from './variants' + +import type { IconButtonProps } from './types' + +export function IconButton({ + theme, + style, + className, + ...props +}: IconButtonProps) { + const classNames = variants({ + theme, + style, + className, + }) + + return +} diff --git a/packages/design-system/lib/components/IconButton/iconButton.module.css b/packages/design-system/lib/components/IconButton/iconButton.module.css new file mode 100644 index 000000000..13d09e659 --- /dev/null +++ b/packages/design-system/lib/components/IconButton/iconButton.module.css @@ -0,0 +1,102 @@ +.iconButton { + border-radius: var(--Corner-radius-rounded); + border-width: 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 10px; + + &:disabled { + cursor: unset; + } +} + +.theme-primary { + background-color: var(--Component-Button-Brand-Primary-Fill-Default); + color: var(--Component-Button-Brand-Primary-On-fill-Default); + + &:hover:not(:disabled) { + background-color: var(--Component-Button-Brand-Primary-Fill-Hover); + color: var(--Component-Button-Brand-Primary-On-fill-Hover); + } + + &:disabled { + background-color: var(--Component-Button-Brand-Primary-Fill-Disabled); + color: var(--Component-Button-Brand-Primary-On-fill-Disabled); + } +} + +.theme-inverted { + background-color: var(--Component-Button-Inverted-Fill-Default); + color: var(--Component-Button-Inverted-On-fill-Default); + + &:hover:not(:disabled) { + background-color: var(--Component-Button-Inverted-Fill-Hover); + color: var(--Component-Button-Inverted-On-fill-Hover); + } + + &:disabled { + background-color: var(--Component-Button-Inverted-Fill-Disabled); + color: var(--Component-Button-Inverted-On-fill-Disabled); + } + + &.style-muted { + color: var(--Component-Button-Muted-On-fill-Inverted); + + &:hover:not(:disabled) { + color: var(--Component-Button-Muted-On-fill-Inverted); + } + + &:disabled { + color: var(--Component-Button-Muted-On-fill-Disabled); + } + } +} + +.theme-tertiary { + background-color: var(--Component-Button-Brand-Tertiary-Fill-Default); + color: var(--Component-Button-Brand-Tertiary-On-fill-Default); + + &:hover:not(:disabled) { + background-color: var(--Component-Button-Brand-Tertiary-Fill-Hover); + color: var(--Component-Button-Brand-Tertiary-On-fill-Hover); + } + + &:disabled { + background-color: var(--Component-Button-Brand-Tertiary-Fill-Disabled); + color: var(--Component-Button-Brand-Tertiary-On-fill-Disabled); + } +} + +.theme-black { + color: var(--Component-Button-Muted-On-fill-Default); + + &:hover:not(:disabled) { + color: var(--Component-Button-Muted-On-fill-Hover-Inverted); + } + + &:disabled { + color: var(--Component-Button-Muted-On-fill-Disabled); + } +} + +.style-elevated { + box-shadow: 0px 0px 8px 1px #0000001a; +} + +.style-faded { + background-color: var(--Component-Button-Inverted-Fill-Faded); +} + +.style-muted { + background-color: var(--Component-Button-Muted-Fill-Default); + + &:hover:not(:disabled) { + background-color: var(--Component-Button-Muted-Fill-Hover-inverted); + } + + &:disabled { + background-color: var(--Component-Button-Muted-Fill-Disabled-inverted); + } +} diff --git a/packages/design-system/lib/components/IconButton/index.tsx b/packages/design-system/lib/components/IconButton/index.tsx new file mode 100644 index 000000000..a492877d0 --- /dev/null +++ b/packages/design-system/lib/components/IconButton/index.tsx @@ -0,0 +1 @@ +export { IconButton } from './IconButton' diff --git a/packages/design-system/lib/components/IconButton/types.ts b/packages/design-system/lib/components/IconButton/types.ts new file mode 100644 index 000000000..2c84912b8 --- /dev/null +++ b/packages/design-system/lib/components/IconButton/types.ts @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..cd361d945 --- /dev/null +++ b/packages/design-system/lib/components/IconButton/variants.ts @@ -0,0 +1,78 @@ +import { cva } from 'class-variance-authority' + +import styles from './iconButton.module.css' + +const variantKeys = { + theme: { + Primary: 'Primary', + Tertiary: 'Tertiary', + Inverted: 'Inverted', + Black: 'Black', + }, + style: { + Normal: 'Normal', + Muted: 'Muted', + Elevated: 'Elevated', + Faded: 'Faded', + }, +} 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]: '', + }, + }, + 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/RateCard/Campaign/index.tsx b/packages/design-system/lib/components/RateCard/Campaign/index.tsx index 5be4a0112..6e7a97ea4 100644 --- a/packages/design-system/lib/components/RateCard/Campaign/index.tsx +++ b/packages/design-system/lib/components/RateCard/Campaign/index.tsx @@ -1,7 +1,7 @@ import { Typography } from '../../Typography' import { Rate, RateTermDetails } from '../types' -import { Button } from '../../Button' +import { IconButton } from '../../IconButton' import { MaterialIcon } from '../../Icons/MaterialIcon' import Modal from '../Modal' import styles from '../rate-card.module.css' @@ -67,13 +67,13 @@ export default function CampaignRateCard({ title={rateTitle} subtitle={paymentTerm} trigger={ - + } > {rateTermDetails.map((termGroup) => ( diff --git a/packages/design-system/lib/components/RateCard/Code/index.tsx b/packages/design-system/lib/components/RateCard/Code/index.tsx index 2aaa9f75c..8254e060a 100644 --- a/packages/design-system/lib/components/RateCard/Code/index.tsx +++ b/packages/design-system/lib/components/RateCard/Code/index.tsx @@ -1,6 +1,6 @@ import { Rate, RateTermDetails } from '../types' -import { Button } from '../../Button' +import { IconButton } from '../../IconButton' import { MaterialIcon } from '../../Icons/MaterialIcon' import { Typography } from '../../Typography' import Modal from '../Modal' @@ -63,13 +63,13 @@ export default function CodeRateCard({ title={rateTitle} subtitle={paymentTerm} trigger={ - + } > {rateTermDetails.map((termGroup) => ( diff --git a/packages/design-system/lib/components/RateCard/NoRateAvailable/index.tsx b/packages/design-system/lib/components/RateCard/NoRateAvailable/index.tsx index d1822f9fb..b69fae1f0 100644 --- a/packages/design-system/lib/components/RateCard/NoRateAvailable/index.tsx +++ b/packages/design-system/lib/components/RateCard/NoRateAvailable/index.tsx @@ -1,4 +1,4 @@ -import { Button } from '../../Button' +import { IconButton } from '../../IconButton' import { MaterialIcon } from '../../Icons/MaterialIcon' import { Typography } from '../../Typography' import styles from '../rate-card.module.css' @@ -34,9 +34,9 @@ export default function NoRateAvailableCard({

    - + {`${rateTitle} / ${paymentTerm}`}

    diff --git a/packages/design-system/lib/components/RateCard/Points/index.tsx b/packages/design-system/lib/components/RateCard/Points/index.tsx index 707ddbd1e..f32a430a7 100644 --- a/packages/design-system/lib/components/RateCard/Points/index.tsx +++ b/packages/design-system/lib/components/RateCard/Points/index.tsx @@ -2,7 +2,7 @@ import { Typography } from '../../Typography' import { RatePointsOption, RateTermDetails } from '../types' import { RadioGroup } from 'react-aria-components' -import { Button } from '../../Button' +import { IconButton } from '../../IconButton' import { MaterialIcon } from '../../Icons/MaterialIcon' import { Radio } from '../../Radio' import Modal from '../Modal' @@ -49,9 +49,9 @@ export default function PointsRateCard({ title={rateTitle} subtitle={paymentTerm} trigger={ - + } > {rateTermDetails.map((termGroup) => ( diff --git a/packages/design-system/lib/components/RateCard/Regular/index.tsx b/packages/design-system/lib/components/RateCard/Regular/index.tsx index 180194fb2..7dfe483f2 100644 --- a/packages/design-system/lib/components/RateCard/Regular/index.tsx +++ b/packages/design-system/lib/components/RateCard/Regular/index.tsx @@ -1,6 +1,6 @@ import { Rate, RateTermDetails } from '../types' -import { Button } from '../../Button' +import { IconButton } from '../../IconButton' import { MaterialIcon } from '../../Icons/MaterialIcon' import { Typography } from '../../Typography' import Modal from '../Modal' @@ -56,13 +56,13 @@ export default function RegularRateCard({ title={rateTitle} subtitle={paymentTerm} trigger={ - + } > {rateTermDetails.map((termGroup) => ( diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 3369f3e3e..d63122b2f 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -16,6 +16,7 @@ "./CodeRateCard": "./dist/components/RateCard/Code/index.js", "./PointsRateCard": "./dist/components/RateCard/Points/index.js", "./NoRateAvailableCard": "./dist/components/RateCard/NoRateAvailable/index.js", + "./IconButton": "./dist/components/IconButton/index.js", "./Icons": "./dist/components/Icons/index.js", "./Icons/BathroomCabinetIcon": "./dist/components/Icons/Nucleo/Amenities_Facilities/bathroom-cabinet-2.js", "./Icons/BedHotelIcon": "./dist/components/Icons/Customised/Amenities_Facilities/BedHotel.js",