@unofficialbox/box-open-elements 0.2.1 → 0.3.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/actions/button.d.ts +15 -0
- package/dist/components/actions/button.js +107 -3
- package/dist/components/actions/menu.d.ts +10 -0
- package/dist/components/actions/menu.js +68 -11
- package/dist/components/collections/table.d.ts +53 -0
- package/dist/components/collections/table.js +323 -0
- package/dist/components/collections/thumbnail-card.d.ts +19 -0
- package/dist/components/collections/thumbnail-card.js +210 -0
- package/dist/components/feedback/badgeable.d.ts +14 -0
- package/dist/components/feedback/badgeable.js +84 -0
- package/dist/components/forms/date-field.d.ts +1 -0
- package/dist/components/forms/date-field.js +7 -2
- package/dist/components/forms/pill-selector-dropdown.d.ts +11 -0
- package/dist/components/forms/pill-selector-dropdown.js +127 -0
- package/dist/components/forms/search-field.d.ts +1 -0
- package/dist/components/forms/search-field.js +7 -2
- package/dist/components/forms/select.d.ts +1 -0
- package/dist/components/forms/select.js +7 -2
- package/dist/components/forms/text-field.d.ts +1 -0
- package/dist/components/forms/text-field.js +8 -2
- package/dist/components/navigation/breadcrumb.d.ts +30 -0
- package/dist/components/navigation/breadcrumb.js +190 -0
- package/dist/components/navigation/tabs.d.ts +4 -0
- package/dist/components/navigation/tabs.js +53 -5
- package/dist/components/overlays/context-menu.d.ts +42 -0
- package/dist/components/overlays/context-menu.js +260 -0
- package/dist/components/overlays/dialog.d.ts +8 -0
- package/dist/components/overlays/dialog.js +45 -2
- package/dist/components/overlays/popover.d.ts +9 -1
- package/dist/components/overlays/popover.js +37 -22
- package/dist/core/form-associated.d.ts +32 -0
- package/dist/core/form-associated.js +114 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/foundations/overlay/index.d.ts +2 -0
- package/dist/foundations/overlay/index.js +1 -0
- package/dist/foundations/overlay/positioning.d.ts +74 -0
- package/dist/foundations/overlay/positioning.js +158 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormAssociatedElement, boeFormFieldErrorStyles, formErrorMessageMarkup, } from "../../core/index.js";
|
|
1
|
+
import { FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDescriptionMarkup, formErrorMessageMarkup, } from "../../core/index.js";
|
|
2
2
|
import { boeControl, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
3
|
const DEFAULT_TAG_NAME = "box-select";
|
|
4
4
|
const escapeHtml = (value) => value
|
|
@@ -64,11 +64,12 @@ const selectStyles = `
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
${boeFormFieldErrorStyles}
|
|
67
|
+
${boeFormFieldSupportStyles}
|
|
67
68
|
`;
|
|
68
69
|
export class BoxSelectElement extends FormAssociatedElement {
|
|
69
70
|
static get observedAttributes() {
|
|
70
71
|
return [
|
|
71
|
-
...FormAssociatedElement.
|
|
72
|
+
...FormAssociatedElement.fieldObservedAttributes,
|
|
72
73
|
"disabled",
|
|
73
74
|
"label",
|
|
74
75
|
"options",
|
|
@@ -78,6 +79,7 @@ export class BoxSelectElement extends FormAssociatedElement {
|
|
|
78
79
|
valueInternal = "";
|
|
79
80
|
selectEl;
|
|
80
81
|
labelEl;
|
|
82
|
+
descriptionEl;
|
|
81
83
|
errorEl;
|
|
82
84
|
get value() {
|
|
83
85
|
return this.valueInternal;
|
|
@@ -147,11 +149,13 @@ export class BoxSelectElement extends FormAssociatedElement {
|
|
|
147
149
|
<style>${selectStyles}</style>
|
|
148
150
|
<label part="field">
|
|
149
151
|
<span part="label"></span>
|
|
152
|
+
${formDescriptionMarkup()}
|
|
150
153
|
<select part="select"></select>
|
|
151
154
|
${formErrorMessageMarkup()}
|
|
152
155
|
</label>
|
|
153
156
|
`;
|
|
154
157
|
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
158
|
+
this.descriptionEl = this.shadowRoot.querySelector('[part="description"]');
|
|
155
159
|
this.selectEl = this.shadowRoot.querySelector('[part="select"]');
|
|
156
160
|
this.errorEl = this.shadowRoot.querySelector('[part="error-message"]');
|
|
157
161
|
}
|
|
@@ -185,6 +189,7 @@ export class BoxSelectElement extends FormAssociatedElement {
|
|
|
185
189
|
else {
|
|
186
190
|
this.selectEl.removeAttribute("disabled");
|
|
187
191
|
}
|
|
192
|
+
this.applyFieldSupport(this.labelEl, this.selectEl, this.descriptionEl);
|
|
188
193
|
this.applyInvalidState(this.selectEl, this.errorEl);
|
|
189
194
|
}
|
|
190
195
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormAssociatedElement, boeFormFieldErrorStyles, formErrorMessageMarkup, } from "../../core/index.js";
|
|
1
|
+
import { FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDescriptionMarkup, formErrorMessageMarkup, } from "../../core/index.js";
|
|
2
2
|
import { boeControl, boeInputControlStyles, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
3
|
const DEFAULT_TAG_NAME = "box-text-field";
|
|
4
4
|
/** Input chrome tracks BUE `@mixin box-inputs` (`src/styles/_inputs.scss`). */
|
|
@@ -34,11 +34,12 @@ const textFieldStyles = `
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
${boeFormFieldErrorStyles}
|
|
37
|
+
${boeFormFieldSupportStyles}
|
|
37
38
|
`;
|
|
38
39
|
export class BoxTextFieldElement extends FormAssociatedElement {
|
|
39
40
|
static get observedAttributes() {
|
|
40
41
|
return [
|
|
41
|
-
...FormAssociatedElement.
|
|
42
|
+
...FormAssociatedElement.fieldObservedAttributes,
|
|
42
43
|
"disabled",
|
|
43
44
|
"label",
|
|
44
45
|
"placeholder",
|
|
@@ -48,6 +49,7 @@ export class BoxTextFieldElement extends FormAssociatedElement {
|
|
|
48
49
|
valueInternal = "";
|
|
49
50
|
inputEl;
|
|
50
51
|
labelEl;
|
|
52
|
+
descriptionEl;
|
|
51
53
|
errorEl;
|
|
52
54
|
get value() {
|
|
53
55
|
return this.valueInternal;
|
|
@@ -102,11 +104,13 @@ export class BoxTextFieldElement extends FormAssociatedElement {
|
|
|
102
104
|
<style>${textFieldStyles}</style>
|
|
103
105
|
<label part="field">
|
|
104
106
|
<span part="label"></span>
|
|
107
|
+
${formDescriptionMarkup()}
|
|
105
108
|
<input type="text" part="input" />
|
|
106
109
|
${formErrorMessageMarkup()}
|
|
107
110
|
</label>
|
|
108
111
|
`;
|
|
109
112
|
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
113
|
+
this.descriptionEl = this.shadowRoot.querySelector('[part="description"]');
|
|
110
114
|
this.inputEl = this.shadowRoot.querySelector('[part="input"]');
|
|
111
115
|
this.errorEl = this.shadowRoot.querySelector('[part="error-message"]');
|
|
112
116
|
}
|
|
@@ -138,6 +142,8 @@ export class BoxTextFieldElement extends FormAssociatedElement {
|
|
|
138
142
|
else {
|
|
139
143
|
this.inputEl.removeAttribute("disabled");
|
|
140
144
|
}
|
|
145
|
+
// After the label text is (re)set, since it clears the required mark.
|
|
146
|
+
this.applyFieldSupport(this.labelEl, this.inputEl, this.descriptionEl);
|
|
141
147
|
this.applyInvalidState(this.inputEl, this.errorEl);
|
|
142
148
|
}
|
|
143
149
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
export interface BreadcrumbItem {
|
|
3
|
+
label: string;
|
|
4
|
+
/** Optional link target. Omit for a purely JS-driven crumb. */
|
|
5
|
+
href?: string;
|
|
6
|
+
/** Value reported on `navigate`; defaults to `href` or `label`. */
|
|
7
|
+
value?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* File-path breadcrumb trail with overflow collapse — box-ui-elements
|
|
11
|
+
* `Breadcrumb`. When the trail is longer than `max-items` the middle crumbs
|
|
12
|
+
* collapse into an ellipsis, always keeping the first crumb and the last two.
|
|
13
|
+
*/
|
|
14
|
+
export declare class BoxBreadcrumbElement extends BaseElement {
|
|
15
|
+
static get observedAttributes(): string[];
|
|
16
|
+
private listEl;
|
|
17
|
+
get items(): BreadcrumbItem[];
|
|
18
|
+
set items(value: BreadcrumbItem[]);
|
|
19
|
+
get label(): string;
|
|
20
|
+
set label(value: string);
|
|
21
|
+
/** Max crumbs shown before the middle collapses to an ellipsis. 0 disables collapse. */
|
|
22
|
+
get maxItems(): number;
|
|
23
|
+
set maxItems(value: number);
|
|
24
|
+
/** Keep the first crumb + last two, collapsing the middle into an ellipsis. */
|
|
25
|
+
private collapse;
|
|
26
|
+
protected renderTemplate(): void;
|
|
27
|
+
protected setupListeners(): void;
|
|
28
|
+
protected update(): void;
|
|
29
|
+
}
|
|
30
|
+
export declare const defineBoxBreadcrumbElement: (tagName?: string) => typeof BoxBreadcrumbElement;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
|
+
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
4
|
+
const DEFAULT_TAG_NAME = "box-breadcrumb";
|
|
5
|
+
const escapeHtml = (value) => value
|
|
6
|
+
.replaceAll("&", "&")
|
|
7
|
+
.replaceAll("<", "<")
|
|
8
|
+
.replaceAll(">", ">")
|
|
9
|
+
.replaceAll('"', """);
|
|
10
|
+
const breadcrumbStyles = `
|
|
11
|
+
:host {
|
|
12
|
+
display: block;
|
|
13
|
+
color: inherit;
|
|
14
|
+
font: inherit;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
[part="list"] {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-wrap: wrap;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: ${boeSpace[1]};
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
list-style: none;
|
|
25
|
+
font-size: 13px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
[part="crumb"] {
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
[part="link"] {
|
|
34
|
+
appearance: none;
|
|
35
|
+
border: none;
|
|
36
|
+
background: none;
|
|
37
|
+
font: inherit;
|
|
38
|
+
padding: 2px 6px;
|
|
39
|
+
border-radius: ${boeRadius.med};
|
|
40
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
41
|
+
text-decoration: none;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
max-width: 22ch;
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
text-overflow: ellipsis;
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
transition:
|
|
48
|
+
color ${boeMotionDuration.interactive} ${boeMotionEasing.standard},
|
|
49
|
+
background ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[part="link"]:hover {
|
|
53
|
+
color: var(--boe-token-text-text, #222222);
|
|
54
|
+
background: var(--boe-token-surface-surface-secondary, #f4f4f4);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
[part="link"]:focus-visible {
|
|
58
|
+
outline: 2px solid var(--boe-token-surface-surface-brand, #0061d5);
|
|
59
|
+
outline-offset: 1px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
[part="link"][aria-current="page"] {
|
|
63
|
+
color: var(--boe-token-text-text, #222222);
|
|
64
|
+
font-weight: 600;
|
|
65
|
+
cursor: default;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[part="separator"] {
|
|
69
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
70
|
+
user-select: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[part="ellipsis"] {
|
|
74
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
75
|
+
padding: 0 2px;
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
/** A collapsed marker inserted when the trail exceeds `max-items`. */
|
|
79
|
+
const ELLIPSIS = Symbol("ellipsis");
|
|
80
|
+
/**
|
|
81
|
+
* File-path breadcrumb trail with overflow collapse — box-ui-elements
|
|
82
|
+
* `Breadcrumb`. When the trail is longer than `max-items` the middle crumbs
|
|
83
|
+
* collapse into an ellipsis, always keeping the first crumb and the last two.
|
|
84
|
+
*/
|
|
85
|
+
export class BoxBreadcrumbElement extends BaseElement {
|
|
86
|
+
static get observedAttributes() {
|
|
87
|
+
return ["items", "label", "max-items"];
|
|
88
|
+
}
|
|
89
|
+
listEl;
|
|
90
|
+
get items() {
|
|
91
|
+
const raw = this.getAttribute("items");
|
|
92
|
+
if (!raw) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(raw);
|
|
97
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
set items(value) {
|
|
104
|
+
this.setAttribute("items", JSON.stringify(value));
|
|
105
|
+
}
|
|
106
|
+
get label() {
|
|
107
|
+
return this.getAttribute("label") ?? "Breadcrumb";
|
|
108
|
+
}
|
|
109
|
+
set label(value) {
|
|
110
|
+
this.setAttribute("label", value);
|
|
111
|
+
}
|
|
112
|
+
/** Max crumbs shown before the middle collapses to an ellipsis. 0 disables collapse. */
|
|
113
|
+
get maxItems() {
|
|
114
|
+
const raw = Number(this.getAttribute("max-items"));
|
|
115
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 4;
|
|
116
|
+
}
|
|
117
|
+
set maxItems(value) {
|
|
118
|
+
this.setAttribute("max-items", String(value));
|
|
119
|
+
}
|
|
120
|
+
/** Keep the first crumb + last two, collapsing the middle into an ellipsis. */
|
|
121
|
+
collapse(items) {
|
|
122
|
+
const max = this.maxItems;
|
|
123
|
+
if (items.length <= max) {
|
|
124
|
+
return items;
|
|
125
|
+
}
|
|
126
|
+
return [items[0], ELLIPSIS, ...items.slice(-2)];
|
|
127
|
+
}
|
|
128
|
+
renderTemplate() {
|
|
129
|
+
if (!this.shadowRoot) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.shadowRoot.innerHTML = `
|
|
133
|
+
<style>${breadcrumbStyles}</style>
|
|
134
|
+
<nav part="nav">
|
|
135
|
+
<ol part="list"></ol>
|
|
136
|
+
</nav>
|
|
137
|
+
`;
|
|
138
|
+
this.listEl = this.shadowRoot.querySelector('[part="list"]');
|
|
139
|
+
}
|
|
140
|
+
setupListeners() {
|
|
141
|
+
this.listEl.addEventListener("click", event => {
|
|
142
|
+
const link = event.target.closest('[part="link"]');
|
|
143
|
+
if (!link || link.getAttribute("aria-current") === "page") {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const value = link.dataset.value ?? "";
|
|
147
|
+
const href = link.getAttribute("href") ?? undefined;
|
|
148
|
+
// JS-only crumbs (no href) shouldn't navigate the browser.
|
|
149
|
+
if (!href) {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
}
|
|
152
|
+
this.dispatchEvent(new CustomEvent("navigate", {
|
|
153
|
+
bubbles: true,
|
|
154
|
+
composed: true,
|
|
155
|
+
detail: { value, href },
|
|
156
|
+
}));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
update() {
|
|
160
|
+
if (!this.listEl) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.shadowRoot?.querySelector('[part="nav"]')?.setAttribute("aria-label", this.label);
|
|
164
|
+
const crumbs = this.collapse(this.items);
|
|
165
|
+
const lastIndex = crumbs.length - 1;
|
|
166
|
+
this.listEl.innerHTML = crumbs
|
|
167
|
+
.map((crumb, index) => {
|
|
168
|
+
const separator = index < lastIndex ? `<span part="separator" aria-hidden="true">/</span>` : "";
|
|
169
|
+
if (crumb === ELLIPSIS) {
|
|
170
|
+
return `<li part="crumb"><span part="ellipsis" aria-label="Hidden path">…</span>${separator}</li>`;
|
|
171
|
+
}
|
|
172
|
+
const isLast = index === lastIndex;
|
|
173
|
+
const value = escapeHtml(crumb.value ?? crumb.href ?? crumb.label);
|
|
174
|
+
const tag = crumb.href && !isLast ? "a" : "button";
|
|
175
|
+
const hrefAttr = crumb.href && !isLast ? ` href="${escapeHtml(crumb.href)}"` : "";
|
|
176
|
+
const typeAttr = tag === "button" ? ' type="button"' : "";
|
|
177
|
+
const current = isLast ? ' aria-current="page"' : "";
|
|
178
|
+
return `<li part="crumb"><${tag} part="link" data-value="${value}"${hrefAttr}${typeAttr}${current}>${escapeHtml(crumb.label)}</${tag}>${separator}</li>`;
|
|
179
|
+
})
|
|
180
|
+
.join("");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export const defineBoxBreadcrumbElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
184
|
+
const existingElement = customElements.get(tagName);
|
|
185
|
+
if (existingElement) {
|
|
186
|
+
return existingElement;
|
|
187
|
+
}
|
|
188
|
+
customElements.define(tagName, BoxBreadcrumbElement);
|
|
189
|
+
return BoxBreadcrumbElement;
|
|
190
|
+
};
|
|
@@ -8,6 +8,8 @@ export declare class BoxTabsElement extends BaseElement {
|
|
|
8
8
|
private valueInternal;
|
|
9
9
|
private lastOptionsJson;
|
|
10
10
|
private tabsEl;
|
|
11
|
+
private panelsEl;
|
|
12
|
+
private readonly uid;
|
|
11
13
|
get label(): string;
|
|
12
14
|
set label(value: string);
|
|
13
15
|
get layout(): "attached" | "separated";
|
|
@@ -19,6 +21,8 @@ export declare class BoxTabsElement extends BaseElement {
|
|
|
19
21
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
20
22
|
private selectValue;
|
|
21
23
|
private renderTabsMarkup;
|
|
24
|
+
/** One tabpanel per option, fed by a slot named for the option value. */
|
|
25
|
+
private renderPanelsMarkup;
|
|
22
26
|
protected renderTemplate(): void;
|
|
23
27
|
protected setupListeners(): void;
|
|
24
28
|
protected update(): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
-
import { boeControl, boeSpace } from "../../foundations/geometry/index.js";
|
|
2
|
+
import { boeControl, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
3
|
import { boeFocusVisibleStyles, } from "../../foundations/tokens/index.js";
|
|
4
4
|
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
5
5
|
const DEFAULT_TAG_NAME = "box-tabs";
|
|
@@ -88,6 +88,22 @@ const tabsStyles = `
|
|
|
88
88
|
outline-offset: -2px;
|
|
89
89
|
box-shadow: none;
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
/* Panels carry no default padding — an empty panel is zero-height, and
|
|
93
|
+
consumers style their own slotted content. */
|
|
94
|
+
[part="panel"] {
|
|
95
|
+
outline: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
[part="panel"][hidden] {
|
|
99
|
+
display: none;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[part="panel"]:focus-visible {
|
|
103
|
+
outline: 2px solid var(--boe-token-surface-surface-brand, #0061d5);
|
|
104
|
+
outline-offset: 2px;
|
|
105
|
+
border-radius: ${boeRadius.med};
|
|
106
|
+
}
|
|
91
107
|
`;
|
|
92
108
|
export class BoxTabsElement extends BaseElement {
|
|
93
109
|
static get observedAttributes() {
|
|
@@ -96,6 +112,8 @@ export class BoxTabsElement extends BaseElement {
|
|
|
96
112
|
valueInternal = "";
|
|
97
113
|
lastOptionsJson = "";
|
|
98
114
|
tabsEl;
|
|
115
|
+
panelsEl;
|
|
116
|
+
uid = `boe-tabs-${Math.random().toString(36).slice(2, 8)}`;
|
|
99
117
|
get label() {
|
|
100
118
|
return this.getAttribute("label") ?? "Tabs";
|
|
101
119
|
}
|
|
@@ -169,17 +187,21 @@ export class BoxTabsElement extends BaseElement {
|
|
|
169
187
|
: index === options.length - 1
|
|
170
188
|
? "last"
|
|
171
189
|
: "middle";
|
|
190
|
+
const selected = option.value === selectedValue;
|
|
191
|
+
const value = escapeHtml(option.value);
|
|
172
192
|
return `
|
|
173
193
|
<button
|
|
174
194
|
type="button"
|
|
175
195
|
part="tab"
|
|
176
196
|
role="tab"
|
|
197
|
+
id="${this.uid}-tab-${value}"
|
|
198
|
+
aria-controls="${this.uid}-panel-${value}"
|
|
177
199
|
data-layout="${this.layout}"
|
|
178
200
|
data-position="${position}"
|
|
179
|
-
data-selected="${String(
|
|
180
|
-
data-value="${
|
|
181
|
-
aria-selected="${String(
|
|
182
|
-
tabindex="${
|
|
201
|
+
data-selected="${String(selected)}"
|
|
202
|
+
data-value="${value}"
|
|
203
|
+
aria-selected="${String(selected)}"
|
|
204
|
+
tabindex="${selected ? "0" : "-1"}"
|
|
183
205
|
>
|
|
184
206
|
${escapeHtml(option.label)}
|
|
185
207
|
</button>
|
|
@@ -187,6 +209,25 @@ export class BoxTabsElement extends BaseElement {
|
|
|
187
209
|
})
|
|
188
210
|
.join("");
|
|
189
211
|
}
|
|
212
|
+
/** One tabpanel per option, fed by a slot named for the option value. */
|
|
213
|
+
renderPanelsMarkup(options, selectedValue) {
|
|
214
|
+
return options
|
|
215
|
+
.map(option => {
|
|
216
|
+
const value = escapeHtml(option.value);
|
|
217
|
+
const selected = option.value === selectedValue;
|
|
218
|
+
return `
|
|
219
|
+
<div
|
|
220
|
+
part="panel"
|
|
221
|
+
role="tabpanel"
|
|
222
|
+
id="${this.uid}-panel-${value}"
|
|
223
|
+
aria-labelledby="${this.uid}-tab-${value}"
|
|
224
|
+
tabindex="0"
|
|
225
|
+
${selected ? "" : "hidden"}
|
|
226
|
+
><slot name="${value}"></slot></div>
|
|
227
|
+
`;
|
|
228
|
+
})
|
|
229
|
+
.join("");
|
|
230
|
+
}
|
|
190
231
|
renderTemplate() {
|
|
191
232
|
if (!this.shadowRoot) {
|
|
192
233
|
return;
|
|
@@ -194,8 +235,10 @@ export class BoxTabsElement extends BaseElement {
|
|
|
194
235
|
this.shadowRoot.innerHTML = `
|
|
195
236
|
<style>${tabsStyles}</style>
|
|
196
237
|
<div part="tabs" role="tablist"></div>
|
|
238
|
+
<div part="panels"></div>
|
|
197
239
|
`;
|
|
198
240
|
this.tabsEl = this.shadowRoot.querySelector('[part="tabs"]');
|
|
241
|
+
this.panelsEl = this.shadowRoot.querySelector('[part="panels"]');
|
|
199
242
|
}
|
|
200
243
|
setupListeners() {
|
|
201
244
|
this.tabsEl.addEventListener("click", event => {
|
|
@@ -254,6 +297,7 @@ export class BoxTabsElement extends BaseElement {
|
|
|
254
297
|
this.tabsEl.setAttribute("aria-label", this.label);
|
|
255
298
|
if (optionsJson !== this.lastOptionsJson) {
|
|
256
299
|
this.tabsEl.innerHTML = this.renderTabsMarkup(options, selectedValue);
|
|
300
|
+
this.panelsEl.innerHTML = this.renderPanelsMarkup(options, selectedValue);
|
|
257
301
|
this.lastOptionsJson = optionsJson;
|
|
258
302
|
return;
|
|
259
303
|
}
|
|
@@ -277,6 +321,10 @@ export class BoxTabsElement extends BaseElement {
|
|
|
277
321
|
tab.setAttribute("aria-selected", String(selected));
|
|
278
322
|
tab.tabIndex = selected ? 0 : -1;
|
|
279
323
|
});
|
|
324
|
+
// Show only the selected tab's panel.
|
|
325
|
+
this.panelsEl.querySelectorAll('[part="panel"]').forEach((panel, index) => {
|
|
326
|
+
panel.hidden = options[index]?.value !== selectedValue;
|
|
327
|
+
});
|
|
280
328
|
}
|
|
281
329
|
}
|
|
282
330
|
export const defineBoxTabsElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
export interface ContextMenuItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
/** Render a divider *before* this item. */
|
|
7
|
+
separator?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Right-click context menu — box-ui-elements `ContextMenu`. The default slot is
|
|
11
|
+
* the target area; right-clicking (or Shift+F10 / the ContextMenu key) opens a
|
|
12
|
+
* menu at the pointer, positioned with `foundations/overlay` so it stays in the
|
|
13
|
+
* viewport. Full keyboard menu: arrows, Home/End, Enter/Space, Escape.
|
|
14
|
+
*/
|
|
15
|
+
export declare class BoxContextMenuElement extends BaseElement {
|
|
16
|
+
static get observedAttributes(): string[];
|
|
17
|
+
private openValue;
|
|
18
|
+
private targetEl;
|
|
19
|
+
private surfaceEl;
|
|
20
|
+
private readonly focusRestore;
|
|
21
|
+
private documentBound;
|
|
22
|
+
private readonly onOutside;
|
|
23
|
+
private readonly onReposition;
|
|
24
|
+
get items(): ContextMenuItem[];
|
|
25
|
+
set items(value: ContextMenuItem[]);
|
|
26
|
+
get disabled(): boolean;
|
|
27
|
+
set disabled(value: boolean);
|
|
28
|
+
get open(): boolean;
|
|
29
|
+
disconnectedCallback(): void;
|
|
30
|
+
protected renderTemplate(): void;
|
|
31
|
+
protected setupListeners(): void;
|
|
32
|
+
private enabledItems;
|
|
33
|
+
private onMenuKeydown;
|
|
34
|
+
private openAt;
|
|
35
|
+
close(): void;
|
|
36
|
+
private selectItem;
|
|
37
|
+
private renderItems;
|
|
38
|
+
private bindDocument;
|
|
39
|
+
private unbindDocument;
|
|
40
|
+
protected update(): void;
|
|
41
|
+
}
|
|
42
|
+
export declare const defineBoxContextMenuElement: (tagName?: string) => typeof BoxContextMenuElement;
|