fix(toast): top-layer container — closes #71#74
Merged
Conversation
Toasts rendered beneath fs-dialog modal backdrops because the container was a plain <div> in normal DOM, while <dialog>.showModal() promotes the dialog and its ::backdrop to the browser top layer (no z-index pierces). Migrate the container to the Popover API (popover="manual"). The setup() function tracks the queue with watch(toasts.value.length, ...): when the queue gains its first toast call .showPopover(), when it empties call .hidePopover(). onMounted promotes if a service has toasts before mount. Defensive guards: - Internal isOpen flag prevents redundant showPopover() calls during multi-toast bursts and redundant hidePopover() during intermediate removals (preserves "popover already open" / "already closed" semantics without relying on :popover-open matching, which happy-dom does not implement). - try/catch around both calls swallows InvalidStateError so rapid show/hide cycles or older browsers do not surface uncaught errors. Single-root <div> output preserved (0.1.1 fragment fix). Fallthrough class/style attributes still land on the container element. No inline style resets — UA :popover-open specificity is documented in CHANGELOG and docs as a consumer migration step rather than papered over. Tests: 11 new assertions in tests/toast.spec.ts cover popover attribute presence, fallthrough preservation, show/hide call shape, no-repeat-show while open, no-hide-on-intermediate-removal, retry after thrown showPopover, mount-with-pretoast, and InvalidStateError swallow on both sides. happy-dom lacks the Popover API natively — methods stubbed on HTMLElement.prototype with vi.spyOn per-test (afterEach restores to prevent prototype-spy accumulation across tests). 100% coverage maintained. Stryker mutation: 98.36% (60/61 killed; one pre-existing equivalent on hide() index===-1 short-circuit, unrelated to this change). Closes #71
Minor bump: top-layer promotion is additive container behavior plus a consumer-visible CSS specificity migration step. UA stylesheet rules for [popover]:popover-open (specificity 0,2,0) override consumer fallthrough positioning classes (specificity 0,1,0); CHANGELOG documents the mechanical override (qualify with [popover].toast-stack or use !important) and the browser baseline (Chrome 114+, Firefox 125+, Safari 17+). No internal fs-packages consumers depend on fs-toast, so no peer-range cascade is required. package-lock.json regenerates cleanly with fs-toast resolving to the workspace symlink (no nested registry copy).
New "Top-Layer Behavior (0.2.0+)" section on docs/packages/toast.md: - explains the Popover API mechanism with link to MDN's primer - captures the CSS specificity gotcha (UA :popover-open overrides consumer fallthrough positioning) with a copy-paste-ready [popover].toast-stack example - states the browser baseline and the older-browser fall-through behavior (try/catch swallows missing-method errors; toasts still render, just without modal coexistence) Mirrors the migration note in CHANGELOG so consumers reading either surface get the same guidance.
Deploying fs-packages with
|
| Latest commit: |
f5deaed
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://48e54958.fs-packages.pages.dev |
| Branch Preview URL: | https://feature-issue-71-toast-top-l.fs-packages.pages.dev |
jasperboerhof
approved these changes
May 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #71 — "Toast onder blur van modal" (toasts rendered beneath the
fs-dialogmodal backdrop in kendo).fs-dialogopens via native<dialog>.showModal(), which promotes the dialog and its::backdropto the browser top layer. Noz-indexvalue can pierce the top layer, sofs-toast's plain-DOM container rendered beneath the blurred backdrop. This PR migrates the container to the Popover API (popover="manual"+.showPopover()/.hidePopover()), the only DOM mechanism (besides another<dialog show modal>) that places an element in the top layer alongside an open modal.Behavior
popover="manual"on its single root<div>.setup()watchestoasts.value.length: 0 → ≥ 1 calls.showPopover(); ≥ 1 → 0 calls.hidePopover().isOpenflag prevents redundant.showPopover()calls during multi-toast bursts and redundant.hidePopover()during intermediate removals.try/catcharound both calls swallowsInvalidStateError(rapid show/hide cycles, older browsers).onMountedpromotes the container if a service has toasts before mount.Migration — CSS Specificity
The UA stylesheet applies
position: fixed; inset: 0; margin: auto; width/height: fit-contentto[popover]:popover-openat specificity(0,2,0). Consumer fallthrough classes like.toast-stack { position: fixed; top: 1rem; right: 1rem }((0,1,0)) do not override it. Consumers must qualify their selector or use!important:fs-toastdeliberately ships no inlinestyleresets — inline style would block consumer overrides entirely. The migration note appears in bothpackages/toast/CHANGELOG.mdanddocs/packages/toast.md.Browser Baseline
Popover API support: Chrome ≥ 114, Firefox ≥ 125, Safari ≥ 17. Older browsers fall through the defensive
try/catch— toasts still render in normal DOM (just without top-layer promotion, so they will render below modal backdrops on those browsers).Version
0.1.1→0.2.0(minor — additive behavior plus consumer-visible CSS specificity migration).No internal fs-packages consumers depend on
fs-toast, so no peer-range cascade is required.package-lock.jsonregenerates cleanly withfs-toastresolving to the workspace symlink (no nested registry copy).CI Gates (all 8 passed locally)
npm audit— 0 vulnerabilitiesnpm run format:check— clean (133 files)npm run lint— 0 warnings, 0 errors (95 rules)npm run build— tsdown ESM + CJS for all 10 packagesnpm run typecheck— clean across all packagesnpm run lint:pkg— publint + attw zero advisories on all 10 packagesnpm run test:coverage— 448 / 448 tests pass; fs-toast 100% coverage (47/47 stmts, 19/19 branches, 13/13 funcs, 39/39 lines)npx stryker run(inpackages/toast) — fs-toast 98.36% mutation score (60 killed / 1 survived; threshold 90%). The one survivor is pre-existing onhide()'sindex === -1short-circuit, unrelated to this change.Test plan
popover="manual"attribute present on container rootshowPopovercalled when first toast added (0 → 1)showPopoverNOT called repeatedly while popover is already open (1 → 2 → 3)hidePopovercalled when last toast removed (1 → 0)hidePopoverNOT called when intermediate toast removed (3 → 2)showPopovercalled again after a hide → show cycleInvalidStateErrorswallowed onshowPopover(rapid re-show)InvalidStateErrorswallowed onhidePopover(rapid re-hide)showPopover, subsequent transition retries (and hide-during-not-open is a no-op)onMountedpromotes container when service already has toasts pre-mount<div>alongsidepopover="manual"happy-dom note
happy-dom does not implement the Popover API natively (verified empirically —
typeof showPopover === 'undefined'). Per orders,showPopover/hidePopoverare stubbed onHTMLElement.prototypewith idempotent??=assignment at file scope, then wrapped per-test withvi.spyOn(cleared viavi.restoreAllMocks()inafterEachto prevent prototype-spy accumulation). Test runtime stays on happy-dom — no jsdom switch.Out of Scope
fs-dialog— unchanged; the dialog package was correct.fs-toastremains component-agnostic; consumer-supplied toast component owns all UX behavior.