Skip to content

Commit f51e8b2

Browse files
authored
Merge pull request #74 from script-development/feature/issue-71-toast-top-layer
fix(toast): top-layer container — closes #71
2 parents 9c35d8b + f5deaed commit f51e8b2

6 files changed

Lines changed: 321 additions & 11 deletions

File tree

docs/packages/toast.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,34 @@ This catches toast-related bugs at build time, not at runtime.
125125
| `show(props)` | `(props) => string` | Show a toast, returns unique ID |
126126
| `hide(id)` | `(id: string) => void` | Remove a toast by ID |
127127
| `ToastContainerComponent` | `Component` | Mount this in your app root |
128+
129+
## Top-Layer Behavior (0.2.0+)
130+
131+
The `ToastContainerComponent` promotes itself to the browser **top layer** via the [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) whenever at least one toast is queued. This keeps toasts visible above any open `<dialog>.showModal()` backdrop — without top-layer promotion, no `z-index` value can pierce a modal's stacking context.
132+
133+
The container declares `popover="manual"` and calls `.showPopover()` on the first toast / `.hidePopover()` after the last toast clears. Defensive try/catch guards swallow `InvalidStateError` so rapid show/hide cycles don't surface uncaught errors.
134+
135+
### CSS Specificity (Migration from 0.1.1)
136+
137+
The UA stylesheet applies `position: fixed; inset: 0; margin: auto; width: fit-content; height: fit-content` to any element matching `[popover]:popover-open` — selector specificity `(0,2,0)`. Consumer fallthrough classes like `.toast-stack { position: fixed; top: 1rem; right: 1rem }` (`(0,1,0)`) do **not** override these UA rules.
138+
139+
If you applied positioning via fallthrough classes in 0.1.1, raise selector specificity in 0.2.0 by qualifying with `[popover]`:
140+
141+
```css
142+
/* Beats UA :popover-open */
143+
[popover].toast-stack {
144+
position: fixed;
145+
top: 1rem;
146+
right: 1rem;
147+
inset: auto;
148+
margin: 0;
149+
width: auto;
150+
height: auto;
151+
}
152+
```
153+
154+
`fs-toast` deliberately ships **no** inline `style` resets so consumer CSS retains full control.
155+
156+
### Browser Baseline
157+
158+
Popover API support: Chrome ≥ 114, Firefox ≥ 125, Safari ≥ 17. On older browsers the container's defensive try/catch swallows the missing-method error — toasts still render in normal DOM, just without top-layer promotion (so they will render below modal backdrops on those browsers).

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/toast/CHANGELOG.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
# @script-development/fs-toast
22

3+
## 0.2.0
4+
5+
### Minor Changes
6+
7+
- Promote `ToastContainerComponent` to the browser top layer via the [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) so toasts remain visible above `<dialog>.showModal()` backdrops (closes [#71](https://github.com/script-development/fs-packages/issues/71)). The container declares `popover="manual"` and calls `.showPopover()` when the queue gains its first toast / `.hidePopover()` when the queue empties. The single-root container output from 0.1.1 is preserved — fallthrough class/style attributes still land on the root `<div>`.
8+
9+
### Migration — CSS Specificity
10+
11+
The UA stylesheet applies the following rules to any element with `[popover]:popover-open`:
12+
13+
```css
14+
[popover]:popover-open {
15+
position: fixed;
16+
inset: 0;
17+
margin: auto;
18+
width: fit-content;
19+
height: fit-content;
20+
}
21+
```
22+
23+
The selector specificity is `(0,2,0)`. A consumer fallthrough class such as `.toast-stack { position: fixed; top: 1rem; right: 1rem }` (specificity `(0,1,0)`) does **not** override it. To restore custom positioning while a toast is queued, raise selector specificity by qualifying with `[popover]` or use `!important`:
24+
25+
```css
26+
[popover].toast-stack {
27+
position: fixed;
28+
top: 1rem;
29+
right: 1rem;
30+
inset: auto;
31+
margin: 0;
32+
width: auto;
33+
height: auto;
34+
}
35+
```
36+
37+
`fs-toast` deliberately ships **no** inline `style` resets — inline style would block consumer overrides entirely. Override at the CSS layer instead.
38+
39+
### Browser Baseline
40+
41+
The Popover API requires Chrome ≥ 114, Firefox ≥ 125, Safari ≥ 17. Older browsers fall through the container's defensive try/catch — the toast queue still renders, just without top-layer promotion (and therefore without modal coexistence).
42+
343
## 0.1.1
444

545
### Patch Changes

packages/toast/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@script-development/fs-toast",
3-
"version": "0.1.1",
3+
"version": "0.2.0",
44
"description": "Component-agnostic toast queue service for Vue 3 — FIFO management, you bring the component",
55
"homepage": "https://packages.script.nl/packages/toast",
66
"license": "MIT",

packages/toast/src/index.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {Component, VNode} from 'vue';
22
import type {ComponentProps} from 'vue-component-type-helpers';
33

4-
import {defineComponent, h, ref} from 'vue';
4+
import {defineComponent, h, onMounted, ref, watch} from 'vue';
55

66
/** Public API of a toast service instance. */
77
export interface ToastService<C extends Component> {
@@ -20,6 +20,11 @@ export interface ToastService<C extends Component> {
2020
* the oldest toast is removed. Each toast component receives an `onClose`
2121
* prop that removes it from the queue when called.
2222
*
23+
* The container promotes itself to the browser top layer (via the Popover API
24+
* with `popover="manual"`) whenever at least one toast is queued, so toasts
25+
* remain visible above `<dialog>.showModal()` backdrops. The container demotes
26+
* back to the normal stacking context when the queue empties.
27+
*
2328
* @param component - The Vue component to render for each toast.
2429
* @param maxToasts - Maximum number of visible toasts (default: 4, minimum: 1).
2530
*/
@@ -50,12 +55,51 @@ export const createToastService = <C extends Component>(component: C, maxToasts
5055

5156
const ToastContainerComponent = defineComponent({
5257
name: 'ToastContainer',
53-
render() {
54-
return h(
55-
'div',
56-
null,
57-
toasts.value.map((toast) => toast.node),
58+
setup() {
59+
const containerRef = ref<HTMLElement | null>(null);
60+
let isOpen = false;
61+
62+
const showContainer = () => {
63+
const el = containerRef.value;
64+
if (!el || isOpen) return;
65+
try {
66+
el.showPopover();
67+
isOpen = true;
68+
} catch {
69+
// Popover API unsupported, or element already open under a different
70+
// code path — leave isOpen false so a later attempt can retry.
71+
}
72+
};
73+
74+
const hideContainer = () => {
75+
const el = containerRef.value;
76+
if (!el || !isOpen) return;
77+
try {
78+
el.hidePopover();
79+
} catch {
80+
// Popover API unsupported, or element already closed — fall through.
81+
}
82+
isOpen = false;
83+
};
84+
85+
onMounted(() => {
86+
if (toasts.value.length > 0) showContainer();
87+
});
88+
89+
watch(
90+
() => toasts.value.length,
91+
(length) => {
92+
if (length > 0) showContainer();
93+
else hideContainer();
94+
},
5895
);
96+
97+
return () =>
98+
h(
99+
'div',
100+
{ref: containerRef, popover: 'manual'},
101+
toasts.value.map((toast) => toast.node),
102+
);
59103
},
60104
});
61105

0 commit comments

Comments
 (0)