mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
ui: an icon button is the same height as the button beside it
The Details/Open pair on every archive card came out 36px beside 28px.
Mantine gives ActionIcon a size scale unrelated to Button's, and defaults
the two differently:
size xs sm md lg xl default
Button 30 36 42 50 60 sm (36)
ActionIcon 18 22 28 34 44 md (28)
So the two never matched, at any size, and every toolbar mixing them was
ragged -- the archive card is just where it shows most.
Mantine already ships the aligned scale as `--ai-size-input-*`, for an
ActionIcon standing beside an Input; those five values are identical to
`--button-height-*`. ui.css points one at the other, which fixes every
size at once including ones no call site uses yet, and IconButton now
defaults to `sm` like Button.
Both halves are needed and both are proven so: dropping just the `sm`
remap reddens the default and `sm` cases while xs/md/lg/xl stay green;
reverting just the default reddens the default case alone, at 42 vs 36 --
the mismatch inverted rather than fixed.
A further test pins the direction. The pair could also be squared up by
cutting the labelled button down to 28px, which would be the wrong fix.
Gallery gains a row of Button/IconButton pairs at all five sizes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
687b41bf20
commit
bfb2e71d02
4 changed files with 101 additions and 4 deletions
|
|
@ -244,10 +244,25 @@ export function ThemeSamplePage() {
|
|||
})}>Show a toast</Button>
|
||||
</Row>
|
||||
</div>
|
||||
<div style={{marginTop: 12}}>
|
||||
<Row>
|
||||
{['xs', 'sm', 'md', 'lg', 'xl'].map(size => <div key={size}
|
||||
style={{display: 'flex', gap: '0.4em', alignItems: 'flex-start'}}>
|
||||
<Button size={size}>{size}</Button>
|
||||
<IconButton size={size} icon='external' label={`Open (${size})`}/>
|
||||
</div>)}
|
||||
</Row>
|
||||
</div>
|
||||
<p style={{fontSize: 12, color: 'var(--muted)', marginBottom: 0}}>
|
||||
In night mode Delete drops its fill for a dashed red outline; dashed means
|
||||
destructive or failed, and nothing else uses it.
|
||||
</p>
|
||||
<p style={{fontSize: 12, color: 'var(--muted)', marginBottom: 0}}>
|
||||
An icon button is exactly as tall as a labelled one at every size, so a
|
||||
group of them lines up. Mantine's own scale for icon buttons is unrelated
|
||||
to its scale for buttons — <code>sm</code> is 22px against a button's 36px
|
||||
— and the two components start from different defaults.
|
||||
</p>
|
||||
</Panel>
|
||||
</Section>
|
||||
|
||||
|
|
|
|||
|
|
@ -101,9 +101,16 @@ export interface IconButtonProps extends Omit<ActionIconProps, 'children'> {
|
|||
component?: any;
|
||||
}
|
||||
|
||||
/** A square, icon-only button. */
|
||||
/**
|
||||
* A square, icon-only button, exactly as tall as a labelled `Button` beside it.
|
||||
*
|
||||
* The height needs BOTH halves of the fix. ui.css aligns ActionIcon's size scale with
|
||||
* Button's, and this default aligns the starting point: Mantine defaults ActionIcon to `md`
|
||||
* where it defaults Button to `sm`, so leaving it alone would trade a 28-vs-36 mismatch for
|
||||
* a 42-vs-36 one.
|
||||
*/
|
||||
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((
|
||||
{role, icon, label, className, ...props}, ref
|
||||
{role, icon, label, className, size = 'sm', ...props}, ref
|
||||
) => {
|
||||
const fromRole = role ? roleProps[role] : undefined;
|
||||
return <ActionIcon
|
||||
|
|
@ -116,7 +123,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((
|
|||
variant={props.variant ?? fromRole?.variant ?? 'default'}
|
||||
className={[fromRole?.className, className].filter(Boolean).join(' ') || undefined}
|
||||
{...props}
|
||||
size={resolveSize(props.size)}
|
||||
size={resolveSize(size)}
|
||||
>
|
||||
{renderIcon(icon)}
|
||||
</ActionIcon>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
ActionInput, Button, Card, Header, Icon, IconStack, Loading, MultiSelect, Panel, PathInput,
|
||||
ActionInput, Button, Card, Header, Icon, IconButton, IconStack, Loading, MultiSelect, Panel, PathInput,
|
||||
Message, Placeholder, Progress, Statistic, StatisticGroup, Status, TabBar, Table,
|
||||
tabClassName, TextInput,
|
||||
} from './index';
|
||||
|
|
@ -1863,3 +1863,56 @@ describe('a card keeps its meta with the title and its actions at the foot', ()
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('a button and an icon button are the same height', () => {
|
||||
/*
|
||||
* Mantine gives ActionIcon a size scale of its own that is nothing like Button's --
|
||||
* `sm` is 22px against a button's 36px -- and defaults it to `md` where Button defaults
|
||||
* to `sm`. So the Details/Open pair on an archive card came out 36px beside 28px, and
|
||||
* every other toolbar that mixes the two was ragged in the same way.
|
||||
*
|
||||
* Mantine already ships the aligned scale as `--ai-size-input-*`, for an ActionIcon
|
||||
* sitting beside an Input; those five values are identical to `--button-height-*`. The
|
||||
* fix points one at the other, so this holds for a size we have never used as well as
|
||||
* the ones we have.
|
||||
*
|
||||
* Heights, so it belongs here: jsdom returns zero for both and the comparison would
|
||||
* pass on any pair of numbers at all.
|
||||
*/
|
||||
const sizes = [undefined, 'xs', 'sm', 'md', 'lg', 'xl'];
|
||||
|
||||
sizes.forEach((size) => {
|
||||
it(`matches at size ${size || '(default)'}`, () => {
|
||||
cy.mountUI(<div style={{display: 'flex', gap: '0.5em', alignItems: 'flex-start'}}>
|
||||
<Button size={size} icon='file alternate'>Details</Button>
|
||||
<IconButton size={size} icon='external' label='Open original URL'/>
|
||||
</div>);
|
||||
|
||||
cy.get('button').should(($buttons) => {
|
||||
expect($buttons.length).to.equal(2);
|
||||
const [button, iconButton] = [...$buttons].map(el => el.getBoundingClientRect());
|
||||
// A zero-height pair would satisfy the equality below without meaning anything.
|
||||
expect(button.height, 'the button has a height at all').to.be.greaterThan(10);
|
||||
expect(iconButton.height, `icon button at size ${size || '(default)'}`)
|
||||
.to.be.closeTo(button.height, 0.5);
|
||||
// Square, which is the whole point of an icon button.
|
||||
expect(iconButton.width, 'the icon button stays square')
|
||||
.to.be.closeTo(iconButton.height, 0.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('grows the icon button rather than shrinking the button', () => {
|
||||
/*
|
||||
* Both could be made equal by cutting the labelled button down to 28px, which would
|
||||
* be the wrong fix -- a 28px target is below the 44px-ish guidance already stretched
|
||||
* by having buttons this small, and every OTHER button on the page would still be 36.
|
||||
* Pinned so a future "make them match" cannot resolve the other way.
|
||||
*/
|
||||
cy.mountUI(<IconButton icon='external' label='Open original URL'/>);
|
||||
|
||||
cy.get('button').should(($el) =>
|
||||
expect($el[0].getBoundingClientRect().height, 'default icon button height')
|
||||
.to.be.at.least(34));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -102,6 +102,28 @@ html[data-theme="night"] .wrolpi-button-danger:hover:not(:disabled) {
|
|||
|
||||
/* The icon inside inherits currentColor, so it follows without another rule. */
|
||||
|
||||
/*
|
||||
* An icon button is the same height as a labelled one, so a group of them lines up.
|
||||
*
|
||||
* Mantine gives ActionIcon a size scale unrelated to Button's -- `sm` is 22px against a
|
||||
* button's 36px -- which left the Details/Open pair on every archive card ragged, and any
|
||||
* other toolbar that mixes the two. It already ships the aligned scale as
|
||||
* `--ai-size-input-*`, for an ActionIcon standing beside an Input; those five values are
|
||||
* identical to `--button-height-*`. Pointing one at the other fixes every size at once,
|
||||
* including ones no call site uses yet.
|
||||
*
|
||||
* The `html` prefix is load-bearing. Mantine declares this scale on ActionIcon's own
|
||||
* hashed class, which has the same specificity as `.mantine-ActionIcon-root` and would win
|
||||
* on source order.
|
||||
*/
|
||||
html .mantine-ActionIcon-root {
|
||||
--ai-size-xs: var(--ai-size-input-xs);
|
||||
--ai-size-sm: var(--ai-size-input-sm);
|
||||
--ai-size-md: var(--ai-size-input-md);
|
||||
--ai-size-lg: var(--ai-size-input-lg);
|
||||
--ai-size-xl: var(--ai-size-input-xl);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------- Theme picker */
|
||||
|
||||
.wrolpi-theme-picker {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue