Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 108 additions & 15 deletions src/fullscreen-image-carousel/fullscreen-image-carousel.style.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import styled, { css } from "styled-components";
import { ClickableIcon } from "../shared/clickable-icon";
import { ImagePlaceholder } from "../shared/image-placeholder";
import { InsetStyleProps } from "../shared/types";
import { Border, Colour, MediaQuery, Radius, Shadow, Spacing } from "../theme";
import {
Border,
Colour,
Font,
MediaQuery,
Radius,
Shadow,
Spacing,
} from "../theme";
import { Typography } from "../typography";
import { StatefulImage } from "./stateful-image";

Expand All @@ -17,6 +25,14 @@ interface ThumbnailItemStyleProps {
$active?: boolean;
}

interface TopActionButtonsStyleProps extends InsetStyleProps {
$hasFileInfo?: boolean | undefined;
}

interface FileInfoTextWrapperStyleProps {
$centerContent?: boolean | undefined;
}

// =============================================================================
// STYLING
// =============================================================================
Expand Down Expand Up @@ -44,23 +60,61 @@ const IconButton = styled(ClickableIcon)`
}
`;

export const TopActionButtons = styled.div<InsetStyleProps>`
position: absolute;
top: ${(props) =>
css`calc(${Spacing["spacing-48"]} + ${props.$insetTop || 0}px)`};
right: ${(props) =>
css`calc(${Spacing["spacing-48"]} + ${props.$insetRight || 0}px)`};
z-index: 5;
export const TopActionButtons = styled.div<TopActionButtonsStyleProps>`
order: -1;
display: flex;
align-items: center;
justify-content: flex-end;
gap: ${Spacing["spacing-16"]};

${MediaQuery.MaxWidth.sm} {
top: ${(props) =>
css`calc(${Spacing["spacing-20"]} + ${props.$insetTop || 0}px)`};
right: ${(props) =>
css`calc(${Spacing["spacing-20"]} + ${props.$insetRight || 0}px)`};
}
${(props) =>
props.$hasFileInfo
? css`
flex-shrink: 0;
background-color: ${Colour["bg-inverse"]};
padding-top: calc(
${Spacing["spacing-24"]} + ${props.$insetTop || 0}px
);
padding-bottom: ${Spacing["spacing-24"]};
padding-left: calc(
${Spacing["spacing-32"]} + ${props.$insetLeft || 0}px
);
padding-right: calc(
${Spacing["spacing-32"]} + ${props.$insetRight || 0}px
);

${MediaQuery.MaxWidth.sm} {
padding-top: calc(
${Spacing["spacing-16"]} + ${props.$insetTop || 0}px
);
padding-bottom: ${Spacing["spacing-16"]};
padding-left: calc(
${Spacing["spacing-20"]} + ${props.$insetLeft || 0}px
);
padding-right: calc(
${Spacing["spacing-20"]} + ${props.$insetRight || 0}px
);
}
`
: css`
position: absolute;
top: calc(
${Spacing["spacing-48"]} + ${props.$insetTop || 0}px
);
right: calc(
${Spacing["spacing-48"]} + ${props.$insetRight || 0}px
);
z-index: 5;

${MediaQuery.MaxWidth.sm} {
top: calc(
${Spacing["spacing-20"]} + ${props.$insetTop || 0}px
);
right: calc(
${Spacing["spacing-20"]} + ${props.$insetRight || 0}px
);
}
`}
`;

export const CloseButton = styled(IconButton)``;
Expand Down Expand Up @@ -114,7 +168,8 @@ export const ImageGalleryContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
flex: 1;
min-height: 0;
`;

export const ImageGalleryWrapper = styled.div`
Expand Down Expand Up @@ -312,3 +367,41 @@ export const ThumbnailImage = styled(StatefulImage)`
height: 100%;
width: 100%;
`;

// -----------------------------------------------------------------------------
// FILE INFO BAR STYLING
// -----------------------------------------------------------------------------

export const FileInfoTextWrapper = styled.div<FileInfoTextWrapperStyleProps>`
flex: 1;
display: flex;
flex-direction: column;
gap: ${Spacing["spacing-8"]};
overflow: hidden;
min-width: 0;
min-height: calc(
${Font.Spec["body-lh-baseline"]} + ${Spacing["spacing-8"]} +
${Font.Spec["body-lh-md"]}
);
${(props) =>
props.$centerContent &&
css`
justify-content: center;
`}
`;

export const FileInfoFileName = styled(Typography.BodyBL)`
color: ${Colour["text-inverse"]};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
`;

export const FileInfoFileSize = styled(Typography.BodyMD)`
color: ${Colour["text-inverse"]};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
`;
44 changes: 43 additions & 1 deletion src/fullscreen-image-carousel/fullscreen-image-carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from "react";
Expand All @@ -31,6 +32,9 @@ import {
Chip,
CloseButton,
DeleteButton,
FileInfoFileName,
FileInfoFileSize,
FileInfoTextWrapper,
FocusableImageRegion,
ImageGalleryContainer,
ImageGallerySlide,
Expand Down Expand Up @@ -98,6 +102,13 @@ export const Component = (
(item) => isCustomItem(item) && !!item.itemLabel?.trim()
);
const carouselItemNoun = hasAnyItemLabel ? "item" : "image";
const hasFileInfo = useMemo(
() =>
items.some(
(item) => item.fileName?.trim() || item.fileSize?.trim()
),
[items]
);

const getItemAriaLabel = useCallback(
(index: number) => {
Expand Down Expand Up @@ -348,6 +359,35 @@ export const Component = (
);
};

const renderFileInfo = () => {
const { fileName, fileSize } = currentItem ?? {};
const trimmedName = fileName?.trim();
const trimmedSize = fileSize?.trim();

return (
<FileInfoTextWrapper
$centerContent={!trimmedSize}
aria-live="polite"
aria-atomic="true"
data-testid="file-info-bar"
>
{trimmedName && (
<FileInfoFileName
weight="semibold"
data-testid="file-info-name"
>
{trimmedName}
</FileInfoFileName>
)}
{trimmedSize && (
<FileInfoFileSize data-testid="file-info-size">
{trimmedSize}
</FileInfoFileSize>
)}
</FileInfoTextWrapper>
);
};

const renderThumbnails = () => {
return (
<ThumbnailContainer
Expand Down Expand Up @@ -443,11 +483,13 @@ export const Component = (

{!hideThumbnail && renderThumbnails()}
</ImageGalleryContainer>

<TopActionButtons
$hasFileInfo={hasFileInfo}
$insetTop={insets?.top}
$insetLeft={insets?.left}
$insetRight={insets?.right}
>
{hasFileInfo && renderFileInfo()}
{!hideMagnifier && !isCustomItem(currentItem) && (
<MagnifierButton
aria-label={zoom === 1 ? "Zoom in" : "Zoom out"}
Expand Down
11 changes: 9 additions & 2 deletions src/fullscreen-image-carousel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ export interface FullscreenImageCarouselProps
insets?: Insets | undefined;
}

export interface FullscreenImageCarouselImageItemProps {
interface FullscreenImageCarouselBaseItemProps {
fileName?: string | undefined;
fileSize?: string | undefined;
}

export interface FullscreenImageCarouselImageItemProps
extends FullscreenImageCarouselBaseItemProps {
type?: "image" | undefined;
src: string;
alt?: string | undefined;
Expand All @@ -38,7 +44,8 @@ export interface FullscreenImageCarouselImageItemProps {
/** @deprecated Use FullscreenImageCarouselImageItemProps instead */
export type FullscreenCarouselItemProps = FullscreenImageCarouselImageItemProps;

export interface FullscreenImageCarouselCustomItemProps {
export interface FullscreenImageCarouselCustomItemProps
extends FullscreenImageCarouselBaseItemProps {
type: "custom";
/** The thumbnail image src. If omitted, a placeholder is shown in the thumbnail strip. */
thumbnailSrc?: string | undefined;
Expand Down
4 changes: 2 additions & 2 deletions stories/fullscreen-image-carousel/doc-elements.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FullscreenCarouselItemProps } from "src/fullscreen-image-carousel";
import { FullscreenImageCarouselImageItemProps } from "src/fullscreen-image-carousel";

const RESOLUTIONS = [
[1600, 900],
Expand All @@ -11,7 +11,7 @@ const RESOLUTIONS = [
];

export const getImages = (size: number) => {
const images: FullscreenCarouselItemProps[] = [];
const images: FullscreenImageCarouselImageItemProps[] = [];
for (let i = 0; i < size; i++) {
const [width, height] = RESOLUTIONS[i % RESOLUTIONS.length];
images.push({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ import { FullscreenImageCarousel } from "@lifesg/react-design-system/fullscreen-

<Canvas of={FullscreenImageCarouselStories.Configurable} />

## With file info

<Canvas of={FullscreenImageCarouselStories.WithFileInfo} />

## With custom content

Slides can render arbitrary content via `renderContent`. If the content is not an image, you are recommended to set `itemLabel` so that the type of content can be described accurately to screen readers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,44 @@ export const Configurable: StoryObj<Component> = {
},
};

export const WithFileInfo: StoryObj<Component> = {
render: (_args) => {
const [show, setShow] = useState(false);
return (
<>
<Button.Default
onClick={() => {
setShow((old) => !old);
}}
>
Show carousel
</Button.Default>
<FullscreenImageCarousel
items={[
{
src: "https://picsum.photos/id/157/1600/900",
fileName: "landscape-photo.jpg",
fileSize: "1.2 MB",
},
{
src: "https://picsum.photos/id/10/1600/900",
fileName:
"this-is-a-very-long-file-name-that-should-be-truncated-when-it-exceeds-the-available-width-in-the-bar.jpg",
fileSize:
"234,567,890.12 MB (123,456,789.99 MB compressed)",
},
{
src: "https://picsum.photos/id/369/1000/1000",
},
]}
show={show}
onClose={() => setShow(false)}
/>
</>
);
},
};

export const WithCustomContent: StoryObj<Component> = {
render: (_args) => {
const [show, setShow] = useState(false);
Expand Down
12 changes: 12 additions & 0 deletions stories/fullscreen-image-carousel/props-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ const DATA: ApiTableSectionProps[] = [
</>
),
},
{
name: "fileName",
description:
"The file name to display in the file info bar at the top",
propTypes: ["string"],
},
{
name: "fileSize",
description:
'The pre-formatted file size string to display in the file info bar at the top (e.g. "2.4 MB")',
propTypes: ["string"],
},
],
},
{
Expand Down
Loading