Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/theme-toggle-footer-slot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@node-core/ui-components': minor
---

Add an optional `footer` prop to `ThemeToggle` that renders custom content (e.g. keyboard shortcut hints) at the bottom of the dropdown, below the theme options.
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,56 @@ describe('ThemeToggle', () => {

assert.equal(selected, 'system');
});

it('renders the footer slot when provided', async () => {
render(
<ThemeToggle
ariaLabel="Select theme"
currentTheme="system"
themeLabels={defaultLabels}
footer={<span>Use ⌘+L for Light, ⌘+D for Dark</span>}
/>
);

await userEvent.click(screen.getByRole('button', { name: 'Select theme' }));

assert.ok(
screen.getByText('Use ⌘+L for Light, ⌘+D for Dark'),
'footer content should be rendered inside the dropdown'
);

const footerId = screen.getByRole('menu').getAttribute('aria-describedby');
assert.ok(
footerId,
'menu content should reference the footer via aria-describedby'
);
assert.equal(
document.getElementById(footerId)?.textContent,
'Use ⌘+L for Light, ⌘+D for Dark',
'aria-describedby should point to the footer element'
);
});

it('does not render a footer separator when no footer is provided', async () => {
render(
<ThemeToggle
ariaLabel="Select theme"
currentTheme="system"
themeLabels={defaultLabels}
/>
);

await userEvent.click(screen.getByRole('button', { name: 'Select theme' }));

assert.equal(
document.querySelector('[class*="dropDownFooter"]'),
null,
'footer element should not exist without the footer prop'
);
assert.equal(
screen.getByRole('menu').getAttribute('aria-describedby'),
null,
'menu content should not reference a footer without the footer prop'
);
});
});
11 changes: 11 additions & 0 deletions packages/ui-components/src/Common/ThemeToggle/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,14 @@
@apply bg-brand-600
text-white;
}

.dropDownFooter {
@apply border-t
border-neutral-200
px-2.5
py-1.5
text-xs
text-neutral-500
dark:border-neutral-800
dark:text-neutral-400;
}
10 changes: 10 additions & 0 deletions packages/ui-components/src/Common/ThemeToggle/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,14 @@ export const DarkSelected: Story = {
},
};

export const WithFooter: Story = {
args: {
ariaLabel: 'Select theme',
currentTheme: 'system',
themeLabels: defaultLabels,
onChange: () => {},
footer: <span>⌘+L Light · ⌘+D Dark</span>,
},
};

export default { component: ThemeToggle } as Meta;
12 changes: 11 additions & 1 deletion packages/ui-components/src/Common/ThemeToggle/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
} from '@heroicons/react/24/outline';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames';
import { useId } from 'react';

import type { FC } from 'react';
import type { FC, ReactNode } from 'react';

import styles from './index.module.css';

Expand All @@ -17,6 +18,7 @@ type ThemeToggleProps = {
currentTheme?: Theme;
ariaLabel?: string;
themeLabels?: { system: string; light: string; dark: string };
footer?: ReactNode;
};

const themeIcons: Record<Theme, typeof SunIcon> = {
Expand All @@ -32,7 +34,9 @@ const ThemeToggle: FC<ThemeToggleProps> = ({
currentTheme = 'system',
ariaLabel = 'Select theme',
themeLabels = { system: 'System', light: 'Light', dark: 'Dark' },
footer,
}) => {
const footerId = useId();
const TriggerIcon = themeIcons[currentTheme];

return (
Expand All @@ -52,6 +56,7 @@ const ThemeToggle: FC<ThemeToggleProps> = ({
align="end"
className={styles.dropDownContent}
sideOffset={5}
aria-describedby={footer ? footerId : undefined}
>
{themes.map(theme => {
const Icon = themeIcons[theme];
Expand All @@ -68,6 +73,11 @@ const ThemeToggle: FC<ThemeToggleProps> = ({
</DropdownMenu.Item>
);
})}
{footer ? (
<div id={footerId} className={styles.dropDownFooter}>
{footer}
</div>
) : null}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
Expand Down
Loading