Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5b307df1b | ||
|
|
b201f6be88 |
4 changed files with 81 additions and 62 deletions
|
|
@ -5,18 +5,15 @@
|
|||
// used).
|
||||
//
|
||||
// `el(tag, attrs, ...children)` creates an element, applying `attrs` as
|
||||
// either the `class`/`html`/`is` special cases or plain attributes, and
|
||||
// either the `class`/`html` special cases or plain attributes, and
|
||||
// appending `children` (strings become text nodes, `null`/`undefined`
|
||||
// entries are skipped so callers can inline conditional children).
|
||||
// `is: 'custom-name'` creates a customized built-in element (e.g.
|
||||
// `el('button', { is: 'hive-btn' }, 'label')` → `<button is="hive-btn">`)
|
||||
// — passed to `document.createElement` itself, since a customized
|
||||
// built-in has to be created with its `is` option up front, not upgraded
|
||||
// after the fact via `setAttribute`.
|
||||
// `tag` can be any custom element's tag name too (e.g. `el('hive-btn',
|
||||
// { variant: 'danger' }, 'label')`) — `document.createElement` upgrades
|
||||
// it automatically if the tag is already registered.
|
||||
export const el = (tag, attrs = {}, ...children) => {
|
||||
const e = attrs.is ? document.createElement(tag, { is: attrs.is }) : document.createElement(tag);
|
||||
const e = document.createElement(tag);
|
||||
for (const [k, v] of Object.entries(attrs)) {
|
||||
if (k === 'is') continue;
|
||||
if (k === 'class') e.className = v;
|
||||
else if (k === 'html') e.innerHTML = v;
|
||||
else e.setAttribute(k, v);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
/* hive-btn.css — scoped stylesheet for the <hive-btn> customized built-in
|
||||
button element (hive-btn.js). Loaded as raw text at build time
|
||||
(esbuild's `text` loader) and turned into a `CSSStyleSheet` adopted by
|
||||
the element's own shadow root. `:host` styles the button itself (the
|
||||
element *is* a real `<button is="hive-btn">`, so `:host(:hover)` /
|
||||
`:host(:disabled)` select genuine native pseudo-classes, not
|
||||
hand-rolled state tracking) — this is the base look every `<hive-btn>`
|
||||
gets; `variant` is an attribute (not a class) since it's a semantic
|
||||
property of the component, not an arbitrary styling hook. */
|
||||
/* hive-btn.css — shadow-DOM stylesheet for the <hive-btn> autonomous
|
||||
custom element (hive-btn.js). Loaded as raw text at build time
|
||||
(esbuild's `text` loader) and adopted into each instance's own shadow
|
||||
root. `:host` is an invisible wrapper (`display: contents`) — the
|
||||
real box is the plain `button` selector below, targeting the actual
|
||||
`<button>` the shadow root wraps (scope-isolated by the shadow
|
||||
boundary already, no clash risk with anything outside). `variant` is
|
||||
an attribute on the HOST (how callers set it, and where hive-dialog's
|
||||
own selectors read it back); `:host([variant="…"])` sets `color`,
|
||||
which then inherits down into the shadow tree the normal way. */
|
||||
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
:host([variant="cancel"]) { color: var(--subtext0); }
|
||||
:host([variant="confirm"]) { color: var(--green); }
|
||||
:host([variant="danger"]) { color: var(--red); }
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
|
|
@ -24,17 +33,14 @@
|
|||
box-shadow: 0 0 0 0 currentColor;
|
||||
transition: box-shadow 0.15s ease;
|
||||
}
|
||||
:host(:hover) {
|
||||
button:hover {
|
||||
background: color-mix(in srgb, var(--fg) 6%, transparent);
|
||||
text-shadow: 0 0 10px currentColor;
|
||||
box-shadow: 0 0 10px -2px currentColor;
|
||||
}
|
||||
:host(:disabled) {
|
||||
button:disabled {
|
||||
opacity: 0.32;
|
||||
cursor: not-allowed;
|
||||
text-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
:host([variant="cancel"]) { color: var(--subtext0); }
|
||||
:host([variant="confirm"]) { color: var(--green); }
|
||||
:host([variant="danger"]) { color: var(--red); }
|
||||
|
|
|
|||
|
|
@ -1,40 +1,58 @@
|
|||
// hive-btn.js — <hive-btn>, a customized built-in `<button>` (`<button
|
||||
// is="hive-btn">`) with a shadow root for style encapsulation, replacing
|
||||
// the light-DOM `.btn` class as the button primitive for shadow-DOM
|
||||
// consumers (starting with modal.js's dialog buttons — see the frontend
|
||||
// components-split discussion on the forge issue tracker).
|
||||
// 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.
|
||||
//
|
||||
// Extending `HTMLButtonElement` (a "customized built-in element", not an
|
||||
// autonomous one) keeps every native `<button>` behaviour for free —
|
||||
// click/keyboard activation, `:disabled`, form participation/submission —
|
||||
// instead of re-implementing them on a generic wrapper. The shadow root
|
||||
// holds only a `<style>`-equivalent (adopted stylesheet) plus a `<slot>`
|
||||
// so the button's light-DOM content (its label, or asyncBtn's swapped-in
|
||||
// spinner span) renders through unchanged — slotted content stays styled
|
||||
// by the light-DOM cascade (global `.spinner` etc. still apply), only the
|
||||
// host element's own box/text styling is shadow-scoped.
|
||||
// `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 (a deliberate
|
||||
// WebKit-team stance, unlikely to change) — fine here since the project
|
||||
// targets recent Firefox only.
|
||||
//
|
||||
// Usage: `el('button', { type: 'button', is: 'hive-btn', variant: 'danger' }, 'label')`
|
||||
// (dom.js's `el()` passes `is` to `document.createElement` so custom-
|
||||
// element upgrade happens at creation, not after). `variant` is one of
|
||||
// 'cancel' | 'confirm' | 'danger' | (unset, for the neutral/default look)
|
||||
// — a plain attribute, not a class, since it's a semantic prop of the
|
||||
// component rather than an arbitrary styling hook.
|
||||
// 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';
|
||||
|
||||
class HiveBtn extends HTMLButtonElement {
|
||||
const OBSERVED = ['disabled', 'type'];
|
||||
|
||||
class HiveBtn extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return OBSERVED;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.shadowRoot) return; // guard: connectedCallback can re-fire (e.g. re-parenting)
|
||||
const root = this.attachShadow({ mode: 'open' });
|
||||
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];
|
||||
root.append(document.createElement('slot'));
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -4,15 +4,13 @@
|
|||
// actions and prompts match each page's chrome instead of a jarring OS
|
||||
// dialog.
|
||||
//
|
||||
// Implemented as two shadow-DOM custom elements (`<hive-dialog>`,
|
||||
// `<hive-toast>`), plus `<hive-btn>` (hive-btn.js) for the dialog's own
|
||||
// buttons — real per-component style encapsulation, CSS in real `.css`
|
||||
// files imported as raw text (see each component's own header comment
|
||||
// for the design rationale: shadow-DOM-vs-light-DOM tradeoffs live atop
|
||||
// the `<hive-dialog>`/`<hive-toast>` classes below, the customized-
|
||||
// built-in choice lives in hive-btn.js). Other `.btn` consumers across
|
||||
// the app stay on the light-DOM `.btn` class for now — migrating them
|
||||
// is a separate follow-up.
|
||||
// Implemented as three shadow-DOM custom elements (`<hive-dialog>`,
|
||||
// `<hive-toast>`, and `<hive-btn>` for the dialog's own buttons) — real
|
||||
// per-component style encapsulation, CSS in real `.css` files imported
|
||||
// as raw text (see each component's own header comment for the design
|
||||
// rationale). Other `.btn` consumers across the app stay on the
|
||||
// light-DOM `.btn` class for now — migrating them is a separate
|
||||
// follow-up.
|
||||
//
|
||||
// `openDialog` is the general primitive (any title/message/content + a row of
|
||||
// buttons); `themedConfirm` is a thin cancel/confirm wrapper with optional
|
||||
|
|
@ -78,8 +76,8 @@ class HiveDialog extends HTMLElement {
|
|||
// overrides it to the 'danger' look regardless (a destructive
|
||||
// confirm button reads as danger, not as a plain confirm).
|
||||
const variant = b.danger ? 'danger' : b.class;
|
||||
const btn = el('button', {
|
||||
type: 'button', is: 'hive-btn',
|
||||
const btn = el('hive-btn', {
|
||||
type: 'button',
|
||||
...(variant ? { variant } : {}),
|
||||
}, b.label);
|
||||
btn.addEventListener('click', () => done(b.value));
|
||||
|
|
|
|||
Loading…
Reference in a new issue