Modal Dialog
The complete modal pattern: backdrop click-to-close, Esc-to-close, focus on open, and copy-to-clipboard.
Live Demo
Share link
Anyone with this URL can view the document.
Source
HTML + JavaScript
<div data-component="share-dialog">
<button data-action="openDialog">Share this document</button>
<span data-show="lastAction" data-bind="lastAction"></span>
<!-- data-event-self: fires only when the click target IS the scrim,
not a click that bubbled up from the dialog box, so no inner
click-stop handler is needed.
data-cloak hides the element until bindings are processed.
If a parent ever clips the scrim, add data-portal="body". -->
<div class="scrim" data-show="open"
data-action="closeDialog" data-event-self data-cloak>
<div class="dialog">
<h3>Share link</h3>
<p class="muted">Anyone with this URL can view the document.</p>
<div class="urlbox">
<!-- data-bind on an input writes the .value PROPERTY.
Do NOT use data-bind-attr="{ value: shareUrl }". -->
<input type="text" data-bind="shareUrl" readonly>
<button data-action="copyUrl"
data-bind-class="copied ? 'ok' : ''"
data-bind="copyLabel">Copy</button>
</div>
<footer>
<button class="btn-ghost" data-action="closeDialog">Done</button>
</footer>
</div>
</div>
</div>
<script>
wildflower.component('share-dialog', {
state: { open: false, shareUrl: '', copied: false, lastAction: '' },
computed: {
copyLabel() { return this.copied ? 'Copied' : 'Copy'; }
},
openDialog() {
this.shareUrl = 'https://example.com/s/' + Math.random().toString(36).slice(2, 10);
this.open = true;
this.lastAction = '';
// Instance prop (not state): a one-shot flag, no extra reactive pass.
this._focusOnNextUpdate = true;
},
closeDialog() {
this.open = false;
this.copied = false;
this.lastAction = 'Dialog was closed';
},
copyUrl() {
try { navigator.clipboard.writeText(this.shareUrl); } catch (e) {}
this.copied = true;
clearTimeout(this._copyTimer);
this._copyTimer = setTimeout(() => { this.copied = false; }, 1500);
},
// Runs after the DOM is committed (the $nextTick equivalent): the place
// for focus, .select(), scroll-into-view, third-party widget init.
onUpdate() {
if (this._focusOnNextUpdate) {
this._focusOnNextUpdate = false;
this.$el('input').el?.select();
}
},
// Esc-to-close while open: a global shortcut, so a document listener.
init() {
this._onKey = (e) => {
if (e.key === 'Escape' && this.open) this.closeDialog();
};
document.addEventListener('keydown', this._onKey);
},
destroy() {
document.removeEventListener('keydown', this._onKey);
clearTimeout(this._copyTimer);
}
});
</script>
Key Points
data-event-selfon the scrim closes only when the backdrop itself is clicked, not when a click bubbles up from inside the dialog, so no inner click-stop handler is neededdata-bindon the<input>writes the value property; never usedata-bind-attr="{ value: ... }"for inputs- Esc-to-close is a global-while-open shortcut, so it uses a
documentkeydown listener added ininit()and removed indestroy() - Focus-on-open uses
onUpdate()(the post-render hook) plus$el, gated by a one-shot instance flag (this._focusOnNextUpdate) rather than reactive state data-cloakprevents a flash of the open dialog before bindings process; if a parent ever clips the scrim, addingdata-portal="body"is a one-attribute fix