@design.estate/dees-catalog 3.97.1 → 3.98.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_bundle/bundle.js +4915 -3518
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-button/dees-button/dees-button.d.ts +35 -0
- package/dist_ts_web/elements/00group-button/dees-button/dees-button.js +128 -5
- package/dist_ts_web/elements/00group-chart/dees-chart-area/component.d.ts +14 -0
- package/dist_ts_web/elements/00group-chart/dees-chart-area/component.js +271 -117
- package/dist_ts_web/elements/00group-chart/dees-chart-area/realtime-updates.d.ts +21 -0
- package/dist_ts_web/elements/00group-chart/dees-chart-area/realtime-updates.js +79 -0
- package/dist_ts_web/elements/00group-feedback/dees-spinner/dees-spinner.js +12 -3
- package/dist_ts_web/elements/00group-form/dees-form-submit/dees-form-submit.d.ts +9 -0
- package/dist_ts_web/elements/00group-form/dees-form-submit/dees-form-submit.js +25 -2
- package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.d.ts +15 -0
- package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.js +36 -2
- package/dist_ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.d.ts +246 -0
- package/dist_ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.demo.js +439 -21
- package/dist_ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.js +857 -23
- package/package.json +3 -3
- package/readme.hints.md +35 -0
- package/readme.md +80 -11
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00group-button/dees-button/dees-button.ts +128 -2
- package/ts_web/elements/00group-chart/dees-chart-area/component.ts +276 -116
- package/ts_web/elements/00group-chart/dees-chart-area/realtime-updates.ts +110 -0
- package/ts_web/elements/00group-feedback/dees-spinner/dees-spinner.ts +11 -2
- package/ts_web/elements/00group-form/dees-form-submit/dees-form-submit.ts +18 -0
- package/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts +29 -0
- package/ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.demo.ts +418 -19
- package/ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.ts +969 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@design.estate/dees-catalog",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.98.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
|
6
6
|
"main": "dist_ts_web/index.js",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@design.estate/dees-wcctools": "^3.9.2",
|
|
37
37
|
"@git.zone/tsbuild": "^4.4.2",
|
|
38
|
-
"@git.zone/tsbundle": "^2.11.
|
|
38
|
+
"@git.zone/tsbundle": "^2.11.3",
|
|
39
39
|
"@git.zone/tstest": "^3.6.6",
|
|
40
40
|
"@git.zone/tswatch": "^3.3.5",
|
|
41
41
|
"@push.rocks/projectinfo": "^5.1.0",
|
|
42
|
-
"@types/node": "^26.1.
|
|
42
|
+
"@types/node": "^26.1.1"
|
|
43
43
|
},
|
|
44
44
|
"files": [
|
|
45
45
|
"ts/**/*",
|
package/readme.hints.md
CHANGED
|
@@ -1114,3 +1114,38 @@ Some components appear in multiple groups via `demoGroups = ['Primary', 'Seconda
|
|
|
1114
1114
|
- Tool cards resolve their kind through `DeesHarnessToolRegistry.default` (open set + `kind` override), so consumers register custom tools without forking components; `server__tool` names auto-classify as MCP with annotation badges.
|
|
1115
1115
|
- The syntax palette moved to the shared `ts_web/elements/00syntax.ts` (`syntaxPaletteStyles` + `syntaxScopeStyles`); codebox and harness markdown consume it — don't re-declare `--syntax-*`/hljs scope colors per component.
|
|
1116
1116
|
- `dees-dataview-codebox` diff mode: set `codeBefore` (against `codeToDisplay`) or a pre-computed `unifiedDiff` patch; `diffView` = `'inline' | 'split'`. Engine lives dependency-free in `codebox.diff.ts` (LCS + intraline segments + context folding + `toUnifiedDiff`). Tool cards use it for file-write bodies via the `diffBlock` render helper.
|
|
1117
|
+
|
|
1118
|
+
## dees-simple-login multi-method rework (2026-07)
|
|
1119
|
+
|
|
1120
|
+
- **The catalog owns no WebAuthn protocol.** A passkey ceremony needs server-issued options and server-side verification, so `@simplewebauthn/browser` is deliberately *not* a dependency. Two mutually exclusive plug-in modes: no handler set → the component dispatches `passkey-login` / `passkey-register` / `provider-login` and stops; handler set (`passkeyLoginHandler` etc.) → the component awaits it, owns the busy state, turns a rejection into that method's error, and does **not** dispatch the request event. Suppressing the event in handler mode is what makes a double-started ceremony impossible — two concurrent `navigator.credentials.get()` calls reject each other.
|
|
1121
|
+
- **`login` is the one exception**: it is a notification, not a request, and always fires with dees-form's detail verbatim (`{ data: { username, password } }`) because cloudly, dcrouter and gitops depend on it.
|
|
1122
|
+
- **Configuration decides availability, `methodOrder` only reorders.** `passkey` (false), `providers` ([]), `password` (true) decide *whether* a method renders; `methodOrder` decides the sequence and appends anything it omits, so an incomplete order can never silently hide a configured method. Default therefore resolves to `['password']`, so the zero-configuration card is the same username/password form with the same API surface. It is **not** pixel-identical to the pre-rework component: three deliberate visual changes ship with it — the submit button is now card-width (the pre-existing `dees-tile dees-form-submit { width: 100% }` never worked because the host is `inline-block`), `.subheader` moved from `--dees-color-text-muted` (0.30 alpha, ~2:1) to `--dees-color-text-secondary`, and `.loginContainer` gained `box-sizing`, `padding` and `overflow-y: auto` so a tall multi-method card scrolls instead of clipping.
|
|
1123
|
+
- **Shadow-DOM reach-in is a real contract.** All three consumers do `shadowRoot.querySelector('dees-form')` then `form.setStatus()` / `form.reset()`, and cloudly's `switchToLoginContent()` hand-reverses the inline styles `switchToSlottedContent()` sets. So: `.loginContainer` / `.login` / `.slotContainer` must keep their names and inline-style choreography, the password `dees-form` must stay the first and only form in shadow order, and the `formData` listener is bound declaratively (`@formData`) because the form is now conditionally rendered and a one-shot `firstUpdated` hook-up would silently stop firing.
|
|
1124
|
+
- **Do not bind `.text` on the credentials `dees-form-submit`.** `dees-form.setStatus(state, text)` writes that property imperatively, and `dees-form.reset()` ends with `setStatus('normal', 'Submit')`. A Lit property binding would clobber a consumer's status text on the next render, so the label is slotted instead — which also means `labels.passwordSubmit` is read at first render only.
|
|
1125
|
+
- The credentials form is wrapped in `dees-tile` **only when password is the sole method**. Once the column holds other methods the column is the card, and a nested tile would inset the fields 24px against full-width buttons and read as a stray box.
|
|
1126
|
+
- Additive primitive gaps closed on the way: `dees-button.fullWidth` (`full-width` attribute — the host being `inline-block` meant `width: 100%` never stretched the `inline-flex` face; the opt-in path also adds `min-width: 0` + label ellipsis), the same forwarded on `dees-form-submit`, `dees-input-text.autocomplete` (needed for `username webauthn` conditional mediation), and `--dees-spinner-color` on `dees-spinner` (it hard-coded `--dees-color-text-primary`, painting a near-black arc on an accent button face in bright theme).
|
|
1127
|
+
- **Never mark a pending `dees-button` disabled** — `.button.disabled { opacity: 0.5 }` washes out the spinner. Guard re-entry in the handler instead; only *sibling* buttons get `disabled`.
|
|
1128
|
+
- Known pre-existing gaps this rework did **not** change: `--dees-color-text-secondary` is 3.44:1 in bright theme (sub-AA for 13px copy); `--dees-color-accent-primary` with white text is 3.65–4.02:1; `dees-tile`'s 16px heading padding vs the 24px content padding this component sets leaves an 8px misalignment; `dees-spinner`'s success/error faces still hard-code `--dees-color-text-primary`.
|
|
1129
|
+
|
|
1130
|
+
## dees-button keyboard operability (2026-07)
|
|
1131
|
+
|
|
1132
|
+
`dees-button` renders its face as a `<div class="button">`, so it had no keyboard affordance at all — nothing built on it (including every action in `dees-simple-login`) was reachable by Tab. Fixed in the primitive rather than per consumer.
|
|
1133
|
+
|
|
1134
|
+
- The face carries `role="button"`, `tabindex` (`0`, or `-1` when `disabled`), `aria-disabled`, and `aria-busy` while `status === 'pending'`. A pending button stays focusable on purpose — pulling focus mid-ceremony strands the user.
|
|
1135
|
+
- **Enter activates on keydown, Space on keyup**, matching native buttons. Space keydown is swallowed so the page cannot scroll under a focused button. Verified with real CDP key events: Space activates without moving a scrollable ancestor's `scrollTop`.
|
|
1136
|
+
- **Keyboard activation goes through a real `click()` on the face, not a direct `clicked` dispatch.** `dees-workspace-diff-editor` (5 call sites) and `profilepicture.modal` (2) bind plain `@click` on `dees-button` rather than `@clicked`; only a real click drives both. `pointer-events: none` does not block a programmatic `click()`, so inertness is enforced in `activateFromKeyboard()` — refusing `disabled` *and* any non-`normal` status, because the status faces already set `pointer-events: none` for the mouse.
|
|
1137
|
+
- `DeesButton.focus()` / `.blur()` forward to the face; without that, `host.focus()` is a silent no-op.
|
|
1138
|
+
- **`dees-form-submit.focus()` is NOT a focus method — it submits.** `dees-form.addBehaviours()` calls `getSubmitButton()?.focus()` to implement "Enter in the last field submits the form". An earlier attempt to make it forward focus properly broke that; it must stay as-is. Keyboard focus reaches the submit through the inner `dees-button` face instead. Pinned by a test.
|
|
1139
|
+
- The `:focus-visible` ring uses the **opaque** `--dees-color-accent-primary`, not `--dees-color-focus-ring`. That shared token is a 0.45/0.55 alpha and composites to 1.85:1 (bright) / 2.31:1 (dark) against the surface behind the button — under the 3:1 WCAG 2.2 SC 1.4.11 wants, and a navy hairline on a dark canvas. Raising the alpha on the token itself would fix every other control too and is the wider change.
|
|
1140
|
+
### Focus containment across the login/app crossfade
|
|
1141
|
+
|
|
1142
|
+
Both halves of `dees-simple-login` live in the DOM permanently, stacked and cross-faded with `opacity` + `pointer-events`. That was sufficient only while nothing inside them could hold focus. Once `dees-button` faces became focusable, `pointer-events: none` stopped containing anything — it does not remove a tab stop, and keyboard activation deliberately bypasses it — so the hidden half became reachable in both directions: Tab out of the password form into the invisible app shell, or Tab back into the invisible login card after signing in and start a passkey ceremony there.
|
|
1143
|
+
|
|
1144
|
+
`inert` is the only thing that removes a hidden-but-rendered subtree from focus and the accessibility tree. It is derived from each container's effective `pointer-events` via a `MutationObserver` on the inline styles, **not** from an internal flag, because consumers reverse the transition by hand: cloudly's `switchToLoginContent()` writes `.login` / `.loginContainer` / `.slotContainer` inline styles directly and knows nothing about `inert`. Keying off a flag it cannot reset would leave its login card permanently inert after a session expiry. Observe only `attributeFilter: ['style']` — `inert` reflects to its own attribute and would otherwise re-trigger the observer.
|
|
1145
|
+
|
|
1146
|
+
### Open follow-ups from this change (separate decisions, deliberately not done here)
|
|
1147
|
+
|
|
1148
|
+
- **`--dees-color-focus-ring` is still sub-3:1 for every control other than `dees-button`.** Inputs, dropdowns and the rest still use the shared token at 0.45/0.55 alpha, which composites to 1.85:1 (bright) / 2.31:1 (dark). `dees-button` was fixed locally by switching its ring to the opaque accent, so the catalog is now *inconsistent*: one control has a compliant focus indicator and the others do not. Raising the token's alpha is the wider fix and needs its own review across every control that reads it.
|
|
1149
|
+
- **`dees-button-exit` is unaudited and very likely has the same keyboard defect.** It renders a decorative X glyph (`.maincontainer` + two line divs) with no click handler, no `role` and no `tabindex`; consumers wire clicks on the host. It is not a `dees-button` consumer, so the keyboard fix does not reach it.
|
|
1150
|
+
|
|
1151
|
+
- Consumer audit: 16 non-demo components use `dees-button`. None wrap it in a `role="menu"/"listbox"/"option"/"tab"` container, none attach keyboard handlers to the button or an ancestor of it (`dees-harness-composer` and `dees-harness-question-card` bind keydown on their own `textarea`/`input`), `dees-modal` has no focus trap, `dees-button-group` only slots, and no component binds both `@click` and `@clicked` on the same button — so no double activation and no nested-role reporting change.
|
package/readme.md
CHANGED
|
@@ -90,10 +90,15 @@ button.text = 'Click me';
|
|
|
90
90
|
shape="pill" // Options: squircle (default rounded rect), pill (capsule for standalone actions)
|
|
91
91
|
status="pending" // Options: normal, pending, success, error
|
|
92
92
|
disabled={false} // Optional: disables the button
|
|
93
|
+
full-width // Optional: stretches the button face to the host width
|
|
93
94
|
@click=${handleClick}
|
|
94
95
|
>Click me</dees-button>
|
|
95
96
|
```
|
|
96
97
|
|
|
98
|
+
The button is keyboard operable: its face carries `role="button"` and `tabindex`, Enter and
|
|
99
|
+
Space activate it (dispatching a real `click`, so both `@click` and `@clicked` fire), a
|
|
100
|
+
disabled button leaves the tab order, and keyboard focus draws a `:focus-visible` ring.
|
|
101
|
+
|
|
97
102
|
#### `DeesBadge`
|
|
98
103
|
Display status indicators or counts with customizable styles.
|
|
99
104
|
|
|
@@ -157,12 +162,22 @@ Loading indicator with customizable appearance.
|
|
|
157
162
|
|
|
158
163
|
```typescript
|
|
159
164
|
<dees-spinner
|
|
160
|
-
size
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
.size=${20} // Optional: diameter in pixels (default 20)
|
|
166
|
+
.status=${'normal'} // Optional: 'normal' | 'pending' | 'success' | 'error'
|
|
167
|
+
.bnw=${true} // Optional: black-and-white treatment
|
|
163
168
|
></dees-spinner>
|
|
164
169
|
```
|
|
165
170
|
|
|
171
|
+
The arc colour follows the `--dees-spinner-color` custom property, falling back to
|
|
172
|
+
`--dees-color-text-primary`. Set it on an ancestor when the spinner sits on a coloured
|
|
173
|
+
surface — an accent button face, for example — so the arc matches that surface's foreground:
|
|
174
|
+
|
|
175
|
+
```css
|
|
176
|
+
dees-button {
|
|
177
|
+
--dees-spinner-color: var(--dees-color-on-accent);
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
166
181
|
#### `DeesToast`
|
|
167
182
|
Notification toast messages with various styles, positions, and auto-dismiss functionality.
|
|
168
183
|
|
|
@@ -325,6 +340,7 @@ Text input field with validation, info tooltips, description text, and context m
|
|
|
325
340
|
value="initial@value.com" // Initial value
|
|
326
341
|
required // Makes the field required
|
|
327
342
|
disabled // Disables the input
|
|
343
|
+
.autocomplete=${'username'} // Autofill hint; unset renders no autocomplete attribute
|
|
328
344
|
.infoText=${'Hover icon tooltip text'} // Shows ⓘ icon on label with hover tooltip
|
|
329
345
|
.description=${'Permanent help text below the input'} // Small text below the input
|
|
330
346
|
.validationFunction=${(value) => { // Auto-validates on every keystroke
|
|
@@ -711,9 +727,14 @@ Submit button component specifically designed for `DeesForm`.
|
|
|
711
727
|
<dees-form-submit
|
|
712
728
|
disabled // Optional: disable submit button
|
|
713
729
|
status="normal" // Options: normal, pending, success, error
|
|
730
|
+
full-width // Optional: stretches the button face to the host width
|
|
714
731
|
>Submit Form</dees-form-submit>
|
|
715
732
|
```
|
|
716
733
|
|
|
734
|
+
Note that `DeesFormSubmit.focus()` submits rather than focuses — `DeesForm` calls it to
|
|
735
|
+
implement "Enter in the last field submits". Keyboard focus reaches the submit through the
|
|
736
|
+
inner `DeesButton` face.
|
|
737
|
+
|
|
717
738
|
---
|
|
718
739
|
|
|
719
740
|
### App Shell (Layout) Components
|
|
@@ -1848,21 +1869,69 @@ Simple application dashboard component for quick prototyping.
|
|
|
1848
1869
|
```
|
|
1849
1870
|
|
|
1850
1871
|
#### `DeesSimpleLogin`
|
|
1851
|
-
|
|
1872
|
+
|
|
1873
|
+
Login surface for an application shell. Offers up to three authentication methods in one
|
|
1874
|
+
card — passkeys, identity providers ("sign in with …") and a username/password form — and
|
|
1875
|
+
renders the authenticated app through its default slot.
|
|
1876
|
+
|
|
1877
|
+
A method is offered only when it is **configured**, so the zero-configuration default is the
|
|
1878
|
+
password form alone:
|
|
1879
|
+
|
|
1880
|
+
```typescript
|
|
1881
|
+
<dees-simple-login name="My Application" @login=${handleLogin}>
|
|
1882
|
+
<!-- authenticated content -->
|
|
1883
|
+
</dees-simple-login>
|
|
1884
|
+
```
|
|
1885
|
+
|
|
1886
|
+
`login` fires with `detail.data.{username,password}`; call `switchToSlottedContent()` once
|
|
1887
|
+
credentials check out.
|
|
1888
|
+
|
|
1889
|
+
Turn on more methods and they render in the canonical order passkey → provider → password:
|
|
1852
1890
|
|
|
1853
1891
|
```typescript
|
|
1854
1892
|
<dees-simple-login
|
|
1855
|
-
.
|
|
1856
|
-
.
|
|
1857
|
-
.
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1893
|
+
name="login.idp.global"
|
|
1894
|
+
.passkey=${true}
|
|
1895
|
+
.providers=${[
|
|
1896
|
+
{ id: 'idp-global', label: 'idp.global', icon: 'lucide:shieldCheck' },
|
|
1897
|
+
{ id: 'workspace', label: 'Workspace SSO', icon: 'lucide:building2' },
|
|
1898
|
+
]}
|
|
1899
|
+
.passkeyLoginHandler=${async (context) => runWebAuthnCeremony(context)}
|
|
1900
|
+
.providerLoginHandler=${async (context) => redirectTo(context.providerId)}
|
|
1861
1901
|
@login=${handleLogin}
|
|
1862
|
-
@forgot-password=${handleForgotPassword}
|
|
1863
1902
|
></dees-simple-login>
|
|
1864
1903
|
```
|
|
1865
1904
|
|
|
1905
|
+
**Configuration** — `passkey` (default `false`), `providers` (default `[]`), `password`
|
|
1906
|
+
(default `true`), `passkeyIntents` (`['authenticate']`, add `'register'` to offer
|
|
1907
|
+
enrollment), `methodOrder`, `labels`, `passkeyAvailable` (override capability detection),
|
|
1908
|
+
`passkeyAutofill` (WebAuthn conditional mediation — inert unless `passkeyLoginHandler` is set,
|
|
1909
|
+
the password method renders a username field, and the browser reports conditional mediation).
|
|
1910
|
+
|
|
1911
|
+
**Who owns the WebAuthn ceremony** — not this component. A ceremony needs server-issued
|
|
1912
|
+
options and server-side verification, so the catalog carries no WebAuthn dependency. Plug
|
|
1913
|
+
one in either way, never both for the same interaction:
|
|
1914
|
+
|
|
1915
|
+
- **Event mode** (no handler set) — the component dispatches `passkey-login`,
|
|
1916
|
+
`passkey-register` or `provider-login` and stops. Report progress back with `setBusy()` /
|
|
1917
|
+
`reportError()`.
|
|
1918
|
+
- **Handler mode** (`passkeyLoginHandler`, `passkeyRegisterHandler`, `providerLoginHandler`,
|
|
1919
|
+
`passwordLoginHandler`) — the component awaits the handler, owns that method's busy state
|
|
1920
|
+
and turns a rejection into that method's error message. The request event is not
|
|
1921
|
+
dispatched, so a ceremony can never start twice.
|
|
1922
|
+
|
|
1923
|
+
`login` is a notification rather than a request and always fires, even in handler mode.
|
|
1924
|
+
|
|
1925
|
+
Busy and error state is independent per method, per passkey intent and per provider, so a
|
|
1926
|
+
failed passkey attempt never blanks the password form. `reportError(target, message)`,
|
|
1927
|
+
`setBusy(target, busy)`, `isBusy(target)` and `getError(target)` each take either a
|
|
1928
|
+
`TDeesLoginMethod` or an `IDeesLoginTarget` (`{ method, intent?, providerId? }`).
|
|
1929
|
+
`clearError(target?)` takes an optional target and clears every error when called without
|
|
1930
|
+
one; `reset()` takes no arguments and clears all busy and error state plus the password form.
|
|
1931
|
+
|
|
1932
|
+
Provider icons are always consumer-supplied Lucide names — the catalog ships no third-party
|
|
1933
|
+
brand assets.
|
|
1934
|
+
|
|
1866
1935
|
---
|
|
1867
1936
|
|
|
1868
1937
|
### Shopping Components
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@design.estate/dees-catalog',
|
|
6
|
-
version: '3.
|
|
6
|
+
version: '3.98.1',
|
|
7
7
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
|
8
8
|
}
|
|
@@ -78,6 +78,15 @@ export class DeesButton extends DeesElement {
|
|
|
78
78
|
})
|
|
79
79
|
accessor insideForm: boolean = false;
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* stretches the inner button face to the host width.
|
|
83
|
+
* The host is `inline-block`, so `width: 100%` on the host alone leaves the
|
|
84
|
+
* `inline-flex` face shrink-wrapped. Opt in for stacked, full-bleed actions
|
|
85
|
+
* (login cards, sheets, empty states).
|
|
86
|
+
*/
|
|
87
|
+
@property({ type: Boolean, reflect: true, attribute: 'full-width' })
|
|
88
|
+
accessor fullWidth: boolean = false;
|
|
89
|
+
|
|
81
90
|
@property({ type: String, reflect: true })
|
|
82
91
|
accessor icon!: string;
|
|
83
92
|
|
|
@@ -124,6 +133,26 @@ export class DeesButton extends DeesElement {
|
|
|
124
133
|
display: none;
|
|
125
134
|
}
|
|
126
135
|
|
|
136
|
+
:host([full-width]) {
|
|
137
|
+
display: block;
|
|
138
|
+
width: 100%;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* A full-width button is sized by its container, so its label has to yield rather
|
|
142
|
+
than spill out of the face when the container is narrower than the text. */
|
|
143
|
+
:host([full-width]) .button {
|
|
144
|
+
width: 100%;
|
|
145
|
+
min-width: 0;
|
|
146
|
+
overflow: hidden;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
:host([full-width]) .textbox {
|
|
150
|
+
min-width: 0;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
text-overflow: ellipsis;
|
|
153
|
+
white-space: nowrap;
|
|
154
|
+
}
|
|
155
|
+
|
|
127
156
|
/* Form spacing styles */
|
|
128
157
|
:host([inside-form]) {
|
|
129
158
|
margin-bottom: var(--dees-spacing-lg);
|
|
@@ -363,10 +392,30 @@ export class DeesButton extends DeesElement {
|
|
|
363
392
|
display: none;
|
|
364
393
|
}
|
|
365
394
|
|
|
366
|
-
/* Focus state
|
|
395
|
+
/* Focus state.
|
|
396
|
+
The base .button rule sets outline:none for the pointer path; the ring is restored
|
|
397
|
+
here for :focus-visible only, so a mouse press never leaves a ring behind while
|
|
398
|
+
keyboard focus is always visible. This rule is the focus affordance keyboard users
|
|
399
|
+
actually see — do not remove it alongside the base outline:none. */
|
|
367
400
|
.button:focus-visible {
|
|
368
|
-
|
|
401
|
+
/* Deliberately NOT --dees-color-focus-ring: that token is a 0.45/0.55 alpha, which
|
|
402
|
+
composites to 1.85:1 (bright) and 2.31:1 (dark) against the surface behind the
|
|
403
|
+
button — under the 3:1 that WCAG 2.2 SC 1.4.11 requires of a focus indicator, and
|
|
404
|
+
effectively invisible as a navy hairline on a dark canvas. The opaque accent
|
|
405
|
+
measures 4.02:1 bright / 5.76:1 dark. Raising the alpha on the shared token would
|
|
406
|
+
fix every other control too and is the wider change. */
|
|
407
|
+
outline: 2px solid var(--dees-color-accent-primary);
|
|
369
408
|
outline-offset: 2px;
|
|
409
|
+
/* The indicator has to appear at once. The base .button rule carries
|
|
410
|
+
transition:all, which animates outline-color up from its initial currentColor —
|
|
411
|
+
measured as ~50ms of a near-white ring on a bright accent face, i.e. briefly
|
|
412
|
+
invisible right when the user needs it. Only the focused state opts out. */
|
|
413
|
+
transition: none;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* A disabled face is removed from the tab order, so it should never show a ring. */
|
|
417
|
+
.button.disabled:focus-visible {
|
|
418
|
+
outline: none;
|
|
370
419
|
}
|
|
371
420
|
|
|
372
421
|
/* Loading spinner */
|
|
@@ -488,7 +537,14 @@ export class DeesButton extends DeesElement {
|
|
|
488
537
|
class="button ${this.isHidden ? 'hidden' : ''} ${actualType} size-${actualSize} shape-${this.shape} ${this.status} ${this.disabled
|
|
489
538
|
? 'disabled'
|
|
490
539
|
: ''}"
|
|
540
|
+
role="button"
|
|
541
|
+
tabindex=${this.disabled ? -1 : 0}
|
|
542
|
+
aria-disabled=${this.disabled ? 'true' : 'false'}
|
|
543
|
+
aria-busy=${this.status === 'pending' ? 'true' : 'false'}
|
|
544
|
+
.ariaLabel=${showText ? null : this.text || null}
|
|
491
545
|
@click="${this.dispatchClick}"
|
|
546
|
+
@keydown=${this.handleKeydown}
|
|
547
|
+
@keyup=${this.handleKeyup}
|
|
492
548
|
>
|
|
493
549
|
${this.status === 'normal' ? html``: html`
|
|
494
550
|
<dees-spinner
|
|
@@ -519,6 +575,76 @@ export class DeesButton extends DeesElement {
|
|
|
519
575
|
);
|
|
520
576
|
}
|
|
521
577
|
|
|
578
|
+
/** the inner face — the element that actually carries focus */
|
|
579
|
+
private get faceElement(): HTMLElement | null {
|
|
580
|
+
return (this.shadowRoot?.querySelector('.button') as HTMLElement | null) || null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* States in which the button must not react to input. `disabled` is obvious; the status
|
|
585
|
+
* faces matter too, because `.button.pending / .success / .error` already set
|
|
586
|
+
* `pointer-events: none` and keyboard activation has to refuse the same interactions a
|
|
587
|
+
* mouse cannot make.
|
|
588
|
+
*/
|
|
589
|
+
private get isInert(): boolean {
|
|
590
|
+
return this.disabled || this.status !== 'normal';
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Focus lives on the inner face inside the shadow root, so `host.focus()` has to forward
|
|
595
|
+
* or it is a silent no-op. Note that `dees-form-submit.focus()` is deliberately *not*
|
|
596
|
+
* wired to this — that method submits the form, and dees-form depends on it.
|
|
597
|
+
*/
|
|
598
|
+
public focus(optionsArg?: FocusOptions): void {
|
|
599
|
+
const face = this.faceElement;
|
|
600
|
+
if (face) {
|
|
601
|
+
face.focus(optionsArg);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
super.focus(optionsArg);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
public blur(): void {
|
|
608
|
+
this.faceElement?.blur();
|
|
609
|
+
super.blur();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private handleKeydown = (eventArg: KeyboardEvent): void => {
|
|
613
|
+
if (eventArg.key === ' ' || eventArg.key === 'Spacebar') {
|
|
614
|
+
// Native buttons activate Space on keyup. Swallow the keydown so the page cannot
|
|
615
|
+
// scroll underneath the focused button.
|
|
616
|
+
eventArg.preventDefault();
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (eventArg.key === 'Enter') {
|
|
620
|
+
eventArg.preventDefault();
|
|
621
|
+
this.activateFromKeyboard();
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
private handleKeyup = (eventArg: KeyboardEvent): void => {
|
|
626
|
+
if (eventArg.key === ' ' || eventArg.key === 'Spacebar') {
|
|
627
|
+
eventArg.preventDefault();
|
|
628
|
+
this.activateFromKeyboard();
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Keyboard activation goes through a real click rather than dispatching `clicked`
|
|
634
|
+
* directly, exactly as a native button does. Several catalog components bind `@click` on
|
|
635
|
+
* dees-button instead of `@clicked` (dees-workspace-diff-editor, the profile picture
|
|
636
|
+
* modal), and both listener styles have to keep working from the keyboard.
|
|
637
|
+
*
|
|
638
|
+
* `pointer-events: none` does not block a programmatic `click()`, so inertness is
|
|
639
|
+
* enforced here rather than left to CSS.
|
|
640
|
+
*/
|
|
641
|
+
private activateFromKeyboard(): void {
|
|
642
|
+
if (this.isInert) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
this.faceElement?.click();
|
|
646
|
+
}
|
|
647
|
+
|
|
522
648
|
public async firstUpdated() {
|
|
523
649
|
// Extract light DOM content (icon + text) and set as properties
|
|
524
650
|
this.extractLightDom();
|