diff --git a/e2e/nextjs-app/src/app/components/navbar/common.ts b/e2e/nextjs-app/src/app/components/navbar/common.ts new file mode 100644 index 0000000000..00967daf50 --- /dev/null +++ b/e2e/nextjs-app/src/app/components/navbar/common.ts @@ -0,0 +1,61 @@ +import type { + NavbarButtonProps, + NavItemProps, +} from "@lifesg/react-design-system/navbar"; + +export const navItems: NavItemProps[] = [ + { + id: "home", + children: "Home", + href: "https://www.life.gov.sg", + }, + { + id: "guides", + children: "Guides", + href: "https://www.life.gov.sg", + }, + { + id: "lifesg-app", + children: "LifeSG app", + href: "https://www.life.gov.sg", + }, +]; + +export const downloadActionButtons: NavbarButtonProps[] = [ + { type: "download" }, +]; + +export const navItemsWithSubmenu: NavItemProps[] = [ + { + id: "home", + children: "Home", + href: "https://www.life.gov.sg", + }, + { + id: "services", + children: "Services", + href: "https://www.life.gov.sg/?section=services-and-tools", + subMenu: [ + { + id: "services-getting-started", + children: "Birth registration", + href: "https://www.life.gov.sg", + }, + { + id: "services-baby-bonus", + children: "Baby bonus", + href: "https://www.life.gov.sg", + }, + { + id: "services-preschool-search", + children: "Preschool search", + href: "https://www.life.gov.sg", + }, + ], + }, + { + id: "lifesg-app", + children: "LifeSG app", + href: "https://www.life.gov.sg", + }, +]; diff --git a/e2e/nextjs-app/src/app/components/navbar/compress.e2e.tsx b/e2e/nextjs-app/src/app/components/navbar/compress.e2e.tsx new file mode 100644 index 0000000000..b2c27495de --- /dev/null +++ b/e2e/nextjs-app/src/app/components/navbar/compress.e2e.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { Navbar } from "@lifesg/react-design-system/navbar"; +import { navItems, downloadActionButtons } from "./common"; + +export default function Story() { + return ( + + ); +} diff --git a/e2e/nextjs-app/src/app/components/navbar/custom-action-button.e2e.tsx b/e2e/nextjs-app/src/app/components/navbar/custom-action-button.e2e.tsx new file mode 100644 index 0000000000..73c884de11 --- /dev/null +++ b/e2e/nextjs-app/src/app/components/navbar/custom-action-button.e2e.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { Navbar } from "@lifesg/react-design-system/navbar"; +import { navItems } from "./common"; +import { Avatar } from "@lifesg/react-design-system/avatar"; + +export default function Story() { + return ( + GovTech }, + uncollapsible: true, + }, + ], + }} + /> + ); +} diff --git a/e2e/nextjs-app/src/app/components/navbar/default.e2e.tsx b/e2e/nextjs-app/src/app/components/navbar/default.e2e.tsx new file mode 100644 index 0000000000..bbdeffff20 --- /dev/null +++ b/e2e/nextjs-app/src/app/components/navbar/default.e2e.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { Navbar } from "@lifesg/react-design-system/navbar"; +import { navItems, downloadActionButtons } from "./common"; + +export default function Story() { + return ( + + ); +} diff --git a/e2e/nextjs-app/src/app/components/navbar/hide-branding.e2e.tsx b/e2e/nextjs-app/src/app/components/navbar/hide-branding.e2e.tsx new file mode 100644 index 0000000000..b1bc72ed03 --- /dev/null +++ b/e2e/nextjs-app/src/app/components/navbar/hide-branding.e2e.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { Navbar } from "@lifesg/react-design-system/navbar"; +import { navItems, downloadActionButtons } from "./common"; + +export default function Story() { + return ( + + ); +} diff --git a/e2e/nextjs-app/src/app/components/navbar/hide-link-indicator.e2e.tsx b/e2e/nextjs-app/src/app/components/navbar/hide-link-indicator.e2e.tsx new file mode 100644 index 0000000000..a3d874916e --- /dev/null +++ b/e2e/nextjs-app/src/app/components/navbar/hide-link-indicator.e2e.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { Navbar } from "@lifesg/react-design-system/navbar"; +import { navItems, downloadActionButtons } from "./common"; + +export default function Story() { + return ( + + ); +} diff --git a/e2e/nextjs-app/src/app/components/navbar/multiple-action-buttons.e2e.tsx b/e2e/nextjs-app/src/app/components/navbar/multiple-action-buttons.e2e.tsx new file mode 100644 index 0000000000..ed4c363f9f --- /dev/null +++ b/e2e/nextjs-app/src/app/components/navbar/multiple-action-buttons.e2e.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Navbar } from "@lifesg/react-design-system/navbar"; +import { navItems } from "./common"; + +export default function Story() { + return ( + + ); +} diff --git a/e2e/nextjs-app/src/app/components/navbar/no-masthead.e2e.tsx b/e2e/nextjs-app/src/app/components/navbar/no-masthead.e2e.tsx new file mode 100644 index 0000000000..38cad3c79f --- /dev/null +++ b/e2e/nextjs-app/src/app/components/navbar/no-masthead.e2e.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { Navbar } from "@lifesg/react-design-system/navbar"; +import { navItems, downloadActionButtons } from "./common"; + +export default function Story() { + return ( + + ); +} diff --git a/e2e/nextjs-app/src/app/components/navbar/secondary-brand.e2e.tsx b/e2e/nextjs-app/src/app/components/navbar/secondary-brand.e2e.tsx new file mode 100644 index 0000000000..f93a778774 --- /dev/null +++ b/e2e/nextjs-app/src/app/components/navbar/secondary-brand.e2e.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { Navbar } from "@lifesg/react-design-system/navbar"; +import { navItems, downloadActionButtons } from "./common"; + +export default function Story() { + return ( + + ); +} diff --git a/e2e/nextjs-app/src/app/components/navbar/single-action-button.e2e.tsx b/e2e/nextjs-app/src/app/components/navbar/single-action-button.e2e.tsx new file mode 100644 index 0000000000..0bc715705a --- /dev/null +++ b/e2e/nextjs-app/src/app/components/navbar/single-action-button.e2e.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { Navbar } from "@lifesg/react-design-system/navbar"; + +export default function Story() { + return ( + + ); +} diff --git a/e2e/nextjs-app/src/app/components/navbar/stretch.e2e.tsx b/e2e/nextjs-app/src/app/components/navbar/stretch.e2e.tsx new file mode 100644 index 0000000000..7ad1d44f52 --- /dev/null +++ b/e2e/nextjs-app/src/app/components/navbar/stretch.e2e.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { Navbar } from "@lifesg/react-design-system/navbar"; +import { navItems, downloadActionButtons } from "./common"; + +export default function Story() { + return ( + + ); +} diff --git a/e2e/nextjs-app/src/app/components/navbar/submenu.e2e.tsx b/e2e/nextjs-app/src/app/components/navbar/submenu.e2e.tsx new file mode 100644 index 0000000000..e60d60cb43 --- /dev/null +++ b/e2e/nextjs-app/src/app/components/navbar/submenu.e2e.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { Navbar } from "@lifesg/react-design-system/navbar"; +import { downloadActionButtons, navItemsWithSubmenu } from "./common"; + +export default function Story() { + return ( + {}} + selectedId="services" + /> + ); +} diff --git a/e2e/nextjs-app/src/proxy.ts b/e2e/nextjs-app/src/proxy.ts index 07356bcf75..e24f7eb1ce 100644 --- a/e2e/nextjs-app/src/proxy.ts +++ b/e2e/nextjs-app/src/proxy.ts @@ -20,9 +20,8 @@ export function proxy(request: NextRequest) { isDev ? "'unsafe-eval'" : "" } https://cdn.jsdelivr.net/npm/@govtechsg/sgds-web-component@3/components/Masthead/index.umd.js; ${styleSrcDirective} - img-src 'self' https://assets.life.gov.sg - https://mylegacy.life.gov.sg https://home.booking.gov.sg blob: data:; - font-src 'self'; + img-src 'self' https://*.life.gov.sg https://*.booking.gov.sg blob: data:; + font-src 'self' https://assets.life.gov.sg; object-src 'none'; base-uri 'self'; form-action 'self'; diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Compress--mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Compress--mount.png new file mode 100644 index 0000000000..ad6b167040 Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Compress--mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Default--mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Default--mount.png new file mode 100644 index 0000000000..57d3ae2093 Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Default--mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Default-dark-mode---mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Default-dark-mode---mount.png new file mode 100644 index 0000000000..749c100dcd Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Default-dark-mode---mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Hide-branding--mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Hide-branding--mount.png new file mode 100644 index 0000000000..e074507a47 Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Hide-branding--mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Hide-link-indicator--mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Hide-link-indicator--mount.png new file mode 100644 index 0000000000..155c79a7f9 Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Hide-link-indicator--mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Collapsed--mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Collapsed--mount.png new file mode 100644 index 0000000000..e3d7506d3a Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Collapsed--mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Collapsed-dark-mode---mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Collapsed-dark-mode---mount.png new file mode 100644 index 0000000000..61026c78be Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Collapsed-dark-mode---mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Drawer--open.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Drawer--open.png new file mode 100644 index 0000000000..c318683e6f Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Drawer--open.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Drawer-dark-mode---open.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Drawer-dark-mode---open.png new file mode 100644 index 0000000000..92fd2058d8 Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Drawer-dark-mode---open.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Secondary-brand--mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Secondary-brand--mount.png new file mode 100644 index 0000000000..4074c20033 Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Secondary-brand--mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Submenu--open.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Submenu--open.png new file mode 100644 index 0000000000..40fd9c71c1 Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-Submenu--open.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-With-avatar--mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-With-avatar--mount.png new file mode 100644 index 0000000000..7c253930f4 Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Mobile-With-avatar--mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Multiple-action-buttons--mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Multiple-action-buttons--mount.png new file mode 100644 index 0000000000..66321643aa Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Multiple-action-buttons--mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-No-masthead--mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-No-masthead--mount.png new file mode 100644 index 0000000000..024e0fb64c Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-No-masthead--mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Secondary-brand--mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Secondary-brand--mount.png new file mode 100644 index 0000000000..5d799e9360 Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Secondary-brand--mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Single-action-button--mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Single-action-button--mount.png new file mode 100644 index 0000000000..7140e44d76 Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Single-action-button--mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Stretch--mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Stretch--mount.png new file mode 100644 index 0000000000..22264780a4 Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Stretch--mount.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Submenu--open.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Submenu--open.png new file mode 100644 index 0000000000..72702e2d93 Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-Submenu--open.png differ diff --git a/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-With-avatar--mount.png b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-With-avatar--mount.png new file mode 100644 index 0000000000..ebd02824f9 Binary files /dev/null and b/e2e/tests/components/navbar/__screenshots__/chromium/Navbar-With-avatar--mount.png differ diff --git a/e2e/tests/components/navbar/navbar.e2e.spec.ts b/e2e/tests/components/navbar/navbar.e2e.spec.ts new file mode 100644 index 0000000000..4f9865841b --- /dev/null +++ b/e2e/tests/components/navbar/navbar.e2e.spec.ts @@ -0,0 +1,242 @@ +import { test as base, Locator, Page } from "@playwright/test"; +import { AbstractStoryPage, compareScreenshot } from "../../utils"; + +class StoryPage extends AbstractStoryPage { + protected readonly component = "navbar"; + + public readonly locators: { + internal: { + mobileMenuButton: Locator; + servicesTrigger: Locator; + servicesMobileTrigger: Locator; + }; + }; + + constructor(page: Page) { + super(page); + + this.locators = { + internal: { + mobileMenuButton: page.getByTestId("button__mobile-menu"), + servicesTrigger: page.getByRole("button", { name: "Services" }), + servicesMobileTrigger: page.getByTestId( + "link__mobile-2-expand-collapse-button" + ), + }, + }; + } + + async openMobileDrawer() { + await this.locators.internal.mobileMenuButton.click(); + } +} + +const test = base.extend<{ story: StoryPage }>({ + story: async ({ page }, use) => { + const story = new StoryPage(page); + await use(story); + }, +}); + +test.describe("Navbar", () => { + // ======================================================================= + // DESKTOP + // ======================================================================= + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("default"); + }); + + test("Default", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("default", { mode: "dark" }); + }); + + test("Default (dark mode)", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("compress"); + }); + + test("Compress", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("stretch", { size: "xxl" }); + }); + + test("Stretch", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("no-masthead"); + }); + + test("No masthead", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("hide-branding"); + }); + + test("Hide branding", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("hide-link-indicator"); + }); + + test("Hide link indicator", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("secondary-brand"); + }); + + test("Secondary brand", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("multiple-action-buttons"); + }); + + test("Multiple action buttons", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("single-action-button"); + }); + + test("Single action button", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("custom-action-button"); + }); + + test("With avatar", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("submenu"); + }); + + test("Submenu", async ({ story }) => { + await story.locators.internal.servicesTrigger.click(); + await compareScreenshot(story, "open", { + fullscreen: true, + }); + }); + }); + + // ======================================================================= + // MOBILE + // ======================================================================= + + test.describe("Mobile", () => { + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("default", { size: "mobile" }); + }); + + test("Collapsed", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + + test("Drawer", async ({ story }) => { + await story.openMobileDrawer(); + await compareScreenshot(story, "open", { + fullscreen: true, + }); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("default", { size: "mobile", mode: "dark" }); + }); + + test("Collapsed (dark mode)", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + + test("Drawer (dark mode)", async ({ story }) => { + await story.openMobileDrawer(); + await compareScreenshot(story, "open", { + fullscreen: true, + }); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("secondary-brand", { size: "mobile" }); + }); + + test("Secondary brand", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("custom-action-button", { size: "mobile" }); + }); + + test("With avatar", async ({ story }) => { + await compareScreenshot(story, "mount"); + }); + }); + + test.describe(() => { + test.beforeEach(async ({ story }) => { + await story.init("submenu", { size: "mobile" }); + }); + + test("Submenu", async ({ story }) => { + await story.openMobileDrawer(); + await story.locators.internal.servicesMobileTrigger.click(); + await compareScreenshot(story, "open", { + fullscreen: true, + }); + }); + }); + }); +}); diff --git a/src/navbar/brand.styles.ts b/src/navbar/brand.styles.ts new file mode 100644 index 0000000000..2906c57b5a --- /dev/null +++ b/src/navbar/brand.styles.ts @@ -0,0 +1,16 @@ +import { css } from "@linaria/core"; + +import { Motion } from "../theme"; + +export const container = css` + display: flex; + justify-content: center; + height: 100%; + + img { + width: auto; + height: 100%; + transition: ${Motion["duration-150"]} ${Motion["ease-default"]}; + object-fit: contain; + } +`; diff --git a/src/navbar/brand.styles.tsx b/src/navbar/brand.styles.tsx deleted file mode 100644 index 0758535553..0000000000 --- a/src/navbar/brand.styles.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import styled from "styled-components"; - -import { V3_Motion } from "../v3_theme"; -import type { BrandType } from "./types"; - -// ============================================================================= -// STYLE INTERFACE, transient props are denoted with $ -// See more https://styled-components.com/docs/api#transient-props -// ============================================================================= -interface StyleProps { - $type?: BrandType | undefined; -} - -// ============================================================================= -// STYLING -// ============================================================================= -export const Container = styled.a` - display: flex; - justify-content: center; - height: 100%; - - img { - width: auto; - height: 100%; - transition: ${V3_Motion["duration-150"]} ${V3_Motion["ease-default"]}; - object-fit: contain; - } -`; diff --git a/src/navbar/brand.tsx b/src/navbar/brand.tsx index 59ebb0a068..c82b6be9ea 100644 --- a/src/navbar/brand.tsx +++ b/src/navbar/brand.tsx @@ -1,7 +1,7 @@ import type React from "react"; import { ImageWithFallback } from "../shared/image-with-fallback/image-with-fallback"; -import { Container } from "./brand.styles"; +import * as styles from "./brand.styles"; import type { BrandType, NavbarBrandingProps } from "./types"; interface Props { @@ -44,24 +44,27 @@ export const Brand = ({ : `Go to ${resources.brandName}` : resources.brandName; - const props = isClickable - ? { - role: "link", - tabIndex: 0, - onClick: handleClick, - } - : {}; - - return ( - + + + ) : ( +
- +
); }; diff --git a/src/navbar/drawer.styles.ts b/src/navbar/drawer.styles.ts new file mode 100644 index 0000000000..2049a13271 --- /dev/null +++ b/src/navbar/drawer.styles.ts @@ -0,0 +1,91 @@ +import { css } from "@linaria/core"; + +import { Colour, MediaQuery, Motion, Shadow, Spacing } from "../theme"; + +export const tokens = { + container: { + viewHeight: "--fds-internal-navbar-drawer-viewHeight", + }, +}; + +export const wrapper = css` + display: none; + + ${MediaQuery.MaxWidth.lg} { + display: flex; + } +`; + +export const container = css` + position: absolute; + overflow-y: auto; + overflow-x: hidden; + display: block; + padding: 0 0 ${Spacing["spacing-16"]}; + background-color: ${Colour.bg}; + box-shadow: ${Shadow["xs-subtle"]}; + outline: none; + + /* reset variable to prevent leaking to child components */ + ${tokens.container.viewHeight}: initial; + height: calc(var(${tokens.container.viewHeight}, 1vh) * 100); + + ${MediaQuery.MaxWidth.lg} { + width: 75%; + } + + ${MediaQuery.MaxWidth.sm} { + width: 100%; + } +`; + +export const containerShown = css` + visibility: visible; + right: 0; + transition: all 300ms ${Motion["ease-entrance"]}; + transition-delay: 200ms; +`; + +export const containerHidden = css` + visibility: hidden; + right: -100%; + transition: all 300ms ${Motion["ease-exit"]}; +`; + +export const content = css` + display: flex; + flex-direction: column; +`; + +// ----------------------------------------------------------------------------- +// NAV CONTENTS +// ----------------------------------------------------------------------------- +export const topBar = css` + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + padding: ${Spacing["spacing-40"]} ${Spacing["spacing-20"]} + ${Spacing["spacing-32"]}; +`; + +export const closeIcon = css` + height: 1.5rem; + width: 1.5rem; +`; + +export const closeButton = css` + position: absolute; + right: calc(${Spacing["spacing-4"]} * -1); + color: ${Colour["icon"]}; + + &:active, + &:focus { + color: ${Colour["icon-hover"]}; + } + + svg { + height: 1.5rem; + width: 1.5rem; + } +`; diff --git a/src/navbar/drawer.styles.tsx b/src/navbar/drawer.styles.tsx deleted file mode 100644 index 481d633e10..0000000000 --- a/src/navbar/drawer.styles.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { CrossIcon } from "@lifesg/react-icons/cross"; -import styled, { css } from "styled-components"; - -import { ClickableIcon } from "../shared/clickable-icon"; -import { - V3_Colour, - V3_MediaQuery, - V3_Motion, - V3_Shadow, - V3_Spacing, -} from "../v3_theme"; - -// ============================================================================= -// STYLE INTERFACE, transient props are denoted with $ -// See more https://styled-components.com/docs/api#transient-props -// ============================================================================= -interface StyleProps { - $show: boolean; - $viewHeight?: number | undefined; -} - -// ============================================================================= -// STYLING HELPERS -// ============================================================================= -const VISIBILITY_STYLE = (show: boolean | undefined) => { - if (show) { - return css` - right: 0; - transition: all 300ms ${V3_Motion["ease-entrance"]}; - transition-delay: 200ms; - `; - } - - return css` - right: -100%; - transition: all 300ms ${V3_Motion["ease-exit"]}; - `; -}; - -// ============================================================================= -// STYLING -// ============================================================================= -export const Wrapper = styled.div` - display: none; - - ${V3_MediaQuery.MaxWidth.lg} { - display: flex; - } -`; - -export const Container = styled.nav` - position: absolute; - overflow-y: auto; - overflow-x: hidden; - height: 100vh; - display: block; - padding: 0 0 ${V3_Spacing["spacing-16"]}; - background-color: ${V3_Colour.bg}; - box-shadow: ${V3_Shadow["xs-subtle"]}; - visibility: ${(props) => (props.$show ? "visible" : "hidden")}; - outline: none; - - ${(props) => VISIBILITY_STYLE(props.$show)} - ${(props) => { - let viewHeight = "1vh"; - if (props.$viewHeight) { - viewHeight = `${props.$viewHeight}px`; - } - return css` - height: calc(${viewHeight} * 100); - `; - }} - - ${V3_MediaQuery.MaxWidth.lg} { - width: 75%; - } - - ${V3_MediaQuery.MaxWidth.sm} { - width: 100%; - } -`; - -export const Content = styled.div` - display: flex; - flex-direction: column; -`; - -// ----------------------------------------------------------------------------- -// NAV CONTENTS -// ----------------------------------------------------------------------------- -export const TopBar = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - position: relative; - padding: ${V3_Spacing["spacing-40"]} ${V3_Spacing["spacing-20"]} - ${V3_Spacing["spacing-32"]}; -`; - -export const CloseIcon = styled(CrossIcon)` - height: 1.5rem; - width: 1.5rem; -`; - -export const CloseButton = styled(ClickableIcon)` - position: absolute; - right: -${V3_Spacing["spacing-4"]}; - color: ${V3_Colour["icon"]}; - - &:active, - &:focus { - color: ${V3_Colour["icon-hover"]}; - } - - svg { - height: 1.5rem; - width: 1.5rem; - } -`; diff --git a/src/navbar/drawer.tsx b/src/navbar/drawer.tsx index 6ae7f4008f..d02e1d9ccb 100644 --- a/src/navbar/drawer.tsx +++ b/src/navbar/drawer.tsx @@ -1,15 +1,12 @@ +import { CrossIcon } from "@lifesg/react-icons/cross"; +import clsx from "clsx"; import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { ClickableIcon } from "../shared/clickable-icon"; +import { useApplyStyle } from "../theme"; import { Brand } from "./brand"; -import { - CloseButton, - CloseIcon, - Container, - Content, - TopBar, - Wrapper, -} from "./drawer.styles"; -import { NavBrandContainer, NavSeparator } from "./navbar.styles"; +import * as styles from "./drawer.styles"; +import * as navbarStyles from "./navbar.styles"; import type { NavbarDrawerProps } from "./types"; const Component = ( @@ -32,6 +29,12 @@ const Component = ( const [viewHeight, setViewHeight] = useState(0); const containerRef = useRef(null); + useApplyStyle(containerRef, { + [styles.tokens.container.viewHeight]: viewHeight + ? `${viewHeight}px` + : null, + }); + const { primary, secondary } = resources; // ============================================================================= @@ -104,7 +107,13 @@ const Component = ( )} {secondary && ( <> - +
( - - +
+
{!hideNavBranding && renderBrand()} - - + - - - + + +
); return ( - - + +
); }; diff --git a/src/navbar/menu.styles.ts b/src/navbar/menu.styles.ts new file mode 100644 index 0000000000..39e7c83109 --- /dev/null +++ b/src/navbar/menu.styles.ts @@ -0,0 +1,43 @@ +import { css } from "@linaria/core"; + +import { lineClampCss } from "../shared/styles"; +import { Border, Colour, Radius, Spacing } from "../theme"; + +export const mobileWrapper = css` + list-style: none; + display: flex; + flex-direction: column; + + margin: 0; + padding: 0; + + border-left: ${Border["width-040"]} solid ${Colour["border-selected"]}; +`; + +export const menuItem = css` + width: 100%; + display: flex; +`; + +export const link = css` + width: 100%; + text-align: left; + color: ${Colour["text"]}; + + margin: 0 ${Spacing["spacing-8"]}; + + /* use border, as padding still shows an extra line after the ellipsis */ + border: ${Border["solid"]} transparent; + border-width: ${Spacing["spacing-12"]} ${Spacing["spacing-8"]}; + border-radius: ${Radius["md"]}; + + ${lineClampCss(2)} + white-space: pre-wrap; + + &:hover, + &:active, + &:focus { + background-color: ${Colour["bg-hover"]}; + color: ${Colour["text"]}; + } +`; diff --git a/src/navbar/menu.styles.tsx b/src/navbar/menu.styles.tsx deleted file mode 100644 index 342b33ed35..0000000000 --- a/src/navbar/menu.styles.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import styled from "styled-components"; - -import { lineClampCss } from "../shared/styles"; -import { Typography } from "../typography"; -import { V3_Border, V3_Colour, V3_Radius, V3_Spacing } from "../v3_theme"; - -export const MobileWrapper = styled.ul` - list-style: none; - display: flex; - flex-direction: column; - - margin: 0; - padding: 0; - - border-left: ${V3_Border["width-040"]} solid ${V3_Colour["border-selected"]}; -`; - -export const MenuItem = styled.li` - width: 100%; - display: flex; -`; - -export const Link = styled(Typography.LinkBL)` - width: 100%; - text-align: left; - color: ${V3_Colour["text"]}; - - margin: 0 ${V3_Spacing["spacing-8"]}; - - // use border, as padding still shows an extra line after the ellipsis - border: ${V3_Border["solid"]} transparent; - border-width: ${V3_Spacing["spacing-12"]} ${V3_Spacing["spacing-8"]}; - border-radius: ${V3_Radius["md"]}; - - ${lineClampCss(2)} - white-space: pre-wrap; - - &:hover, - &:active, - &:focus { - background-color: ${V3_Colour["bg-hover"]}; - color: ${V3_Colour["text"]}; - } -`; diff --git a/src/navbar/menu.tsx b/src/navbar/menu.tsx index be81180618..d8c0a21e8e 100644 --- a/src/navbar/menu.tsx +++ b/src/navbar/menu.tsx @@ -1,6 +1,7 @@ import type React from "react"; -import { Link, MenuItem, MobileWrapper } from "./menu.styles"; +import { Typography } from "../typography"; +import * as styles from "./menu.styles"; import type { NavItemCommonProps } from "./types"; interface Props { @@ -22,14 +23,15 @@ export const Menu = ({ items, onItemClick }: Props): JSX.Element => { if (!items?.length) return <>; return ( - +
    {items.map((item, index) => { const { children, options, ...otherItemAttrs } = item; const testId = `menu__mobile-${index + 1}`; return ( - - + ({ items, onItemClick }: Props): JSX.Element => { underlineStyle="none" > {children} - - + + ); })} - +
); }; diff --git a/src/navbar/navbar-action-buttons.styles.ts b/src/navbar/navbar-action-buttons.styles.ts new file mode 100644 index 0000000000..906b318393 --- /dev/null +++ b/src/navbar/navbar-action-buttons.styles.ts @@ -0,0 +1,135 @@ +import { css } from "@linaria/core"; + +import { MediaQuery, Spacing } from "../theme"; + +// ============================================================================= +// WRAPPER +// ============================================================================= +export const wrapper = css` + display: flex; + list-style: none; + margin-left: ${Spacing["spacing-64"]}; + flex-shrink: 0; + + ${MediaQuery.MaxWidth.lg} { + display: none; + } +`; + +export const mobileWrapper = css` + display: none; + + ${MediaQuery.MaxWidth.lg} { + display: flex; + list-style: none; + margin-left: ${Spacing["spacing-64"]}; + flex-shrink: 0; + } +`; + +export const drawerWrapper = css` + display: none; + list-style: none; + + ${MediaQuery.MaxWidth.lg} { + display: flex; + flex-direction: column; + margin-top: ${Spacing["spacing-40"]}; + width: max-content; + min-width: 22rem; + max-width: 24rem; + } + + ${MediaQuery.MaxWidth.sm} { + width: 100%; + max-width: unset; + min-width: unset; + } +`; + +// ============================================================================= +// BUTTON ITEMS +// ============================================================================= +export const buttonItem = css` + position: relative; + display: flex; + align-items: center; + + &:not(:last-of-type) { + margin-right: ${Spacing["spacing-16"]}; + } + + ${MediaQuery.MaxWidth.lg} { + width: 100%; + padding: 0 0 0 ${Spacing["spacing-16"]}; + justify-content: center; + + &:not(:last-of-type) { + margin-right: 0; + margin-bottom: 0; + } + } +`; + +export const buttonItemMobile = css` + ${MediaQuery.MaxWidth.lg} { + &:not(:last-of-type) { + margin-bottom: ${Spacing["spacing-16"]}; + } + } + + ${MediaQuery.MaxWidth.sm} { + padding: 0 ${Spacing["spacing-16"]}; + } +`; + +export const actionButton = css` + ${MediaQuery.MaxWidth.lg} { + width: 100%; + } +`; + +// ============================================================================= +// DOWNLOAD APP +// ============================================================================= +export const downloadAppWrapper = css` + display: none; + + ${MediaQuery.MaxWidth.lg} { + display: flex; + flex-direction: column; + margin-top: ${Spacing["spacing-40"]}; + } +`; + +export const downloadAppTitle = css` + margin-bottom: ${Spacing["spacing-8"]}; +`; + +export const downloadAppImageLinkWrapper = css` + display: flex; +`; + +export const downloadAppImageLink = css` + &:not(:last-child) { + margin-right: ${Spacing["spacing-16"]}; + } + + img { + width: auto; + height: auto; + object-fit: contain; + } + + ${MediaQuery.MaxWidth.lg} { + img { + max-width: 11rem; + } + } + + ${MediaQuery.MaxWidth.sm} { + img { + max-width: 100%; + } + } +`; diff --git a/src/navbar/navbar-action-buttons.styles.tsx b/src/navbar/navbar-action-buttons.styles.tsx deleted file mode 100644 index 9528dba5a9..0000000000 --- a/src/navbar/navbar-action-buttons.styles.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import styled, { css } from "styled-components"; - -import { Button } from "../button"; -import { Typography } from "../typography"; -import { V3_MediaQuery, V3_Spacing } from "../v3_theme"; - -// ============================================================================= -// WRAPPER -// ============================================================================= -export const Wrapper = styled.ul` - display: flex; - list-style: none; - margin-left: ${V3_Spacing["spacing-64"]}; - flex-shrink: 0; - - ${V3_MediaQuery.MaxWidth.lg} { - display: none; - } -`; - -export const MobileWrapper = styled.ul` - display: none; - - ${V3_MediaQuery.MaxWidth.lg} { - display: flex; - list-style: none; - margin-left: ${V3_Spacing["spacing-64"]}; - flex-shrink: 0; - } -`; - -export const DrawerWrapper = styled.ul` - display: none; - list-style: none; - - ${V3_MediaQuery.MaxWidth.lg} { - display: flex; - flex-direction: column; - margin-top: ${V3_Spacing["spacing-40"]}; - width: max-content; - min-width: 22rem; - max-width: 24rem; - } - - ${V3_MediaQuery.MaxWidth.sm} { - width: 100%; - max-width: unset; - min-width: unset; - } -`; - -// ============================================================================= -// BUTTON ITEMS -// ============================================================================= -export const ButtonItem = styled.li<{ $mobile?: boolean }>` - position: relative; - display: flex; - align-items: center; - - &:not(:last-of-type) { - margin-right: ${V3_Spacing["spacing-16"]}; - } - - ${V3_MediaQuery.MaxWidth.lg} { - width: 100%; - padding: 0 0 0 ${V3_Spacing["spacing-16"]}; - justify-content: center; - - &:not(:last-of-type) { - margin-right: 0; - margin-bottom: ${(props) => - props.$mobile ? V3_Spacing["spacing-16"] : "0"}; - } - } - - ${V3_MediaQuery.MaxWidth.sm} { - ${(props) => { - if (props.$mobile) { - return css` - padding: 0 ${V3_Spacing["spacing-16"]}; - `; - } - }} - } -`; - -export const ActionButton = styled(Button.Small)` - ${V3_MediaQuery.MaxWidth.lg} { - width: 100%; - } -`; - -// ============================================================================= -// DOWNLOAD APP -// ============================================================================= -export const DownloadAppWrapper = styled.div` - display: none; - - ${V3_MediaQuery.MaxWidth.lg} { - display: flex; - flex-direction: column; - margin-top: ${V3_Spacing["spacing-40"]}; - } -`; - -export const DownloadAppTitle = styled(Typography.BodyMD)` - margin-bottom: ${V3_Spacing["spacing-8"]}; -`; - -export const DownloadAppImageLinkWrapper = styled.div` - display: flex; -`; - -export const DownloadAppImageLink = styled.a` - &:not(:last-child) { - margin-right: ${V3_Spacing["spacing-16"]}; - } - - img { - width: auto; - height: auto; - object-fit: contain; - } - - ${V3_MediaQuery.MaxWidth.lg} { - img { - max-width: 11rem; - } - } - - ${V3_MediaQuery.MaxWidth.sm} { - img { - max-width: 100%; - } - } -`; diff --git a/src/navbar/navbar-action-buttons.tsx b/src/navbar/navbar-action-buttons.tsx index 12e6b62d7c..a9165ca89f 100644 --- a/src/navbar/navbar-action-buttons.tsx +++ b/src/navbar/navbar-action-buttons.tsx @@ -1,18 +1,11 @@ +import clsx from "clsx"; import findIndex from "lodash/findIndex"; import type React from "react"; +import { Button } from "../button"; import type { ButtonProps } from "../button/types"; -import { - ActionButton, - ButtonItem, - DownloadAppImageLink, - DownloadAppImageLinkWrapper, - DownloadAppTitle, - DownloadAppWrapper, - DrawerWrapper, - MobileWrapper, - Wrapper, -} from "./navbar-action-buttons.styles"; +import { Typography } from "../typography"; +import * as styles from "./navbar-action-buttons.styles"; import type { NavbarActionButtonsProps, NavbarButtonProps } from "./types"; const APP_STORE_ICON = @@ -93,12 +86,16 @@ export const NavbarActionButtons = ({ // ============================================================================= const renderDownloadAppMobileView = (args?: ButtonProps) => { return ( - - +
+ {(args && args.children) || "Download the app"} - - - + +
); }; @@ -140,14 +138,19 @@ export const NavbarActionButtons = ({ component = isMobile ? ( renderDownloadAppMobileView(actionButton.args) ) : ( - Download the app - + ); break; case "button": { @@ -158,9 +161,14 @@ export const NavbarActionButtons = ({ }`; component = ( - @@ -179,12 +187,15 @@ export const NavbarActionButtons = ({ if (component) { return ( - {component} - + ); } }); @@ -202,9 +213,9 @@ export const NavbarActionButtons = ({ return ( <> {collapsableActionButtons.length > 0 && ( - +
    {renderButtons(mobile, collapsableActionButtons)} - +
)} ); @@ -212,14 +223,14 @@ export const NavbarActionButtons = ({ return ( <> {uncollapsableActionButtons.length > 0 && ( - +
    {renderButtons(false, uncollapsableActionButtons)} - +
)} {actionButtons.desktop.length > 0 && ( - +
    {renderButtons(mobile, actionButtons.desktop)} - +
)} ); diff --git a/src/navbar/navbar-items.styles.ts b/src/navbar/navbar-items.styles.ts new file mode 100644 index 0000000000..beb4f72808 --- /dev/null +++ b/src/navbar/navbar-items.styles.ts @@ -0,0 +1,174 @@ +import { css } from "@linaria/core"; + +import { Colour, ComponentToken, Font, MediaQuery } from "../theme"; + +// ============================================================================= +// WRAPPER +// ============================================================================= + +export const wrapper = css` + display: flex; + list-style: none; + position: relative; + + ${MediaQuery.MaxWidth.lg} { + display: none; + } +`; + +export const wrapperAlignLeft = css` + margin-right: auto; +`; + +export const mobileWrapper = css` + display: none; + list-style: none; + + ${MediaQuery.MaxWidth.lg} { + display: flex; + flex-direction: column; + overflow: hidden; + } +`; + +// ============================================================================= +// LINK ITEMS +// ============================================================================= +export const linkItem = css` + display: flex; + margin: 0 1rem; + + &:first-child { + margin-left: 0; + } + + ${MediaQuery.MaxWidth.lg} { + flex-direction: column; + padding: 0.125rem 0; + width: 100%; + margin-left: 0rem; + } +`; + +export const linkItemHiddenBranding = css` + &:first-child { + margin-left: -0.5rem; + } + + ${MediaQuery.MaxWidth.lg} { + &:first-child { + margin-left: 0rem; + } + } +`; + +export const link = css` + display: flex; + position: relative; + align-items: center; + text-align: center; + color: ${ComponentToken.Navbar["link-colour-text"]}; + height: 100%; + + &:active, + &:hover, + &:focus { + color: ${ComponentToken.Navbar["link-colour-text-hover"]}; + } + + ${MediaQuery.MaxWidth.lg} { + width: 100%; + padding: 0.5rem 1rem; + text-align: left; + align-items: flex-start; + } +`; + +export const linkSelected = css` + &:active, + &:hover, + &:focus { + color: ${ComponentToken.Navbar["link-colour-text-selected-hover"]}; + } +`; + +export const linkWeightRegular = css` + ${Font["body-md-regular"]} +`; + +export const linkWeightSemibold = css` + ${Font["body-md-semibold"]} +`; + +export const linkWeightBold = css` + ${Font["body-md-bold"]} +`; + +export const linkButton = css` + background: none; + border: 0; + padding: 0; + cursor: pointer; + text-align: left; + margin: 0; + box-shadow: none; + font: inherit; + color: inherit; +`; + +export const linkLabel = css` + flex: 1; + margin-top: 0.25rem; + + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + white-space: pre-wrap; +`; + +export const linkIndicator = css` + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 0.25rem; + background-color: ${Colour["border-selected"]}; + + ${MediaQuery.MaxWidth.lg} { + left: 0; + right: unset; + top: 0; + bottom: 0; + height: 100%; + width: 0.25rem; + } +`; + +export const linkIndicatorSelected = css` + &:hover { + background-color: ${Colour["border-selected-hover"]}; + } +`; + +export const linkIconContainer = css` + padding-left: 0.5rem; + margin-right: -0.5rem; +`; + +export const expandCollapseButton = css` + padding: 0.5rem; + transform: rotate(180deg); + transition: transform 300ms ease-in-out; + margin: auto 0.25rem auto 0; +`; + +export const expandCollapseButtonExpanded = css` + transform: rotate(0deg); +`; + +export const chevronIcon = css` + height: 1.25rem; + width: 1.25rem; + color: ${Colour.icon}; +`; diff --git a/src/navbar/navbar-items.styles.tsx b/src/navbar/navbar-items.styles.tsx deleted file mode 100644 index 6d8319c5bb..0000000000 --- a/src/navbar/navbar-items.styles.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { ChevronUpIcon } from "@lifesg/react-icons/chevron-up"; -import styled, { css } from "styled-components"; - -import { ClickableIcon } from "../shared/clickable-icon"; -import type { TypographyWeight } from "../typography"; -import { V3_Colour, V3_Font, V3_MediaQuery } from "../v3_theme"; -import { V3_ThemeNavbar } from "../v3_theme/components/theme-helper"; - -// ============================================================================= -// STYLE INTERFACE, transient props are denoted with $ -// See more https://styled-components.com/docs/api#transient-props -// ============================================================================= -interface StyleProps { - $selected: boolean; -} - -interface WrapperStyleProps { - $alignLeft: boolean | undefined; -} - -interface ItemStyleProps { - $hiddenBranding: boolean | undefined; -} - -// ============================================================================= -// WRAPPER -// ============================================================================= - -export const Wrapper = styled.ul` - display: flex; - list-style: none; - position: relative; - - ${(props) => props.$alignLeft && "margin-right: auto;"} - - ${V3_MediaQuery.MaxWidth.lg} { - display: none; - } -`; - -export const MobileWrapper = styled.ul` - display: none; - list-style: none; - - ${V3_MediaQuery.MaxWidth.lg} { - display: flex; - flex-direction: column; - overflow: hidden; - } -`; - -// ============================================================================= -// LINK ITEMS -// ============================================================================= -export const LinkItem = styled.li` - display: flex; - margin: 0 1rem; - - &:first-child { - // negative margin to preserve touch target size for link - margin-left: ${(props) => (props.$hiddenBranding ? "-0.5rem" : "0")}; - } - - ${V3_MediaQuery.MaxWidth.lg} { - flex-direction: column; - padding: 0.125rem 0; - width: 100%; - margin-left: 0rem; - } -`; - -const linkCss = css<{ $selected: boolean; weight: TypographyWeight }>` - ${(props) => V3_Font[`body-md-${props.weight}`]} - - display: flex; - position: relative; - align-items: center; - text-align: center; - color: ${V3_ThemeNavbar["navbar-link-colour-text"]}; - height: 100%; - - &:active, - &:hover, - &:focus { - color: ${(props) => - props.$selected - ? V3_ThemeNavbar["navbar-link-colour-text-selected-hover"] - : V3_ThemeNavbar["navbar-link-colour-text-hover"]}; - } - - ${V3_MediaQuery.MaxWidth.lg} { - width: 100%; - padding: 0.5rem 1rem; - text-align: left; - align-items: flex-start; - } -`; -export const Link = styled.a<{ $selected: boolean; weight: TypographyWeight }>` - ${linkCss} -`; - -export const LinkButton = styled.button<{ - $selected: boolean; - weight: TypographyWeight; -}>` - ${linkCss} - background: none; - border: 0; - padding: 0; - cursor: pointer; - text-align: left; - margin: 0; - box-shadow: none; - font: inherit; - color: inherit; -`; - -export const LinkLabel = styled.div` - flex: 1; - margin-top: 0.25rem; - - overflow: hidden; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 3; - white-space: pre-wrap; -`; - -export const LinkIndicator = styled.div` - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 0.25rem; - background-color: ${V3_Colour["border-selected"]}; - - &:hover { - ${(props) => props.$selected && V3_Colour["border-selected-hover"]}; - } - - ${V3_MediaQuery.MaxWidth.lg} { - left: 0; - right: unset; - top: 0; - bottom: 0; - height: 100%; - width: 0.25rem; - } -`; - -export const LinkIconContainer = styled.div` - padding-left: 0.5rem; - margin-right: -0.5rem; -`; - -export const ExpandCollapseButton = styled(ClickableIcon)` - padding: 0.5rem; - transform: rotate(${(props) => (props.$selected ? 0 : 180)}deg); - transition: transform 300ms ease-in-out; - margin: auto 0.25rem auto 0; -`; - -export const ChevronIcon = styled(ChevronUpIcon)` - height: 1.25rem; - width: 1.25rem; - color: ${V3_Colour.icon}; - &:hover { - ${(props) => - props.$selected - ? V3_Colour["icon-selected-hover"] - : V3_Colour["icon-hover"]}; - } -`; diff --git a/src/navbar/navbar-items.tsx b/src/navbar/navbar-items.tsx index 670e681a6f..4caa36d92b 100644 --- a/src/navbar/navbar-items.tsx +++ b/src/navbar/navbar-items.tsx @@ -1,28 +1,32 @@ +import { ChevronUpIcon } from "@lifesg/react-icons/chevron-up"; +import clsx from "clsx"; import type React from "react"; import { useEffect, useRef, useState } from "react"; import { Menu as DesktopMenu } from "../menu"; +import { ClickableIcon } from "../shared/clickable-icon"; import type { TypographyWeight } from "../typography"; import { useId } from "../util"; import { Menu as MobileMenu } from "./menu"; -import { - ChevronIcon, - ExpandCollapseButton, - Link, - LinkButton, - LinkIconContainer, - LinkIndicator, - LinkItem, - LinkLabel, - MobileWrapper, - Wrapper, -} from "./navbar-items.styles"; +import * as styles from "./navbar-items.styles"; import type { NavItemCommonProps, NavItemLinkProps, NavItemProps, } from "./types"; +const getLinkWeightClass = (weight: TypographyWeight) => { + switch (weight) { + case "bold": + return styles.linkWeightBold; + case "semibold": + return styles.linkWeightSemibold; + case "regular": + default: + return styles.linkWeightRegular; + } +}; + interface Props { items: NavItemProps[]; selectedId?: string | undefined; @@ -181,41 +185,53 @@ export const NavbarItems = ({ const renderIndicator = () => showIndicator ? ( - ) : null; const renderMobileChevron = () => mobile && hasSubMenu ? ( - - + - - - + + +
) : null; + const linkClassName = clsx( + styles.link, + getLinkWeightClass(textWeight), + selected && styles.linkSelected + ); + const renderLink = () => ( - - {children} +
{children}
{renderIndicator()} - + ); const renderLinkWithSubmenu = () => { @@ -224,10 +240,9 @@ export const NavbarItems = ({ if (mobile) { return ( <> - ({ onClick={handleLinkClick(item, index)} {...options} > - {children} +
{children}
{renderIndicator()} {renderMobileChevron()} - + {isMobileExpanded && renderMobileSubMenu(subMenu!)} @@ -259,27 +274,32 @@ export const NavbarItems = ({ ); }} > - - {children} +
{children}
{renderIndicator()} -
+ ); }; return ( - +
  • {hasSubMenu ? renderLinkWithSubmenu() : renderLink()} - +
  • ); }; @@ -298,11 +318,19 @@ export const NavbarItems = ({ if (items && items.length > 0) { return mobile ? ( - {renderItems()} +
      + {renderItems()} +
    ) : ( - +
      {renderItems()} - +
    ); } diff --git a/src/navbar/navbar-logo-data.ts b/src/navbar/navbar-logo-data.ts index 502a60814f..25acf10e0a 100644 --- a/src/navbar/navbar-logo-data.ts +++ b/src/navbar/navbar-logo-data.ts @@ -1,4 +1,4 @@ -import type { V3_ResourceScheme } from "../v3_theme/types"; +import type { ThemeType } from "../theme/types"; import type { NavbarResourcesProps } from "./types"; const DEFAULT_RESOURCES_LOGO: NavbarResourcesProps = { @@ -53,7 +53,7 @@ const SUPPORTGOWHERE_RESOURCE_LOGO: NavbarResourcesProps = { }, }; -export const getDefaultResourceLogo = (resourceScheme?: V3_ResourceScheme) => { +export const getDefaultResourceLogo = (resourceScheme?: ThemeType) => { switch (resourceScheme) { case "bookingsg": return BOOKINGSG_RESOURCES_LOGO; @@ -64,6 +64,7 @@ export const getDefaultResourceLogo = (resourceScheme?: V3_ResourceScheme) => { case "spf": return SPF_RESOURCE_LOGO; case "supportgowhere": + case "sgw-digital-lobby": return SUPPORTGOWHERE_RESOURCE_LOGO; case "imda": return IMDA_RESOURCE_LOGO; diff --git a/src/navbar/navbar.styles.ts b/src/navbar/navbar.styles.ts new file mode 100644 index 0000000000..4a7957fb24 --- /dev/null +++ b/src/navbar/navbar.styles.ts @@ -0,0 +1,150 @@ +import { css } from "@linaria/core"; + +import { + Border, + Colour, + ComponentToken, + MediaQuery, + Motion, + Shadow, +} from "../theme"; + +export const tokens = { + nav: { + height: "--fds-internal-navbar-nav-height", + }, +}; + +export const wrapper = css` + background-color: ${ComponentToken.Navbar["colour-bg"]}; + z-index: 30; + top: 0; + left: 0; + right: 0; + width: 100%; +`; + +export const wrapperFixed = css` + position: sticky; +`; + +export const wrapperRelative = css` + position: relative; +`; + +export const wrapperDark = css` + border-bottom: ${Border["width-010"]} ${Border["solid"]} ${Colour["border"]}; +`; + +export const wrapperLight = css` + box-shadow: ${Shadow["xs-subtle"]}; +`; + +export const nav = css` + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + position: relative; + transition: ${Motion["duration-350"]} ${Motion["ease-standard"]}; + + ${tokens.nav.height}: initial; + height: var(${tokens.nav.height}); + + ${MediaQuery.MaxWidth.lg} { + height: ${ComponentToken.Navbar["mobile-height"]}; + } +`; + +export const navDark = css` + height: calc(var(${tokens.nav.height}) - 1px); + + ${MediaQuery.MaxWidth.lg} { + height: calc(${ComponentToken.Navbar["mobile-height"]} - 1px); + } +`; + +export const navElementsContainer = css` + display: flex; + height: 100%; + flex: 1; + justify-content: flex-end; + ${MediaQuery.MaxWidth.lg} { + margin-left: 0rem; + } +`; + +export const navElementsContainerWithBranding = css` + margin-left: 5rem; + + ${MediaQuery.MaxWidth.lg} { + margin-left: 0; + } +`; + +export const mobileMenuButton = css` + display: none; + + ${MediaQuery.MaxWidth.lg} { + display: flex; + padding: 0 1.5rem; + margin-right: -1.5rem; + } +`; + +export const mobileMenuIcon = css` + height: 1.5rem; + width: 1.5rem; + color: ${ComponentToken.Navbar["colour-icon"]}; +`; + +export const navBrandContainer = css` + display: flex; + flex-direction: row; + align-items: center; + flex-shrink: 0; +`; + +export const navBrandContainerCompressed = css` + height: ${ComponentToken.Navbar["compressed-logo-height"]}; +`; + +export const navBrandContainerFull = css` + height: ${ComponentToken.Navbar["full-logo-height"]}; +`; + +export const navBrandContainerResponsive = css` + ${MediaQuery.MaxWidth.lg} { + height: ${ComponentToken.Navbar["mobile-logo-height"]}; + } + + ${MediaQuery.MaxWidth.xxs} { + height: 1.25rem; + } +`; + +export const navSeparator = css` + display: flex; + background-color: ${Colour.border}; + height: 100%; + width: 2px; +`; + +export const navSeparatorCompressed = css` + margin: 0 1rem; +`; + +export const navSeparatorFull = css` + margin: 0 1.5rem; +`; + +export const navSeparatorResponsive = css` + ${MediaQuery.MaxWidth.lg} { + margin: 0 1rem; + } + + ${MediaQuery.MaxWidth.sm} { + margin: 0 0.75rem; + } +`; diff --git a/src/navbar/navbar.styles.tsx b/src/navbar/navbar.styles.tsx deleted file mode 100644 index 538cd7b080..0000000000 --- a/src/navbar/navbar.styles.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { MenuIcon } from "@lifesg/react-icons/menu"; -import styled, { css } from "styled-components"; - -import { ClickableIcon } from "../shared/clickable-icon"; -import { - V3_Border, - V3_Colour, - V3_MediaQuery, - V3_Motion, - V3_Shadow, -} from "../v3_theme"; -import { V3_ThemeNavbar } from "../v3_theme/components/theme-helper"; - -// ============================================================================= -// STYLE INTERFACE -// ============================================================================= -interface StyleProps { - $compress?: boolean; - $fixed?: boolean; - $hideNavBranding?: boolean; -} - -// ============================================================================= -// STYLING -// ============================================================================= -export const Wrapper = styled.div` - position: ${(props) => (props.$fixed ? "sticky" : "relative")}; - background-color: ${V3_ThemeNavbar["navbar-colour-bg"]}; - z-index: 30; - top: 0; - left: 0; - right: 0; - width: 100%; - ${(props) => { - return props.theme?.colourMode === "dark" - ? css` - border-bottom: ${V3_Border["width-010"]} ${V3_Border["solid"]} - ${V3_Colour["border"]}; - ` - : css` - box-shadow: ${V3_Shadow["xs-subtle"]}; - `; - }} -`; - -export const Nav = styled.nav` - height: ${(props) => { - const baseHeight = props.$compress - ? V3_ThemeNavbar["navbar-compressed-height"](props) - : V3_ThemeNavbar["navbar-full-height"](props); - return props.theme?.colourMode === "dark" - ? `calc(${baseHeight} - 1px)` - : `${baseHeight}`; - }}; - width: 100%; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - position: relative; - transition: ${V3_Motion["duration-350"]} ${V3_Motion["ease-standard"]}; - - ${V3_MediaQuery.MaxWidth.lg} { - height: ${(props) => - props.theme?.colourMode === "dark" - ? `calc(${V3_ThemeNavbar["navbar-mobile-height"](props)} - 1px)` - : `${V3_ThemeNavbar["navbar-mobile-height"](props)}`}; - } -`; - -export const NavElementsContainer = styled.div` - display: flex; - height: 100%; - margin-left: ${(props) => (props.$hideNavBranding ? "0" : "5rem")}; - flex: 1; - justify-content: flex-end; - - ${V3_MediaQuery.MaxWidth.lg} { - margin-left: 0rem; - } -`; - -export const MobileMenuButton = styled(ClickableIcon)` - display: none; - - ${V3_MediaQuery.MaxWidth.lg} { - display: flex; - padding: 0 1.5rem; - margin-right: -1.5rem; - } -`; - -export const MobileMenuIcon = styled(MenuIcon)` - height: 1.5rem; - width: 1.5rem; - color: ${V3_ThemeNavbar["navbar-colour-icon"]}; -`; - -export const NavBrandContainer = styled.div` - display: flex; - flex-direction: row; - align-items: center; - flex-shrink: 0; - - height: ${(props) => - props.$compress - ? V3_ThemeNavbar["navbar-compressed-logo-height"] - : V3_ThemeNavbar["navbar-full-logo-height"]}; - - ${V3_MediaQuery.MaxWidth.lg} { - height: ${V3_ThemeNavbar["navbar-mobile-logo-height"]}; - } - - ${V3_MediaQuery.MaxWidth.xxs} { - height: 1.25rem; - } -`; - -export const NavSeparator = styled.div` - display: flex; - background-color: ${V3_Colour.border}; - height: 100%; - width: 2px; - margin: 0 ${(props) => (props.$compress ? 1 : 1.5)}rem; - - ${V3_MediaQuery.MaxWidth.lg} { - margin: 0 1rem; - } - - ${V3_MediaQuery.MaxWidth.sm} { - margin: 0 0.75rem; - } -`; diff --git a/src/navbar/navbar.tsx b/src/navbar/navbar.tsx index 1904d4059e..0a04db2f79 100644 --- a/src/navbar/navbar.tsx +++ b/src/navbar/navbar.tsx @@ -1,3 +1,5 @@ +import { MenuIcon } from "@lifesg/react-icons/menu"; +import clsx from "clsx"; import type React from "react"; import { forwardRef, @@ -12,18 +14,18 @@ import { ThemeContext } from "styled-components"; import { Layout } from "../layout"; import { Masthead } from "../masthead/masthead"; import { Overlay } from "../overlay/overlay"; -import { V3_Breakpoint } from "../v3_theme"; +import { ClickableIcon } from "../shared/clickable-icon"; +import type { ThemeType } from "../theme"; +import { + Breakpoint, + ComponentToken, + parsePxOrRemValue, + useApplyStyle, + useDesignToken, +} from "../theme"; import { Brand } from "./brand"; import { Drawer } from "./drawer"; -import { - MobileMenuButton, - MobileMenuIcon, - Nav, - NavBrandContainer, - NavElementsContainer, - NavSeparator, - Wrapper, -} from "./navbar.styles"; +import * as styles from "./navbar.styles"; import { NavbarActionButtons } from "./navbar-action-buttons"; import { NavbarHelper } from "./navbar-helper"; import { NavbarItems } from "./navbar-items"; @@ -73,13 +75,23 @@ const Component = ( const mobileMenuRef = useRef(null); const isStretch = layout === "stretch"; const elementRef = useRef(null); + const navRef = useRef(null); const theme = useContext(ThemeContext); - const defaultResource = getDefaultResourceLogo(theme?.resourceScheme); - const tabletWidth = V3_Breakpoint["lg-max"]({ theme }); + const defaultResource = getDefaultResourceLogo( + theme?.resourceScheme as ThemeType | undefined + ); + const tabletWidthToken = useDesignToken(Breakpoint["lg-max"]); + const tabletWidth = parsePxOrRemValue(tabletWidthToken || "1200px"); const primary = resources?.primary || defaultResource.primary; const secondary = resources?.secondary; + useApplyStyle(navRef, { + [styles.tokens.nav.height]: compress + ? ComponentToken.Navbar["compressed-height"] + : ComponentToken.Navbar["full-height"], + }); + useImperativeHandle( ref, () => @@ -150,7 +162,7 @@ const Component = ( }; const handleBrandClick = ( - event: React.MouseEvent, + _event: React.MouseEvent, type: BrandType ) => { if (onBrandClick) { @@ -252,7 +264,16 @@ const Component = ( ); const renderBrand = () => ( - +
    {primary && ( ( )} {secondary && ( <> - +
    ( /> )} - +
    ); const renderMobileMenuButton = () => { @@ -284,16 +313,17 @@ const Component = ( (actionButtons && hasCollapsibleActionButtons(actionButtons)) ) { return ( - - - + + ); } @@ -303,11 +333,23 @@ const Component = ( const renderNavbar = () => { return ( -
    )} - + {!hideNavElements && renderDrawer()} @@ -331,16 +373,22 @@ const Component = ( }; return ( - {masthead && } {renderNavbar()} - + ); };