Skip to content

feat: add the icon button - #16

Merged
NoNamer777 merged 8 commits into
mainfrom
feat/icon-button
Oct 2, 2026
Merged

NoNamer777 merged 8 commits into
mainfrom
feat/icon-button

Conversation

@NoNamer777

@NoNamer777 NoNamer777 commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Why

Toolbars and panel headers need actions that show only an icon, such as closing a panel. The Design system Figma file has an Icon button component for this, and Button points to it for any action with only an icon. The package had no code for it yet.

What

IconButtonComponent (button[dma-icon-button]) is built after the Icon button Figma component. It shares the variants, the sizes, the Loading state, and the colors of every state with ButtonComponent.

  • Shared styles. The first commit moves the variant and state styles of the button into a variants($disabled) SCSS mixin, and the icon button includes the same mixin. The Figma spec asks for the two to stay in step, and a shared mixin keeps them in step by construction. The only difference between them is the disabled selector, which the mixin takes as a parameter. The compiled CSS of the button is byte-identical before and after, and the existing button specs pass unchanged.
  • Sizes. Each size is a fixed square (32, 40, or 48px) with spacing/0 padding, and uses the radius of the button in that size. The icon is the projected content and takes the size of the icon button through ICON_SIZE, like the icons in a button.
  • Disabled. The Figma spec says a disabled icon button still shows its tooltip, so it can't use the native disabled attribute, which drops focus and hover. The disabled input sets aria-disabled="true" and blocks clicks through blockClicksWhile(). A data-disabled attribute drives the disabled colors, because loading sets aria-disabled too but keeps the Default colors. A host binding keeps the native attribute off, even when a template writes disabled. A spec covers that case, and it fails without the binding.
  • Accessible name. aria-label is a required input until Tooltip exists. The Figma spec allows an aria-label that matches the tooltip text. Making it optional later won't break consumers.
  • Loading. The icon button uses injectLoadingState() and the same grid-cell overlay as the button, so circle-notch replaces the icon after 300ms.
  • Harness. IconButtonHarness has the label, disabled, variant, size, and loading filters, and getIcon() returns the IconHarness of the icon.
  • Testing helpers. Review feedback found that the button and icon button specs carried the same setup function and the same color and live region helpers. They now live in @dnd-mapp/ui/testing as setupHarness(), getColors(), resolveColors(), resolveColor(), and getLiveRegion(). They sit in a published entry point rather than a spec-only module, because apps that use these components need the same checks in their own tests. getColors() and resolveColors() take whatever color names the caller gives them, mirroring getFrame() and resolveFrame(). That way the button can check label and the icon button can check icon without one shared type for both.

Stories mirror the Figma page in both themes, and the MDX doc, the readme, and the changelog describe the component. Adding a component and the testing helpers is a minor bump.

Test plan

  • pnpm run format-check, lint-md, lint-ts, and typecheck
  • pnpm run build
  • pnpm run test-ci: 187 tests pass (75 new), with coverage above the thresholds
  • pnpm run build-storybook, then compare the States and Sizes stories with the Icon button Figma page in the light and the dark theme
  • Open the Storybook preview of this pull request, tab to a disabled icon button in States, and check that it takes focus and shows the focus ring

Notes

  • Once Tooltip exists, the icon button should take its name from the tooltip through aria-labelledby.
  • The variant tokens and transforms (ButtonVariant, ButtonSize) are reused as they are, rather than aliased under icon button names, since the Figma spec defines the two components with the same values.
  • The @dnd-mapp/ui/testing entry point now imports @angular/cdk and @angular/core/testing for setupHarness(). Both are already peer dependencies. The readme no longer says the entry point works without @angular/cdk.

The variant colors, their states, and the focus ring move into the
`variants` mixin of `_button-variants.scss`. It takes the selector of a
disabled control, so the icon button can share the styles while it
marks itself disabled without the native attribute. The compiled CSS of
the button stays the same.
`button[dma-icon-button]` is a button with an icon and no visible label,
after the `Icon button` Figma component. It shares the variants, sizes,
and Loading state of the button, and its variant styles through the
shared mixin. Each size is a fixed square with no padding.

A disabled icon button keeps its tooltip, so the `disabled` input sets
`aria-disabled` and blocks clicks itself. The native attribute stays
off, even when a template sets it. `aria-label` is required until a
tooltip can name the icon button.

`IconButtonHarness` finds icon buttons by label, variant, size, and
whether they're disabled or loading.

@dnd-mapp-bot dnd-mapp-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Can we add a utility function for setting up the test environment and providing the test fixture, harness, harnessloader, etc.?

Comment on lines +91 to +112
function resolveColor(token: string | null, context: HTMLElement) {
return token === null ? transparent : resolveStyle('color', token, context);
}

function toColors({ fill, border, icon }: Look, context: HTMLElement) {
return {
fill: resolveColor(fill, context),
border: resolveColor(border, context),
icon: resolveColor(icon, context),
};
}

async function getColors(button: IconButtonHarness) {
const host = await button.host();

return {
fill: await host.getCssValue('background-color'),
border: await host.getCssValue('border-top-color'),
// The icon takes the color of the button, through `currentColor`.
icon: await (await (await button.getIcon()).host()).getCssValue('fill'),
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions, and the getLiveRegion function, look like they're duplicated from button tests. Can we centralize it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f709b7a and f384bf0. resolveColor(), resolveColors(), getColors(), and getLiveRegion() now live in @dnd-mapp/ui/testing, so apps can use them in their own tests too. transparent became the null case of resolveColor().

The Look interfaces stay in each spec, because the two components check different colors: the button checks a label color on its host, and the icon button checks an icon color on the fill of its icon. getColors() and resolveColors() take whatever keys you give them, so each spec maps its own keys with a short getLook() function instead of forcing both components into one shape.

The button and icon button specs each carried the same setup function
and the same color and live region helpers. Move them to
`@dnd-mapp/ui/testing`, so the specs share them and apps can use them
in their own tests.

`setupHarness()` creates a host component and loads a harness inside
it. `getColors()` and `resolveColors()` read and resolve colors under
the same keys, so one component can name its own colors, and
`getLiveRegion()` returns the polite live region.
Replace the local setup function, color helpers, and live region
helper of the button and icon button specs with the ones from
`@dnd-mapp/ui/testing`. Each spec keeps a `getLook()` function, since
the button reads its label color from its host and the icon button
reads its icon color from the fill of its icon.
@NoNamer777
NoNamer777 temporarily deployed to storybook-preview October 2, 2026 10:11 — with GitHub Actions Inactive
@NoNamer777

Copy link
Copy Markdown
Member Author

Added setupHarness(host, query) to @dnd-mapp/ui/testing in f709b7a. It creates the host component through the TestBed and returns { fixture, element, loader, harness }. The query can be a harness class or a predicate such as ButtonHarness.with({ text: 'Save map' }). Both specs now call it through a one-line setup() that passes in their own harness (f384bf0).

@NoNamer777
NoNamer777 merged commit 7cf1b41 into main Oct 2, 2026
6 checks passed
@NoNamer777
NoNamer777 deleted the feat/icon-button branch October 2, 2026 10:16

This branch was previously deployed

1 inactive deployment
storybook-preview — f384bf09 Deployed Oct 2, 2026 by NoNamer777 via Deploy Storybook preview #36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants