@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,10 +22,11 @@ export default {
|
|
|
22
22
|
\`\`\`
|
|
23
23
|
|
|
24
24
|
### Behavior
|
|
25
|
-
- The
|
|
26
|
-
-
|
|
27
|
-
- If found,
|
|
28
|
-
- The
|
|
25
|
+
- The headline keeps its markup: \`<em>\`, \`<strong>\`, \`<br>\` and links render as written, and the first occurrence of \`highlight-text\` is marked where it sits; a later occurrence renders as plain text
|
|
26
|
+
- The headline is a copy of the content, so a script listener attached to an element inside it does not carry over; links still work, because the \`href\` does
|
|
27
|
+
- If \`highlight-text\` is not found, or crosses an element boundary (\`family <em>member</em>\`), the content renders through the slot as it is, without decoration
|
|
28
|
+
- The mark is a \`<span class="highlight">\` with a bar and dot drawn at each end
|
|
29
|
+
- Editing the content after it renders re-renders the headline
|
|
29
30
|
|
|
30
31
|
### Notes
|
|
31
32
|
- \`sub-title\` renders a \`bd-p kind="small"\` above the headline, **not** a heading
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { LitElement, css } from "lit";
|
|
2
2
|
import { iconSize } from "../shared/token-scales.js";
|
|
3
3
|
|
|
4
|
+
/* One sheet per distinct size and colour, shared by every icon that asks for
|
|
5
|
+
the same pair: a page of forty check icons adopts one sheet, not forty. */
|
|
6
|
+
const instanceSheets = new Map();
|
|
7
|
+
|
|
8
|
+
const instanceSheet = (size, color) => {
|
|
9
|
+
const key = `${size}|${color}`;
|
|
10
|
+
let sheet = instanceSheets.get(key);
|
|
11
|
+
if (!sheet) {
|
|
12
|
+
sheet = new CSSStyleSheet();
|
|
13
|
+
sheet.replaceSync(
|
|
14
|
+
`:host { --bd-icon-size: ${size}; }` + (color ? ` svg { color: ${color}; }` : "")
|
|
15
|
+
);
|
|
16
|
+
instanceSheets.set(key, sheet);
|
|
17
|
+
}
|
|
18
|
+
return sheet;
|
|
19
|
+
};
|
|
20
|
+
|
|
4
21
|
/**
|
|
5
22
|
* Base for the tintable interface icons.
|
|
6
23
|
*
|
|
@@ -47,8 +64,9 @@ export class BdIconElement extends LitElement {
|
|
|
47
64
|
static styles = css`
|
|
48
65
|
:host {
|
|
49
66
|
display: inline-flex;
|
|
50
|
-
/* --bd-icon-size is component-local and always set
|
|
51
|
-
fallback is only reached if a subclass forgets to
|
|
67
|
+
/* --bd-icon-size is component-local and always set by the instance
|
|
68
|
+
sheet below, so the fallback is only reached if a subclass forgets to
|
|
69
|
+
call super. */
|
|
52
70
|
height: var(--bd-icon-size, var(--icon-md-size));
|
|
53
71
|
}
|
|
54
72
|
|
|
@@ -83,17 +101,42 @@ export class BdIconElement extends LitElement {
|
|
|
83
101
|
this.height = null;
|
|
84
102
|
}
|
|
85
103
|
|
|
104
|
+
/* WHY NOT THE HOST'S STYLE ATTRIBUTE.
|
|
105
|
+
This used to write --bd-icon-size and color into this.style on every
|
|
106
|
+
update. The host's style attribute belongs to the page, and with no color
|
|
107
|
+
prop the update removed the colour outright, so
|
|
108
|
+
<bd-check-icon style="color: green"> rendered in the inherited text
|
|
109
|
+
colour, and so did any icon a script coloured after its first render.
|
|
110
|
+
The inline size meant no stylesheet could set --bd-icon-size either.
|
|
111
|
+
|
|
112
|
+
Both now live in a sheet adopted into this shadow root, and the host's
|
|
113
|
+
style attribute is never read or written:
|
|
114
|
+
|
|
115
|
+
size a :host rule. The page's CSS on the element wins over it, as it
|
|
116
|
+
already did for height -- header.css.js sets one on
|
|
117
|
+
.bd-header-signature -- and now does for --bd-icon-size too.
|
|
118
|
+
colour a rule on the svg, so the color prop still beats the page's CSS
|
|
119
|
+
on the host, as the inline colour did. Unset, nothing is
|
|
120
|
+
written and the artwork's currentColor follows the host,
|
|
121
|
+
wherever its colour came from. */
|
|
86
122
|
willUpdate() {
|
|
87
123
|
// An explicit pixel size wins, for the callers that still pass one.
|
|
88
124
|
const px = this.height ?? this.width;
|
|
89
125
|
const { defaultSize } = this.constructor;
|
|
90
126
|
// a step first, then an off-scale default, then the caller's pixels
|
|
91
127
|
const step = iconSize(this.size) || iconSize(defaultSize) || defaultSize;
|
|
92
|
-
|
|
128
|
+
const size = Number.isFinite(px) ? `${px}px` : step;
|
|
93
129
|
|
|
94
130
|
// No colour of its own by default: the artwork is currentColor, so it
|
|
95
|
-
// follows whatever it sits in.
|
|
96
|
-
|
|
97
|
-
|
|
131
|
+
// follows whatever it sits in. A value CSS would not accept as a colour is
|
|
132
|
+
// dropped, as an inline one was -- and it never reaches the sheet's text.
|
|
133
|
+
const color = this.color && CSS.supports("color", this.color) ? this.color : "";
|
|
134
|
+
|
|
135
|
+
this._baseSheets ??= this.renderRoot.adoptedStyleSheets;
|
|
136
|
+
const sheet = instanceSheet(size, color);
|
|
137
|
+
if (this._instanceSheet !== sheet) {
|
|
138
|
+
this._instanceSheet = sheet;
|
|
139
|
+
this.renderRoot.adoptedStyleSheets = [...this._baseSheets, sheet];
|
|
140
|
+
}
|
|
98
141
|
}
|
|
99
142
|
}
|
|
@@ -81,6 +81,13 @@ per the foundation rule it should be a token reference such as
|
|
|
81
81
|
text it sits beside. A raw hex value here is the same drift the tokens exist to
|
|
82
82
|
prevent.
|
|
83
83
|
|
|
84
|
+
### Styling an icon from the page
|
|
85
|
+
An icon never writes its own \`style\` attribute, so whatever the page puts
|
|
86
|
+
there stays. With no \`color\` prop, \`style="color: …"\` or any stylesheet
|
|
87
|
+
colour on the icon reaches the artwork; the \`color\` prop, when set, beats both.
|
|
88
|
+
A stylesheet can size an icon with \`height\` or \`--bd-icon-size\` on the
|
|
89
|
+
element, and either beats the \`size\` prop.
|
|
90
|
+
|
|
84
91
|
### Known inconsistencies
|
|
85
92
|
- **\`bd-laurel-icon\` keeps an 80px default**, off the scale, which stops at 64.
|
|
86
93
|
Shrinking the awards wreath by a fifth is a design decision rather than a
|
|
@@ -84,112 +84,23 @@ export default css`
|
|
|
84
84
|
box-shadow: var(--shadow-xl);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
/*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.bd-img.bordered-thick {
|
|
97
|
-
border: var(--border-width-4) solid var(--color-border-disabled);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/* Responsive Behavior */
|
|
101
|
-
.bd-img.responsive {
|
|
102
|
-
width: 100%;
|
|
103
|
-
height: auto;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/* Aspect Ratio Containers */
|
|
107
|
-
.bd-img.aspect-16-9 {
|
|
108
|
-
aspect-ratio: 16 / 9;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.bd-img.aspect-4-3 {
|
|
112
|
-
aspect-ratio: 4 / 3;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.bd-img.aspect-1-1 {
|
|
116
|
-
aspect-ratio: 1 / 1;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/* Hover Effects */
|
|
120
|
-
.bd-img.hover-zoom:hover {
|
|
121
|
-
transform: scale(1.05);
|
|
122
|
-
transition: transform var(--transition-duration-slow) var(--transition-easing-smooth);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.bd-img.hover-shadow:hover {
|
|
126
|
-
box-shadow: var(--shadow-lg);
|
|
127
|
-
transition: box-shadow var(--transition-duration-slow) var(--transition-easing-smooth);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.bd-img:focus-visible {
|
|
131
|
-
/* the one focus ring — see Foundations/Focus */
|
|
132
|
-
outline: none;
|
|
133
|
-
box-shadow: var(--focus-outline-primary);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/* Loading state */
|
|
137
|
-
.bd-img:not([src]) {
|
|
138
|
-
background-color: var(--color-surface-muted);
|
|
139
|
-
/* no token: the dimension scale stops at 64px */
|
|
140
|
-
min-height: 100px;
|
|
141
|
-
display: flex;
|
|
142
|
-
align-items: center;
|
|
143
|
-
justify-content: center;
|
|
144
|
-
/* --color-text-muted, not --color-text-disabled. Loading is not disabled,
|
|
145
|
-
and the difference is not academic: the disabled text role carries a
|
|
146
|
-
contrast exemption -- WCAG 1.4.3 excuses disabled controls from the
|
|
147
|
-
4.5:1 minimum -- and this label claims that exemption without being
|
|
148
|
-
entitled to it. It is a sentence a reader is meant to read while they
|
|
149
|
-
wait.
|
|
150
|
-
|
|
151
|
-
Measured on this ground: neutral-400 on neutral-100 is 2.51:1, which
|
|
152
|
-
misses even the 3:1 floor for non-text. --color-text-muted is
|
|
153
|
-
neutral-600 at 5.12:1 and is the role this package already uses for
|
|
154
|
-
low-emphasis readable text -- caption and form help text both resolve
|
|
155
|
-
to it.
|
|
156
|
-
|
|
157
|
-
The ground stays --color-surface-muted. Loading is a state, muted is
|
|
158
|
-
the state surface, and bd-skeleton -- the package's other loading
|
|
159
|
-
affordance -- already sits on it. Only the ink was choosing by value.
|
|
160
|
-
See "Choosing a semantic role" in the package README. */
|
|
161
|
-
color: var(--color-text-muted);
|
|
162
|
-
/* no role: alt text shown when the image has no src -- a glyph/marker/placeholder box, not running
|
|
163
|
-
prose. See list-item.css.js's .bd-list-item-marker::before for the
|
|
164
|
-
argument. */
|
|
165
|
-
font-size: var(--typography-fontSize-sm);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/* The placeholder copy is a local property rather than a string in the
|
|
169
|
-
stylesheet: content: "Image loading..." cannot be translated, and this
|
|
170
|
-
atom and bd-picture used two different sentences for the same state. */
|
|
171
|
-
.bd-img:not([src])::before {
|
|
172
|
-
content: var(--bd-img-loading-label, "Loading image");
|
|
173
|
-
}
|
|
87
|
+
/* Gone from here, because nothing could reach them: bordered,
|
|
88
|
+
bordered-primary and bordered-thick, responsive, aspect-16-9 / 4-3 /
|
|
89
|
+
1-1, hover-zoom and hover-shadow had no prop, and the only way to set
|
|
90
|
+
one was to pass its name as fit, which now accepts only the five values
|
|
91
|
+
above. :focus-visible styled an <img>, which never takes focus. And the
|
|
92
|
+
:not([src]) loading placeholder could not appear: the template always
|
|
93
|
+
writes a src attribute, and an <img> renders no ::before even without
|
|
94
|
+
one. bd-picture keeps its own placeholder, which does work. */
|
|
174
95
|
|
|
175
96
|
/* Smooth transitions for radius and shadow changes */
|
|
176
97
|
.bd-img {
|
|
177
98
|
transition: border-radius var(--transition-duration-normal) var(--transition-easing-smooth), box-shadow var(--transition-duration-normal) var(--transition-easing-smooth);
|
|
178
99
|
}
|
|
179
100
|
|
|
180
|
-
/* Performance optimization for transforms */
|
|
181
|
-
.bd-img.hover-zoom {
|
|
182
|
-
will-change: transform;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/* A hover that scales the image is the category of motion the setting
|
|
186
|
-
exists for, and this atom had no guard at all. */
|
|
187
101
|
@media (prefers-reduced-motion: reduce) {
|
|
188
|
-
.bd-img
|
|
189
|
-
.bd-img.hover-zoom:hover,
|
|
190
|
-
.bd-img.hover-shadow:hover {
|
|
102
|
+
.bd-img {
|
|
191
103
|
transition: none;
|
|
192
|
-
transform: none;
|
|
193
104
|
}
|
|
194
105
|
}
|
|
195
106
|
}
|
|
@@ -5,6 +5,11 @@ import imgCSS from "./image.css.js";
|
|
|
5
5
|
|
|
6
6
|
// the radius steps image.css.js implements — the full --radius-* scale
|
|
7
7
|
const RADIUS_STEPS = ['none', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', 'full'];
|
|
8
|
+
/* fit and shadow become class names, so an unknown value used to become a
|
|
9
|
+
class too -- fit="bordered" reached a rule nothing documented. Only the
|
|
10
|
+
values image.css.js implements get through; anything else is the default. */
|
|
11
|
+
const FITS = ['cover', 'contain', 'fill', 'none', 'scale-down'];
|
|
12
|
+
const SHADOWS = ['none', 'sm', 'md', 'lg', 'xl'];
|
|
8
13
|
|
|
9
14
|
export class BdImg extends LitElement {
|
|
10
15
|
static properties = {
|
|
@@ -32,10 +37,10 @@ export class BdImg extends LitElement {
|
|
|
32
37
|
|
|
33
38
|
render() {
|
|
34
39
|
const baseClass = 'bd-img';
|
|
35
|
-
const fitClass = this.fit
|
|
40
|
+
const fitClass = FITS.includes(this.fit) ? this.fit : 'cover';
|
|
36
41
|
const radius = RADIUS_STEPS.includes(this.radius) ? this.radius : 'none';
|
|
37
42
|
const radiusClass = `radius-${radius}`;
|
|
38
|
-
const shadowClass = `shadow-${this.shadow
|
|
43
|
+
const shadowClass = `shadow-${SHADOWS.includes(this.shadow) ? this.shadow : 'none'}`;
|
|
39
44
|
|
|
40
45
|
const finalClass = `${baseClass} ${fitClass} ${radiusClass} ${shadowClass}`;
|
|
41
46
|
|
|
@@ -23,7 +23,7 @@ It includes:
|
|
|
23
23
|
| State | Description |
|
|
24
24
|
|---|---|
|
|
25
25
|
| \`default\` | Idle, no interaction |
|
|
26
|
-
| \`focused\` |
|
|
26
|
+
| \`focused\` | Has keyboard focus — set it to move focus into the field; it reflects whether the field is focused |
|
|
27
27
|
| \`invalid\` | Failed validation — red border + helper text |
|
|
28
28
|
| \`validated\` | Passed validation — green border + check icon |
|
|
29
29
|
| \`disabled\` | Non-interactive, visually muted |
|
|
@@ -86,7 +86,8 @@ the events for your own use.
|
|
|
86
86
|
\`invalid\` sets \`aria-invalid="true"\`, and when \`tooltip\` is present the input
|
|
87
87
|
points at it with \`aria-describedby\` — so a screen reader announces both that
|
|
88
88
|
the field is wrong and why. Put the reason in \`tooltip\`; it doubles as the
|
|
89
|
-
help text and the error message.
|
|
89
|
+
help text and the error message. \`bd-custom-dropdown\` and
|
|
90
|
+
\`bd-custom-input-clipboard\` do the same.
|
|
90
91
|
|
|
91
92
|
The required asterisk is \`aria-hidden\`, because the native \`required\`
|
|
92
93
|
attribute is what actually conveys it.
|
|
@@ -129,26 +130,19 @@ everything else is not:
|
|
|
129
130
|
| | \`bd-custom-input\` | \`bd-custom-dropdown\` | \`bd-custom-input-clipboard\` |
|
|
130
131
|
|---|---|---|---|
|
|
131
132
|
| \`label\`, \`kind\`, \`required\`, \`invalid\`, \`validated\`, \`disabled\`, \`tooltip\` | yes | yes | yes |
|
|
132
|
-
| \`value\` | yes |
|
|
133
|
+
| \`value\` | yes | yes | yes |
|
|
133
134
|
| \`type\`, \`placeholder\`, \`focused\` | yes | **no** | yes |
|
|
134
|
-
| \`name
|
|
135
|
+
| \`name\` | yes | yes | yes |
|
|
136
|
+
| \`inputmode\`, \`autocomplete\`, \`enterkeyhint\` | yes | **no** | **no** |
|
|
135
137
|
| \`options\` | **no** | yes | **no** |
|
|
136
|
-
| emits | \`bd-input\`, \`bd-change\` |
|
|
138
|
+
| emits | \`bd-input\`, \`bd-change\` | \`bd-input\`, \`bd-change\` | \`bd-input\`, \`bd-change\` |
|
|
137
139
|
|
|
138
140
|
Storybook shows one shared control panel for all three, so a knob that does
|
|
139
141
|
nothing on the story you are looking at is expected — check this table.
|
|
140
142
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
nothing observable from outside the component, so it cannot currently be read
|
|
145
|
-
without reaching into its shadow root. It needs the same \`value\` +
|
|
146
|
-
\`bd-input\`/\`bd-change\` treatment the input has.
|
|
147
|
-
- **The clipboard field's event is not the input's.** It fires \`input\` — the
|
|
148
|
-
native event's own name — with the string as \`detail\` rather than
|
|
149
|
-
\`{ value, name }\`, and it is neither \`bubbles\` nor \`composed\`, so it reaches
|
|
150
|
-
a listener on the element itself and nothing further up. Renaming it to
|
|
151
|
-
\`bd-input\` would align the three, and is a breaking change.
|
|
143
|
+
All three emit the same events with the same \`detail\`, so one form handler
|
|
144
|
+
reads them. The clipboard field used to fire \`input\` — the native event's
|
|
145
|
+
name — with a bare string; a paste fires \`bd-input\` and then \`bd-change\`.
|
|
152
146
|
|
|
153
147
|
### The clipboard field pastes, it does not copy
|
|
154
148
|
The button reads the system clipboard with \`navigator.clipboard.readText()\` and
|
|
@@ -222,7 +216,7 @@ misses 2.5.5 (AAA, 44px) at every size.
|
|
|
222
216
|
},
|
|
223
217
|
focused: {
|
|
224
218
|
control : 'boolean',
|
|
225
|
-
description: '
|
|
219
|
+
description: 'Moves keyboard focus into the field, and reflects whether it has focus. The ring is the browser\'s real focus ring.',
|
|
226
220
|
table : {
|
|
227
221
|
type : { summary: 'boolean' },
|
|
228
222
|
defaultValue: { summary: false },
|
|
@@ -405,7 +399,8 @@ export const InputDefault = {
|
|
|
405
399
|
};
|
|
406
400
|
|
|
407
401
|
/**
|
|
408
|
-
*
|
|
402
|
+
* An input that takes focus when it renders, through the focused prop -- the
|
|
403
|
+
* ring is the real one, because the field really is focused.
|
|
409
404
|
*/
|
|
410
405
|
export const InputFocused = {
|
|
411
406
|
name : 'Input — Focused',
|
|
@@ -415,7 +410,7 @@ export const InputFocused = {
|
|
|
415
410
|
docs: {
|
|
416
411
|
description: {
|
|
417
412
|
story:
|
|
418
|
-
'
|
|
413
|
+
'`focused` moves keyboard focus into the field when the story renders, so the ring shown is the browser\'s own. Clicking elsewhere clears it, and the prop follows.'
|
|
419
414
|
}
|
|
420
415
|
}
|
|
421
416
|
}
|
|
@@ -50,7 +50,12 @@ export default css`
|
|
|
50
50
|
shared one; the radius is the default for an element with no radius of its
|
|
51
51
|
own, so the ring is not a hard rectangle. */
|
|
52
52
|
.paste-button:focus-visible {
|
|
53
|
-
|
|
53
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
54
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
55
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
56
|
+
outer band. */
|
|
57
|
+
outline: var(--border-width-2) solid transparent;
|
|
58
|
+
outline-offset: var(--focus-outline-offset);
|
|
54
59
|
box-shadow: var(--focus-outline-primary);
|
|
55
60
|
border-radius: var(--focus-outline-radius);
|
|
56
61
|
}
|
|
@@ -14,12 +14,14 @@ class CustomInputClipboard extends LitElement {
|
|
|
14
14
|
kind : { type: String },
|
|
15
15
|
placeholder: { type: String },
|
|
16
16
|
required : { type: Boolean },
|
|
17
|
-
focused : { type: Boolean },
|
|
17
|
+
focused : { type: Boolean, reflect: true },
|
|
18
18
|
tooltip : { type: String },
|
|
19
19
|
invalid : { type: Boolean },
|
|
20
20
|
validated : { type: Boolean, reflect: true },
|
|
21
21
|
disabled : { type: Boolean, reflect: true },
|
|
22
|
-
value : { type: String }
|
|
22
|
+
value : { type: String },
|
|
23
|
+
/** Carried on the event detail, as on bd-custom-input. */
|
|
24
|
+
name : { type: String }
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
constructor() {
|
|
@@ -37,13 +39,32 @@ class CustomInputClipboard extends LitElement {
|
|
|
37
39
|
this.validated = false;
|
|
38
40
|
this.disabled = false;
|
|
39
41
|
this.value = "";
|
|
42
|
+
this.name = "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* THE EVENTS ARE bd-custom-input's.
|
|
46
|
+
This fired an event named input -- the native event's own name -- with
|
|
47
|
+
the bare string as detail, neither bubbling nor composed. The native
|
|
48
|
+
input from the inner <input> is composed and already leaves the shadow
|
|
49
|
+
root, so a listener on the host heard every keystroke twice, once with a
|
|
50
|
+
detail and once without, and a paste once. Now it is bd-input on every
|
|
51
|
+
change and bd-change on commit, each { value, name }, bubbling and
|
|
52
|
+
composed, as on bd-custom-input: one form handler reads all three fields.
|
|
53
|
+
A paste is a commit as well as a change, so it fires both. */
|
|
54
|
+
_emit(type) {
|
|
55
|
+
this.dispatchEvent(new CustomEvent(type, {
|
|
56
|
+
detail : { value: this.value, name: this.name },
|
|
57
|
+
bubbles : true,
|
|
58
|
+
composed: true
|
|
59
|
+
}));
|
|
40
60
|
}
|
|
41
61
|
|
|
42
62
|
async pasteFromClipboard() {
|
|
43
63
|
try {
|
|
44
64
|
const text = await navigator.clipboard.readText();
|
|
45
65
|
this.value = text;
|
|
46
|
-
this.
|
|
66
|
+
this._emit("bd-input");
|
|
67
|
+
this._emit("bd-change");
|
|
47
68
|
} catch (err) {
|
|
48
69
|
console.error("Clipboard paste failed:", err);
|
|
49
70
|
}
|
|
@@ -51,7 +72,11 @@ class CustomInputClipboard extends LitElement {
|
|
|
51
72
|
|
|
52
73
|
handleInput(e) {
|
|
53
74
|
this.value = e.target.value;
|
|
54
|
-
this.
|
|
75
|
+
this._emit("bd-input");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
handleChange() {
|
|
79
|
+
this._emit("bd-change");
|
|
55
80
|
}
|
|
56
81
|
|
|
57
82
|
willUpdate(changed) {
|
|
@@ -61,6 +86,21 @@ class CustomInputClipboard extends LitElement {
|
|
|
61
86
|
if (changed.has("size") || changed.has("kind")) this.size = resolveSize(this.size);
|
|
62
87
|
}
|
|
63
88
|
|
|
89
|
+
/* focused is the field's real focus, both ways. Setting it moves keyboard
|
|
90
|
+
focus into the <input> and clearing it blurs the field; focusing or
|
|
91
|
+
leaving the field by hand sets it, and it reflects, so the attribute says
|
|
92
|
+
whether the field has focus. It used to add a .focused class that painted
|
|
93
|
+
the focus ring on a field that did not have focus -- a second, false
|
|
94
|
+
indicator on any page that set it. The ring is now only the browser's
|
|
95
|
+
:focus-visible, which a text field shows whenever it is focused. */
|
|
96
|
+
updated(changed) {
|
|
97
|
+
if (!changed.has('focused')) return;
|
|
98
|
+
const input = this.shadowRoot.querySelector('input');
|
|
99
|
+
const has = this.shadowRoot.activeElement === input;
|
|
100
|
+
if (this.focused && !has) input?.focus();
|
|
101
|
+
if (!this.focused && has) input?.blur();
|
|
102
|
+
}
|
|
103
|
+
|
|
64
104
|
render() {
|
|
65
105
|
const inputClass = {
|
|
66
106
|
sm: "input-sm",
|
|
@@ -88,6 +128,10 @@ class CustomInputClipboard extends LitElement {
|
|
|
88
128
|
? "form-help-text--success"
|
|
89
129
|
: "form-help-text--default";
|
|
90
130
|
|
|
131
|
+
// The hint is the field's description, and the state is announced, not
|
|
132
|
+
// only drawn: invalid used to be a red border alone and the hint was never
|
|
133
|
+
// pointed at, so the reason a field was invalid reached a screen reader
|
|
134
|
+
// not at all. Both as bd-custom-input does them.
|
|
91
135
|
return html`
|
|
92
136
|
<div class="input-container">
|
|
93
137
|
${this.label
|
|
@@ -101,15 +145,19 @@ class CustomInputClipboard extends LitElement {
|
|
|
101
145
|
<div class="input-with-button">
|
|
102
146
|
<input
|
|
103
147
|
id="clipboard-input"
|
|
104
|
-
class="form-input-bd ${inputClass} ${
|
|
105
|
-
? "focused"
|
|
106
|
-
: ""} ${inputStateClass}"
|
|
148
|
+
class="form-input-bd ${inputClass} ${inputStateClass}"
|
|
107
149
|
type="${this.type}"
|
|
108
150
|
placeholder="${this.placeholder}"
|
|
109
151
|
.value="${this.value}"
|
|
110
152
|
?required="${this.required}"
|
|
111
153
|
?disabled="${this.disabled}"
|
|
154
|
+
name="${this.name || nothing}"
|
|
155
|
+
aria-invalid="${this.invalid ? "true" : nothing}"
|
|
156
|
+
aria-describedby="${this.tooltip ? "clipboard-input-help" : nothing}"
|
|
157
|
+
@focus="${() => { this.focused = true; }}"
|
|
158
|
+
@blur="${() => { this.focused = false; }}"
|
|
112
159
|
@input="${this.handleInput}"
|
|
160
|
+
@change="${this.handleChange}"
|
|
113
161
|
/>
|
|
114
162
|
<button
|
|
115
163
|
class="paste-button"
|
|
@@ -139,7 +187,7 @@ class CustomInputClipboard extends LitElement {
|
|
|
139
187
|
</div>
|
|
140
188
|
|
|
141
189
|
${this.tooltip
|
|
142
|
-
? html`<div class="form-help-text ${helpTextClass}">
|
|
190
|
+
? html`<div id="clipboard-input-help" class="form-help-text ${helpTextClass}">
|
|
143
191
|
${this.tooltip}
|
|
144
192
|
</div>`
|
|
145
193
|
: ""}
|
|
@@ -18,7 +18,7 @@ class CustomInput extends LitElement {
|
|
|
18
18
|
kind : { type: String },
|
|
19
19
|
placeholder : { type: String },
|
|
20
20
|
required : { type: Boolean },
|
|
21
|
-
focused : { type: Boolean },
|
|
21
|
+
focused : { type: Boolean, reflect: true },
|
|
22
22
|
tooltip : { type: String },
|
|
23
23
|
invalid : { type: Boolean },
|
|
24
24
|
validated : { type: Boolean, reflect: true },
|
|
@@ -86,6 +86,21 @@ class CustomInput extends LitElement {
|
|
|
86
86
|
if (changed.has("size") || changed.has("kind")) this.size = resolveSize(this.size);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/* focused is the field's real focus, both ways. Setting it moves keyboard
|
|
90
|
+
focus into the <input> and clearing it blurs the field; focusing or
|
|
91
|
+
leaving the field by hand sets it, and it reflects, so the attribute says
|
|
92
|
+
whether the field has focus. It used to add a .focused class that painted
|
|
93
|
+
the focus ring on a field that did not have focus -- a second, false
|
|
94
|
+
indicator on any page that set it. The ring is now only the browser's
|
|
95
|
+
:focus-visible, which a text field shows whenever it is focused. */
|
|
96
|
+
updated(changed) {
|
|
97
|
+
if (!changed.has('focused')) return;
|
|
98
|
+
const input = this.shadowRoot.querySelector('input');
|
|
99
|
+
const has = this.shadowRoot.activeElement === input;
|
|
100
|
+
if (this.focused && !has) input?.focus();
|
|
101
|
+
if (!this.focused && has) input?.blur();
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
render() {
|
|
90
105
|
const inputClass = { sm: "input-sm", md: "input-md", lg: "input-lg" }[this.size];
|
|
91
106
|
const labelClass = { sm: "label-sm", md: "label-md", lg: "label-lg" }[this.size];
|
|
@@ -116,7 +131,7 @@ class CustomInput extends LitElement {
|
|
|
116
131
|
: nothing}
|
|
117
132
|
<input
|
|
118
133
|
id="${INPUT_ID}"
|
|
119
|
-
class="form-input-bd ${inputClass} ${
|
|
134
|
+
class="form-input-bd ${inputClass} ${inputStateClass}"
|
|
120
135
|
type="${this.type}"
|
|
121
136
|
name="${this.name || nothing}"
|
|
122
137
|
placeholder="${this.placeholder}"
|
|
@@ -128,6 +143,8 @@ class CustomInput extends LitElement {
|
|
|
128
143
|
?disabled="${this.disabled}"
|
|
129
144
|
aria-invalid="${this.invalid ? "true" : nothing}"
|
|
130
145
|
aria-describedby="${this.tooltip ? HELP_ID : nothing}"
|
|
146
|
+
@focus="${() => { this.focused = true; }}"
|
|
147
|
+
@blur="${() => { this.focused = false; }}"
|
|
131
148
|
@input="${this._onInput}"
|
|
132
149
|
@change="${this._onChange}"
|
|
133
150
|
/>
|
|
@@ -4,6 +4,13 @@ import '../../tokens/tokens.js';
|
|
|
4
4
|
import { labelCSS } from './label.css.js';
|
|
5
5
|
import { resolveSize } from '../shared/prop-vocabulary.js';
|
|
6
6
|
|
|
7
|
+
let labelIds = 0;
|
|
8
|
+
|
|
9
|
+
/* Controls whose own <label for> would have worked: the ones a label can be
|
|
10
|
+
for. A custom element is not here -- see the note on _target. */
|
|
11
|
+
const LABELABLE = new Set(["input", "select", "textarea", "button", "meter", "output", "progress"]);
|
|
12
|
+
const ACTIVATES = new Set(["checkbox", "radio"]);
|
|
13
|
+
|
|
7
14
|
export class BdLabel extends LitElement {
|
|
8
15
|
static styles = [labelCSS];
|
|
9
16
|
|
|
@@ -17,6 +24,7 @@ export class BdLabel extends LitElement {
|
|
|
17
24
|
super();
|
|
18
25
|
this.size = 'md';
|
|
19
26
|
this.required = false;
|
|
27
|
+
this.addEventListener('click', e => this._activate(e));
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
willUpdate(changed) {
|
|
@@ -24,9 +32,87 @@ export class BdLabel extends LitElement {
|
|
|
24
32
|
if (changed.has('size')) this.size = resolveSize(this.size, 'md');
|
|
25
33
|
}
|
|
26
34
|
|
|
35
|
+
/* WHY for IS DONE BY HAND.
|
|
36
|
+
The <label> this renders is in its own shadow root, and a for reference
|
|
37
|
+
resolves only in the tree it is written in, so <bd-label for="email">
|
|
38
|
+
pointed at nothing: the control went unnamed and a click on the label did
|
|
39
|
+
nothing. It now does what a native label's for does, for a native control
|
|
40
|
+
in the same tree as this element:
|
|
41
|
+
|
|
42
|
+
name the control gets aria-labelledby pointing at this element,
|
|
43
|
+
added to whatever it already lists, and taken back when for
|
|
44
|
+
changes or this element leaves -- so the control is named by
|
|
45
|
+
the label's text
|
|
46
|
+
activate a click on the label focuses the control, and toggles it
|
|
47
|
+
when it is a checkbox or a radio
|
|
48
|
+
|
|
49
|
+
The control may not exist yet when this renders -- it can come later in
|
|
50
|
+
the page, or be rendered after -- so until it is found the root is
|
|
51
|
+
watched for it. */
|
|
52
|
+
connectedCallback() {
|
|
53
|
+
super.connectedCallback();
|
|
54
|
+
this._link();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
disconnectedCallback() {
|
|
58
|
+
this._unlink();
|
|
59
|
+
super.disconnectedCallback();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
updated(changed) {
|
|
63
|
+
if (changed.has('for')) this._link();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* The control, when for names one in this element's own tree. A custom
|
|
67
|
+
element is skipped: its real control is inside its own shadow root, where
|
|
68
|
+
aria-labelledby on the host does not reach -- label a bd-custom-input
|
|
69
|
+
with its own label prop. */
|
|
70
|
+
get _target() {
|
|
71
|
+
if (!this.for || !this.isConnected) return null;
|
|
72
|
+
const el = this.getRootNode().getElementById?.(this.for);
|
|
73
|
+
return el && LABELABLE.has(el.localName) ? el : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_link() {
|
|
77
|
+
this._unlink();
|
|
78
|
+
if (!this.for || !this.isConnected) return;
|
|
79
|
+
const target = this._target;
|
|
80
|
+
if (!target) {
|
|
81
|
+
// not there yet: watch for it, and stop as soon as it is
|
|
82
|
+
const root = this.getRootNode();
|
|
83
|
+
this._waiting = new MutationObserver(() => { if (this._target) this._link(); });
|
|
84
|
+
this._waiting.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['id'] });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!this.id) this.id = `bd-label-${++labelIds}`;
|
|
88
|
+
const ids = (target.getAttribute('aria-labelledby') || '').split(/\s+/).filter(Boolean);
|
|
89
|
+
if (!ids.includes(this.id)) target.setAttribute('aria-labelledby', [...ids, this.id].join(' '));
|
|
90
|
+
this._linked = { target, id: this.id };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_unlink() {
|
|
94
|
+
this._waiting?.disconnect();
|
|
95
|
+
this._waiting = null;
|
|
96
|
+
if (!this._linked) return;
|
|
97
|
+
const { target, id } = this._linked;
|
|
98
|
+
const ids = (target.getAttribute('aria-labelledby') || '').split(/\s+/).filter(t => t && t !== id);
|
|
99
|
+
if (ids.length) target.setAttribute('aria-labelledby', ids.join(' '));
|
|
100
|
+
else target.removeAttribute('aria-labelledby');
|
|
101
|
+
this._linked = null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_activate(e) {
|
|
105
|
+
const target = this._target;
|
|
106
|
+
if (!target || target.disabled) return;
|
|
107
|
+
// a link or a control slotted into the label keeps its own click
|
|
108
|
+
if (e.composedPath().some(n => n !== this && n.matches?.('a[href], button, input, select, textarea'))) return;
|
|
109
|
+
target.focus();
|
|
110
|
+
if (target.localName === 'input' && ACTIVATES.has(target.type)) target.click();
|
|
111
|
+
}
|
|
112
|
+
|
|
27
113
|
render() {
|
|
28
114
|
return html`
|
|
29
|
-
<label
|
|
115
|
+
<label>
|
|
30
116
|
<slot></slot>${this.required
|
|
31
117
|
? html`<span class="required" aria-hidden="true">*</span>`
|
|
32
118
|
: nothing}
|