Skip to content

chore(release): 2.8.0 - #232

Merged
ndlabdev merged 23 commits into
mainfrom
dev
Sep 27, 2026
Merged

ndlabdev merged 23 commits into
mainfrom
dev

Conversation

@ndlabdev

Copy link
Copy Markdown
Owner

Summary

Release 2.8.0. Minor, because the release adds a new component and three other additive changes. No export was removed: I diffed the published 2.7.0 surface against this build earlier in the cycle and it is purely additive.

Type of change

  • 🐛 Bug fix
  • ✨ New feature / component
  • 📖 Documentation
  • ♻️ Refactor / chore
  • ⚠️ Breaking change

Behaviour changes worth checking on upgrade

None of these remove an export, but each is observable.

  • ThemeModeButton now has a fixed accessible name of Toggle theme instead of Switch to light mode and Switch to dark mode. Anything selecting the button by accessible name needs updating. This is the one I expect people to hit.
  • ThemeModeButton renders two <svg> elements instead of one, with CSS choosing the visible one. The hidden icon is display: none, so layout is unchanged and ordinary svg styling still applies. Selectors that count children no longer match.
  • ThemeModeButton no longer inherits its size from a surrounding FieldGroup, so the button and its icons always agree.
  • The Table column resize handle is now a focusable separator with tabindex and aria-value*. Selectors aimed at a bare span will not match.
  • Input slots now reserve padding and accept pointer events. An existing ui workaround still behaves: the caller's ui.base wins over the variant padding, verified at 40px against 36px, and the control stays clickable.

Changes

[2.8.0] - 2026-09-27

Added

  • InputPassword — password field built on Input. A visibility toggle whose accessible name follows the next action, purpose mapping to the right autocomplete value for sign in, sign up and password change, a caps lock warning, and a guarantee that pasting is never blocked. An optional strength meter renders a score you supply: the library never judges a password itself, since scoring without a dictionary is misleading and a rule set that suits one policy penalises another. The screen reader announcement is debounced and fires on level changes rather than on every keystroke. (#228)
  • ThemeMode — dark mode handler for the root layout, replacing the setup that required a second package. (#224)
  • Exports — toggleMode, setMode, resetMode, mode, userPrefersMode and systemPrefersMode are re-exported from the package root, alongside resetConfig. The docs already referenced these; now they resolve. (#224)
  • NavigationMenu — align prop (start | center | end, default center) places the dropdown under the open trigger. (#220)

Changed

  • mode-watcher moved from peerDependencies to dependencies, matching every other runtime library. Dark mode now works without installing anything beyond sv5ui, including on package managers that do not auto install peers. (#224)

Fixed

  • Input — leadingSlot and trailingSlot now reserve room in the field, so their content no longer sits on top of the text, and pointer events reach them, so a button placed in a slot is clickable. A decorative leadingIcon or trailingIcon stays click through, and slot content is inert while the field is disabled or loading. The loading spinner also takes precedence over a slot instead of being swallowed by it. (#229)
  • Table — the column resize handle is a focusable separator with aria-valuenow, arrow key resizing and a larger step while shift is held, plus Home and End for the bounds. It was mouse only before: no role, no tabindex and no key handler, so keyboard users could not resize a column at all. The drag also moves to the shared pointer hook, so touch and pen work rather than mouse alone. (#226)
  • ThemeModeButton — renders both mode icons and lets CSS pick the visible one, so server and client markup are identical. The wrong glyph no longer sticks after a reload in dark mode. The accessible name is now a mode-independent Toggle theme. (#224)
  • Modal, Slideover, Drawer, Popover — with portal={false}, nested floating layers (Select, DatePicker, DropdownMenu, Tooltip, ...) no longer hide behind the container when an ancestor has a z-index above 50. (#217)
  • DatePicker, DateRangePicker — the calendar is anchored to the field instead of the calendar icon, so it opens under the field's start edge. (#218)
  • NavigationMenu — the dropdown opens under the trigger that opened it, slides between triggers and stays inside the menu. The 8px gap under the items is now reserved only while the list overflows. (#220)
  • Tabs — the indicator follows the active tab when the text direction flips between ltr and rtl. (#216)

Checklist

  • pnpm check passes (0 errors, 0 warnings)
  • pnpm lint passes
  • pnpm test passes
  • pnpm build passes, publint reports no issues
  • Version bumped in package.json
  • CHANGELOG.md rotated under [2.8.0]

Notes

Tests: 3909 passing across 108 files, up from 3847 at 2.7.0.

One thing to watch during the CI run on this PR: several specs assert on icons that are not in the bundled set, so they fetch from the icon API at test time and can fail intermittently. I saw Accordion and ToggleGroup fail once this cycle and pass on a rerun. If CI goes red on those two, rerun before investigating.

ndlabdev and others added 23 commits September 17, 2026 19:15
Modal, Slideover, Drawer and Popover rendered with portal={false} hid
every floating layer opened inside them (Select, DatePicker,
DropdownMenu, Tooltip, a nested Modal, ...) whenever an ancestor set a
z-index above 50. The layers were portalled to <body> at z-50 while the
inline panel belonged to the higher stacking context.

While such a container is open, nested layers now mount into a host
placed right before its panel, in the same stacking context and one
level above it. The host is removed once the panel has unmounted, so the
closed DOM is unchanged, and portalled containers are not affected.

Closes #217
DatePicker and DateRangePicker positioned their calendar against the
calendar icon button, which sits at the end of the field. With the
default align="start" the popover opened at the icon's left edge,
hundreds of pixels right of the field and over neighbouring content.

The popover is now anchored to the field root, so it opens flush under
the field's left edge, matching how Select places its list.
align="center" now centres on the field and align="end" is unchanged.

Closes #218
A horizontal NavigationMenu rendered every dropdown in one shared panel
pinned to the menu's left edge, so a trigger near the end opened a panel
hundreds of pixels away, detached from it when the content was narrow.

The panel now follows the open trigger. The component measures the
trigger's inline start and width; CSS places the panel with clamp() so it
stays inside the menu, using the panel width the viewport already
exposes. It slides between triggers, appears in place when the menu
first opens, follows the list when it scrolls, and respects dir="rtl".
Below the sm breakpoint the panel keeps its full width.

A new align prop ('start' | 'center' | 'end', default 'center') chooses
where the panel sits relative to the trigger. The docs page gains a
Dropdown Alignment section to switch it live.

The list also reserved 8px under its items for the overlay scrollbar at
all times. That room is now added only while the list overflows, so a
menu that fits no longer shows the gap.

Closes #220
The sliding indicator was positioned from the active trigger's offset,
and only recomputed when the value, orientation or items changed, or
when the list resized. Flipping the page from ltr to rtl mirrors the
triggers without any of those, so the indicator kept its old offset and
sat under the wrong tab, with the active label rendered in the on
indicator color over the plain background.

The indicator is now recomputed when a dir attribute changes anywhere in
the document. A direction set only through CSS is still not detected.

Closes #216
fix(portal): keep nested floating layers above inline containers
fix(date-picker): anchor the calendar popover to the whole field
…t-align

fix(navigation-menu): open the dropdown panel under its trigger
fix(tabs): move the indicator when the text direction changes
mode-watcher was a required peer dependency while every other runtime
library sits in dependencies. Users had to install it themselves, and
package managers that do not auto install peers failed to resolve it at
all. It moves to dependencies at the same range.

toggleMode and resetConfig were referenced by the setup docs but never
exported from the package root, so copying those snippets broke the
build. A ThemeMode component now wraps the watcher and the mode
functions are re-exported, which keeps the underlying library an
implementation detail and leaves room to replace it later.

ThemeModeButton chose its icon from mode.current, which is undefined
during SSR, so the server always rendered the light branch. Svelte does
not repair mismatched html blocks, so the wrong glyph stuck after a
reload in dark mode. Both icons now render and CSS picks the visible
one, and the accessible name no longer depends on the resolved mode.

The docs pages are left untouched, so the findings about setup snippets
stay open.

Refs #224
The handle was a bare span carrying only onmousedown, with no role, no
tabindex and no key handler. Keyboard users could not reach it at all,
let alone resize a column. A probe measured the column staying at 150px
after three arrow presses while a mouse drag moved it to 230px, and one
Tab from a button above the table jumped straight past the whole table.

The drag also listened for mousemove and mouseup on document, so touch
and pen never started a resize, even though the element already carried
touch-none and therefore blocked scrolling that began on it.

It is now a focusable separator with aria-orientation, aria-valuenow,
aria-valuemin, aria-valuemax and aria-valuetext, resized by the arrow
keys with a larger step while shift is held, and by Home and End for the
bounds. The drag moves onto usePointerDrag, which ColorPicker and
Resizable already use, so every pointer type works.

The svelte-ignore for static element interactions is gone, since the
element is now interactive. One remains for the noninteractive tabindex
rule, which misreads a separator carrying aria-valuenow as passive. That
is the same false positive Resizable already suppresses.

Closes #226
fix(theme-mode): export dark mode API, fix SSR icon mismatch
fix(table): make the column resize handle keyboard and touch accessible
The two slots were part of the public API but had no test coverage, and
three separate things were wrong with them.

isLeading and isTrailing ignored the slots, so the padding compound
variants never applied. The same icon measured 36px of padding through
the leadingIcon prop and 12px through the slot, leaving the content
overlapping the text by 22px. Both flags now account for the slots.

The slot wrapper carries pointer-events-none so that a decorative icon
stays click through and clicking it focuses the field. That also made
interactive slot content impossible to click: a button in a slot never
received a single click, while the cursor still changed on hover.
Pointer events are now re-enabled only on a wrapper that actually holds
a slot, and only while the field is neither disabled nor loading, so a
control inside a disabled field stays inert.

A slot also took precedence over the loading branch, so a field with a
slot went disabled during loading with no spinner and no other
indicator. Loading now wins, matching how Button swaps its leading icon
for the spinner.

Also corrected the loading prop documentation, which claimed it
optionally disables interaction while the input is always disabled while
loading. That text ships in the published type declarations.

Closes #229
Requested in #228. The first answer was to decline and document the
composition instead, which was wrong on two counts: ThemeModeButton is
89 lines and does less, so thinness is not where this library draws its
line, and the two extra bundled icons measure at about half a kilobyte
before compression.

The component wraps Input rather than reimplementing it the way
InputTags and InputNumber do, because those two need to change what sits
inside the field box while this one only needs a trailing control and
blocks below. FormField and Form integration comes for free as a result:
label association, error state and aria-describedby all flow through.

What it handles: a visibility toggle whose accessible name follows the
next action, purpose mapping to the correct autocomplete value for sign
in, sign up and password change, a caps lock warning cleared on release
or blur, and a guarantee that pasting is never intercepted.

Scoring stays with the caller. strength takes a number and the component
renders the meter, picks the colour and debounces the screen reader
announcement so it fires on level changes rather than on every
keystroke. A naive scorer is worse than no meter: counting character
classes rates Password1! as strong while penalising a long passphrase
for having no digit, and scoring properly needs a dictionary or a breach
list, neither of which belongs in a UI library.

Adds passwordShow and passwordHide to the icon defaults and regenerates
the bundled set.

Closes #228
fix(input): make leading and trailing slots usable
feat(input-password): add a password field with a caller scored meter
@ndlabdev ndlabdev self-assigned this Sep 27, 2026
@ndlabdev
ndlabdev merged commit a488192 into main Sep 27, 2026
2 checks passed
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.

1 participant