Fix/SW-1563 accessibility
* 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
This commit is contained in:
@@ -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) {
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div className={styles.captions}>
|
||||
<Typography variant="Link/sm">
|
||||
<AriaButton
|
||||
<ButtonRAC
|
||||
className={styles.addressButton}
|
||||
onPress={() => setActiveMarker(hotel.id)}
|
||||
>
|
||||
{address}
|
||||
</AriaButton>
|
||||
</ButtonRAC>
|
||||
</Typography>
|
||||
<Divider variant="vertical" color="beige" />
|
||||
<p>
|
||||
|
||||
@@ -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 (
|
||||
<article className={className}>
|
||||
<div className={styles.wrapper}>
|
||||
<Button
|
||||
size="Medium"
|
||||
variant="Icon"
|
||||
<IconButton
|
||||
theme="Black"
|
||||
style="Muted"
|
||||
className={styles.closeButton}
|
||||
onPress={handleClose}
|
||||
aria-label={intl.formatMessage({
|
||||
@@ -63,7 +63,7 @@ export default function HotelMapCard({
|
||||
className={styles.closeIcon}
|
||||
color="CurrentColor"
|
||||
/>
|
||||
</Button>
|
||||
</IconButton>
|
||||
{image ? (
|
||||
<DialogImage
|
||||
image={image.src}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useMap } from "@vis.gl/react-google-maps"
|
||||
import { cx } from "class-variance-authority"
|
||||
import { useState } from "react"
|
||||
import { Button as AriaButton } from "react-aria-components"
|
||||
import { Button as ButtonRAC } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
@@ -136,12 +136,12 @@ export default function Sidebar({
|
||||
}`}
|
||||
>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<AriaButton
|
||||
<ButtonRAC
|
||||
className={styles.sidebarToggle}
|
||||
onPress={toggleFullScreenSidebar}
|
||||
>
|
||||
{isFullScreenSidebar ? viewAsMapMsg : viewAsListMsg}
|
||||
</AriaButton>
|
||||
</ButtonRAC>
|
||||
</Typography>
|
||||
<div className={styles.sidebarContent}>
|
||||
<Typography variant="Title/sm">
|
||||
@@ -168,7 +168,7 @@ export default function Sidebar({
|
||||
{pois.map((poi) => (
|
||||
<li key={poi.name} className={styles.poiItem}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<AriaButton
|
||||
<ButtonRAC
|
||||
className={cx(styles.poiButton, {
|
||||
[styles.active]: activePoi === poi.name,
|
||||
})}
|
||||
@@ -188,7 +188,7 @@ export default function Sidebar({
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</AriaButton>
|
||||
</ButtonRAC>
|
||||
</Typography>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.imageWrapper}>
|
||||
{images.slice(0, 3).map((image, index) => (
|
||||
{lightboxImages.slice(0, 3).map((image, index) => (
|
||||
<ButtonRAC
|
||||
key={image.src}
|
||||
className={styles.imageButton}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "See all photos",
|
||||
})}
|
||||
onPress={() =>
|
||||
setLightboxState({
|
||||
activeIndex: index,
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Image
|
||||
key={index}
|
||||
src={image.imageSizes.medium}
|
||||
alt={image.metaData.altText}
|
||||
title={image.metaData.title}
|
||||
src={image.src}
|
||||
alt={image.alt}
|
||||
title={image.caption || ""}
|
||||
width={index === 0 ? 752 : 292}
|
||||
height={index === 0 ? 540 : 266}
|
||||
onClick={() => setLightboxIsOpen(true)}
|
||||
className={styles.image}
|
||||
/>
|
||||
</ButtonRAC>
|
||||
))}
|
||||
{images.length > 1 && (
|
||||
<>
|
||||
<Button
|
||||
theme="base"
|
||||
intent="inverted"
|
||||
size="small"
|
||||
onClick={() => setLightboxIsOpen(true)}
|
||||
variant="Primary"
|
||||
color="Inverted"
|
||||
size="Small"
|
||||
onPress={() =>
|
||||
setLightboxState({
|
||||
activeIndex: 0,
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
typography="Body/Supporting text (caption)/smBold"
|
||||
className={styles.seeAllButton}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
@@ -56,8 +79,9 @@ export default function PreviewImages({
|
||||
},
|
||||
{ title: hotelName }
|
||||
)}
|
||||
isOpen={lightboxIsOpen}
|
||||
onClose={() => setLightboxIsOpen(false)}
|
||||
isOpen={lightboxState.isOpen}
|
||||
activeIndex={lightboxState.activeIndex}
|
||||
onClose={() => setLightboxState({ activeIndex: 0, isOpen: false })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -3,18 +3,25 @@
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 0 var(--Spacing-x2);
|
||||
z-index: 0;
|
||||
max-width: var(--max-width-page);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.imageButton {
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image {
|
||||
object-fit: cover;
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 30vh;
|
||||
cursor: pointer;
|
||||
max-width: var(--max-width-page);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,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 DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
@@ -115,17 +116,18 @@ export default function SummaryUI({
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
{dt(booking.toDate).locale(lang).format("ddd, D MMM")} ({nightsMsg})
|
||||
</Body>
|
||||
<Button
|
||||
<IconButton
|
||||
onPress={handleToggleSummary}
|
||||
className={styles.chevronButton}
|
||||
variant="Icon"
|
||||
theme="Black"
|
||||
style="Muted"
|
||||
>
|
||||
<MaterialIcon
|
||||
icon="keyboard_arrow_down"
|
||||
size={20}
|
||||
color="CurrentColor"
|
||||
/>
|
||||
</Button>
|
||||
</IconButton>
|
||||
</header>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
{rooms.map(({ room }, idx) => {
|
||||
|
||||
@@ -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})
|
||||
</Body>
|
||||
<Button onPress={toggleSummaryOpen} variant="Icon">
|
||||
<IconButton onPress={toggleSummaryOpen} theme="Black" style="Muted">
|
||||
<MaterialIcon
|
||||
icon="keyboard_arrow_down"
|
||||
size={20}
|
||||
color="CurrentColor"
|
||||
/>
|
||||
</Button>
|
||||
</IconButton>
|
||||
</header>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
{rooms.map((room, idx) => {
|
||||
|
||||
@@ -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" })}
|
||||
</h3>
|
||||
</Typography>
|
||||
<Button variant="Icon" onPress={() => setIsOpen(false)}>
|
||||
<IconButton
|
||||
theme="Black"
|
||||
style="Muted"
|
||||
onPress={() => setIsOpen(false)}
|
||||
>
|
||||
<MaterialIcon icon="close" size={24} color="CurrentColor" />
|
||||
</Button>
|
||||
</IconButton>
|
||||
</div>
|
||||
<Form close={() => setIsOpen(false)} />
|
||||
</Dialog>
|
||||
|
||||
@@ -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}
|
||||
<AriaButton
|
||||
<ButtonRAC
|
||||
onPress={() => deleteSelectedPackage(pkg.code)}
|
||||
className={styles.removeButton}
|
||||
>
|
||||
<MaterialIcon icon="close" size={16} color="CurrentColor" />
|
||||
</AriaButton>
|
||||
</ButtonRAC>
|
||||
</span>
|
||||
</Typography>
|
||||
))}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={styles.fullViewContainer}>
|
||||
<Button
|
||||
intent="text"
|
||||
size="small"
|
||||
variant="icon"
|
||||
className={styles.fullViewCloseButton}
|
||||
onClick={onClose}
|
||||
<IconButton
|
||||
theme="Inverted"
|
||||
style="Muted"
|
||||
className={styles.closeButton}
|
||||
onPress={onClose}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Close",
|
||||
})}
|
||||
>
|
||||
<MaterialIcon
|
||||
icon="close"
|
||||
size={32}
|
||||
className={styles.fullViewCloseIcon}
|
||||
color="Icon/Inverted"
|
||||
/>
|
||||
</Button>
|
||||
<div className={styles.fullViewHeader}>
|
||||
<span className={styles.imagePosition}>
|
||||
<Caption color="white">
|
||||
<MaterialIcon icon="close" color="CurrentColor" size={24} />
|
||||
</IconButton>
|
||||
<div className={styles.header}>
|
||||
<Typography variant="Tag/sm">
|
||||
<span className={styles.imageCount}>
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
{`${currentIndex + 1} / ${totalImages}`}
|
||||
</Caption>
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={styles.fullViewImageContainer}>
|
||||
<div className={styles.imageContainer}>
|
||||
<AnimatePresence initial={false} custom={animateLeft}>
|
||||
<motion.div
|
||||
key={image.src}
|
||||
@@ -86,7 +84,7 @@ export default function FullView({
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
transition={{ duration: 0.3 }}
|
||||
className={styles.fullViewImage}
|
||||
className={styles.imageWrapper}
|
||||
drag="x"
|
||||
onDragEnd={(_e, info) => 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}
|
||||
/>
|
||||
|
||||
<div className={styles.fullViewFooter}>
|
||||
{image.caption && !hideLabel && (
|
||||
<Body color="white">{image.caption}</Body>
|
||||
)}
|
||||
</div>
|
||||
{image.caption && !hideLabel ? (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.footer}>{image.caption}</p>
|
||||
</Typography>
|
||||
) : null}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
@@ -112,8 +110,8 @@ export default function FullView({
|
||||
onClick={handlePrev}
|
||||
>
|
||||
<MaterialIcon
|
||||
icon="arrow_forward"
|
||||
color="Icon/Interactive/Default"
|
||||
icon="arrow_back"
|
||||
color="CurrentColor"
|
||||
className={styles.leftTransformIcon}
|
||||
/>
|
||||
</motion.button>
|
||||
@@ -121,7 +119,7 @@ export default function FullView({
|
||||
className={`${styles.navigationButton} ${styles.fullViewNextButton}`}
|
||||
onClick={handleNext}
|
||||
>
|
||||
<MaterialIcon icon="arrow_forward" color="Icon/Interactive/Default" />
|
||||
<MaterialIcon icon="arrow_forward" color="CurrentColor" />
|
||||
</motion.button>
|
||||
</div>
|
||||
)
|
||||
160
apps/scandic-web/components/Lightbox/Gallery/gallery.module.css
Normal file
160
apps/scandic-web/components/Lightbox/Gallery/gallery.module.css
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={styles.galleryContainer}>
|
||||
<Button
|
||||
intent="text"
|
||||
size="small"
|
||||
<IconButton
|
||||
theme="Black"
|
||||
style="Muted"
|
||||
className={styles.closeButton}
|
||||
onClick={onClose}
|
||||
onPress={onClose}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Close",
|
||||
})}
|
||||
>
|
||||
<MaterialIcon
|
||||
icon="chevron_left"
|
||||
color="Icon/Intense"
|
||||
size={32}
|
||||
color="CurrentColor"
|
||||
size={24}
|
||||
className={styles.mobileCloseIcon}
|
||||
/>
|
||||
<MaterialIcon
|
||||
icon="close"
|
||||
size={32}
|
||||
color="CurrentColor"
|
||||
size={24}
|
||||
className={styles.desktopCloseIcon}
|
||||
/>
|
||||
</Button>
|
||||
</IconButton>
|
||||
|
||||
{/* Desktop Gallery */}
|
||||
<div className={styles.desktopGallery}>
|
||||
<div className={styles.galleryHeader}>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.galleryHeader}>
|
||||
{mainImage.caption && !hideLabel && (
|
||||
<div className={styles.imageCaption}>
|
||||
<Caption color="textMediumContrast">{mainImage.caption}</Caption>
|
||||
</div>
|
||||
<span className={styles.imageCaption}>{mainImage.caption}</span>
|
||||
)}
|
||||
</div>
|
||||
</p>
|
||||
</Typography>
|
||||
<div className={styles.mainImageWrapper}>
|
||||
<AnimatePresence initial={false} custom={animateLeft}>
|
||||
<motion.div
|
||||
@@ -102,6 +105,13 @@ export default function Gallery({
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<ButtonRAC
|
||||
onPress={onImageClick}
|
||||
className={styles.imageButton}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Open image",
|
||||
})}
|
||||
>
|
||||
<Image
|
||||
src={mainImage.src}
|
||||
@@ -109,28 +119,21 @@ export default function Gallery({
|
||||
fill
|
||||
sizes="(min-width: 1000px) 1000px, 100vw"
|
||||
className={styles.image}
|
||||
onClick={onImageClick}
|
||||
/>
|
||||
</ButtonRAC>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
<motion.button
|
||||
className={`${styles.navigationButton} ${styles.galleryPrevButton}`}
|
||||
onClick={handlePrev}
|
||||
>
|
||||
<MaterialIcon
|
||||
icon="arrow_forward"
|
||||
color="Icon/Interactive/Default"
|
||||
className={styles.leftTransformIcon}
|
||||
/>
|
||||
<MaterialIcon icon="arrow_back" color="CurrentColor" />
|
||||
</motion.button>
|
||||
<motion.button
|
||||
className={`${styles.navigationButton} ${styles.galleryNextButton}`}
|
||||
onClick={handleNext}
|
||||
>
|
||||
<MaterialIcon
|
||||
icon="arrow_forward"
|
||||
color="Icon/Interactive/Default"
|
||||
/>
|
||||
<MaterialIcon icon="arrow_forward" color="CurrentColor" />
|
||||
</motion.button>
|
||||
</div>
|
||||
<div className={styles.desktopThumbnailGrid}>
|
||||
@@ -139,11 +142,17 @@ export default function Gallery({
|
||||
<motion.div
|
||||
key={image.smallSrc || image.src}
|
||||
className={styles.thumbnailContainer}
|
||||
onClick={() => 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 }}
|
||||
>
|
||||
<ButtonRAC
|
||||
className={styles.imageButton}
|
||||
onPress={() => onSelectImage(image)}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Open image",
|
||||
})}
|
||||
>
|
||||
<Image
|
||||
src={image.smallSrc || image.src}
|
||||
@@ -152,6 +161,7 @@ export default function Gallery({
|
||||
sizes="200px"
|
||||
className={styles.image}
|
||||
/>
|
||||
</ButtonRAC>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
@@ -160,19 +170,21 @@ export default function Gallery({
|
||||
|
||||
{/* Mobile Gallery */}
|
||||
<div className={styles.mobileGallery}>
|
||||
<div className={styles.mobileGalleryContent}>
|
||||
<div className={styles.thumbnailGrid}>
|
||||
{images.map((image, index) => (
|
||||
<motion.div
|
||||
key={image.smallSrc || image.src}
|
||||
className={`${styles.thumbnailContainer} ${index % 3 === 0 ? styles.fullWidthImage : ""}`}
|
||||
onClick={() => {
|
||||
onSelectImage(image)
|
||||
onImageClick()
|
||||
}}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
>
|
||||
<ButtonRAC
|
||||
className={styles.imageButton}
|
||||
aria-label={intl.formatMessage({ defaultMessage: "Open image" })}
|
||||
onPress={() => {
|
||||
onSelectImage(image)
|
||||
onImageClick()
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={image.smallSrc || image.src}
|
||||
@@ -181,11 +193,10 @@ export default function Gallery({
|
||||
sizes="100vw"
|
||||
className={styles.image}
|
||||
/>
|
||||
</ButtonRAC>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
57
apps/scandic-web/components/Lightbox/lightbox.module.css
Normal file
57
apps/scandic-web/components/Lightbox/lightbox.module.css
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,7 @@ const meta: Meta<typeof Button> = {
|
||||
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: <MaterialIcon icon="favorite" size={24} />,
|
||||
variant: 'Icon',
|
||||
},
|
||||
}
|
||||
|
||||
export const IconWithColor: Story = {
|
||||
args: {
|
||||
onPress: fn(),
|
||||
children: (
|
||||
<MaterialIcon
|
||||
icon="check_circle"
|
||||
size={24}
|
||||
color="Icon/Feedback/Success"
|
||||
/>
|
||||
),
|
||||
variant: 'Icon',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client'
|
||||
|
||||
import { Button as ButtonRAC } from 'react-aria-components'
|
||||
|
||||
import { variants } from './variants'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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<typeof IconButton> = {
|
||||
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<typeof IconButton>
|
||||
|
||||
export const PrimaryDefault: Story = {
|
||||
args: {
|
||||
onPress: fn(),
|
||||
children: <MaterialIcon icon="search" size={24} color="CurrentColor" />,
|
||||
theme: 'Primary',
|
||||
},
|
||||
}
|
||||
|
||||
export const PrimaryDisabled: Story = {
|
||||
args: {
|
||||
...PrimaryDefault.args,
|
||||
isDisabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const InvertedDefault: Story = {
|
||||
args: {
|
||||
onPress: fn(),
|
||||
children: (
|
||||
<MaterialIcon icon="arrow_forward" size={24} color="CurrentColor" />
|
||||
),
|
||||
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: <MaterialIcon icon="close" size={24} color="CurrentColor" />,
|
||||
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: <MaterialIcon icon="arrow_back" size={24} color="CurrentColor" />,
|
||||
theme: 'Tertiary',
|
||||
style: 'Elevated',
|
||||
},
|
||||
}
|
||||
|
||||
export const TertiaryDisabled: Story = {
|
||||
args: {
|
||||
...TertiaryElevated.args,
|
||||
isDisabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const BlackMuted: Story = {
|
||||
args: {
|
||||
onPress: fn(),
|
||||
children: <MaterialIcon icon="close" size={24} color="CurrentColor" />,
|
||||
theme: 'Black',
|
||||
},
|
||||
}
|
||||
|
||||
export const BlackMutedDisabled: Story = {
|
||||
args: {
|
||||
...BlackMuted.args,
|
||||
isDisabled: true,
|
||||
},
|
||||
}
|
||||
@@ -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 <ButtonRAC {...props} className={classNames} />
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { IconButton } from './IconButton'
|
||||
10
packages/design-system/lib/components/IconButton/types.ts
Normal file
10
packages/design-system/lib/components/IconButton/types.ts
Normal file
@@ -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<ComponentProps<typeof Button>, 'style'>,
|
||||
VariantProps<typeof variants> {}
|
||||
78
packages/design-system/lib/components/IconButton/variants.ts
Normal file
78
packages/design-system/lib/components/IconButton/variants.ts
Normal file
@@ -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)
|
||||
@@ -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={
|
||||
<Button variant="Icon" size="Small">
|
||||
<IconButton theme="Black" style="Muted">
|
||||
<MaterialIcon
|
||||
icon="info"
|
||||
size={20}
|
||||
color="Icon/Default"
|
||||
/>
|
||||
</Button>
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
{rateTermDetails.map((termGroup) => (
|
||||
|
||||
@@ -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={
|
||||
<Button variant="Icon" size="Small">
|
||||
<IconButton theme="Black" style="Muted">
|
||||
<MaterialIcon
|
||||
icon="info"
|
||||
size={20}
|
||||
color="Icon/Default"
|
||||
/>
|
||||
</Button>
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
{rateTermDetails.map((termGroup) => (
|
||||
|
||||
@@ -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({
|
||||
<header>
|
||||
<Typography variant="Tag/sm">
|
||||
<h3 className={`${styles.title} ${styles.textDisabled}`}>
|
||||
<Button variant="Icon" size="Small">
|
||||
<IconButton theme="Black" style="Muted">
|
||||
<MaterialIcon icon="info" size={20} color="Icon/Default" />
|
||||
</Button>
|
||||
</IconButton>
|
||||
{`${rateTitle} / ${paymentTerm}`}
|
||||
</h3>
|
||||
</Typography>
|
||||
|
||||
@@ -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={
|
||||
<Button variant="Icon" size="Small">
|
||||
<IconButton theme="Black" style="Muted">
|
||||
<MaterialIcon icon="info" size={20} color="Icon/Default" />
|
||||
</Button>
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
{rateTermDetails.map((termGroup) => (
|
||||
|
||||
@@ -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={
|
||||
<Button variant="Icon" size="Small">
|
||||
<IconButton theme="Black" style="Muted">
|
||||
<MaterialIcon
|
||||
icon="info"
|
||||
size={20}
|
||||
color="Icon/Default"
|
||||
/>
|
||||
</Button>
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
{rateTermDetails.map((termGroup) => (
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user