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
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import { Button } from "@lifesg/react-design-system/button";
import { Drawer } from "@lifesg/react-design-system/drawer";
import { Typography } from "@lifesg/react-design-system/typography";
import { useState } from "react";

import styles from "./drawer.module.css";

export default function Story() {
const [show, setShow] = useState(false);

return (
<>
<Button onClick={() => setShow(true)}>Open drawer</Button>
<Drawer
data-testid="drawer"
show={show}
heading="Replace this slot in the right panel with your content"
onClose={() => setShow(false)}
onOverlayClick={() => setShow(false)}
customCallToAction={
<div className={styles["cta"]}>
<Button onClick={() => setShow(false)}>
Save changes
</Button>
<Button
styleType="light"
onClick={() => setShow(false)}
>
Cancel
</Button>
</div>
}
>
<div className={styles["content"]}>
<Typography.BodyBL>Drawer content</Typography.BodyBL>
</div>
</Drawer>
</>
);
}
6 changes: 6 additions & 0 deletions e2e/nextjs-app/src/app/components/drawer/drawer.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
padding: 1rem;
}

.cta {
display: flex;
gap: 1rem;
white-space: nowrap;
}

.item-margin-bottom {
margin-bottom: 1rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use client";

import { LocalNavDropdown } from "@lifesg/react-design-system/local-nav";
import { DocIcon } from "@lifesg/react-icons/doc";
import { ExclamationCircleFillIcon } from "@lifesg/react-icons/exclamation-circle-fill";
import clsx from "clsx";
import { useState } from "react";

import styles from "./title-addon.module.css";

const LeftIcon = () => <DocIcon aria-hidden className={styles["icon"]} />;
const RightIcon = () => (
<ExclamationCircleFillIcon
aria-label="Has errors"
className={styles["right-icon"]}
/>
);

// One item per combination, with a wrapping title to exercise first-line
// alignment of the left addon and the selected tick. A final short-title item
// captures right-addon alignment on a single line.
const ITEMS = [
{
id: "section-1",
title: "A long section title that wraps onto multiple lines",
titleAddon: { left: <LeftIcon />, right: <RightIcon /> },
},
{
id: "section-2",
title: "A long section title that wraps onto multiple lines",
titleAddon: { left: <LeftIcon /> },
},
{
id: "section-3",
title: "A long section title that wraps onto multiple lines",
titleAddon: { right: <RightIcon /> },
},
{
id: "section-4",
title: "Short title",
titleAddon: { right: <RightIcon /> },
},
];

export default function Story() {
const [selectedItemIndex, setSelectedItemIndex] = useState(0);

return (
<div className={clsx("story-background", styles["container"])}>
<LocalNavDropdown
data-testid="local-nav-dropdown"
defaultLabel="Jump to section"
items={ITEMS}
selectedItemIndex={selectedItemIndex}
stickyOffset={0}
onNavItemSelect={(_e, _item, index) =>
setSelectedItemIndex(index)
}
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import { LocalNavMenu } from "@lifesg/react-design-system/local-nav";
import { DocIcon } from "@lifesg/react-icons/doc";
import { ExclamationCircleFillIcon } from "@lifesg/react-icons/exclamation-circle-fill";
import clsx from "clsx";
import { useState } from "react";

import styles from "./title-addon.module.css";

const LeftIcon = () => <DocIcon aria-hidden className={styles["icon"]} />;
const RightIcon = () => (
<ExclamationCircleFillIcon
aria-label="Has errors"
className={styles["right-icon"]}
/>
);

// One item per combination, with a wrapping title to exercise first-line
// alignment of the left addon and the selected tick. A final short-title item
// captures right-addon alignment on a single line.
const ITEMS = [
{
id: "section-1",
title: "A long section title that wraps onto multiple lines",
titleAddon: { left: <LeftIcon />, right: <RightIcon /> },
},
{
id: "section-2",
title: "A long section title that wraps onto multiple lines",
titleAddon: { left: <LeftIcon /> },
},
{
id: "section-3",
title: "A long section title that wraps onto multiple lines",
Comment thread
qroll marked this conversation as resolved.
titleAddon: { right: <RightIcon /> },
},
{
id: "section-4",
title: "Short title",
titleAddon: { right: <RightIcon /> },
},
];

export default function Story() {
const [selectedItemIndex, setSelectedItemIndex] = useState(0);

return (
<div className={clsx("story-background", styles["container"])}>
<LocalNavMenu
data-testid="local-nav-menu"
items={ITEMS}
selectedItemIndex={selectedItemIndex}
onNavItemSelect={(_e, _item, index) =>
setSelectedItemIndex(index)
}
/>
</div>
);
}
14 changes: 14 additions & 0 deletions e2e/nextjs-app/src/app/components/local-nav/title-addon.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.container {
max-width: 320px;
}

.icon {
width: 1.25rem;
height: 1.25rem;
}

.right-icon {
width: 1.25rem;
height: 1.25rem;
color: var(--fds-colour-icon-error);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 30 additions & 1 deletion e2e/tests/components/drawer/drawer.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { test as base, expect, Locator, Page } from "@playwright/test";
import { AbstractStoryPage, compareScreenshot } from "../../utils";
import { viewport } from "../../consts";

class StoryPage extends AbstractStoryPage {
protected readonly component = "drawer";
Expand Down Expand Up @@ -119,6 +118,36 @@ test.describe("Drawer", () => {
});
});

test.describe("Custom call to action", () => {
test.describe(() => {
test.beforeEach(async ({ story }) => {
await story.init("custom-call-to-action");
});

// Wide drawer: CTA sits inline to the right of the (truncated) heading.
test("Wide", async ({ story }) => {
await story.open();
await compareScreenshot(story, "cta-open", {
fullscreen: true,
});
});
});

test.describe(() => {
test.beforeEach(async ({ story }) => {
await story.init("custom-call-to-action", { size: "mobile" });
});

// Narrow drawer: CTA wraps below the heading, buttons flushed left.
test("Narrow (stacked)", async ({ story }) => {
await story.open();
await compareScreenshot(story, "cta-open-mobile", {
fullscreen: true,
});
});
});
});

test.describe(() => {
test.beforeEach(async ({ story }) => {
await story.init("sticky-header");
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions e2e/tests/components/local-nav/local-nav.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ test.describe("Local nav", () => {
await compareScreenshot(story, "hover");
});
});
test.describe("Menu with title addon", () => {
test.beforeEach(async ({ story }) => {
await story.init("menu-title-addon");
});

// Left addon + selected tick align to the first line of a wrapped
// title; right addon stays vertically centred and flushed right.
test("Default", async ({ story }) => {
await compareScreenshot(story, "addon-mount");
});
});
test.describe("Dropdown", () => {
test.beforeEach(async ({ story }) => {
await story.init("dropdown");
Expand Down Expand Up @@ -148,6 +159,18 @@ test.describe("Local nav", () => {
});
});
});
test.describe("Dropdown with title addon", () => {
test.beforeEach(async ({ story }) => {
await story.init("dropdown-title-addon");
});

// Left addon + selected tick align to the first line of a wrapped
// title; right addon stays vertically centred and flushed right.
test("Default", async ({ story }) => {
await story.locators.dropdownLabel.click();
await compareScreenshot(story, "addon-open", { fullscreen: true });
});
});
test.describe(() => {
test.beforeEach(async ({ story }) => {
await story.init("dropdown", { mode: "dark" });
Expand Down
19 changes: 19 additions & 0 deletions src/drawer/drawer.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,25 @@ export const header = css`
}
`;

// Stack the call-to-action below the heading when the drawer is too narrow
// to fit both on one line.
export const headerStacked = css`
flex-direction: column;
align-items: stretch;
gap: ${Spacing["spacing-16"]};
`;

export const callToAction = css`
display: flex;
margin-left: auto;
`;

// When stacked, flush left so the buttons line up with the heading (the
// header's left padding already clears the close icon).
export const callToActionStacked = css`
margin-left: 0;
`;

export const closeButton = css`
color: ${Colour["icon"]};
padding: 0;
Expand Down
Loading
Loading