From bc3fdd1e84648f069ce9088a905eb18b8dbb2a10 Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Thu, 30 Jul 2026 12:30:35 +1000 Subject: [PATCH] Shrink button icons to 16px at both sizes (#308) * Shrink button icons to 16px at both sizes The medium button provided its nested icons and spinner a 24px icon size, which read as oversized against the 40px control and left the icon larger than the pending spinner IconButton already drew at 16px. Both button sizes now take a 16px icon, so the size-to-icon lookup table collapses to a single constant. * Show the flat icon size in the button docs examples The size example was label-only, so nothing in the docs showed that the icon holds at 16px while the control grows. Give both sizes a start icon. Drop the explicit aria-hidden from the icons example: Icon already defaults it to true when no title is set, so the prop taught readers a step they can skip. --- .../docs/components/actions/button/index.mdx | 9 +- apps/docs/src/examples/button/icons.tsx | 4 +- apps/docs/src/examples/button/sizes.tsx | 9 +- .../react/src/button/button.browser.test.tsx | 96 +++++++++++++++++++ .../react/src/button/primitive/index.tsx | 3 +- .../react/src/sizing/button-sizing.ts | 7 +- 6 files changed, 113 insertions(+), 15 deletions(-) create mode 100644 packages/@luke-ui/react/src/button/button.browser.test.tsx diff --git a/apps/docs/content/docs/components/actions/button/index.mdx b/apps/docs/content/docs/components/actions/button/index.mdx index 1f635eda..ce85f857 100644 --- a/apps/docs/content/docs/components/actions/button/index.mdx +++ b/apps/docs/content/docs/components/actions/button/index.mdx @@ -20,7 +20,7 @@ Keep related controls at the same size. ## Appearance @@ -58,9 +58,10 @@ primary action at the end of a narrow form or panel. ## Icons -Use `startIcon` or `endIcon` with a visible label. `Button` supplies the icon size, so the nested -`Icon` does not need its own `size` prop. Use [`IconButton`](/components/actions/icon-button) only -when the icon is familiar without a text label. +Use `startIcon` or `endIcon` with a visible label. `Button` sizes nested icons at 16px for both +`small` and `medium`, so the `Icon` needs no `size` prop of its own — pass one only to override it +deliberately. Use [`IconButton`](/components/actions/icon-button) only when the icon is familiar +without a text label. - - + diff --git a/apps/docs/src/examples/button/sizes.tsx b/apps/docs/src/examples/button/sizes.tsx index cb19da75..953ca638 100644 --- a/apps/docs/src/examples/button/sizes.tsx +++ b/apps/docs/src/examples/button/sizes.tsx @@ -1,11 +1,16 @@ import { Box } from '@luke-ui/react/box'; import { Button } from '@luke-ui/react/button'; +import { Icon } from '@luke-ui/react/icon'; export default function Sizes() { return ( - - + + ); } diff --git a/packages/@luke-ui/react/src/button/button.browser.test.tsx b/packages/@luke-ui/react/src/button/button.browser.test.tsx new file mode 100644 index 00000000..8e32b2c4 --- /dev/null +++ b/packages/@luke-ui/react/src/button/button.browser.test.tsx @@ -0,0 +1,96 @@ +import '../../dist/themes/tactile.css'; +import '../stylesheet.css.js'; +import { act } from 'react'; +import type { ReactNode } from 'react'; +import type { Root } from 'react-dom/client'; +import { createRoot } from 'react-dom/client'; +import { afterEach, expect, test } from 'vite-plus/test'; +import spritesheetHref from '../../dist/spritesheet.svg?url'; +import { IconButton } from '../icon-button/index.js'; +import { Icon, IconSpritesheetProvider } from '../icon/index.js'; +import { themeRootClassName } from '../theme/index.js'; +import { tactileThemeClassName } from '../themes/index.js'; +import { Button } from './index.js'; + +const mounted: Array<{ container: HTMLElement; root: Root }> = []; + +afterEach(() => { + for (const { container, root } of mounted) { + act(() => root.unmount()); + container.remove(); + } + mounted.length = 0; +}); + +test('renders a 16px start icon in a medium button', () => { + const { container } = mountFixture( + , + ); + + const icon = container.querySelector('svg'); + if (!(icon instanceof SVGElement)) throw new Error('Expected icon.'); + + const styles = getComputedStyle(icon); + expect(styles.inlineSize).toBe('16px'); + expect(styles.blockSize).toBe('16px'); +}); + +test('renders a 16px start icon in a small button', () => { + const { container } = mountFixture( + , + ); + + const icon = container.querySelector('svg'); + if (!(icon instanceof SVGElement)) throw new Error('Expected icon.'); + + const styles = getComputedStyle(icon); + expect(styles.inlineSize).toBe('16px'); + expect(styles.blockSize).toBe('16px'); +}); + +test('renders a 16px icon in a medium icon button while the button itself stays 40px', () => { + const { container } = mountFixture(); + + const button = container.firstElementChild; + if (!(button instanceof HTMLElement)) throw new Error('Expected button.'); + expect(getComputedStyle(button).inlineSize).toBe('40px'); + + const icon = button.querySelector('svg'); + if (!(icon instanceof SVGElement)) throw new Error('Expected icon.'); + + const styles = getComputedStyle(icon); + expect(styles.inlineSize).toBe('16px'); + expect(styles.blockSize).toBe('16px'); +}); + +test('an explicit icon size overrides the button-provided icon size', () => { + const { container } = mountFixture( + , + ); + + const icon = container.querySelector('svg'); + if (!(icon instanceof SVGElement)) throw new Error('Expected icon.'); + + const styles = getComputedStyle(icon); + expect(styles.inlineSize).toBe('32px'); + expect(styles.blockSize).toBe('32px'); +}); + +function mountFixture(node: ReactNode) { + const container = document.body.appendChild(document.createElement('div')); + container.className = `${themeRootClassName} ${tactileThemeClassName}`; + const root = createRoot(container); + mounted.push({ container, root }); + + act(() => { + root.render({node}); + }); + + return { container }; +} diff --git a/packages/@luke-ui/react/src/button/primitive/index.tsx b/packages/@luke-ui/react/src/button/primitive/index.tsx index 35e6aff2..6c377344 100644 --- a/packages/@luke-ui/react/src/button/primitive/index.tsx +++ b/packages/@luke-ui/react/src/button/primitive/index.tsx @@ -55,10 +55,9 @@ export function Button(props: ButtonProps): JSX.Element { tone = 'neutral', ...restProps } = props; - const iconSize = BUTTON_ICON_SIZE[size]; return ( - + { diff --git a/packages/@luke-ui/react/src/sizing/button-sizing.ts b/packages/@luke-ui/react/src/sizing/button-sizing.ts index 3bb72f70..26bc0256 100644 --- a/packages/@luke-ui/react/src/sizing/button-sizing.ts +++ b/packages/@luke-ui/react/src/sizing/button-sizing.ts @@ -1,7 +1,4 @@ import type { IconSize } from '../types/icon-size.js'; -/** Maps button size to the appropriate icon/spinner size. */ -export const BUTTON_ICON_SIZE: Record<'medium' | 'small', IconSize> = { - medium: 'medium', - small: 'xsmall', -}; +/** Icon/spinner size used inside buttons. Both button sizes take a 16px icon. */ +export const BUTTON_ICON_SIZE: IconSize = 'xsmall'; -- 2.51.2