@unofficialbox/box-open-elements 0.11.0 → 0.13.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 +41 -8
- package/dist/components/feedback/indicator.d.ts +34 -0
- package/dist/components/feedback/indicator.js +167 -0
- package/dist/components/feedback/nudge.js +0 -1
- package/dist/components/feedback/toast.d.ts +4 -5
- package/dist/components/feedback/toast.js +19 -46
- 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 +37 -0
- package/dist/components/output/formatted-number.js +106 -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 +17 -1
- package/dist/entries/code-block.d.ts +1 -0
- package/dist/entries/code-block.js +1 -0
- package/dist/entries/comment-thread.d.ts +1 -0
- package/dist/entries/comment-thread.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 +10 -0
- package/dist/index.js +12 -0
- package/dist/patterns/comments/comment-thread.d.ts +79 -0
- package/dist/patterns/comments/comment-thread.js +561 -0
- package/dist/patterns/comments/index.d.ts +2 -0
- package/dist/patterns/comments/index.js +5 -0
- package/dist/patterns/comments/types.d.ts +39 -0
- package/dist/patterns/comments/types.js +8 -0
- package/dist/patterns/preview/annotation-thread.d.ts +67 -48
- package/dist/patterns/preview/annotation-thread.js +96 -440
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { boeRadius } from "../../foundations/geometry/index.js";
|
|
3
|
+
import { boeFocusRingShadow } from "../../foundations/tokens/index.js";
|
|
4
|
+
const DEFAULT_TAG_NAME = "box-tile-group";
|
|
5
|
+
const escapeHtml = (value) => value
|
|
6
|
+
.replaceAll("&", "&")
|
|
7
|
+
.replaceAll("<", "<")
|
|
8
|
+
.replaceAll(">", ">")
|
|
9
|
+
.replaceAll('"', """)
|
|
10
|
+
.replaceAll("'", "'");
|
|
11
|
+
/** Attribute payloads are author input — validate every record. */
|
|
12
|
+
export const isTileOptionRecord = (value) => {
|
|
13
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const item = value;
|
|
17
|
+
return typeof item.id === "string" && typeof item.label === "string";
|
|
18
|
+
};
|
|
19
|
+
const tileStyles = `
|
|
20
|
+
:host {
|
|
21
|
+
display: block;
|
|
22
|
+
color: inherit;
|
|
23
|
+
font: inherit;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:host([hidden]) {
|
|
27
|
+
display: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
[part="group"] {
|
|
31
|
+
display: grid;
|
|
32
|
+
/* Tiles carry a label and often a description, so they need width to be
|
|
33
|
+
worth choosing over radios. Below the minimum they stack rather than
|
|
34
|
+
squeeze. */
|
|
35
|
+
grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
|
|
36
|
+
gap: 0.6rem;
|
|
37
|
+
margin: 0;
|
|
38
|
+
padding: 0;
|
|
39
|
+
border: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
[part="legend"] {
|
|
43
|
+
padding: 0 0 0.5rem;
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
color: var(--boe-token-text-text, #222222);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
[part="legend"]:empty {
|
|
49
|
+
display: none;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[part="tile"] {
|
|
53
|
+
position: relative;
|
|
54
|
+
display: grid;
|
|
55
|
+
gap: 0.2rem;
|
|
56
|
+
padding: 0.75rem 0.85rem;
|
|
57
|
+
border: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 85%, transparent);
|
|
58
|
+
border-radius: ${boeRadius.large};
|
|
59
|
+
background: var(--boe-token-surface-surface, #ffffff);
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
[part="tile"]:hover:not([data-disabled="true"]) {
|
|
64
|
+
background: var(--boe-token-surface-surface-hover, #f4f4f4);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[part="tile"][data-selected="true"] {
|
|
68
|
+
border-color: var(--boe-token-surface-surface-brand, #0061d5);
|
|
69
|
+
background: var(--boe-token-surface-item-surface-selected, #f2f7fd);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
[part="tile"][data-disabled="true"] {
|
|
73
|
+
cursor: not-allowed;
|
|
74
|
+
opacity: 0.5;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* The ring is drawn on the tile the native control sits inside, because the
|
|
78
|
+
control itself is visually hidden — without this, keyboard focus would be
|
|
79
|
+
invisible. */
|
|
80
|
+
[part="tile"]:has(input:focus-visible) {
|
|
81
|
+
box-shadow: ${boeFocusRingShadow};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Hidden from sight, not from the accessibility tree or the tab order: the
|
|
85
|
+
tile is a real radio or checkbox, so grouping, arrow-key behaviour and
|
|
86
|
+
form participation are the platform's rather than reimplemented. */
|
|
87
|
+
[part="control"] {
|
|
88
|
+
position: absolute;
|
|
89
|
+
inline-size: 1px;
|
|
90
|
+
block-size: 1px;
|
|
91
|
+
padding: 0;
|
|
92
|
+
margin: -1px;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
clip-path: inset(50%);
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
[part="tile-label"] {
|
|
99
|
+
font-weight: 600;
|
|
100
|
+
color: var(--boe-token-text-text, #222222);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
[part="tile-description"] {
|
|
104
|
+
font-size: 0.82rem;
|
|
105
|
+
line-height: 1.45;
|
|
106
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
[part="tile-description"]:empty {
|
|
110
|
+
display: none;
|
|
111
|
+
}
|
|
112
|
+
`;
|
|
113
|
+
/**
|
|
114
|
+
* Choose one option, or several, as cards rather than radios.
|
|
115
|
+
*
|
|
116
|
+
* ```html
|
|
117
|
+
* <box-tile-group name="plan" options='[{"id":"team","label":"Team"}]'></box-tile-group>
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* Each tile wraps a real `<input type="radio">` or `<input type="checkbox">`,
|
|
121
|
+
* visually hidden but present in the tab order and the accessibility tree. That
|
|
122
|
+
* is the whole design: grouping, arrow-key navigation between radios, the
|
|
123
|
+
* roving focus rules, and form participation all come from the platform rather
|
|
124
|
+
* than being reimplemented on `<div>`s and then subtly diverging from what a
|
|
125
|
+
* screen reader user expects.
|
|
126
|
+
*
|
|
127
|
+
* Use it where the choice deserves explaining — a plan, a permission level, a
|
|
128
|
+
* retention policy — and a bare radio label would not have room. Where the
|
|
129
|
+
* options are one word each, radios are smaller and better.
|
|
130
|
+
*/
|
|
131
|
+
export class TileGroup extends BaseElement {
|
|
132
|
+
static tagName = DEFAULT_TAG_NAME;
|
|
133
|
+
static get observedAttributes() {
|
|
134
|
+
return ["legend", "multiple", "name", "options", "value"];
|
|
135
|
+
}
|
|
136
|
+
groupEl;
|
|
137
|
+
legendEl;
|
|
138
|
+
optionsSignature = "";
|
|
139
|
+
/** The choices, as JSON. */
|
|
140
|
+
get options() {
|
|
141
|
+
const raw = this.getAttribute("options");
|
|
142
|
+
if (!raw) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(raw);
|
|
147
|
+
return Array.isArray(parsed) && parsed.every(isTileOptionRecord)
|
|
148
|
+
? parsed
|
|
149
|
+
: [];
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
set options(value) {
|
|
156
|
+
this.setAttribute("options", JSON.stringify(value));
|
|
157
|
+
}
|
|
158
|
+
/** Several selections rather than one. Switches radios for checkboxes. */
|
|
159
|
+
get multiple() {
|
|
160
|
+
return this.hasAttribute("multiple");
|
|
161
|
+
}
|
|
162
|
+
set multiple(value) {
|
|
163
|
+
this.toggleAttribute("multiple", value);
|
|
164
|
+
}
|
|
165
|
+
/** The radio group name. Required for radios to behave as one group. */
|
|
166
|
+
get name() {
|
|
167
|
+
return this.getAttribute("name") ?? "tile-group";
|
|
168
|
+
}
|
|
169
|
+
set name(value) {
|
|
170
|
+
this.setAttribute("name", value);
|
|
171
|
+
}
|
|
172
|
+
/** Selected ids, comma-separated. A single id when not `multiple`. */
|
|
173
|
+
get value() {
|
|
174
|
+
return this.getAttribute("value") ?? "";
|
|
175
|
+
}
|
|
176
|
+
set value(next) {
|
|
177
|
+
if (!next) {
|
|
178
|
+
this.removeAttribute("value");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.setAttribute("value", next);
|
|
182
|
+
}
|
|
183
|
+
/** Selected ids as an array, which is what a host actually wants. */
|
|
184
|
+
get selected() {
|
|
185
|
+
return this.value
|
|
186
|
+
.split(",")
|
|
187
|
+
.map(part => part.trim())
|
|
188
|
+
.filter(Boolean);
|
|
189
|
+
}
|
|
190
|
+
get legend() {
|
|
191
|
+
return this.getAttribute("legend") ?? "";
|
|
192
|
+
}
|
|
193
|
+
set legend(value) {
|
|
194
|
+
this.setAttribute("legend", value);
|
|
195
|
+
}
|
|
196
|
+
optionsKey() {
|
|
197
|
+
return `${this.getAttribute("options") ?? ""}|${String(this.multiple)}|${this.name}`;
|
|
198
|
+
}
|
|
199
|
+
rebuild() {
|
|
200
|
+
const type = this.multiple ? "checkbox" : "radio";
|
|
201
|
+
this.groupEl.innerHTML = this.options
|
|
202
|
+
.map(option => {
|
|
203
|
+
const id = escapeHtml(option.id);
|
|
204
|
+
return `
|
|
205
|
+
<label part="tile" data-option-id="${id}" data-selected="false" data-disabled="${option.disabled ? "true" : "false"}">
|
|
206
|
+
<input
|
|
207
|
+
part="control"
|
|
208
|
+
type="${type}"
|
|
209
|
+
name="${escapeHtml(this.name)}"
|
|
210
|
+
value="${id}"
|
|
211
|
+
${option.disabled ? "disabled" : ""}
|
|
212
|
+
/>
|
|
213
|
+
<span part="tile-label">${escapeHtml(option.label)}</span>
|
|
214
|
+
<span part="tile-description">${escapeHtml(option.description ?? "")}</span>
|
|
215
|
+
</label>
|
|
216
|
+
`;
|
|
217
|
+
})
|
|
218
|
+
.join("");
|
|
219
|
+
}
|
|
220
|
+
syncSelection() {
|
|
221
|
+
const selected = new Set(this.selected);
|
|
222
|
+
this.groupEl.querySelectorAll('[part="tile"]').forEach(tile => {
|
|
223
|
+
const id = tile.dataset.optionId ?? "";
|
|
224
|
+
const isSelected = selected.has(id);
|
|
225
|
+
tile.dataset.selected = isSelected ? "true" : "false";
|
|
226
|
+
const control = tile.querySelector('[part="control"]');
|
|
227
|
+
if (control) {
|
|
228
|
+
control.checked = isSelected;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
readSelection() {
|
|
233
|
+
return Array.from(this.groupEl.querySelectorAll('[part="control"]'))
|
|
234
|
+
.filter(input => input.checked)
|
|
235
|
+
.map(input => input.value);
|
|
236
|
+
}
|
|
237
|
+
renderTemplate() {
|
|
238
|
+
if (!this.shadowRoot) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
this.shadowRoot.innerHTML = `
|
|
242
|
+
<style>${tileStyles}</style>
|
|
243
|
+
<fieldset part="group">
|
|
244
|
+
<legend part="legend"></legend>
|
|
245
|
+
</fieldset>
|
|
246
|
+
`;
|
|
247
|
+
this.groupEl = this.shadowRoot.querySelector('[part="group"]');
|
|
248
|
+
this.legendEl = this.shadowRoot.querySelector('[part="legend"]');
|
|
249
|
+
}
|
|
250
|
+
setupListeners() {
|
|
251
|
+
this.groupEl.addEventListener("change", () => {
|
|
252
|
+
const next = this.readSelection();
|
|
253
|
+
this.value = next.join(",");
|
|
254
|
+
this.dispatchEvent(new CustomEvent("tile-change", {
|
|
255
|
+
bubbles: true,
|
|
256
|
+
composed: true,
|
|
257
|
+
detail: { selected: next },
|
|
258
|
+
}));
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
update() {
|
|
262
|
+
if (!this.groupEl) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const nextKey = this.optionsKey();
|
|
266
|
+
if (nextKey !== this.optionsSignature) {
|
|
267
|
+
this.optionsSignature = nextKey;
|
|
268
|
+
// The legend lives inside the fieldset and is wiped by the rebuild, so it
|
|
269
|
+
// is re-created rather than queried once at render time.
|
|
270
|
+
this.rebuild();
|
|
271
|
+
const legend = document.createElement("legend");
|
|
272
|
+
legend.setAttribute("part", "legend");
|
|
273
|
+
this.groupEl.prepend(legend);
|
|
274
|
+
this.legendEl = legend;
|
|
275
|
+
}
|
|
276
|
+
this.legendEl.textContent = this.legend;
|
|
277
|
+
this.syncSelection();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
TileGroup.register();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* A block of code, rendered as written.
|
|
4
|
+
*
|
|
5
|
+
* ```html
|
|
6
|
+
* <box-code-block code="npm i @unofficialbox/box-open-elements"></box-code-block>
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* The code is set as `textContent`, never as markup — a snippet is the most
|
|
10
|
+
* likely string on any page to contain angle brackets, and rendering it as HTML
|
|
11
|
+
* would both corrupt what the reader sees and hand an injection point to
|
|
12
|
+
* whatever produced the snippet.
|
|
13
|
+
*
|
|
14
|
+
* There is no syntax highlighting, deliberately. Doing it properly means
|
|
15
|
+
* shipping a grammar per language, and doing it improperly means mis-colouring
|
|
16
|
+
* code that a reader is trying to trust. A host that needs highlighting can
|
|
17
|
+
* slot pre-rendered markup into the default slot instead.
|
|
18
|
+
*
|
|
19
|
+
* Long lines scroll rather than wrap: a wrapped line silently changes what the
|
|
20
|
+
* reader believes the source says. `wrap` opts into wrapping where the content
|
|
21
|
+
* has no meaningful column structure.
|
|
22
|
+
*/
|
|
23
|
+
export declare class CodeBlock extends BaseElement {
|
|
24
|
+
static readonly tagName: string;
|
|
25
|
+
static get observedAttributes(): string[];
|
|
26
|
+
private preEl;
|
|
27
|
+
private codeEl;
|
|
28
|
+
private copyEl;
|
|
29
|
+
private slotEl;
|
|
30
|
+
private resetTimer;
|
|
31
|
+
/** The source to render. */
|
|
32
|
+
get code(): string;
|
|
33
|
+
set code(value: string);
|
|
34
|
+
/**
|
|
35
|
+
* The language, surfaced as a `data-language` hook and in the code element's
|
|
36
|
+
* class the way `highlight.js` and friends expect. Nothing here reads it.
|
|
37
|
+
*/
|
|
38
|
+
get language(): string;
|
|
39
|
+
set language(value: string);
|
|
40
|
+
/** Accessible name for the copy button. */
|
|
41
|
+
get copyLabel(): string;
|
|
42
|
+
set copyLabel(value: string);
|
|
43
|
+
private copy;
|
|
44
|
+
private markCopied;
|
|
45
|
+
disconnectedCallback(): void;
|
|
46
|
+
protected renderTemplate(): void;
|
|
47
|
+
protected setupListeners(): void;
|
|
48
|
+
protected update(): void;
|
|
49
|
+
}
|