+
{heading}
+ {customCallToAction ? (
+
+ {customCallToAction}
+
+ ) : null}
{children}
+ {/* Rendered last so assistive tech reaches the heading
+ and content before the close button; centred against
+ the header via its measured height. */}
diff --git a/src/drawer/types.ts b/src/drawer/types.ts
index 4bd25542ee..610656a3c0 100644
--- a/src/drawer/types.ts
+++ b/src/drawer/types.ts
@@ -23,4 +23,11 @@ export interface DrawerProps {
onClose?: (() => void) | undefined;
/** Called when the user clicks the background overlay. */
onOverlayClick?: (() => void) | undefined;
+ /**
+ * Optional element rendered in the header, e.g. a link or button.
+ *
+ * Sits beside the title on wider drawers, and wraps onto its own line
+ * below the title once the drawer is narrow.
+ */
+ customCallToAction?: React.ReactNode | undefined;
}
diff --git a/src/local-nav/local-nav-dropdown/local-nav-dropdown.styles.ts b/src/local-nav/local-nav-dropdown/local-nav-dropdown.styles.ts
index 7be3cc5133..dd799fc2a8 100644
--- a/src/local-nav/local-nav-dropdown/local-nav-dropdown.styles.ts
+++ b/src/local-nav/local-nav-dropdown/local-nav-dropdown.styles.ts
@@ -99,6 +99,28 @@ export const navItemList = css`
max-height: var(${tokens.navItemList.viewportHeight});
`;
+export const titleContainer = css`
+ display: flex;
+ align-items: center;
+ gap: ${Spacing["spacing-8"]};
+ flex: 1;
+`;
+
+/* Keeps the left addon aligned with the first line when the title wraps. */
+export const leftAddon = css`
+ display: inline-flex;
+ align-items: center;
+ align-self: flex-start;
+ min-height: ${Font.Spec["body-lh-baseline"]};
+`;
+
+/* Flushed right; stays vertically centred against the (possibly wrapped) title. */
+export const rightAddon = css`
+ display: inline-flex;
+ align-items: center;
+ margin-left: auto;
+`;
+
export const navItemLabel = css`
${Font["body-baseline-regular"]}
color: ${Colour["text"]};
diff --git a/src/local-nav/local-nav-dropdown/local-nav-dropdown.tsx b/src/local-nav/local-nav-dropdown/local-nav-dropdown.tsx
index f10e16d61a..4072b454b8 100644
--- a/src/local-nav/local-nav-dropdown/local-nav-dropdown.tsx
+++ b/src/local-nav/local-nav-dropdown/local-nav-dropdown.tsx
@@ -283,7 +283,7 @@ const Component = (
renderItem,
index,
}: LocalNavDropdownItemComponentProps) => {
- const { id, title } = item;
+ const { id, title, titleAddon } = item;
if (renderItem) {
return (
@@ -324,15 +324,42 @@ const Component = (
listItemRefs.current[index] = el as HTMLLIElement;
}}
>
- {isSelected &&
}
-
- {title}
-
+ {isSelected && (
+
+
+
+ )}
+ {titleAddon ? (
+
+ {titleAddon.left && (
+
+ {titleAddon.left}
+
+ )}
+
+ {title}
+
+ {titleAddon.right && (
+
+ {titleAddon.right}
+
+ )}
+
+ ) : (
+
+ {title}
+
+ )}
);
};
diff --git a/src/local-nav/local-nav-menu/local-nav-menu.styles.ts b/src/local-nav/local-nav-menu/local-nav-menu.styles.ts
index c4fbe5ddf0..ea1f419312 100644
--- a/src/local-nav/local-nav-menu/local-nav-menu.styles.ts
+++ b/src/local-nav/local-nav-menu/local-nav-menu.styles.ts
@@ -1,6 +1,6 @@
import { css } from "@linaria/core";
-import { Colour, Radius, Spacing } from "../../theme";
+import { Colour, Font, Radius, Spacing } from "../../theme";
export const nav = css`
list-style-type: none;
@@ -12,6 +12,28 @@ export const textLabel = css`
margin: 0;
`;
+export const titleContainer = css`
+ display: flex;
+ align-items: center;
+ gap: ${Spacing["spacing-8"]};
+ width: 100%;
+`;
+
+/* Keeps the left addon aligned with the first line when the title wraps. */
+export const leftAddon = css`
+ display: inline-flex;
+ align-items: center;
+ align-self: flex-start;
+ min-height: ${Font.Spec["body-lh-baseline"]};
+`;
+
+/* Flushed right; stays vertically centred against the (possibly wrapped) title. */
+export const rightAddon = css`
+ display: inline-flex;
+ align-items: center;
+ margin-left: auto;
+`;
+
export const navItem = css`
display: block;
position: relative;
diff --git a/src/local-nav/local-nav-menu/local-nav-menu.tsx b/src/local-nav/local-nav-menu/local-nav-menu.tsx
index d4842621c3..a8e7a536f3 100644
--- a/src/local-nav/local-nav-menu/local-nav-menu.tsx
+++ b/src/local-nav/local-nav-menu/local-nav-menu.tsx
@@ -51,13 +51,13 @@ const Component = (
renderItem,
index,
}: LocalNavMenuItemComponentProps) => {
- const { id, title } = item;
+ const { id, title, titleAddon } = item;
const renderTitle = () => {
if (renderItem) {
return renderItem(item, { selected: isSelected });
}
- return (
+ const label = (
);
+ if (!titleAddon) {
+ return label;
+ }
+ return (
+
+ {titleAddon.left && (
+
+ {titleAddon.left}
+
+ )}
+ {label}
+ {titleAddon.right && (
+
+ {titleAddon.right}
+
+ )}
+
+ );
};
return (
diff --git a/src/local-nav/types.ts b/src/local-nav/types.ts
index bf7ab10781..7f67b5a0b4 100644
--- a/src/local-nav/types.ts
+++ b/src/local-nav/types.ts
@@ -1,3 +1,15 @@
+// @storybookSection LocalNavMenu, LocalNavDropdown
+/**
+ * Configuration for the optional addon elements displayed alongside a
+ * navigation item's title. Both slots may be used at the same time.
+ */
+export interface TitleAddonProps {
+ /** Element rendered immediately before the title. */
+ left?: JSX.Element | undefined;
+ /** Element rendered after the title, flushed to the far right of the item. */
+ right?: JSX.Element | undefined;
+}
+
// @storybookSection LocalNavMenu, LocalNavDropdown
/**
* Data for a single local navigation item.
@@ -5,6 +17,13 @@
export interface LocalNavItemProps {
title: string | React.ReactNode;
id?: string | undefined;
+ /**
+ * Optional addon element rendered alongside the item title.
+ *
+ * Ignored when a custom `renderItem` is provided, since the caller then
+ * controls the item's layout.
+ */
+ titleAddon?: TitleAddonProps | undefined;
}
// @storybookSkipProps
diff --git a/stories/drawer/drawer.mdx b/stories/drawer/drawer.mdx
index 4e42c62b2f..c3c2dd75d1 100644
--- a/stories/drawer/drawer.mdx
+++ b/stories/drawer/drawer.mdx
@@ -20,6 +20,12 @@ import { Drawer } from "@lifesg/react-design-system/drawer";
+## Custom call-to-action
+
+You can also specify a custom call to action that will appear on the header of the `Drawer`.
+
+
+
## Component API
diff --git a/stories/drawer/drawer.stories.tsx b/stories/drawer/drawer.stories.tsx
index 74bf5c73c6..368139de7b 100644
--- a/stories/drawer/drawer.stories.tsx
+++ b/stories/drawer/drawer.stories.tsx
@@ -85,3 +85,47 @@ export const HandlingContentOverflow: StoryObj = {
);
},
};
+
+export const WithCustomCallToAction: StoryObj = {
+ render: () => {
+ const [show, setShow] = useState(false);
+ const openDrawer = () => setShow(true);
+ const closeDrawer = () => setShow(false);
+ return (
+ <>
+
+ Open drawer
+
+
+
+ Confirm
+
+
+ Cancel
+
+
+ }
+ >
+