@unofficialbox/box-open-elements 0.12.0 → 0.14.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/dist/components/feedback/alert.d.ts +2 -0
- package/dist/components/feedback/alert.js +40 -2
- package/dist/components/feedback/indicator.d.ts +34 -0
- package/dist/components/feedback/indicator.js +167 -0
- package/dist/components/feedback/toast.js +1 -17
- package/dist/components/feedback/tone.d.ts +18 -0
- package/dist/components/feedback/tone.js +23 -0
- package/dist/components/forms/calendar.d.ts +25 -0
- package/dist/components/forms/calendar.js +94 -2
- package/dist/components/forms/tile-group.d.ts +58 -0
- package/dist/components/forms/tile-group.js +280 -0
- package/dist/components/output/code-block.d.ts +49 -0
- package/dist/components/output/code-block.js +236 -0
- package/dist/components/output/formatted-date.d.ts +38 -0
- package/dist/components/output/formatted-date.js +90 -0
- package/dist/components/output/formatted-file-size.d.ts +31 -0
- package/dist/components/output/formatted-file-size.js +61 -0
- package/dist/components/output/formatted-number.d.ts +53 -0
- package/dist/components/output/formatted-number.js +130 -0
- package/dist/components/output/formatted-value.d.ts +44 -0
- package/dist/components/output/formatted-value.js +100 -0
- package/dist/components/output/index.d.ts +6 -0
- package/dist/components/output/index.js +9 -0
- package/dist/components/output/relative-time.d.ts +41 -0
- package/dist/components/output/relative-time.js +82 -0
- package/dist/element-maps.d.ts +15 -1
- package/dist/entries/code-block.d.ts +1 -0
- package/dist/entries/code-block.js +1 -0
- package/dist/entries/formatted-date.d.ts +1 -0
- package/dist/entries/formatted-date.js +1 -0
- package/dist/entries/formatted-file-size.d.ts +1 -0
- package/dist/entries/formatted-file-size.js +1 -0
- package/dist/entries/formatted-number.d.ts +1 -0
- package/dist/entries/formatted-number.js +1 -0
- package/dist/entries/indicator.d.ts +1 -0
- package/dist/entries/indicator.js +1 -0
- package/dist/entries/relative-time.d.ts +1 -0
- package/dist/entries/relative-time.js +1 -0
- package/dist/entries/tile-group.d.ts +1 -0
- package/dist/entries/tile-group.js +1 -0
- package/dist/foundations/format/format.d.ts +82 -0
- package/dist/foundations/format/format.js +122 -0
- package/dist/foundations/format/index.d.ts +1 -0
- package/dist/foundations/format/index.js +3 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +11 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
-
import { toneAccessibleLabel } from "./tone.js";
|
|
2
|
+
import { toneAccessibleLabel, toneIcon } from "./tone.js";
|
|
3
3
|
import { boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
4
4
|
import { boeNeutralInteractiveStyles } from "../../foundations/tokens/index.js";
|
|
5
5
|
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
@@ -15,7 +15,13 @@ const alertStyles = `
|
|
|
15
15
|
display: none;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/* The accent colours the glyph only. The tinted backgrounds below are pinned
|
|
19
|
+
by the colour conformance manifest against upstream box-ui-elements and are
|
|
20
|
+
deliberately not derived from it; upstream has no glyph, so adding one
|
|
21
|
+
costs no conformance. */
|
|
18
22
|
[part="alert"] {
|
|
23
|
+
--alert-accent: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
24
|
+
|
|
19
25
|
display: flex;
|
|
20
26
|
align-items: start;
|
|
21
27
|
justify-content: space-between;
|
|
@@ -29,23 +35,44 @@ const alertStyles = `
|
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
[part="alert"][data-tone="info"] {
|
|
38
|
+
--alert-accent: var(--boe-token-surface-surface-brand, #0061d5);
|
|
32
39
|
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 10%, #fff);
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
[part="alert"][data-tone="success"] {
|
|
43
|
+
--alert-accent: var(--boe-token-surface-status-surface-success, #26c281);
|
|
36
44
|
background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 10%, #fff);
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
[part="alert"][data-tone="error"] {
|
|
48
|
+
--alert-accent: var(--boe-token-surface-status-surface-error, #ed3757);
|
|
40
49
|
background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 10%, #fff);
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
[part="alert"][data-tone="warning"],
|
|
44
53
|
[part="alert"][data-tone="inprogress"] {
|
|
54
|
+
--alert-accent: var(--boe-token-surface-status-surface-inprogress, #f5b31b);
|
|
45
55
|
background: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 10%, #fff);
|
|
46
56
|
}
|
|
47
57
|
|
|
58
|
+
[part="icon"] {
|
|
59
|
+
flex: 0 0 auto;
|
|
60
|
+
inline-size: 20px;
|
|
61
|
+
block-size: 20px;
|
|
62
|
+
/* Full strength: with only a 10% tint behind it, the glyph is where the
|
|
63
|
+
tone actually reads. */
|
|
64
|
+
color: var(--alert-accent);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[part="icon"] svg {
|
|
68
|
+
display: block;
|
|
69
|
+
inline-size: 100%;
|
|
70
|
+
block-size: 100%;
|
|
71
|
+
}
|
|
72
|
+
|
|
48
73
|
[part="content"] {
|
|
74
|
+
flex: 1 1 auto;
|
|
75
|
+
min-inline-size: 0;
|
|
49
76
|
display: grid;
|
|
50
77
|
gap: ${boeSpace[1]};
|
|
51
78
|
line-height: 1.45;
|
|
@@ -123,7 +150,9 @@ export class Alert extends BaseElement {
|
|
|
123
150
|
}
|
|
124
151
|
openValue = true;
|
|
125
152
|
alertEl;
|
|
153
|
+
iconEl;
|
|
126
154
|
toneLabelEl;
|
|
155
|
+
renderedTone = null;
|
|
127
156
|
titleEl;
|
|
128
157
|
messageEl;
|
|
129
158
|
richSlot;
|
|
@@ -196,6 +225,7 @@ export class Alert extends BaseElement {
|
|
|
196
225
|
this.shadowRoot.innerHTML = `
|
|
197
226
|
<style>${alertStyles}</style>
|
|
198
227
|
<div part="alert" role="status" aria-live="polite">
|
|
228
|
+
<span part="icon" aria-hidden="true"></span>
|
|
199
229
|
<div part="content">
|
|
200
230
|
<span part="tone-label" class="sr-only"></span>
|
|
201
231
|
<h2 part="title" id="alert-title" hidden></h2>
|
|
@@ -206,6 +236,7 @@ export class Alert extends BaseElement {
|
|
|
206
236
|
</div>
|
|
207
237
|
`;
|
|
208
238
|
this.alertEl = this.shadowRoot.querySelector('[part="alert"]');
|
|
239
|
+
this.iconEl = this.shadowRoot.querySelector('[part="icon"]');
|
|
209
240
|
this.toneLabelEl = this.shadowRoot.querySelector('[part="tone-label"]');
|
|
210
241
|
this.titleEl = this.shadowRoot.querySelector('[part="title"]');
|
|
211
242
|
this.messageEl = this.shadowRoot.querySelector('[part~="description"]');
|
|
@@ -237,7 +268,14 @@ export class Alert extends BaseElement {
|
|
|
237
268
|
return;
|
|
238
269
|
}
|
|
239
270
|
this.alertEl.dataset.tone = this.tone;
|
|
240
|
-
|
|
271
|
+
const tone = this.tone;
|
|
272
|
+
this.toneLabelEl.textContent = toneAccessibleLabel(tone);
|
|
273
|
+
// Literal markup keyed by tone, so re-writing it on every update would be
|
|
274
|
+
// wasted work — and would restart any transition on the glyph.
|
|
275
|
+
if (tone !== this.renderedTone) {
|
|
276
|
+
this.renderedTone = tone;
|
|
277
|
+
this.iconEl.innerHTML = toneIcon(tone);
|
|
278
|
+
}
|
|
241
279
|
if (this.heading) {
|
|
242
280
|
this.alertEl.setAttribute("aria-labelledby", "alert-title");
|
|
243
281
|
this.alertEl.removeAttribute("aria-label");
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* A status mark, for dense lists where a full badge would not fit.
|
|
4
|
+
*
|
|
5
|
+
* ```html
|
|
6
|
+
* <box-indicator tone="success" label="Signed"></box-indicator>
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* Distinct in shape as well as colour, so a status column survives being read
|
|
10
|
+
* by someone who cannot separate green from amber — the same rule the alert and
|
|
11
|
+
* toast glyphs follow.
|
|
12
|
+
*
|
|
13
|
+
* When there is no visible `label` the tone is still stated for assistive
|
|
14
|
+
* technology, because a bare dot otherwise announces nothing at all. A host
|
|
15
|
+
* whose surrounding text already says "Signed" should pass that as the label
|
|
16
|
+
* rather than leaving a reader to infer it from a colour.
|
|
17
|
+
*/
|
|
18
|
+
export declare class Indicator extends BaseElement {
|
|
19
|
+
static readonly tagName: string;
|
|
20
|
+
static get observedAttributes(): string[];
|
|
21
|
+
private shapeEl;
|
|
22
|
+
private labelEl;
|
|
23
|
+
private toneLabelEl;
|
|
24
|
+
private renderedTone;
|
|
25
|
+
/** The status. Unknown tones render the neutral disc. */
|
|
26
|
+
get tone(): string;
|
|
27
|
+
set tone(value: string);
|
|
28
|
+
/** Visible text beside the mark. Optional. */
|
|
29
|
+
get label(): string;
|
|
30
|
+
set label(value: string);
|
|
31
|
+
protected renderTemplate(): void;
|
|
32
|
+
protected setupListeners(): void;
|
|
33
|
+
protected update(): void;
|
|
34
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { toneAccessibleLabel } from "./tone.js";
|
|
3
|
+
const DEFAULT_TAG_NAME = "box-indicator";
|
|
4
|
+
/**
|
|
5
|
+
* Indicator shapes, one per tone.
|
|
6
|
+
*
|
|
7
|
+
* The shape is the point. A row of coloured dots is unreadable to anyone who
|
|
8
|
+
* cannot separate the colours, and status is exactly the information a reader
|
|
9
|
+
* most needs from a dense list. Carbon ships both an icon and a shape
|
|
10
|
+
* indicator for this reason; here the shape *is* the indicator, and colour is
|
|
11
|
+
* the redundant channel rather than the only one.
|
|
12
|
+
*/
|
|
13
|
+
const INDICATOR_SHAPES = {
|
|
14
|
+
// A filled disc: the neutral resting state.
|
|
15
|
+
info: `<svg viewBox="0 0 12 12" fill="currentColor" focusable="false"><circle cx="6" cy="6" r="5"/></svg>`,
|
|
16
|
+
// A disc with a tick cut into it, matching the success glyph on alert/toast.
|
|
17
|
+
success: `<svg viewBox="0 0 12 12" fill="currentColor" focusable="false"><path d="M6 1a5 5 0 100 10A5 5 0 006 1zm2.5 3.6L5.6 8a.7.7 0 01-1 0L3.5 6.8a.7.7 0 111-1l.6.7 2.4-2.8a.7.7 0 011 .9z"/></svg>`,
|
|
18
|
+
// A triangle, distinguishable from a disc at a glance and at small sizes.
|
|
19
|
+
warning: `<svg viewBox="0 0 12 12" fill="currentColor" focusable="false"><path d="M5.5 1.4 0.5 10a.6.6 0 00.5.9h10a.6.6 0 00.5-.9l-5-8.6a.6.6 0 00-1 0z"/></svg>`,
|
|
20
|
+
// A diamond: a square is too close to a disc once antialiased at 10px.
|
|
21
|
+
error: `<svg viewBox="0 0 12 12" fill="currentColor" focusable="false"><path d="M6 0.6 11.4 6 6 11.4.6 6z"/></svg>`,
|
|
22
|
+
// A hollow ring: nothing has happened yet, so nothing is filled in.
|
|
23
|
+
pending: `<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" focusable="false"><circle cx="6" cy="6" r="4"/></svg>`,
|
|
24
|
+
};
|
|
25
|
+
/** The shape for a tone, falling back to the neutral disc. */
|
|
26
|
+
const indicatorShape = (tone) => INDICATOR_SHAPES[tone] ?? INDICATOR_SHAPES.info;
|
|
27
|
+
const indicatorStyles = `
|
|
28
|
+
:host {
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 0.4rem;
|
|
32
|
+
color: inherit;
|
|
33
|
+
font: inherit;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
:host([hidden]) {
|
|
37
|
+
display: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
[part="shape"] {
|
|
41
|
+
flex: 0 0 auto;
|
|
42
|
+
inline-size: 0.62rem;
|
|
43
|
+
block-size: 0.62rem;
|
|
44
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
[part="shape"] svg {
|
|
48
|
+
display: block;
|
|
49
|
+
inline-size: 100%;
|
|
50
|
+
block-size: 100%;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:host([tone="info"]) [part="shape"] {
|
|
54
|
+
color: var(--boe-token-surface-surface-brand, #0061d5);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
:host([tone="success"]) [part="shape"] {
|
|
58
|
+
color: var(--boe-token-surface-status-surface-success, #26c281);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
:host([tone="error"]) [part="shape"] {
|
|
62
|
+
color: var(--boe-token-surface-status-surface-error, #ed3757);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
:host([tone="warning"]) [part="shape"] {
|
|
66
|
+
color: var(--boe-token-surface-status-surface-inprogress, #f5b31b);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
[part="label"] {
|
|
70
|
+
min-inline-size: 0;
|
|
71
|
+
overflow-wrap: anywhere;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* With no label the dot stands alone, and an empty span would still take a
|
|
75
|
+
line box and the flex gap beside it. */
|
|
76
|
+
[part="label"]:empty {
|
|
77
|
+
display: none;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.sr-only {
|
|
81
|
+
position: absolute;
|
|
82
|
+
inline-size: 1px;
|
|
83
|
+
block-size: 1px;
|
|
84
|
+
padding: 0;
|
|
85
|
+
margin: -1px;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
clip-path: inset(50%);
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
border: 0;
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
92
|
+
/**
|
|
93
|
+
* A status mark, for dense lists where a full badge would not fit.
|
|
94
|
+
*
|
|
95
|
+
* ```html
|
|
96
|
+
* <box-indicator tone="success" label="Signed"></box-indicator>
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* Distinct in shape as well as colour, so a status column survives being read
|
|
100
|
+
* by someone who cannot separate green from amber — the same rule the alert and
|
|
101
|
+
* toast glyphs follow.
|
|
102
|
+
*
|
|
103
|
+
* When there is no visible `label` the tone is still stated for assistive
|
|
104
|
+
* technology, because a bare dot otherwise announces nothing at all. A host
|
|
105
|
+
* whose surrounding text already says "Signed" should pass that as the label
|
|
106
|
+
* rather than leaving a reader to infer it from a colour.
|
|
107
|
+
*/
|
|
108
|
+
export class Indicator extends BaseElement {
|
|
109
|
+
static tagName = DEFAULT_TAG_NAME;
|
|
110
|
+
static get observedAttributes() {
|
|
111
|
+
return ["label", "tone"];
|
|
112
|
+
}
|
|
113
|
+
shapeEl;
|
|
114
|
+
labelEl;
|
|
115
|
+
toneLabelEl;
|
|
116
|
+
renderedTone = null;
|
|
117
|
+
/** The status. Unknown tones render the neutral disc. */
|
|
118
|
+
get tone() {
|
|
119
|
+
return this.getAttribute("tone") ?? "info";
|
|
120
|
+
}
|
|
121
|
+
set tone(value) {
|
|
122
|
+
this.setAttribute("tone", value);
|
|
123
|
+
}
|
|
124
|
+
/** Visible text beside the mark. Optional. */
|
|
125
|
+
get label() {
|
|
126
|
+
return this.getAttribute("label") ?? "";
|
|
127
|
+
}
|
|
128
|
+
set label(value) {
|
|
129
|
+
if (!value) {
|
|
130
|
+
this.removeAttribute("label");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.setAttribute("label", value);
|
|
134
|
+
}
|
|
135
|
+
renderTemplate() {
|
|
136
|
+
if (!this.shadowRoot) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.shadowRoot.innerHTML = `
|
|
140
|
+
<style>${indicatorStyles}</style>
|
|
141
|
+
<span part="shape" aria-hidden="true"></span>
|
|
142
|
+
<span part="tone-label" class="sr-only"></span>
|
|
143
|
+
<span part="label"></span>
|
|
144
|
+
`;
|
|
145
|
+
this.shapeEl = this.shadowRoot.querySelector('[part="shape"]');
|
|
146
|
+
this.toneLabelEl = this.shadowRoot.querySelector('[part="tone-label"]');
|
|
147
|
+
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
148
|
+
}
|
|
149
|
+
setupListeners() {
|
|
150
|
+
// Display only.
|
|
151
|
+
}
|
|
152
|
+
update() {
|
|
153
|
+
if (!this.shapeEl) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const tone = this.tone;
|
|
157
|
+
if (tone !== this.renderedTone) {
|
|
158
|
+
this.renderedTone = tone;
|
|
159
|
+
this.shapeEl.innerHTML = indicatorShape(tone);
|
|
160
|
+
}
|
|
161
|
+
this.labelEl.textContent = this.label;
|
|
162
|
+
// Only speak the tone when no visible label carries it; otherwise a reader
|
|
163
|
+
// hears "Success Signed" where the screen says "Signed".
|
|
164
|
+
this.toneLabelEl.textContent = this.label ? "" : toneAccessibleLabel(tone);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
Indicator.register();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
-
import { toneAccessibleLabel } from "./tone.js";
|
|
2
|
+
import { toneAccessibleLabel, toneIcon } from "./tone.js";
|
|
3
3
|
import { boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
4
4
|
import { boeFocusVisibleStyles } from "../../foundations/tokens/index.js";
|
|
5
5
|
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
@@ -7,22 +7,6 @@ const DEFAULT_TAG_NAME = "box-toast";
|
|
|
7
7
|
const TOAST_MODES = new Set(["dismissible", "sticky"]);
|
|
8
8
|
/** Narrow an author-supplied mode, falling back to `dismissible`. */
|
|
9
9
|
export const resolveToastMode = (value) => TOAST_MODES.has(value) ? value : "dismissible";
|
|
10
|
-
/**
|
|
11
|
-
* Status glyphs, one per tone.
|
|
12
|
-
*
|
|
13
|
-
* A toast is read at a glance and often out of the corner of an eye, so the
|
|
14
|
-
* shape has to carry the meaning before the colour does — a round tick and a
|
|
15
|
-
* warning triangle are distinguishable to a reader who cannot separate green
|
|
16
|
-
* from amber. Literal markup, never author input, so innerHTML is safe here.
|
|
17
|
-
*/
|
|
18
|
-
const TONE_ICONS = {
|
|
19
|
-
info: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 3.4a1.15 1.15 0 110 2.3 1.15 1.15 0 010-2.3zM11.1 15H8.9V9.3h2.2V15z"/></svg>`,
|
|
20
|
-
success: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm4.06 5.86-5 5.2a1 1 0 01-1.44 0L5.94 11.3A1 1 0 117.38 9.9l1.04 1.08 4.28-4.52a1 1 0 011.36 1.38z"/></svg>`,
|
|
21
|
-
warning: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M9.13 2.6 1.4 15.9a1 1 0 00.87 1.5h15.46a1 1 0 00.87-1.5L10.87 2.6a1 1 0 00-1.74 0zM11.1 15.4H8.9v-2.2h2.2v2.2zm0-3.4H8.9V7.6h2.2V12z"/></svg>`,
|
|
22
|
-
error: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 2c1.29 0 2.48.41 3.46 1.1L5.1 13.46A6 6 0 0110 4zm0 12c-1.29 0-2.48-.41-3.46-1.1l8.36-8.36A6 6 0 0110 16z"/></svg>`,
|
|
23
|
-
};
|
|
24
|
-
/** The tone glyph, falling back to the info mark for an unknown tone. */
|
|
25
|
-
const toneIcon = (tone) => TONE_ICONS[tone] ?? TONE_ICONS.info;
|
|
26
10
|
const DISMISS_ICON = `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" focusable="false"><path d="M5.5 5.5l9 9M14.5 5.5l-9 9"/></svg>`;
|
|
27
11
|
const toastStyles = `
|
|
28
12
|
:host {
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* readers, and both must say the *same* words: a reader who hears "In progress"
|
|
6
6
|
* from an alert should not hear "Inprogress" from a toast reporting the same
|
|
7
7
|
* thing. One definition is what keeps them in step.
|
|
8
|
+
*
|
|
9
|
+
* The glyphs live here for the same reason. Both surfaces render them, and a
|
|
10
|
+
* success tick that differs between an alert and a toast reporting the same
|
|
11
|
+
* outcome is a defect a reader would notice before either team did.
|
|
8
12
|
*/
|
|
9
13
|
/**
|
|
10
14
|
* The tone as a word, for assistive technology.
|
|
@@ -14,3 +18,17 @@
|
|
|
14
18
|
* a host that invents a tone should still get something spoken.
|
|
15
19
|
*/
|
|
16
20
|
export declare const toneAccessibleLabel: (tone: string) => string;
|
|
21
|
+
/**
|
|
22
|
+
* Status glyphs, one per tone.
|
|
23
|
+
*
|
|
24
|
+
* These surfaces are read at a glance, often out of the corner of an eye, so
|
|
25
|
+
* the shape has to carry the meaning before the colour does — a round tick and
|
|
26
|
+
* a warning triangle are distinguishable to a reader who cannot separate green
|
|
27
|
+
* from amber. That matters most where the fill is faint: `box-alert` tints its
|
|
28
|
+
* background by only 10%, which on its own is a weak signal.
|
|
29
|
+
*
|
|
30
|
+
* Literal markup, never author input, so innerHTML is safe here.
|
|
31
|
+
*/
|
|
32
|
+
export declare const TONE_ICONS: Record<string, string>;
|
|
33
|
+
/** The tone glyph, falling back to the info mark for an unknown tone. */
|
|
34
|
+
export declare const toneIcon: (tone: string) => string;
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* readers, and both must say the *same* words: a reader who hears "In progress"
|
|
6
6
|
* from an alert should not hear "Inprogress" from a toast reporting the same
|
|
7
7
|
* thing. One definition is what keeps them in step.
|
|
8
|
+
*
|
|
9
|
+
* The glyphs live here for the same reason. Both surfaces render them, and a
|
|
10
|
+
* success tick that differs between an alert and a toast reporting the same
|
|
11
|
+
* outcome is a defect a reader would notice before either team did.
|
|
8
12
|
*/
|
|
9
13
|
/**
|
|
10
14
|
* The tone as a word, for assistive technology.
|
|
@@ -29,3 +33,22 @@ export const toneAccessibleLabel = (tone) => {
|
|
|
29
33
|
return tone.charAt(0).toUpperCase() + tone.slice(1);
|
|
30
34
|
}
|
|
31
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Status glyphs, one per tone.
|
|
38
|
+
*
|
|
39
|
+
* These surfaces are read at a glance, often out of the corner of an eye, so
|
|
40
|
+
* the shape has to carry the meaning before the colour does — a round tick and
|
|
41
|
+
* a warning triangle are distinguishable to a reader who cannot separate green
|
|
42
|
+
* from amber. That matters most where the fill is faint: `box-alert` tints its
|
|
43
|
+
* background by only 10%, which on its own is a weak signal.
|
|
44
|
+
*
|
|
45
|
+
* Literal markup, never author input, so innerHTML is safe here.
|
|
46
|
+
*/
|
|
47
|
+
export const TONE_ICONS = {
|
|
48
|
+
info: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 3.4a1.15 1.15 0 110 2.3 1.15 1.15 0 010-2.3zM11.1 15H8.9V9.3h2.2V15z"/></svg>`,
|
|
49
|
+
success: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm4.06 5.86-5 5.2a1 1 0 01-1.44 0L5.94 11.3A1 1 0 117.38 9.9l1.04 1.08 4.28-4.52a1 1 0 011.36 1.38z"/></svg>`,
|
|
50
|
+
warning: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M9.13 2.6 1.4 15.9a1 1 0 00.87 1.5h15.46a1 1 0 00.87-1.5L10.87 2.6a1 1 0 00-1.74 0zM11.1 15.4H8.9v-2.2h2.2v2.2zm0-3.4H8.9V7.6h2.2V12z"/></svg>`,
|
|
51
|
+
error: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 2c1.29 0 2.48.41 3.46 1.1L5.1 13.46A6 6 0 0110 4zm0 12c-1.29 0-2.48-.41-3.46-1.1l8.36-8.36A6 6 0 0110 16z"/></svg>`,
|
|
52
|
+
};
|
|
53
|
+
/** The tone glyph, falling back to the info mark for an unknown tone. */
|
|
54
|
+
export const toneIcon = (tone) => TONE_ICONS[tone] ?? TONE_ICONS.info;
|
|
@@ -27,6 +27,31 @@ export declare class Calendar extends BaseElement {
|
|
|
27
27
|
private isOutOfRange;
|
|
28
28
|
private goToMonth;
|
|
29
29
|
private shiftMonth;
|
|
30
|
+
/**
|
|
31
|
+
* `single` (default) or `range`.
|
|
32
|
+
*
|
|
33
|
+
* Range mode uses `start`/`end` rather than overloading `value`: a single
|
|
34
|
+
* attribute holding two dates would have to invent a separator, and every
|
|
35
|
+
* host would then have to parse it back out.
|
|
36
|
+
*/
|
|
37
|
+
get mode(): string;
|
|
38
|
+
set mode(next: string);
|
|
39
|
+
/** First date of the range, ISO. */
|
|
40
|
+
get start(): string;
|
|
41
|
+
set start(next: string);
|
|
42
|
+
/** Last date of the range, ISO. Empty while only one end has been picked. */
|
|
43
|
+
get end(): string;
|
|
44
|
+
set end(next: string);
|
|
45
|
+
/**
|
|
46
|
+
* Advance the range by one click.
|
|
47
|
+
*
|
|
48
|
+
* Three states, cycling: nothing chosen picks a start; a start alone closes
|
|
49
|
+
* the range; a complete range starts over. Picking an earlier day as the
|
|
50
|
+
* second click swaps the ends rather than rejecting it — a reader dragging
|
|
51
|
+
* backwards through a calendar means the same thing as dragging forwards,
|
|
52
|
+
* and refusing it would be pedantry.
|
|
53
|
+
*/
|
|
54
|
+
private selectRange;
|
|
30
55
|
private selectDate;
|
|
31
56
|
/** Move roving focus by `delta` days, following into the neighbouring month if needed. */
|
|
32
57
|
private moveActive;
|
|
@@ -172,6 +172,15 @@ const calendarStyles = `
|
|
|
172
172
|
background: var(--boe-token-surface-surface-brand-hover, #006ae9);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/* The span between the endpoints. A tint rather than the brand fill, so the
|
|
176
|
+
two chosen dates stay the things the eye lands on — the middle is context,
|
|
177
|
+
not a selection the reader made. */
|
|
178
|
+
[part~="day"][data-in-range="true"]:not([data-selected="true"]) {
|
|
179
|
+
background: var(--boe-token-surface-item-surface-selected, #f2f7fd);
|
|
180
|
+
border-color: transparent;
|
|
181
|
+
color: var(--boe-token-text-text, #222222);
|
|
182
|
+
}
|
|
183
|
+
|
|
175
184
|
[part~="day"]:focus-visible {
|
|
176
185
|
outline: none;
|
|
177
186
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 24%, transparent);
|
|
@@ -185,7 +194,7 @@ const calendarStyles = `
|
|
|
185
194
|
export class Calendar extends BaseElement {
|
|
186
195
|
static tagName = DEFAULT_TAG_NAME;
|
|
187
196
|
static get observedAttributes() {
|
|
188
|
-
return ["disabled", "max", "min", "month", "today", "value"];
|
|
197
|
+
return ["disabled", "end", "max", "min", "mode", "month", "start", "today", "value"];
|
|
189
198
|
}
|
|
190
199
|
/** The date the roving tabindex currently points at. */
|
|
191
200
|
activeDate = null;
|
|
@@ -283,11 +292,83 @@ export class Calendar extends BaseElement {
|
|
|
283
292
|
const next = new Date(Date.UTC(y, m - 1 + delta, 1));
|
|
284
293
|
this.goToMonth(next.getUTCFullYear(), next.getUTCMonth() + 1);
|
|
285
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* `single` (default) or `range`.
|
|
297
|
+
*
|
|
298
|
+
* Range mode uses `start`/`end` rather than overloading `value`: a single
|
|
299
|
+
* attribute holding two dates would have to invent a separator, and every
|
|
300
|
+
* host would then have to parse it back out.
|
|
301
|
+
*/
|
|
302
|
+
get mode() {
|
|
303
|
+
return this.getAttribute("mode") === "range" ? "range" : "single";
|
|
304
|
+
}
|
|
305
|
+
set mode(next) {
|
|
306
|
+
this.setAttribute("mode", next);
|
|
307
|
+
}
|
|
308
|
+
/** First date of the range, ISO. */
|
|
309
|
+
get start() {
|
|
310
|
+
return this.getAttribute("start") ?? "";
|
|
311
|
+
}
|
|
312
|
+
set start(next) {
|
|
313
|
+
if (!next) {
|
|
314
|
+
this.removeAttribute("start");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
this.setAttribute("start", next);
|
|
318
|
+
}
|
|
319
|
+
/** Last date of the range, ISO. Empty while only one end has been picked. */
|
|
320
|
+
get end() {
|
|
321
|
+
return this.getAttribute("end") ?? "";
|
|
322
|
+
}
|
|
323
|
+
set end(next) {
|
|
324
|
+
if (!next) {
|
|
325
|
+
this.removeAttribute("end");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
this.setAttribute("end", next);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Advance the range by one click.
|
|
332
|
+
*
|
|
333
|
+
* Three states, cycling: nothing chosen picks a start; a start alone closes
|
|
334
|
+
* the range; a complete range starts over. Picking an earlier day as the
|
|
335
|
+
* second click swaps the ends rather than rejecting it — a reader dragging
|
|
336
|
+
* backwards through a calendar means the same thing as dragging forwards,
|
|
337
|
+
* and refusing it would be pedantry.
|
|
338
|
+
*/
|
|
339
|
+
selectRange(iso) {
|
|
340
|
+
const start = this.start;
|
|
341
|
+
const end = this.end;
|
|
342
|
+
if (!start || end) {
|
|
343
|
+
this.start = iso;
|
|
344
|
+
this.end = "";
|
|
345
|
+
}
|
|
346
|
+
else if (iso < start) {
|
|
347
|
+
this.end = start;
|
|
348
|
+
this.start = iso;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
this.end = iso;
|
|
352
|
+
}
|
|
353
|
+
this.dispatchEvent(new CustomEvent("range-changed", {
|
|
354
|
+
bubbles: true,
|
|
355
|
+
composed: true,
|
|
356
|
+
detail: { end: this.end, start: this.start },
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
286
359
|
selectDate(date) {
|
|
287
360
|
const iso = toISO(date);
|
|
288
361
|
if (this.disabled || this.isOutOfRange(iso)) {
|
|
289
362
|
return;
|
|
290
363
|
}
|
|
364
|
+
if (this.mode === "range") {
|
|
365
|
+
this.selectRange(iso);
|
|
366
|
+
this.setAttribute("month", `${date.y}-${pad(date.m)}`);
|
|
367
|
+
this.activeDate = date;
|
|
368
|
+
this.update();
|
|
369
|
+
this.focusActive();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
291
372
|
this.setAttribute("value", iso);
|
|
292
373
|
// Keep the grid on the month of the chosen day.
|
|
293
374
|
this.setAttribute("month", `${date.y}-${pad(date.m)}`);
|
|
@@ -446,6 +527,9 @@ export class Calendar extends BaseElement {
|
|
|
446
527
|
}
|
|
447
528
|
const { y, m } = this.displayedMonth();
|
|
448
529
|
const selected = parseISO(this.value);
|
|
530
|
+
const isRange = this.mode === "range";
|
|
531
|
+
const rangeStart = isRange ? this.start : "";
|
|
532
|
+
const rangeEnd = isRange ? this.end : "";
|
|
449
533
|
const active = this.resolveActiveDate();
|
|
450
534
|
const activeIso = toISO({ y, m, d: clampDay(y, m, active.d) });
|
|
451
535
|
const todayIso = toISO(this.resolveToday());
|
|
@@ -459,7 +543,14 @@ export class Calendar extends BaseElement {
|
|
|
459
543
|
}
|
|
460
544
|
for (let day = 1; day <= total; day += 1) {
|
|
461
545
|
const iso = toISO({ y, m, d: day });
|
|
462
|
-
const isSelected =
|
|
546
|
+
const isSelected = isRange
|
|
547
|
+
? iso === rangeStart || iso === rangeEnd
|
|
548
|
+
: selected
|
|
549
|
+
? toISO(selected) === iso
|
|
550
|
+
: false;
|
|
551
|
+
// Strictly between the ends: the endpoints are "selected", not "in range",
|
|
552
|
+
// so they keep the brand fill rather than the tint.
|
|
553
|
+
const inRange = isRange && rangeStart !== "" && rangeEnd !== "" && iso > rangeStart && iso < rangeEnd;
|
|
463
554
|
const isToday = iso === todayIso;
|
|
464
555
|
const isActive = iso === activeIso;
|
|
465
556
|
const outOfRange = this.isOutOfRange(iso);
|
|
@@ -470,6 +561,7 @@ export class Calendar extends BaseElement {
|
|
|
470
561
|
role="gridcell"
|
|
471
562
|
data-date="${iso}"
|
|
472
563
|
data-selected="${isSelected ? "true" : "false"}"
|
|
564
|
+
data-in-range="${inRange ? "true" : "false"}"
|
|
473
565
|
data-today="${isToday ? "true" : "false"}"
|
|
474
566
|
tabindex="${isActive && !disabled ? "0" : "-1"}"
|
|
475
567
|
aria-selected="${isSelected ? "true" : "false"}"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
/** One choice. `description` is the second line; `disabled` greys it out. */
|
|
3
|
+
export interface TileOption {
|
|
4
|
+
description?: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
/** Attribute payloads are author input — validate every record. */
|
|
10
|
+
export declare const isTileOptionRecord: (value: unknown) => value is TileOption;
|
|
11
|
+
/**
|
|
12
|
+
* Choose one option, or several, as cards rather than radios.
|
|
13
|
+
*
|
|
14
|
+
* ```html
|
|
15
|
+
* <box-tile-group name="plan" options='[{"id":"team","label":"Team"}]'></box-tile-group>
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Each tile wraps a real `<input type="radio">` or `<input type="checkbox">`,
|
|
19
|
+
* visually hidden but present in the tab order and the accessibility tree. That
|
|
20
|
+
* is the whole design: grouping, arrow-key navigation between radios, the
|
|
21
|
+
* roving focus rules, and form participation all come from the platform rather
|
|
22
|
+
* than being reimplemented on `<div>`s and then subtly diverging from what a
|
|
23
|
+
* screen reader user expects.
|
|
24
|
+
*
|
|
25
|
+
* Use it where the choice deserves explaining — a plan, a permission level, a
|
|
26
|
+
* retention policy — and a bare radio label would not have room. Where the
|
|
27
|
+
* options are one word each, radios are smaller and better.
|
|
28
|
+
*/
|
|
29
|
+
export declare class TileGroup extends BaseElement {
|
|
30
|
+
static readonly tagName: string;
|
|
31
|
+
static get observedAttributes(): string[];
|
|
32
|
+
private groupEl;
|
|
33
|
+
private legendEl;
|
|
34
|
+
private optionsSignature;
|
|
35
|
+
/** The choices, as JSON. */
|
|
36
|
+
get options(): TileOption[];
|
|
37
|
+
set options(value: TileOption[]);
|
|
38
|
+
/** Several selections rather than one. Switches radios for checkboxes. */
|
|
39
|
+
get multiple(): boolean;
|
|
40
|
+
set multiple(value: boolean);
|
|
41
|
+
/** The radio group name. Required for radios to behave as one group. */
|
|
42
|
+
get name(): string;
|
|
43
|
+
set name(value: string);
|
|
44
|
+
/** Selected ids, comma-separated. A single id when not `multiple`. */
|
|
45
|
+
get value(): string;
|
|
46
|
+
set value(next: string);
|
|
47
|
+
/** Selected ids as an array, which is what a host actually wants. */
|
|
48
|
+
get selected(): string[];
|
|
49
|
+
get legend(): string;
|
|
50
|
+
set legend(value: string);
|
|
51
|
+
private optionsKey;
|
|
52
|
+
private rebuild;
|
|
53
|
+
private syncSelection;
|
|
54
|
+
private readSelection;
|
|
55
|
+
protected renderTemplate(): void;
|
|
56
|
+
protected setupListeners(): void;
|
|
57
|
+
protected update(): void;
|
|
58
|
+
}
|