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';