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
Binary file modified packages/components/button-toggle/__screenshots__/02-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/components/button-toggle/__screenshots__/03-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/components/button-toggle/__screenshots__/04-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions packages/components/button-toggle/button-toggle-tokens.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@
--kbq-button-toggle-size-item-padding-vertical: var(--kbq-size-3xs);
--kbq-button-toggle-size-item-content-gap-horizontal: var(--kbq-size-xxs);
--kbq-button-toggle-size-item-focus-outline-width: var(--kbq-size-3xs);
// How far an item's hit area reaches past the pill it paints. Measured from the inner button's
// border box, one item border inside the item's own — hence the border-width term in each. `-gap` is
// the half of the inter-item gap an item claims; the other two cover the group's padding. The
// pseudo-element they offset is in button-toggle.scss, next to the note on the two other places
// this repo extends a hit area the same way.
--kbq-button-toggle-size-item-hit-area-gap: calc(
var(--kbq-size-border-width) + var(--kbq-button-toggle-size-container-content-gap-horizontal) / 2
);
--kbq-button-toggle-size-item-hit-area-horizontal: calc(
var(--kbq-size-border-width) + var(--kbq-button-toggle-size-container-padding-horizontal)
);
--kbq-button-toggle-size-item-hit-area-vertical: calc(
var(--kbq-size-border-width) + var(--kbq-button-toggle-size-container-padding-vertical)
);
/* THEME TOKENS */
--kbq-button-toggle-container-background: var(--kbq-background-contrast-fade);
--kbq-button-toggle-item-default-background: var(--kbq-background-transparent);
Expand Down
78 changes: 75 additions & 3 deletions packages/components/button-toggle/button-toggle.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,31 @@

.kbq-button-toggle {
display: flex;
overflow: hidden;
justify-content: flex-start;
align-items: center;

// Must not clip: the button's hit area below reaches outside this box. Not `overflow: clip` with
// an `overflow-clip-margin` — Chromium rejects math functions there, so the margin cannot follow
// the tokens the offsets come from and computes to `0px`, clipping the hit area away.

// What the item stops containing, and why nothing here takes it back: a toggle projects its
// content into the button's own `.kbq-button-text`, which keeps the `overflow: hidden` of
// `kbq-truncate-line()`, so content still cannot spill sideways past the button's content box.
// Content *taller* than the fixed item height no longer is — and cannot be re-clipped from
// inside the button, since the only boxes there with a definite height are the button itself,
// whose `::before` this exists for, and the label box, which would clip its own descenders.
overflow: visible;

// What that clip used to give for free: a flex item's automatic minimum size is zero only while
// its `overflow` is non-`visible`, and that is what lets the label truncate.
min-width: 0;

// Content-box on purpose, and load-bearing. `--kbq-button-toggle-size-item-height` is 22px,
// not the 24px of the design's `--kbq-size-xxl`: DS-3136 subtracted the border it added here
// from the token rather than from the rendered box, so the item only reaches its designed
// 24px while the border sits outside the declared height. A global `border-box` reset in a
// consuming application would pull the border back inside and leave the item 2px short —
// and the inner button, which is sized from the same token, would overflow the shrunken
// content box and be clipped by `overflow: hidden` above.
// and the inner button, which is sized from the same token, would then spill past it.
box-sizing: content-box;

height: var(--kbq-button-toggle-size-item-height);
Expand Down Expand Up @@ -60,6 +74,21 @@
button-toggle-size-item-focus-outline-width]
);

// Hit area over the gutters the group's `gap` and `padding` leave around the pill this button
// paints, so the highlight does not drop out between segments. On the button, not the item
// host, so pointer events resolve to it and the gutter is clickable too. `::before` because
// `::after` is the `.kbq-progress` shimmer. Offsets are per orientation, below.

// The third place this repo extends a hit area with a transparent pseudo-element, after
// `.kbq-navbar-item`'s `:before` (DS-2558) and `select.item-active-area()`. Neither is
// reusable here: the mixin claims a fixed strip on one side of a plain vertical list, with
// no gap to tile between two neighbours, no group padding at the ends, and no orientation
// to swap the axes for.
&:before {
content: '';
position: absolute;
}

& .kbq-button-wrapper {
width: 100%;
}
Expand Down Expand Up @@ -117,13 +146,56 @@
}
}

// Hit-area offsets, one orientation each — the vertical block below is this one with the axes
// swapped. The gap is split at its midline so two neighbours tile it rather than overlap, which would
// hand the whole gutter to whichever paints last; the outermost sides have no neighbour to share with
// and take the group's padding instead, leaving no point inside the group unclaimed.

// `:first-of-type`/`:last-of-type`, not `:first-child`/`:last-child`: the group is a bare directive
// with no template, so a consumer's own element — a divider, a leading icon — is rendered as a real
// child among the toggles and would take the positional match away from the toggle that actually sits
// at the edge, putting the dead zone back. The class only ever lands on a `kbq-button-toggle` element,
// so matching by type reads the toggles alone.
&:not(.kbq-button-toggle_vertical) {
.kbq-button-toggle {
> .kbq-button,
> .kbq-button-icon {
max-width: 100%;

height: var(--kbq-button-toggle-size-item-height);

&:before {
inset-block: calc(-1 * var(--kbq-button-toggle-size-item-hit-area-vertical));
inset-inline: calc(-1 * var(--kbq-button-toggle-size-item-hit-area-gap));
}
}

&:first-of-type > :is(.kbq-button, .kbq-button-icon):before {
inset-inline-start: calc(-1 * var(--kbq-button-toggle-size-item-hit-area-horizontal));
}

&:last-of-type > :is(.kbq-button, .kbq-button-icon):before {
inset-inline-end: calc(-1 * var(--kbq-button-toggle-size-item-hit-area-horizontal));
}
}
}

&.kbq-button-toggle_vertical {
.kbq-button-toggle {
> .kbq-button,
> .kbq-button-icon {
&:before {
inset-inline: calc(-1 * var(--kbq-button-toggle-size-item-hit-area-horizontal));
inset-block: calc(-1 * var(--kbq-button-toggle-size-item-hit-area-gap));
}
}

&:first-of-type > :is(.kbq-button, .kbq-button-icon):before {
inset-block-start: calc(-1 * var(--kbq-button-toggle-size-item-hit-area-vertical));
}

&:last-of-type > :is(.kbq-button, .kbq-button-icon):before {
inset-block-end: calc(-1 * var(--kbq-button-toggle-size-item-hit-area-vertical));
}
}
}
Expand Down
Loading