frontend: hive-btn — drop the is= customized-built-in, use an autonomous element instead
Per review: don't use is=, it reads as a hack (and it is one — is=-upgraded
built-ins can never host a shadow root, which is what caused the crash this PR
fixes in the first place). <hive-btn> is now a normal autonomous custom element
wrapping a real <button> inside its own shadow root, so it gets its shadow
encapsulation back (matching hive-dialog/hive-toast) instead of the document-
level stylesheet workaround from the previous commit.
delegatesFocus: true on the shadow root means .focus() on the host (what
modal.js calls for autofocus) reaches the inner button directly. The inner
button's native click is a composed event, so host-level click listeners
(what modal.js/themedPrompt already use) keep working unchanged.
modal.js: el('button', { is: 'hive-btn', ... }) -> el('hive-btn', { ... }) at
the one call site. dom.js: removed the is= special case from el() entirely —
it existed only to support this one now-gone usage. Build clean.
This commit is contained in:
parent
b201f6be88
commit
e5b307df1b
4 changed files with 84 additions and 75 deletions
|
|
@ -1,43 +1,58 @@
|
|||
// hive-btn.js — <hive-btn>, a customized built-in `<button>` (`<button
|
||||
// is="hive-btn">`) — the button primitive for modal.js's dialog buttons.
|
||||
// Extending `HTMLButtonElement` keeps native button behaviour (click/
|
||||
// keyboard activation, `:disabled`) for free.
|
||||
// hive-btn.js — <hive-btn>, the button primitive for modal.js's dialog
|
||||
// buttons. Autonomous custom element (previously a customized built-in,
|
||||
// `<button is="hive-btn">` — dropped: `is=` upgrades can't host a shadow
|
||||
// root at all, which crashed every themed dialog, and reads as a hack
|
||||
// regardless once you know that). A real `<button>` lives inside the
|
||||
// shadow root instead, so native click/keyboard activation and
|
||||
// `:disabled` still come for free — just one level down from the host.
|
||||
//
|
||||
// NOT shadow-DOM-encapsulated: a customized built-in can't host a shadow
|
||||
// root — `attachShadow()` throws `NotSupportedError` unconditionally for
|
||||
// any element that isn't an autonomous custom element or on the HTML
|
||||
// spec's short fixed list (article/aside/div/span/etc., no `button`),
|
||||
// and `is=`-upgraded built-ins are excluded regardless of tag. The prior
|
||||
// version called `attachShadow()` here anyway — it shipped and passed
|
||||
// review since nothing in CI exercises real browser DOM, then broke the
|
||||
// very first live dialog open. Styled via a stylesheet adopted once on
|
||||
// `document` instead, scoped with `[is="hive-btn"]` (hive-btn.css) — the
|
||||
// light-DOM approach the rest of the app's `.btn` consumers already use.
|
||||
// (`<hive-dialog>`/`<hive-toast>` in modal.js are unaffected — genuine
|
||||
// autonomous custom elements, valid shadow hosts.)
|
||||
// `delegatesFocus: true` on the shadow root means `.focus()` on the host
|
||||
// (what modal.js calls) focuses the inner button directly, and a click
|
||||
// anywhere on the host focuses it too, matching native `<button>` feel.
|
||||
// The inner button's native `click` is a composed event, so it bubbles
|
||||
// out through the shadow boundary — callers add `click` listeners on the
|
||||
// `<hive-btn>` host exactly as they would on a plain `<button>`.
|
||||
//
|
||||
// Customized built-ins aren't supported in Safari/WebKit — fine here
|
||||
// since the project targets recent Firefox only.
|
||||
//
|
||||
// Usage: `el('button', { type: 'button', is: 'hive-btn', variant: 'danger' }, 'label')`.
|
||||
// `variant` ∈ 'cancel' | 'confirm' | 'danger' | unset (neutral default).
|
||||
// Usage: `el('hive-btn', { type: 'button', variant: 'danger' }, 'label')`.
|
||||
// `variant` ∈ 'cancel' | 'confirm' | 'danger' | unset (neutral default) —
|
||||
// reflected straight through to the inner button so hive-btn.css's
|
||||
// `:host([variant="…"])` selectors keep working unchanged.
|
||||
// `disabled`/`type` are likewise plain attributes on the host, mirrored
|
||||
// onto the inner button on connect and on every attribute change.
|
||||
|
||||
import hiveBtnCss from './hive-btn.css';
|
||||
|
||||
// Adopted once, module-wide — every <hive-btn> instance shares the same
|
||||
// document-level stylesheet rather than each instance re-adopting it.
|
||||
let styleInstalled = false;
|
||||
function ensureStyleInstalled() {
|
||||
if (styleInstalled) return;
|
||||
styleInstalled = true;
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(hiveBtnCss);
|
||||
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
||||
}
|
||||
const OBSERVED = ['disabled', 'type'];
|
||||
|
||||
class HiveBtn extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return OBSERVED;
|
||||
}
|
||||
|
||||
class HiveBtn extends HTMLButtonElement {
|
||||
connectedCallback() {
|
||||
ensureStyleInstalled();
|
||||
if (this._btn) {
|
||||
this._sync();
|
||||
return; // already built (e.g. re-parenting re-fires connectedCallback)
|
||||
}
|
||||
const root = this.attachShadow({ mode: 'open', delegatesFocus: true });
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(hiveBtnCss);
|
||||
root.adoptedStyleSheets = [sheet];
|
||||
const btn = document.createElement('button');
|
||||
btn.append(document.createElement('slot'));
|
||||
root.append(btn);
|
||||
this._btn = btn;
|
||||
this._sync();
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
this._sync();
|
||||
}
|
||||
|
||||
_sync() {
|
||||
if (!this._btn) return;
|
||||
this._btn.disabled = this.hasAttribute('disabled');
|
||||
this._btn.type = this.getAttribute('type') || 'button';
|
||||
}
|
||||
}
|
||||
customElements.define('hive-btn', HiveBtn, { extends: 'button' });
|
||||
customElements.define('hive-btn', HiveBtn);
|
||||
|
|
|
|||
Loading…
Reference in a new issue