@repobit/dex-system-design 1.0.1 → 2.0.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/CHANGELOG.md +89 -0
- package/README.md +43 -12
- package/package.json +6 -4
- package/schemas/component-manifest.json +207 -49
- package/schemas/manifest-omissions.json +2 -0
- package/src/components/accordion/accordion.css.js +12 -1
- package/src/components/agent-endpoints/agent-endpoints.js +1 -1
- package/src/components/alert/alert.css.js +22 -1
- package/src/components/anchor/anchor-nav.css.js +6 -1
- package/src/components/annotation/annotation.css.js +6 -1
- package/src/components/architecture.stories.js +6 -4
- package/src/components/avatar/avatar.js +37 -11
- package/src/components/avatar/avatar.stories.js +10 -6
- package/src/components/awards/awards-flex.css.js +31 -129
- package/src/components/awards/awards-flex.js +66 -116
- package/src/components/awards/awards-flex.stories.js +8 -6
- package/src/components/back/back.css.js +6 -1
- package/src/components/back/back.js +24 -12
- package/src/components/back/back.stories.js +3 -3
- package/src/components/badge/badge.js +6 -3
- package/src/components/badge/badge.stories.js +13 -9
- package/src/components/bento-grid/bento-grid.js +1 -1
- package/src/components/blockquote/blockquote.css.js +12 -3
- package/src/components/blockquote/blockquote.js +12 -5
- package/src/components/blockquote/blockquote.stories.js +6 -4
- package/src/components/button/button.css.js +46 -25
- package/src/components/button/button.js +44 -43
- package/src/components/button/button.stories.js +2 -2
- package/src/components/caption/caption.css.js +10 -0
- package/src/components/caption/caption.stories.js +4 -4
- package/src/components/carousel/carousel.css.js +19 -116
- package/src/components/carousel/carousel.js +70 -74
- package/src/components/carousel/carousel.stories.js +9 -8
- package/src/components/checkbox/checkbox.css.js +6 -1
- package/src/components/code/code.css.js +6 -1
- package/src/components/code/code.js +48 -7
- package/src/components/code/code.stories.js +7 -4
- package/src/components/divider/divider-horizontal.js +11 -3
- package/src/components/divider/divider-vertical.js +12 -5
- package/src/components/divider/divider.css.js +4 -1
- package/src/components/divider/divider.stories.js +6 -6
- package/src/components/dropdown/dropdown.js +67 -13
- package/src/components/dropdown/dropdown.stories.js +15 -14
- package/src/components/fieldset/fieldset.stories.js +41 -16
- package/src/components/footer/footer-nav-menu.css.js +6 -1
- package/src/components/footer/footer.css.js +6 -1
- package/src/components/header/header-code.js +1 -1
- package/src/components/header/header.css.js +9 -4
- package/src/components/heading/heading.stories.js +64 -31
- package/src/components/highlight/highlight.js +74 -34
- package/src/components/highlight/highlight.stories.js +5 -4
- package/src/components/icons/icon-element.js +49 -6
- package/src/components/icons/icons.stories.js +7 -0
- package/src/components/image/image.css.js +9 -98
- package/src/components/image/image.js +7 -2
- package/src/components/input/custom-form.stories.js +14 -19
- package/src/components/input/input-clipboard.css.js +6 -1
- package/src/components/input/input-clipboard.js +56 -8
- package/src/components/input/input.js +19 -2
- package/src/components/label/label.js +87 -1
- package/src/components/label/label.stories.js +21 -12
- package/src/components/link/link.css.js +52 -3
- package/src/components/link/link.js +52 -17
- package/src/components/link/link.stories.js +114 -3
- package/src/components/list/list-item/list-item.css.js +4 -6
- package/src/components/list/list-item/list-item.js +53 -5
- package/src/components/list/list-item/list-item.stories.js +22 -0
- package/src/components/list/list.js +27 -0
- package/src/components/modal/modal.css.js +6 -1
- package/src/components/pagination-dots/pagination-dots.css.js +161 -0
- package/src/components/pagination-dots/pagination-dots.js +202 -0
- package/src/components/pagination-dots/pagination-dots.stories.js +171 -0
- package/src/components/paragraph/paragraph.css.js +37 -1
- package/src/components/paragraph/paragraph.js +19 -2
- package/src/components/paragraph/paragraph.stories.js +40 -6
- package/src/components/picture/picture.css.js +8 -67
- package/src/components/picture/picture.js +15 -8
- package/src/components/pricing-cards/pricing-card.css.js +12 -2
- package/src/components/pricing-cards/pricing-card.js +4 -4
- package/src/components/progress/progress.js +24 -4
- package/src/components/progress/progress.stories.js +8 -6
- package/src/components/radio/radio.css.js +6 -1
- package/src/components/radio/radio.js +48 -3
- package/src/components/radio/radio.stories.js +1 -1
- package/src/components/shared/form-field.css.js +30 -4
- package/src/components/shared/token-scales.js +16 -0
- package/src/components/spinner/spinner.css.js +31 -4
- package/src/components/spinner/spinner.js +26 -4
- package/src/components/spinner/spinner.stories.js +10 -7
- package/src/components/switch/switch.css.js +21 -3
- package/src/components/switch/switch.js +8 -1
- package/src/components/switch/switch.stories.js +2 -1
- package/src/components/tab-list/tab-list.css.js +6 -1
- package/src/components/terminal/terminal-cursor.js +11 -0
- package/src/components/terminal/terminal-message.js +1 -1
- package/src/components/termsOfUse/terms.css.js +6 -1
- package/src/components/textarea/textarea.stories.js +23 -0
- package/src/components/tooltip/tooltip.css.js +24 -1
- package/src/components/tooltip/tooltip.js +135 -5
- package/src/components/tooltip/tooltip.stories.js +20 -14
- package/src/components/trust-panel/trust-panel.js +2 -2
- package/src/components/video/video.js +38 -3
- package/src/components/video/video.stories.js +10 -5
- package/src/components/video-player/video-player.css.js +6 -1
- package/src/tokens/elevation.stories.js +2 -3
- package/src/tokens/focus.stories.js +81 -15
- package/src/tokens/motion.stories.js +0 -6
- package/src/tokens/tokens.css +174 -97
- package/src/tokens/tokens.js +174 -97
- package/src/tokens/vertical-rhythm.stories.js +1 -1
- package/src/stories/Button.js +0 -20
- package/src/stories/Button.stories.js +0 -46
- package/src/stories/Configure.mdx +0 -364
- package/src/stories/Header.js +0 -45
- package/src/stories/Header.stories.js +0 -24
- package/src/stories/Page.js +0 -62
- package/src/stories/Page.stories.js +0 -20
- package/src/stories/assets/accessibility.png +0 -0
- package/src/stories/assets/accessibility.svg +0 -1
- package/src/stories/assets/addon-library.png +0 -0
- package/src/stories/assets/assets.png +0 -0
- package/src/stories/assets/avif-test-image.avif +0 -0
- package/src/stories/assets/context.png +0 -0
- package/src/stories/assets/discord.svg +0 -1
- package/src/stories/assets/docs.png +0 -0
- package/src/stories/assets/figma-plugin.png +0 -0
- package/src/stories/assets/github.svg +0 -1
- package/src/stories/assets/share.png +0 -0
- package/src/stories/assets/styling.png +0 -0
- package/src/stories/assets/testing.png +0 -0
- package/src/stories/assets/theming.png +0 -0
- package/src/stories/assets/tutorials.svg +0 -1
- package/src/stories/assets/youtube.svg +0 -1
- package/src/stories/button.css +0 -30
- package/src/stories/header.css +0 -32
- package/src/stories/page.css +0 -68
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
import { LitElement, html } from 'lit';
|
|
1
|
+
import { LitElement, html, nothing } from 'lit';
|
|
2
2
|
// Installs the design tokens into document scope; see src/tokens/tokens.js
|
|
3
3
|
import '../../tokens/tokens.js';
|
|
4
4
|
import { tooltipCSS } from './tooltip.css.js';
|
|
5
5
|
|
|
6
|
+
let descriptionIds = 0;
|
|
7
|
+
|
|
6
8
|
export class BdTooltip extends LitElement {
|
|
7
9
|
static styles = [tooltipCSS];
|
|
8
10
|
|
|
9
11
|
static properties = {
|
|
10
|
-
text: { type: String }
|
|
12
|
+
text : { type: String },
|
|
13
|
+
/* Set by Escape and cleared once neither the pointer nor focus is on the
|
|
14
|
+
tooltip any more, so the next hover or focus shows it again. */
|
|
15
|
+
_dismissed: { type: Boolean, state: true }
|
|
11
16
|
};
|
|
12
17
|
|
|
13
18
|
constructor() {
|
|
14
19
|
super();
|
|
15
20
|
this.text = '';
|
|
21
|
+
this._dismissed = false;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
/* The bubble is centred on its trigger and is wider than it, so near a
|
|
@@ -119,7 +125,31 @@ export class BdTooltip extends LitElement {
|
|
|
119
125
|
Both boxes, because they move for different reasons: the host changes
|
|
120
126
|
when the trigger resizes, the bubble when its own text reflows.
|
|
121
127
|
ResizeObserver fires once on observe, which is the settled first pass. */
|
|
122
|
-
|
|
128
|
+
this._observe();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ESCAPE. WCAG 1.4.13 asks that content shown on hover or focus can be
|
|
132
|
+
dismissed without moving the pointer or focus. The pointer may be over the
|
|
133
|
+
trigger while focus is anywhere on the page, so the key is heard on the
|
|
134
|
+
document, and only acted on while this bubble is actually showing.
|
|
135
|
+
|
|
136
|
+
Capture phase, and the event is marked handled with preventDefault rather
|
|
137
|
+
than stopped: a tooltip is the innermost thing that can be open, so its
|
|
138
|
+
Escape should not also close a native <dialog> it sits in, and a dialog's
|
|
139
|
+
cancel is exactly what preventDefault on that keydown withholds.
|
|
140
|
+
Propagation is left alone, as bd-modal leaves it, so a page that wants to
|
|
141
|
+
hear the key still does. */
|
|
142
|
+
get _showing() {
|
|
143
|
+
return !this._dismissed && (this.matches(':hover') || this.matches(':focus-within'));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Started on first render and again on every reconnect. It used to start
|
|
147
|
+
only in firstUpdated, and disconnectedCallback tears it down, so a tooltip
|
|
148
|
+
moved in the DOM -- removed and re-inserted, which a framework reordering
|
|
149
|
+
a list does -- came back with no observer and never re-placed itself when
|
|
150
|
+
its trigger or text changed size. */
|
|
151
|
+
_observe() {
|
|
152
|
+
if (typeof ResizeObserver === 'undefined' || this._ro) return;
|
|
123
153
|
this._ro = new ResizeObserver(() => this._place());
|
|
124
154
|
this._ro.observe(this);
|
|
125
155
|
const bubble = this.renderRoot?.querySelector('.bubble');
|
|
@@ -128,27 +158,127 @@ export class BdTooltip extends LitElement {
|
|
|
128
158
|
|
|
129
159
|
connectedCallback() {
|
|
130
160
|
super.connectedCallback();
|
|
161
|
+
// a first connection is handled by firstUpdated, once the bubble exists
|
|
162
|
+
if (this.hasUpdated) this._observe();
|
|
131
163
|
this._onShow = () => this._place();
|
|
132
164
|
/* Both reveals the stylesheet already honours, and resize because the
|
|
133
165
|
measurement is only true for the width it was taken at. */
|
|
134
166
|
this.addEventListener('mouseenter', this._onShow);
|
|
135
167
|
this.addEventListener('focusin', this._onShow);
|
|
136
168
|
window.addEventListener('resize', this._onShow);
|
|
169
|
+
|
|
170
|
+
this._onKeydown = e => {
|
|
171
|
+
if (e.key !== 'Escape' || e.defaultPrevented || !this._showing) return;
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
this._dismissed = true;
|
|
174
|
+
};
|
|
175
|
+
/* Back to normal once the pointer and focus have both gone: a pointer
|
|
176
|
+
leaving while focus stays inside keeps it dismissed, and the reverse. */
|
|
177
|
+
this._onLeave = e => {
|
|
178
|
+
if (!this._dismissed) return;
|
|
179
|
+
const pointerGone = e.type === 'mouseleave' || !this.matches(':hover');
|
|
180
|
+
const focusGone = e.type === 'focusout'
|
|
181
|
+
? !this.contains(e.relatedTarget) && !this.shadowRoot.contains(e.relatedTarget)
|
|
182
|
+
: !this.matches(':focus-within');
|
|
183
|
+
if (pointerGone && focusGone) this._dismissed = false;
|
|
184
|
+
};
|
|
185
|
+
document.addEventListener('keydown', this._onKeydown, true);
|
|
186
|
+
this.addEventListener('mouseleave', this._onLeave);
|
|
187
|
+
this.addEventListener('focusout', this._onLeave);
|
|
137
188
|
}
|
|
138
189
|
|
|
139
190
|
disconnectedCallback() {
|
|
140
191
|
this.removeEventListener('mouseenter', this._onShow);
|
|
141
192
|
this.removeEventListener('focusin', this._onShow);
|
|
142
193
|
window.removeEventListener('resize', this._onShow);
|
|
194
|
+
document.removeEventListener('keydown', this._onKeydown, true);
|
|
195
|
+
this.removeEventListener('mouseleave', this._onLeave);
|
|
196
|
+
this.removeEventListener('focusout', this._onLeave);
|
|
143
197
|
this._ro?.disconnect();
|
|
144
198
|
this._ro = undefined;
|
|
145
199
|
super.disconnectedCallback();
|
|
146
200
|
}
|
|
147
201
|
|
|
202
|
+
/* DESCRIBING THE TRIGGER.
|
|
203
|
+
The bubble had role="tooltip" and nothing pointed at it, so a screen
|
|
204
|
+
reader landing on the trigger heard its name and not the tooltip, and
|
|
205
|
+
the bubble's text sat in the page as a stray line. It cannot be pointed
|
|
206
|
+
at directly: the bubble is in this shadow root and the trigger is not,
|
|
207
|
+
and an id reference never crosses that boundary. So the trigger is
|
|
208
|
+
described from its own side:
|
|
209
|
+
|
|
210
|
+
a native control aria-describedby names a hidden copy of the text,
|
|
211
|
+
kept as a child of this element in a hidden slot --
|
|
212
|
+
the same tree as the trigger, so the reference
|
|
213
|
+
resolves
|
|
214
|
+
bd-button, the text goes to its description property, which
|
|
215
|
+
bd-button-link, wires aria-describedby inside its own shadow root,
|
|
216
|
+
bd-link where its real control is
|
|
217
|
+
|
|
218
|
+
Once described, the bubble is hidden from assistive technology so the
|
|
219
|
+
text is not read twice. Any other custom element as trigger keeps the
|
|
220
|
+
bubble exposed and goes undescribed -- its control is out of reach. */
|
|
221
|
+
_describe() {
|
|
222
|
+
this._undescribe();
|
|
223
|
+
const slot = this.shadowRoot?.querySelector('slot');
|
|
224
|
+
const trigger = slot?.assignedElements()[0];
|
|
225
|
+
if (!trigger || !this.text) return;
|
|
226
|
+
|
|
227
|
+
// a custom element that has not upgraded yet has no description to set:
|
|
228
|
+
// come back once it has
|
|
229
|
+
if (trigger.localName.includes('-') && !customElements.get(trigger.localName)) {
|
|
230
|
+
customElements.whenDefined(trigger.localName).then(() => this._describe());
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if ('description' in trigger && trigger.localName.includes('-')) {
|
|
235
|
+
trigger.description = this.text;
|
|
236
|
+
this._described = { trigger, kind: 'property' };
|
|
237
|
+
} else if (!trigger.localName.includes('-')) {
|
|
238
|
+
const node = document.createElement('span');
|
|
239
|
+
node.id = `bd-tooltip-description-${++descriptionIds}`;
|
|
240
|
+
node.slot = 'description';
|
|
241
|
+
node.hidden = true;
|
|
242
|
+
node.textContent = this.text;
|
|
243
|
+
this.append(node);
|
|
244
|
+
const ids = (trigger.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
|
|
245
|
+
trigger.setAttribute('aria-describedby', [...ids, node.id].join(' '));
|
|
246
|
+
this._described = { trigger, kind: 'idref', node };
|
|
247
|
+
}
|
|
248
|
+
this.requestUpdate();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_undescribe() {
|
|
252
|
+
const d = this._described;
|
|
253
|
+
if (!d) return;
|
|
254
|
+
if (d.kind === 'property') {
|
|
255
|
+
d.trigger.description = '';
|
|
256
|
+
} else {
|
|
257
|
+
const ids = (d.trigger.getAttribute('aria-describedby') || '').split(/\s+/).filter(t => t && t !== d.node.id);
|
|
258
|
+
if (ids.length) d.trigger.setAttribute('aria-describedby', ids.join(' '));
|
|
259
|
+
else d.trigger.removeAttribute('aria-describedby');
|
|
260
|
+
d.node.remove();
|
|
261
|
+
}
|
|
262
|
+
this._described = null;
|
|
263
|
+
this.requestUpdate();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
updated(changed) {
|
|
267
|
+
if (changed.has('text') && this.hasUpdated) this._describe();
|
|
268
|
+
}
|
|
269
|
+
|
|
148
270
|
render() {
|
|
149
271
|
return html`
|
|
150
|
-
<slot></slot>
|
|
151
|
-
|
|
272
|
+
<slot @slotchange=${() => this._describe()}></slot>
|
|
273
|
+
<!-- the trigger's description: hidden, but slotted, because a node
|
|
274
|
+
assigned to no slot is not in the flat tree and Chrome then drops
|
|
275
|
+
an aria-describedby that points at it -->
|
|
276
|
+
<span hidden><slot name="description"></slot></span>
|
|
277
|
+
<span
|
|
278
|
+
class="bubble ${this._dismissed ? 'bubble--dismissed' : ''}"
|
|
279
|
+
role="tooltip"
|
|
280
|
+
aria-hidden="${this._described ? 'true' : nothing}"
|
|
281
|
+
>${this.text}</span>
|
|
152
282
|
`;
|
|
153
283
|
}
|
|
154
284
|
}
|
|
@@ -33,23 +33,29 @@ Anything required to complete a task belongs in visible text or in
|
|
|
33
33
|
\`<bd-help-text>\`, because a tooltip is unreachable on touch, invisible in
|
|
34
34
|
print, and gone the moment the pointer leaves.
|
|
35
35
|
|
|
36
|
-
### Accessibility
|
|
37
|
-
The
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
may never be announced.
|
|
36
|
+
### Accessibility
|
|
37
|
+
The trigger is described by the tooltip: a screen reader reads the \`text\`
|
|
38
|
+
after the trigger's name. How depends on the trigger, because the bubble sits
|
|
39
|
+
in this element's shadow root and an \`aria-describedby\` cannot point into it:
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
- **a native control** — \`<button>\`, \`<a>\`, \`<input>\` — gets
|
|
42
|
+
\`aria-describedby\` naming a hidden copy of the text kept inside the
|
|
43
|
+
tooltip, added to any ids it already lists and removed when it leaves;
|
|
44
|
+
- **\`bd-button\`, \`bd-button-link\` and \`bd-link\`** get the text through
|
|
45
|
+
their \`description\` property, which wires the description inside their own
|
|
46
|
+
shadow root, where the real control is;
|
|
47
|
+
- **any other custom element** cannot be reached, so it is not described and
|
|
48
|
+
the bubble stays exposed instead. Prefer a native control, a
|
|
49
|
+
\`bd-button\` or a \`bd-link\` as the trigger.
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<bd-button aria-describedby="trackers-hint">What is this?</bd-button>
|
|
47
|
-
</bd-tooltip>
|
|
48
|
-
<span id="trackers-hint" hidden>Blocks trackers across every browser</span>
|
|
49
|
-
\`\`\`
|
|
51
|
+
Once the trigger is described, the bubble itself is hidden from assistive
|
|
52
|
+
technology, so the text is not read twice.
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
The bubble meets WCAG 1.4.13. It is hoverable: the pointer can move from the
|
|
55
|
+
trigger onto the bubble, across the gap between them, and it stays open. It is
|
|
56
|
+
dismissible: Escape hides it without moving the pointer or focus, and it shows
|
|
57
|
+
again on the next hover or focus. That Escape is marked handled, so a native
|
|
58
|
+
\`<dialog>\` the tooltip sits in stays open on the same keypress.
|
|
53
59
|
`
|
|
54
60
|
}
|
|
55
61
|
}
|
|
@@ -36,7 +36,7 @@ class TrustPanel extends LitElement {
|
|
|
36
36
|
return html`
|
|
37
37
|
<div class="bd-trustpanel-card">
|
|
38
38
|
${card.eyebrow
|
|
39
|
-
? html`<bd-p variant="
|
|
39
|
+
? html`<bd-p variant="eyebrow" class="bd-trustpanel-card-eyebrow">${card.eyebrow}</bd-p>`
|
|
40
40
|
: nothing}
|
|
41
41
|
|
|
42
42
|
<div class="bd-trustpanel-card-logos">
|
|
@@ -66,7 +66,7 @@ class TrustPanel extends LitElement {
|
|
|
66
66
|
return html`
|
|
67
67
|
<div class="bd-ensemble bd-trustpanel-wrapper">
|
|
68
68
|
${this.eyebrow
|
|
69
|
-
? html`<bd-p variant="
|
|
69
|
+
? html`<bd-p variant="eyebrow" class="bd-trustpanel-eyebrow"><slot name="eyebrow">${this.eyebrow}</slot></bd-p>`
|
|
70
70
|
: nothing}
|
|
71
71
|
|
|
72
72
|
${this.title
|
|
@@ -9,9 +9,13 @@ export class BdVideo extends LitElement {
|
|
|
9
9
|
static properties = {
|
|
10
10
|
src : { type: String },
|
|
11
11
|
poster : { type: String },
|
|
12
|
-
|
|
12
|
+
/* On unless told otherwise -- see the story's Notes. A default-on boolean
|
|
13
|
+
cannot be turned off by leaving the attribute out, so the attribute
|
|
14
|
+
reads controls="false" as off; the property takes false as it is. */
|
|
15
|
+
controls: { converter: { fromAttribute: value => value !== 'false' } },
|
|
13
16
|
muted : { type: Boolean },
|
|
14
|
-
loop : { type: Boolean }
|
|
17
|
+
loop : { type: Boolean },
|
|
18
|
+
_media : { state: true }
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
constructor() {
|
|
@@ -21,8 +25,38 @@ export class BdVideo extends LitElement {
|
|
|
21
25
|
this.controls = true;
|
|
22
26
|
this.muted = false;
|
|
23
27
|
this.loop = false;
|
|
28
|
+
this._media = [];
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
/* WHY THE TRACKS ARE COPIED IN.
|
|
32
|
+
This rendered <video><slot></slot></video>, and a media element builds
|
|
33
|
+
its text tracks and sources only from real <track> and <source> children
|
|
34
|
+
-- a slot is neither, so the captions the story told readers to slot in
|
|
35
|
+
never reached the player. The <track> and <source> children are now
|
|
36
|
+
copied into the <video> itself, and copied again whenever they change. */
|
|
37
|
+
connectedCallback() {
|
|
38
|
+
super.connectedCallback();
|
|
39
|
+
this._collect();
|
|
40
|
+
this._observer = new MutationObserver(() => this._collect());
|
|
41
|
+
this._observer.observe(this, { childList: true, subtree: true, attributes: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
disconnectedCallback() {
|
|
45
|
+
this._observer?.disconnect();
|
|
46
|
+
super.disconnectedCallback();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_collect() {
|
|
50
|
+
this._media = [...this.children]
|
|
51
|
+
.filter(el => el.localName === 'track' || el.localName === 'source')
|
|
52
|
+
.map(el => el.cloneNode(true));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* muted is a property, not only an attribute. The attribute is read once,
|
|
56
|
+
when the media loads -- it is what lets a muted video autoplay -- and
|
|
57
|
+
changing it afterwards mutes nothing, so muted = true on a playing video
|
|
58
|
+
left the sound on. Both are set: the attribute for the load, the
|
|
59
|
+
property for every change after it. */
|
|
26
60
|
render() {
|
|
27
61
|
return html`
|
|
28
62
|
<video
|
|
@@ -30,9 +64,10 @@ export class BdVideo extends LitElement {
|
|
|
30
64
|
poster=${this.poster || nothing}
|
|
31
65
|
?controls=${this.controls}
|
|
32
66
|
?muted=${this.muted}
|
|
67
|
+
.muted=${this.muted}
|
|
33
68
|
?loop=${this.loop}
|
|
34
69
|
playsinline
|
|
35
|
-
|
|
70
|
+
>${this._media}</video>
|
|
36
71
|
`;
|
|
37
72
|
}
|
|
38
73
|
}
|
|
@@ -29,9 +29,10 @@ export default {
|
|
|
29
29
|
| \`muted\` | boolean | \`false\` |
|
|
30
30
|
| \`loop\` | boolean | \`false\` |
|
|
31
31
|
|
|
32
|
-
### Captions go
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
### Captions go inside the element
|
|
33
|
+
\`<track>\` and \`<source>\` children are copied into the \`<video>\` — a media
|
|
34
|
+
element only reads tracks and sources from its own children — and copied again
|
|
35
|
+
when they change. That is how you supply captions. **Recorded video needs captions** — WCAG 1.2.2 at level A —
|
|
35
36
|
and this element does nothing to add them for you.
|
|
36
37
|
|
|
37
38
|
### Notes
|
|
@@ -41,7 +42,11 @@ screen.
|
|
|
41
42
|
\`controls\` defaults to \`true\` on purpose: a video a user cannot pause is a
|
|
42
43
|
2.2.2 failure the moment it plays for more than five seconds. If you turn
|
|
43
44
|
controls off, the video must also be \`muted\` and \`loop\` and behave as
|
|
44
|
-
decoration.
|
|
45
|
+
decoration. In HTML that is \`controls="false"\`: leaving the attribute out
|
|
46
|
+
cannot turn off a default that is on. From script, \`.controls = false\`.
|
|
47
|
+
|
|
48
|
+
\`muted\` follows the property as well as the attribute, so changing it on a
|
|
49
|
+
playing video takes effect.
|
|
45
50
|
|
|
46
51
|
There is no \`autoplay\` prop, deliberately — autoplay with sound is both
|
|
47
52
|
disruptive and blocked by most browsers anyway.
|
|
@@ -91,7 +96,7 @@ export const Default = () => html`
|
|
|
91
96
|
* them.
|
|
92
97
|
*/
|
|
93
98
|
export const Decorative = () => html`
|
|
94
|
-
<bd-video poster="/images/bd-header-img.jpg"
|
|
99
|
+
<bd-video poster="/images/bd-header-img.jpg" controls="false" muted loop></bd-video>
|
|
95
100
|
`;
|
|
96
101
|
|
|
97
102
|
/**
|
|
@@ -124,7 +124,12 @@ export default css`
|
|
|
124
124
|
--radius-lg corner and the close button is a circle. */
|
|
125
125
|
.bd-video-thumb:focus-visible,
|
|
126
126
|
.bd-video-close:focus-visible {
|
|
127
|
-
|
|
127
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
128
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
129
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
130
|
+
outer band. */
|
|
131
|
+
outline: var(--border-width-2) solid transparent;
|
|
132
|
+
outline-offset: var(--focus-outline-offset);
|
|
128
133
|
box-shadow: var(--focus-outline-primary);
|
|
129
134
|
}
|
|
130
135
|
|
|
@@ -75,15 +75,14 @@ export const Shadows = () => {
|
|
|
75
75
|
const surface = all.filter(token => !token.includes('focus'));
|
|
76
76
|
const rings = all.filter(token => token.includes('focus'));
|
|
77
77
|
|
|
78
|
-
const ladder = ['--shadow-sm', '--shadow-md', '--shadow-lg', '--shadow-xl'
|
|
78
|
+
const ladder = ['--shadow-sm', '--shadow-md', '--shadow-lg', '--shadow-xl'];
|
|
79
79
|
|
|
80
80
|
return html`
|
|
81
81
|
${elevationStyles}
|
|
82
82
|
${docsPage({
|
|
83
83
|
title: 'Elevation',
|
|
84
84
|
lede : html`Shadows are the only elevation mechanism in the system — there is no z-index
|
|
85
|
-
scale. <code>sm</code> through <code>
|
|
86
|
-
<code>--shadow-inner</code> is an inset press state.`,
|
|
85
|
+
scale. <code>sm</code> through <code>xl</code> form a ladder.`,
|
|
87
86
|
note : referenceNote,
|
|
88
87
|
children: html`
|
|
89
88
|
${refSection({
|
|
@@ -73,10 +73,20 @@ const focusStyles = html`
|
|
|
73
73
|
margin-bottom: var(--spacing-6);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
.focus-demo--brand {
|
|
77
|
+
background: var(--color-blue-500);
|
|
78
|
+
padding: var(--spacing-24);
|
|
79
|
+
border-radius: var(--radius-md);
|
|
80
|
+
}
|
|
81
|
+
|
|
76
82
|
.focus-demo--dark .ring-label {
|
|
77
83
|
color: var(--color-neutral-300);
|
|
78
84
|
}
|
|
79
85
|
|
|
86
|
+
.focus-demo--brand .ring-label {
|
|
87
|
+
color: var(--color-neutral-0);
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
/* A genuinely interactive control, so the real :focus-visible can be tested. */
|
|
81
91
|
.try-me {
|
|
82
92
|
font: inherit;
|
|
@@ -117,9 +127,14 @@ export const FocusRings = () => {
|
|
|
117
127
|
// -offset and -radius are lengths, not rings; they would render as empty swatches
|
|
118
128
|
const NOT_RINGS = ['--focus-outline-offset', '--focus-outline-radius'];
|
|
119
129
|
const rings = tokens.filter(token => token.startsWith('--focus-outline-') && !NOT_RINGS.includes(token));
|
|
130
|
+
// the -traditional pair is an outline, not a shadow list; as a box-shadow it draws nothing
|
|
131
|
+
const ringStyle = token => token.includes('-traditional')
|
|
132
|
+
? `outline: var(${token}); outline-offset: var(--focus-outline-offset)`
|
|
133
|
+
: `box-shadow: var(${token})`;
|
|
120
134
|
const ringColour = resolve('--color-blue-500');
|
|
121
135
|
const onLight = contrast(ringColour, PAPER);
|
|
122
136
|
const onDark = contrast(ringColour, INK);
|
|
137
|
+
const onBrand = contrast(resolve('--color-neutral-0'), ringColour);
|
|
123
138
|
|
|
124
139
|
return html`
|
|
125
140
|
${focusStyles}
|
|
@@ -130,6 +145,22 @@ export const FocusRings = () => {
|
|
|
130
145
|
inner band of <code>--color-neutral-0</code> and an outer band of the state colour. The
|
|
131
146
|
inner band is what makes the ring visible on both light and dark backgrounds.
|
|
132
147
|
<br /><br />
|
|
148
|
+
<b>CTAs are the exception: bd-button draws an offset outline.</b>
|
|
149
|
+
<code>--focus-outline-traditional</code> (blue) or
|
|
150
|
+
<code>--focus-outline-traditional-inverse</code> (white), held off the control by
|
|
151
|
+
<code>--focus-outline-offset</code>. The gap is transparent, so it is always the surface
|
|
152
|
+
the button sits on, whether that is white, neutral-900 or brand blue. That means it
|
|
153
|
+
cannot merge into a white fill, and it never paints a dark band onto a brand hero. Only
|
|
154
|
+
the colour follows the surface: blue under <code>surface="light"</code>, white under
|
|
155
|
+
<code>surface="dark"</code> and on the white presets.
|
|
156
|
+
<br /><br />
|
|
157
|
+
<b>Figma cannot do this.</b> Its rings are spread shadows, so the gap has to be a painted
|
|
158
|
+
band, and the library keeps one ring per surface: <code>focus/ring-primary</code> on
|
|
159
|
+
light and <code>focus/ring-inverse</code> (<code>--focus-outline-inverse</code>, a
|
|
160
|
+
<code>--color-neutral-900</code> band against the control, a white band outside it) on
|
|
161
|
+
dark and brand. The code and the library are meant to differ here, so do not copy the
|
|
162
|
+
Figma ring back into bd-button.
|
|
163
|
+
<br /><br />
|
|
133
164
|
The ring is a <code>box-shadow</code>, so it already follows the radius of whatever it
|
|
134
165
|
sits on — a component with its own radius should set nothing and let the ring take its
|
|
135
166
|
shape. <code>--focus-outline-radius</code> is the default for the ones that have no
|
|
@@ -137,8 +168,10 @@ export const FocusRings = () => {
|
|
|
137
168
|
note: html`WCAG 2.2 <b>1.4.11 Non-text Contrast</b> asks for <b>3:1</b> between a focus
|
|
138
169
|
indicator and its adjacent background. The blue ring is
|
|
139
170
|
${onLight.toFixed(2)}${levelBadge(onLight)} on white and
|
|
140
|
-
${onDark.toFixed(2)}${levelBadge(onDark)} on <code>--color-neutral-900</code
|
|
141
|
-
|
|
171
|
+
${onDark.toFixed(2)}${levelBadge(onDark)} on <code>--color-neutral-900</code>, but
|
|
172
|
+
1.00:1 on a blue-500 hero. That is why every two-band ring here has a white band, and why
|
|
173
|
+
a CTA on a dark or brand surface takes a white outline: ${onBrand.toFixed(2)}${levelBadge(onBrand)}
|
|
174
|
+
on blue-500. ${referenceNote}`,
|
|
142
175
|
children: html`
|
|
143
176
|
${refSection({
|
|
144
177
|
name : 'The rule',
|
|
@@ -146,13 +179,24 @@ export const FocusRings = () => {
|
|
|
146
179
|
children: html`
|
|
147
180
|
<div class="specimen">
|
|
148
181
|
<p class="rule-note">
|
|
149
|
-
Every interactive component shows the same ring on keyboard focus
|
|
150
|
-
and it is two lines:
|
|
182
|
+
Every interactive component shows the same ring on keyboard focus:
|
|
151
183
|
</p>
|
|
152
184
|
<pre class="rule-code"><code>.control:focus-visible {
|
|
153
|
-
outline:
|
|
185
|
+
outline: var(--border-width-2) solid transparent;
|
|
186
|
+
outline-offset: var(--focus-outline-offset);
|
|
154
187
|
box-shadow: var(--focus-outline-primary);
|
|
155
188
|
}</code></pre>
|
|
189
|
+
<p class="rule-note">
|
|
190
|
+
<b>The transparent outline is not decoration.</b> Windows High
|
|
191
|
+
Contrast and every other forced-colors mode drop
|
|
192
|
+
<code>box-shadow</code>, so a ring drawn only as a shadow
|
|
193
|
+
disappears there and leaves a keyboard user with no cue at all. An
|
|
194
|
+
outline is kept and repainted in a system colour, and
|
|
195
|
+
<code>--focus-outline-offset</code> puts it where the ring's outer
|
|
196
|
+
band would be. It draws nothing anywhere else. Writing
|
|
197
|
+
<code>outline: none</code> beside the ring is what lost it, on
|
|
198
|
+
every atom that did.
|
|
199
|
+
</p>
|
|
156
200
|
<p class="rule-note">
|
|
157
201
|
<b>Always <code>:focus-visible</code>, never <code>:focus</code>.</b>
|
|
158
202
|
<code>:focus</code> fires on a mouse click too, which rings
|
|
@@ -167,13 +211,19 @@ export const FocusRings = () => {
|
|
|
167
211
|
accordion triggers did exactly that.
|
|
168
212
|
</p>
|
|
169
213
|
<p class="rule-note">
|
|
170
|
-
<b>Text fields
|
|
171
|
-
|
|
172
|
-
<code>--
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
214
|
+
<b>Text fields add their border.</b> Inputs, textareas and selects
|
|
215
|
+
take the same ring, and also turn their own border to
|
|
216
|
+
<code>--form-input-focus-borderColor</code>, so the field's edge
|
|
217
|
+
changes with it. (<code>--shadow-focus-outline-blue</code>, a
|
|
218
|
+
translucent glow, is not used by any field; only the blog
|
|
219
|
+
newsletter reads it.) Everything else you can operate — links,
|
|
220
|
+
checkboxes, radios, switches, tabs, accordion triggers, the
|
|
221
|
+
scrollable code block — uses the ring alone.
|
|
222
|
+
</p>
|
|
223
|
+
<p class="rule-note">
|
|
224
|
+
<b>bd-button is the other exception.</b> It sits on light, dark
|
|
225
|
+
and brand surfaces, so it uses the offset outline, whose
|
|
226
|
+
transparent gap works on all three. See the note at the top.
|
|
177
227
|
</p>
|
|
178
228
|
</div>
|
|
179
229
|
`
|
|
@@ -198,7 +248,7 @@ export const FocusRings = () => {
|
|
|
198
248
|
token => html`
|
|
199
249
|
<div>
|
|
200
250
|
<span class="ring-label">${token.replace('--focus-outline-', '')}</span>
|
|
201
|
-
<div class="ring" style
|
|
251
|
+
<div class="ring" style=${ringStyle(token)}>Button</div>
|
|
202
252
|
</div>
|
|
203
253
|
`
|
|
204
254
|
)}
|
|
@@ -208,14 +258,30 @@ export const FocusRings = () => {
|
|
|
208
258
|
})}
|
|
209
259
|
${refSection({
|
|
210
260
|
name : 'On a dark surface',
|
|
211
|
-
meta : '
|
|
261
|
+
meta : 'on a white control, primary merges into the fill; the outline\'s gap is the surface',
|
|
212
262
|
children: html`
|
|
213
263
|
<div class="focus-demo focus-demo--dark">
|
|
214
264
|
${rings.map(
|
|
215
265
|
token => html`
|
|
216
266
|
<div>
|
|
217
267
|
<span class="ring-label">${token.replace('--focus-outline-', '')}</span>
|
|
218
|
-
<div class="ring" style
|
|
268
|
+
<div class="ring" style=${ringStyle(token)}>Button</div>
|
|
269
|
+
</div>
|
|
270
|
+
`
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
`
|
|
274
|
+
})}
|
|
275
|
+
${refSection({
|
|
276
|
+
name : 'On a brand surface',
|
|
277
|
+
meta : 'inverse paints a black band here; traditional-inverse leaves the gap blue',
|
|
278
|
+
children: html`
|
|
279
|
+
<div class="focus-demo focus-demo--brand">
|
|
280
|
+
${rings.map(
|
|
281
|
+
token => html`
|
|
282
|
+
<div>
|
|
283
|
+
<span class="ring-label">${token.replace('--focus-outline-', '')}</span>
|
|
284
|
+
<div class="ring" style=${ringStyle(token)}>Button</div>
|
|
219
285
|
</div>
|
|
220
286
|
`
|
|
221
287
|
)}
|
|
@@ -199,12 +199,6 @@ export const Motion = () => {
|
|
|
199
199
|
`
|
|
200
200
|
)}
|
|
201
201
|
</div>
|
|
202
|
-
<p class="docs-lede" style="margin-top: var(--spacing-16)">
|
|
203
|
-
<code>--transition-easing-bounce</code> is
|
|
204
|
-
<code>${resolve('--transition-easing-bounce')}</code> — the negative and
|
|
205
|
-
greater-than-one control points make it overshoot at both ends. Use it for a
|
|
206
|
-
deliberate spring, not for hover states.
|
|
207
|
-
</p>
|
|
208
202
|
`
|
|
209
203
|
})}
|
|
210
204
|
|