@repobit/dex-system-design 1.0.1 → 2.0.0
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 +81 -0
- package/README.md +43 -12
- package/package.json +4 -2
- 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
|
@@ -22,16 +22,19 @@ unknown. When you know the proportion done, use \`<bd-progress>\` instead.
|
|
|
22
22
|
### Props
|
|
23
23
|
| Prop | Values | Default |
|
|
24
24
|
|---|---|---|
|
|
25
|
-
| \`size\` | \`
|
|
25
|
+
| \`size\` | \`sm\`, \`md\`, \`lg\` — \`small\` / \`default\` / \`large\` still resolve | \`md\` |
|
|
26
26
|
| \`label\` | the accessible name | \`Loading\` |
|
|
27
27
|
|
|
28
28
|
### Accessibility
|
|
29
|
-
|
|
30
|
-
announced politely rather than interrupting.
|
|
29
|
+
\`label\` is the text of a visually hidden \`role="status"\` region, so the wait
|
|
30
|
+
is announced politely rather than interrupting. The text arrives just after
|
|
31
|
+
the region does, which is what lets a screen reader treat it as news, and a
|
|
32
|
+
later change to \`label\` is announced too. Give it a specific \`label\` where
|
|
31
33
|
you can — "Checking your licence" is more use than the default "Loading".
|
|
32
34
|
|
|
33
|
-
Under \`prefers-reduced-motion\` it stops spinning
|
|
34
|
-
still reads as active
|
|
35
|
+
Under \`prefers-reduced-motion\` it stops spinning: the arc stays, still and
|
|
36
|
+
at full contrast, and the disc behind it pulses, so it still reads as active
|
|
37
|
+
without rotating and never fades below 3:1.
|
|
35
38
|
|
|
36
39
|
### Note on colour
|
|
37
40
|
The spinner's track and head are fixed at \`--color-neutral-200\` and
|
|
@@ -47,9 +50,9 @@ let the two converge.
|
|
|
47
50
|
argTypes: {
|
|
48
51
|
size: {
|
|
49
52
|
control : { type: 'select' },
|
|
50
|
-
options : ['
|
|
53
|
+
options : ['sm', 'md', 'lg'],
|
|
51
54
|
description: 'Diameter of the spinner.',
|
|
52
|
-
table : { type: { summary: 'string' }, defaultValue: { summary:
|
|
55
|
+
table : { type: { summary: 'string' }, defaultValue: { summary: 'md' }, category: 'Appearance' }
|
|
53
56
|
},
|
|
54
57
|
label: {
|
|
55
58
|
control : 'text',
|
|
@@ -58,10 +58,22 @@ export const switchCSS = css`
|
|
|
58
58
|
flex: none;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/* The thumb's inset and travel are derived from the two boxes they sit
|
|
62
|
+
between, in px like those boxes. They were --spacing-2 and --spacing-16,
|
|
63
|
+
which are rem, inside a track that is --dimension-* and so is not: at a
|
|
64
|
+
24px root the thumb rode 3px in and travelled 24px, and finished at 47px
|
|
65
|
+
in a 40px track. A knob has to stay on the track it belongs to at any text
|
|
66
|
+
size, so all four lengths are now one unit. Inset is half the height left
|
|
67
|
+
over; travel is the width left over after both insets: 2px and 16px. */
|
|
68
|
+
.track {
|
|
69
|
+
--bd-switch-inset: calc((var(--dimension-24) - var(--dimension-20)) / 2);
|
|
70
|
+
--bd-switch-travel: calc(var(--dimension-40) - var(--dimension-20) - 2 * var(--bd-switch-inset));
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
.thumb {
|
|
62
74
|
position: absolute;
|
|
63
|
-
top: var(--
|
|
64
|
-
left: var(--
|
|
75
|
+
top: var(--bd-switch-inset);
|
|
76
|
+
left: var(--bd-switch-inset);
|
|
65
77
|
width: var(--dimension-20);
|
|
66
78
|
height: var(--dimension-20);
|
|
67
79
|
/* the knob on the track, white on both tracks: --color-action-on-fill,
|
|
@@ -81,13 +93,19 @@ export const switchCSS = css`
|
|
|
81
93
|
If a second component ever needs it, that is when it earns a name. */
|
|
82
94
|
label:hover .track { background: var(--color-neutral-500); }
|
|
83
95
|
:host([checked]) label:hover .track { background: var(--color-action-primary-hover); }
|
|
84
|
-
:host([checked]) .thumb { transform: translateX(var(--
|
|
96
|
+
:host([checked]) .thumb { transform: translateX(var(--bd-switch-travel)); }
|
|
85
97
|
|
|
86
98
|
/* the one focus ring — see Foundations/Focus. On the row rather than the
|
|
87
99
|
track, so the target and the indicator are the same shape here as on
|
|
88
100
|
bd-checkbox and bd-radio. */
|
|
89
101
|
label:has(input:focus-visible) {
|
|
90
102
|
box-shadow: var(--focus-outline-primary);
|
|
103
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
104
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
105
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
106
|
+
outer band. */
|
|
107
|
+
outline: var(--border-width-2) solid transparent;
|
|
108
|
+
outline-offset: var(--focus-outline-offset);
|
|
91
109
|
border-radius: var(--focus-outline-radius);
|
|
92
110
|
}
|
|
93
111
|
|
|
@@ -31,7 +31,14 @@ export class BdSwitch extends LitElement {
|
|
|
31
31
|
?disabled=${this.disabled}
|
|
32
32
|
@change=${e => {
|
|
33
33
|
this.checked = e.target.checked;
|
|
34
|
-
|
|
34
|
+
// bubbles and composed, as every sibling form atom's event is: with
|
|
35
|
+
// neither, it reached a listener on the switch and nothing above
|
|
36
|
+
// it, so a form wrapper delegating bd-change heard nothing
|
|
37
|
+
this.dispatchEvent(new CustomEvent('bd-change', {
|
|
38
|
+
detail : this.checked,
|
|
39
|
+
bubbles : true,
|
|
40
|
+
composed: true
|
|
41
|
+
}));
|
|
35
42
|
}}
|
|
36
43
|
/>
|
|
37
44
|
<span class="track"><span class="thumb"></span></span>
|
|
@@ -33,7 +33,8 @@ Content can also come from the default slot, which renders after \`label\` — s
|
|
|
33
33
|
set one or the other, not both.
|
|
34
34
|
|
|
35
35
|
### Events
|
|
36
|
-
\`bd-change\` — fires on toggle with \`detail\` set to the new boolean.
|
|
36
|
+
\`bd-change\` — fires on toggle with \`detail\` set to the new boolean. It bubbles and
|
|
37
|
+
is composed, so a form wrapper can listen for it on itself.
|
|
37
38
|
|
|
38
39
|
### Accessibility
|
|
39
40
|
It is a real \`<input type="checkbox">\` with \`role="switch"\`, inside a
|
|
@@ -111,7 +111,12 @@ export const tabListCSS = css`
|
|
|
111
111
|
|
|
112
112
|
.bd-tab-button:focus-visible {
|
|
113
113
|
/* the one focus ring — see Foundations/Focus */
|
|
114
|
-
|
|
114
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
115
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
116
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
117
|
+
outer band. */
|
|
118
|
+
outline: var(--border-width-2) solid transparent;
|
|
119
|
+
outline-offset: var(--focus-outline-offset);
|
|
115
120
|
box-shadow: var(--focus-outline-primary);
|
|
116
121
|
border-radius: var(--focus-outline-radius);
|
|
117
122
|
}
|
|
@@ -32,6 +32,17 @@ class TerminalCursor extends LitElement {
|
|
|
32
32
|
opacity: 0;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
/* A cursor that blinks for as long as the page is open is exactly the
|
|
37
|
+
motion the setting exists for, and this one had no guard: every
|
|
38
|
+
other animated element in the package stops or softens under
|
|
39
|
+
prefers-reduced-motion. Here it stops, and the cursor stays shown --
|
|
40
|
+
a steady block still says "this is where the terminal is". */
|
|
41
|
+
@media (prefers-reduced-motion: reduce) {
|
|
42
|
+
.bd-terminal-cursor {
|
|
43
|
+
animation: none;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
35
46
|
`
|
|
36
47
|
];
|
|
37
48
|
|
|
@@ -160,7 +160,7 @@ class TerminalMessage extends LitElement {
|
|
|
160
160
|
${this.items && this.items.length
|
|
161
161
|
? html`
|
|
162
162
|
<bd-list class="bd-terminalmsg-list">
|
|
163
|
-
${this.items.map((item) => html`<bd-li variant="bullet" size="sm" color="
|
|
163
|
+
${this.items.map((item) => html`<bd-li variant="bullet" size="sm" color="inverse">${item}</bd-li>`)}
|
|
164
164
|
</bd-list>
|
|
165
165
|
`
|
|
166
166
|
: nothing}
|
|
@@ -12,7 +12,12 @@ export default css`
|
|
|
12
12
|
|
|
13
13
|
.bd-terms-text-a:focus-visible{
|
|
14
14
|
/* the one focus ring — see Foundations/Focus */
|
|
15
|
-
|
|
15
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
16
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
17
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
18
|
+
outer band. */
|
|
19
|
+
outline: var(--border-width-2) solid transparent;
|
|
20
|
+
outline-offset: var(--focus-outline-offset);
|
|
16
21
|
box-shadow: var(--focus-outline-primary);
|
|
17
22
|
border-radius: var(--focus-outline-radius);
|
|
18
23
|
}
|
|
@@ -119,6 +119,11 @@ The vertical resize handle is left enabled, so a user can grow the field beyond
|
|
|
119
119
|
control : 'boolean',
|
|
120
120
|
description: 'Blocks editing.',
|
|
121
121
|
table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'State' }
|
|
122
|
+
},
|
|
123
|
+
name: {
|
|
124
|
+
control : 'text',
|
|
125
|
+
description: 'Carried on the bd-change detail as { value, name }, for your own submit handling. NOT submitted with a form — the <textarea> is in a shadow root.',
|
|
126
|
+
table : { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'Content' }
|
|
122
127
|
}
|
|
123
128
|
}
|
|
124
129
|
};
|
|
@@ -127,6 +132,24 @@ export const Default = () => html`
|
|
|
127
132
|
<bd-textarea label="What happened?" placeholder="Tell us what happened"></bd-textarea>
|
|
128
133
|
`;
|
|
129
134
|
|
|
135
|
+
/**
|
|
136
|
+
* `name` reaches no form -- the <textarea> is in this element's shadow root --
|
|
137
|
+
* so it is carried on the event instead. Type, then click away: the line
|
|
138
|
+
* below shows the bd-change detail as it arrives.
|
|
139
|
+
*/
|
|
140
|
+
export const NameOnTheEvent = {
|
|
141
|
+
name : 'name on the event',
|
|
142
|
+
render: () => html`
|
|
143
|
+
<bd-textarea
|
|
144
|
+
label="What happened?"
|
|
145
|
+
name="incident"
|
|
146
|
+
rows="2"
|
|
147
|
+
@bd-change=${e => { e.currentTarget.nextElementSibling.textContent = `bd-change detail: ${JSON.stringify(e.detail)}`; }}
|
|
148
|
+
></bd-textarea>
|
|
149
|
+
<p style="font-family: var(--typography-fontFamily-mono); font-size: var(--typography-fontSize-sm)">bd-change detail: (type, then click away)</p>
|
|
150
|
+
`
|
|
151
|
+
};
|
|
152
|
+
|
|
130
153
|
export const Sizes = {
|
|
131
154
|
name : 'All Sizes',
|
|
132
155
|
render: () => html`
|
|
@@ -64,8 +64,31 @@ export const tooltipCSS = css`
|
|
|
64
64
|
transition: opacity var(--transition-duration-fast) var(--transition-easing-smooth);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/* Shown means reachable. WCAG 1.4.13 asks that the pointer can move onto
|
|
68
|
+
content that appeared on hover without it disappearing. With
|
|
69
|
+
pointer-events: none left on, the pointer passed through the bubble to
|
|
70
|
+
whatever lay beneath it, the host lost :hover, and the bubble hid. The
|
|
71
|
+
bubble is in this shadow tree, so hovering it keeps :host(:hover). */
|
|
67
72
|
:host(:hover) .bubble,
|
|
68
|
-
:host(:focus-within) .bubble { opacity: 1; }
|
|
73
|
+
:host(:focus-within) .bubble { opacity: 1; pointer-events: auto; }
|
|
74
|
+
|
|
75
|
+
/* Escape hides it while the pointer or focus is still there; see the note
|
|
76
|
+
on _showing in tooltip.js. Same specificity as the rule above and later,
|
|
77
|
+
so it wins, and it takes pointer events away with the opacity. */
|
|
78
|
+
:host(:hover) .bubble--dismissed,
|
|
79
|
+
:host(:focus-within) .bubble--dismissed { opacity: 0; pointer-events: none; }
|
|
80
|
+
|
|
81
|
+
/* The bubble floats --spacing-8 above the trigger, and the pointer has to
|
|
82
|
+
cross that gap to reach it: this fills it, so the host stays hovered
|
|
83
|
+
across the way. It inherits pointer-events from the bubble, so it catches
|
|
84
|
+
nothing while the bubble is hidden. */
|
|
85
|
+
.bubble::after {
|
|
86
|
+
content: "";
|
|
87
|
+
position: absolute;
|
|
88
|
+
inset-inline: 0;
|
|
89
|
+
top: 100%;
|
|
90
|
+
bottom: calc(-1 * var(--spacing-8));
|
|
91
|
+
}
|
|
69
92
|
|
|
70
93
|
@media (prefers-reduced-motion: reduce) {
|
|
71
94
|
.bubble { transition: none; }
|
|
@@ -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({
|