@brandsync/wc 1.8.0 → 1.9.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/custom-elements.json +139 -11
- package/dist/brandsync-wc/brandsync-wc.esm.js +1 -1
- package/dist/brandsync-wc/p-0eed6813.entry.js +1 -0
- package/dist/brandsync-wc/{p-ee7da23d.entry.js → p-107b1c5d.entry.js} +1 -1
- package/dist/brandsync-wc/p-2181378f.entry.js +1 -0
- package/dist/brandsync-wc/p-27bbed83.entry.js +1 -0
- package/dist/brandsync-wc/p-36bf37d9.entry.js +1 -0
- package/dist/brandsync-wc/p-3b64e3cd.entry.js +1 -0
- package/dist/brandsync-wc/p-6fd7dd6a.entry.js +1 -0
- package/dist/brandsync-wc/p-9f6fe37d.entry.js +1 -0
- package/dist/brandsync-wc/{p-03b08508.entry.js → p-c3f72584.entry.js} +1 -1
- package/dist/brandsync-wc/p-fc240aa0.entry.js +1 -0
- package/dist/cjs/brandsync-wc.cjs.js +1 -1
- package/dist/cjs/bs-breadcrumb-overflow.cjs.entry.js +13 -1
- package/dist/cjs/bs-breadcrumb.cjs.entry.js +15 -1
- package/dist/cjs/bs-data-table.cjs.entry.js +18 -4
- package/dist/cjs/bs-icon-button.cjs.entry.js +2 -2
- package/dist/cjs/bs-inline-tab.cjs.entry.js +5 -1
- package/dist/cjs/bs-navigation-drawer-item.cjs.entry.js +1 -1
- package/dist/cjs/bs-navigation-drawer.cjs.entry.js +118 -5
- package/dist/cjs/bs-navigation-header.cjs.entry.js +1 -1
- package/dist/cjs/bs-tab.cjs.entry.js +5 -1
- package/dist/cjs/bs-tabs.cjs.entry.js +47 -2
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/collection/components/bs-breadcrumb/bs-breadcrumb/bs-breadcrumb.cmp.test.js +63 -0
- package/dist/collection/components/bs-breadcrumb/bs-breadcrumb/bs-breadcrumb.js +32 -1
- package/dist/collection/components/bs-breadcrumb/bs-breadcrumb-overflow/bs-breadcrumb-overflow.cmp.test.js +45 -0
- package/dist/collection/components/bs-breadcrumb/bs-breadcrumb-overflow/bs-breadcrumb-overflow.js +30 -1
- package/dist/collection/components/bs-button/bs-icon-button/bs-icon-button.cmp.test.js +20 -0
- package/dist/collection/components/bs-button/bs-icon-button/bs-icon-button.css +21 -4
- package/dist/collection/components/bs-button/bs-icon-button/bs-icon-button.js +3 -2
- package/dist/collection/components/bs-button/bs-icon-button/bs-icon-button.stories.js +10 -0
- package/dist/collection/components/bs-data-table/bs-data-table.cmp.test.js +19 -0
- package/dist/collection/components/bs-data-table/bs-data-table.js +19 -5
- package/dist/collection/components/bs-data-table/bs-data-table.stories.js +8 -1
- package/dist/collection/components/bs-logo/bs-logo.cmp.test.js +52 -3
- package/dist/collection/components/bs-menu/bs-menu.stories.js +54 -0
- package/dist/collection/components/bs-tab/bs-inline-tab/bs-inline-tab.js +24 -1
- package/dist/collection/components/bs-tab/bs-tab/bs-tab.js +24 -1
- package/dist/collection/components/bs-tab/bs-tabs/bs-tabs.cmp.test.js +88 -0
- package/dist/collection/components/bs-tab/bs-tabs/bs-tabs.js +83 -2
- package/dist/collection/components/bs-tab/bs-tabs/bs-tabs.stories.js +24 -0
- package/dist/collection/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer/bs-navigation-drawer.cmp.test.js +173 -5
- package/dist/collection/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer/bs-navigation-drawer.css +106 -34
- package/dist/collection/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer/bs-navigation-drawer.js +181 -5
- package/dist/collection/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer/bs-navigation-drawer.stories.js +48 -0
- package/dist/collection/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer-item/bs-navigation-drawer-item.js +1 -1
- package/dist/collection/components/ui-shell/bs-navigation-header/bs-navigation-header/bs-navigation-header.cmp.test.js +68 -0
- package/dist/collection/components/ui-shell/bs-navigation-header/bs-navigation-header/bs-navigation-header.css +31 -17
- package/dist/components/bs-attachment.js +1 -1
- package/dist/components/bs-breadcrumb-overflow.js +1 -1
- package/dist/components/bs-breadcrumb.js +1 -1
- package/dist/components/bs-carousel.js +1 -1
- package/dist/components/bs-data-table.js +1 -1
- package/dist/components/bs-dialog.js +1 -1
- package/dist/components/bs-file-upload-item.js +1 -1
- package/dist/components/bs-icon-button.js +1 -1
- package/dist/components/bs-inline-tab.js +1 -1
- package/dist/components/bs-navigation-drawer-item.js +1 -1
- package/dist/components/bs-navigation-drawer.js +1 -1
- package/dist/components/bs-navigation-header.js +1 -1
- package/dist/components/bs-snackbar.js +1 -1
- package/dist/components/bs-tab.js +1 -1
- package/dist/components/bs-tabs.js +1 -1
- package/dist/components/p-BoeorHNv.js +1 -0
- package/dist/esm/brandsync-wc.js +1 -1
- package/dist/esm/bs-breadcrumb-overflow.entry.js +14 -2
- package/dist/esm/bs-breadcrumb.entry.js +16 -2
- package/dist/esm/bs-data-table.entry.js +18 -4
- package/dist/esm/bs-icon-button.entry.js +2 -2
- package/dist/esm/bs-inline-tab.entry.js +5 -1
- package/dist/esm/bs-navigation-drawer-item.entry.js +1 -1
- package/dist/esm/bs-navigation-drawer.entry.js +119 -6
- package/dist/esm/bs-navigation-header.entry.js +1 -1
- package/dist/esm/bs-tab.entry.js +5 -1
- package/dist/esm/bs-tabs.entry.js +48 -3
- package/dist/esm/loader.js +1 -1
- package/dist/types/components/bs-breadcrumb/bs-breadcrumb/bs-breadcrumb.d.ts +10 -0
- package/dist/types/components/bs-breadcrumb/bs-breadcrumb-overflow/bs-breadcrumb-overflow.d.ts +8 -0
- package/dist/types/components/bs-button/bs-icon-button/bs-icon-button.d.ts +2 -1
- package/dist/types/components/bs-button/bs-icon-button/bs-icon-button.stories.d.ts +1 -0
- package/dist/types/components/bs-data-table/bs-data-table.d.ts +16 -2
- package/dist/types/components/bs-menu/bs-menu.stories.d.ts +1 -0
- package/dist/types/components/bs-tab/bs-inline-tab/bs-inline-tab.d.ts +4 -0
- package/dist/types/components/bs-tab/bs-tab/bs-tab.d.ts +4 -0
- package/dist/types/components/bs-tab/bs-tabs/bs-tabs.d.ts +17 -0
- package/dist/types/components/bs-tab/bs-tabs/bs-tabs.stories.d.ts +1 -0
- package/dist/types/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer/bs-navigation-drawer.d.ts +50 -1
- package/dist/types/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer/bs-navigation-drawer.stories.d.ts +2 -0
- package/dist/types/components.d.ts +130 -2
- package/package.json +1 -1
- package/dist/brandsync-wc/p-002d258d.entry.js +0 -1
- package/dist/brandsync-wc/p-153da845.entry.js +0 -1
- package/dist/brandsync-wc/p-2e7a1082.entry.js +0 -1
- package/dist/brandsync-wc/p-612cf79c.entry.js +0 -1
- package/dist/brandsync-wc/p-652d4068.entry.js +0 -1
- package/dist/brandsync-wc/p-bc69a96c.entry.js +0 -1
- package/dist/brandsync-wc/p-bf53ad52.entry.js +0 -1
- package/dist/brandsync-wc/p-c7f4c114.entry.js +0 -1
- package/dist/components/p-BkHYJIyj.js +0 -1
|
@@ -7,6 +7,7 @@ const bsTabsCss = () => `:host{display:inline-block;--bs-tabs-tab-gap:var(--bs-s
|
|
|
7
7
|
const BsTabs = class {
|
|
8
8
|
constructor(hostRef) {
|
|
9
9
|
index.registerInstance(this, hostRef);
|
|
10
|
+
this.bsChange = index.createEvent(this, "bsChange");
|
|
10
11
|
}
|
|
11
12
|
get el() { return index.getElement(this); }
|
|
12
13
|
/** Which tab-button family this group wraps. Governs the wrapper's own visual treatment (gap-only
|
|
@@ -16,6 +17,16 @@ const BsTabs = class {
|
|
|
16
17
|
/** Layout direction of the tab bar itself. Sets `aria-orientation` on the internal wrapper and
|
|
17
18
|
* switches `flex-direction`. */
|
|
18
19
|
orientation = 'horizontal';
|
|
20
|
+
/** The selected tab's `value`. Mutable and reflected: it follows the user's selection, and
|
|
21
|
+
* setting it from outside (attribute or property) selects the matching tab -- without emitting
|
|
22
|
+
* `bsChange`, which only fires for user-driven changes. A tab's value is its own `value` prop,
|
|
23
|
+
* or, for a tab without one, its zero-based position in the group as a string (`"0"`, `"1"`...).
|
|
24
|
+
* If unset, it is read from whichever tab is already `selected` once the group loads. */
|
|
25
|
+
value;
|
|
26
|
+
/** Fires when the user selects a different tab (click, or arrow/Home/End), with the new
|
|
27
|
+
* `value`. Does not fire when `value` is set programmatically. `bsSelect` on the individual tab
|
|
28
|
+
* still fires as before. */
|
|
29
|
+
bsChange;
|
|
19
30
|
/**
|
|
20
31
|
* Catches `bsSelect` bubbling up from any slotted `<bs-tab>`/`<bs-inline-tab>` (Stencil's
|
|
21
32
|
* `@Event()` defaults to `bubbles: true, composed: true`, and `@Listen` without a target listens
|
|
@@ -24,15 +35,46 @@ const BsTabs = class {
|
|
|
24
35
|
* `if (this.disabled) return;`), so no special-casing is needed here.
|
|
25
36
|
*/
|
|
26
37
|
onTabSelect(ev) {
|
|
27
|
-
const tabs = this.
|
|
38
|
+
const tabs = this.tabs();
|
|
28
39
|
tabs.forEach(tab => {
|
|
29
40
|
tab.selected = tab === ev.target;
|
|
30
41
|
});
|
|
31
42
|
this.syncRovingTabindex();
|
|
43
|
+
const index = tabs.findIndex(tab => tab === ev.target);
|
|
44
|
+
if (index === -1)
|
|
45
|
+
return;
|
|
46
|
+
const value = this.tabValue(tabs[index], index);
|
|
47
|
+
if (value !== this.value) {
|
|
48
|
+
this.value = value;
|
|
49
|
+
this.bsChange.emit({ value });
|
|
50
|
+
}
|
|
32
51
|
}
|
|
33
52
|
componentDidLoad() {
|
|
53
|
+
if (this.value !== undefined) {
|
|
54
|
+
this.applyValue();
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Uncontrolled: report whichever tab the consumer already marked `selected`.
|
|
58
|
+
const tabs = this.tabs();
|
|
59
|
+
const index = tabs.findIndex(t => t.selected);
|
|
60
|
+
if (index !== -1)
|
|
61
|
+
this.value = this.tabValue(tabs[index], index);
|
|
62
|
+
}
|
|
34
63
|
this.syncRovingTabindex();
|
|
35
64
|
}
|
|
65
|
+
tabValue(tab, index) {
|
|
66
|
+
return tab.value ?? String(index);
|
|
67
|
+
}
|
|
68
|
+
/** Selects the tab whose value matches `this.value` (and deselects the rest). */
|
|
69
|
+
applyValue() {
|
|
70
|
+
this.tabs().forEach((tab, i) => {
|
|
71
|
+
tab.selected = this.tabValue(tab, i) === this.value;
|
|
72
|
+
});
|
|
73
|
+
this.syncRovingTabindex();
|
|
74
|
+
}
|
|
75
|
+
onValueChange() {
|
|
76
|
+
this.applyValue();
|
|
77
|
+
}
|
|
36
78
|
syncRovingTabindex() {
|
|
37
79
|
const tabs = this.tabs();
|
|
38
80
|
const anySelected = tabs.some(t => t.selected);
|
|
@@ -75,9 +117,12 @@ const BsTabs = class {
|
|
|
75
117
|
}
|
|
76
118
|
render() {
|
|
77
119
|
const vertical = this.orientation === 'vertical';
|
|
78
|
-
return (index.h("div", { key: '
|
|
120
|
+
return (index.h("div", { key: '251521d36dcf4d6960df344285bc9a060ff87abb', part: "tablist", role: "tablist", "aria-orientation": this.orientation, class: `bs-tabs bs-tabs--${this.type} ${vertical ? 'bs-tabs--vertical' : ''}` }, index.h("slot", { key: '02216f048eaedb13f00891c30e1c49d15a509c1d', onSlotchange: () => this.syncRovingTabindex() })));
|
|
79
121
|
}
|
|
80
122
|
static get watchers() { return {
|
|
123
|
+
"value": [{
|
|
124
|
+
"onValueChange": 0
|
|
125
|
+
}],
|
|
81
126
|
"orientation": [{
|
|
82
127
|
"syncRovingTabindex": 0
|
|
83
128
|
}]
|
package/dist/cjs/loader.cjs.js
CHANGED
|
@@ -5,7 +5,7 @@ var index = require('./index-kk9YXF2S.js');
|
|
|
5
5
|
const defineCustomElements = async (win, options) => {
|
|
6
6
|
if (typeof window === 'undefined') return undefined;
|
|
7
7
|
await index.globalScripts();
|
|
8
|
-
return index.bootstrapLazy([["bs-carousel.cjs",[[769,"bs-carousel",{"tracker":[1],"slidesPerView":[2,"slides-per-view"],"currentPage":[1538,"current-page"],"autoplay":[4],"interval":[2],"paused":[1540],"label":[1],"previousLabel":[1,"previous-label"],"nextLabel":[1,"next-label"],"pauseLabel":[1,"pause-label"],"playLabel":[1,"play-label"],"slideCount":[32],"interacting":[32]},[[1,"mouseenter","onMouseEnter"],[1,"mouseleave","onMouseLeave"],[0,"focusin","onFocusIn"],[0,"focusout","onFocusOut"]],{"currentPage":[{"onCurrentPageChange":0}],"autoplay":[{"updateTimer":0}],"paused":[{"updateTimer":0}],"interval":[{"updateTimer":0}],"interacting":[{"updateTimer":0}],"slideCount":[{"updateTimer":0}],"slidesPerView":[{"updateTimer":0}]}]]],["bs-file-upload-item.cjs",[[769,"bs-file-upload-item",{"state":[513],"fileName":[1,"file-name"],"fileSize":[1,"file-size"],"progress":[2],"message":[1],"thumbnailSrc":[1,"thumbnail-src"],"thumbnailAlt":[1,"thumbnail-alt"],"cancelLabel":[1,"cancel-label"],"removeLabel":[1,"remove-label"]}]]],["bs-input.cjs",[[849,"bs-input",{"name":[513],"value":[1025],"type":[1],"label":[1],"placeholder":[1],"description":[1],"error":[1],"disabled":[516],"required":[4],"step":[2],"min":[2],"max":[2],"options":[16],"rows":[2],"chips":[16],"titleOptions":[16],"initialsTitle":[1,"initials-title"],"countryOptions":[16],"country":[1],"length":[2],"open":[1540],"hasIcon":[32],"hasEndIcon":[32],"passwordVisible":[32],"chipDraft":[32]},[[0,"keydown","onHostKeydown"],[0,"focusout","onHostFocusOut"]],{"value":[{"onFormStateChange":0}],"type":[{"onFormStateChange":0}],"chips":[{"onFormStateChange":0}],"required":[{"onFormStateChange":0}]}]]],["bs-snackbar.cjs",[[769,"bs-snackbar",{"variant":[1],"loading":[4],"actionLabel":[1,"action-label"]}]]],["bs-attachment.cjs",[[513,"bs-attachment",{"type":[1],"fileName":[1,"file-name"],"imageSrc":[1,"image-src"],"imageAlt":[1,"image-alt"],"loading":[4],"removable":[4]}]]],["bs-breadcrumb-overflow.cjs",[[513,"bs-breadcrumb-overflow",{"items":[16],"overflowLabel":[1,"overflow-label"],"size":[513],"open":[32]},[[0,"keydown","onHostKeydown"],[0,"focusout","onHostFocusOut"]]]]],["bs-chatbot-header.cjs",[[769,"bs-chatbot-header",{"heading":[1],"expanded":[1540]}]]],["bs-dialog.cjs",[[769,"bs-dialog",{"open":[1540],"heading":[1],"size":[1],"centered":[516],"hasFooter":[32],"hasImage":[32],"hasIcon":[32]},[[4,"keydown","onKeydown"],[4,"focusin","onFocusIn"]]]]],["bs-file-upload.cjs",[[769,"bs-file-upload",{"direction":[513],"heading":[1],"description":[1],"buttonLabel":[1,"button-label"],"accept":[1],"multiple":[4],"disabled":[516],"dragging":[1540]}]]],["bs-navigation-drawer.cjs",[[769,"bs-navigation-drawer",{"heading":[1],"collapsible":[4],"collapsed":[1540],"logoBackground":[1,"logo-background"],"showLogo":[4,"show-logo"]},
|
|
8
|
+
return index.bootstrapLazy([["bs-carousel.cjs",[[769,"bs-carousel",{"tracker":[1],"slidesPerView":[2,"slides-per-view"],"currentPage":[1538,"current-page"],"autoplay":[4],"interval":[2],"paused":[1540],"label":[1],"previousLabel":[1,"previous-label"],"nextLabel":[1,"next-label"],"pauseLabel":[1,"pause-label"],"playLabel":[1,"play-label"],"slideCount":[32],"interacting":[32]},[[1,"mouseenter","onMouseEnter"],[1,"mouseleave","onMouseLeave"],[0,"focusin","onFocusIn"],[0,"focusout","onFocusOut"]],{"currentPage":[{"onCurrentPageChange":0}],"autoplay":[{"updateTimer":0}],"paused":[{"updateTimer":0}],"interval":[{"updateTimer":0}],"interacting":[{"updateTimer":0}],"slideCount":[{"updateTimer":0}],"slidesPerView":[{"updateTimer":0}]}]]],["bs-file-upload-item.cjs",[[769,"bs-file-upload-item",{"state":[513],"fileName":[1,"file-name"],"fileSize":[1,"file-size"],"progress":[2],"message":[1],"thumbnailSrc":[1,"thumbnail-src"],"thumbnailAlt":[1,"thumbnail-alt"],"cancelLabel":[1,"cancel-label"],"removeLabel":[1,"remove-label"]}]]],["bs-input.cjs",[[849,"bs-input",{"name":[513],"value":[1025],"type":[1],"label":[1],"placeholder":[1],"description":[1],"error":[1],"disabled":[516],"required":[4],"step":[2],"min":[2],"max":[2],"options":[16],"rows":[2],"chips":[16],"titleOptions":[16],"initialsTitle":[1,"initials-title"],"countryOptions":[16],"country":[1],"length":[2],"open":[1540],"hasIcon":[32],"hasEndIcon":[32],"passwordVisible":[32],"chipDraft":[32]},[[0,"keydown","onHostKeydown"],[0,"focusout","onHostFocusOut"]],{"value":[{"onFormStateChange":0}],"type":[{"onFormStateChange":0}],"chips":[{"onFormStateChange":0}],"required":[{"onFormStateChange":0}]}]]],["bs-snackbar.cjs",[[769,"bs-snackbar",{"variant":[1],"loading":[4],"actionLabel":[1,"action-label"]}]]],["bs-attachment.cjs",[[513,"bs-attachment",{"type":[1],"fileName":[1,"file-name"],"imageSrc":[1,"image-src"],"imageAlt":[1,"image-alt"],"loading":[4],"removable":[4]}]]],["bs-breadcrumb-overflow.cjs",[[513,"bs-breadcrumb-overflow",{"items":[16],"overflowLabel":[1,"overflow-label"],"size":[513],"open":[32]},[[0,"keydown","onHostKeydown"],[0,"focusout","onHostFocusOut"]]]]],["bs-chatbot-header.cjs",[[769,"bs-chatbot-header",{"heading":[1],"expanded":[1540]}]]],["bs-dialog.cjs",[[769,"bs-dialog",{"open":[1540],"heading":[1],"size":[1],"centered":[516],"hasFooter":[32],"hasImage":[32],"hasIcon":[32]},[[4,"keydown","onKeydown"],[4,"focusin","onFocusIn"]]]]],["bs-file-upload.cjs",[[769,"bs-file-upload",{"direction":[513],"heading":[1],"description":[1],"buttonLabel":[1,"button-label"],"accept":[1],"multiple":[4],"disabled":[516],"dragging":[1540]}]]],["bs-navigation-drawer.cjs",[[769,"bs-navigation-drawer",{"heading":[1],"collapsible":[4],"collapsed":[1540],"logoBackground":[1,"logo-background"],"showLogo":[4,"show-logo"],"open":[1540],"mobile":[32]},[[4,"keydown","onDocumentKeydown"],[4,"focusin","onDocumentFocusIn"]],{"collapsed":[{"onCollapsedChange":0}],"mobile":[{"onMobileChange":0}],"open":[{"onOpenChange":0}]}]]],["bs-navigation-header.cjs",[[769,"bs-navigation-header",{"alignment":[1],"ariaLabel":[1,"aria-label"],"skipToContentHref":[1,"skip-to-content-href"],"skipToContentLabel":[1,"skip-to-content-label"],"logoBackground":[1,"logo-background"]}]]],["bs-accordion.cjs",[[769,"bs-accordion",{"expanded":[516],"size":[1],"disabled":[4],"showIcon":[4,"show-icon"],"showPreview":[4,"show-preview"]}]]],["bs-ai-disclaimer.cjs",[[257,"bs-ai-disclaimer"]]],["bs-ai-greeting.cjs",[[513,"bs-ai-greeting",{"assistantName":[1,"assistant-name"],"productName":[1,"product-name"]}]]],["bs-ai-thinking.cjs",[[513,"bs-ai-thinking",{"label":[1]}]]],["bs-attachment-list.cjs",[[769,"bs-attachment-list",{"ariaLabel":[1,"aria-label"],"canScrollRight":[32]}]]],["bs-avatar.cjs",[[513,"bs-avatar",{"type":[1],"size":[1],"initials":[1],"src":[1],"alt":[1],"ariaLabel":[1,"aria-label"],"disabled":[516]}]]],["bs-badge.cjs",[[769,"bs-badge",{"variant":[1]}]]],["bs-breadcrumb.cjs",[[769,"bs-breadcrumb",{"href":[1],"current":[516],"size":[513]}]]],["bs-breadcrumbs.cjs",[[769,"bs-breadcrumbs",{"label":[1],"size":[1]},null,{"size":[{"onSizeChange":0}]}]]],["bs-button-skeleton.cjs",[[513,"bs-button-skeleton",{"size":[1]}]]],["bs-card.cjs",[[769,"bs-card",{"surface":[1],"hasImage":[32]}]]],["bs-chatbot-feedback.cjs",[[513,"bs-chatbot-feedback",{"rating":[1538],"submitted":[1540],"comment":[1025],"heading":[1],"subtitle":[1]}]]],["bs-chatbot-response-action.cjs",[[769,"bs-chatbot-response-action",{"sourcesCount":[2,"sources-count"],"menuOpen":[1540,"menu-open"]},[[4,"click","onDocumentClick"],[4,"keydown","onKeydown"]]]]],["bs-chatbot-sources-drawer.cjs",[[769,"bs-chatbot-sources-drawer",{"heading":[1],"count":[2],"slottedCount":[32]}]]],["bs-chatbot-suggestion-button.cjs",[[769,"bs-chatbot-suggestion-button",{"disabled":[4]}]]],["bs-checkbox.cjs",[[849,"bs-checkbox",{"name":[513],"value":[1],"required":[4],"size":[1],"checked":[1540],"indeterminate":[1540],"disabled":[516],"error":[4],"ariaLabel":[1,"aria-label"]},null,{"checked":[{"syncFormValue":0}],"value":[{"syncFormValue":0}],"required":[{"syncFormValue":0}],"indeterminate":[{"onIndeterminateChange":0}]}]]],["bs-checkbox-skeleton.cjs",[[513,"bs-checkbox-skeleton",{"size":[1]}]]],["bs-chip-filter.cjs",[[785,"bs-chip-filter",{"size":[1],"selected":[516],"dropdown":[4],"disabled":[4],"ariaLabel":[1,"aria-label"],"hasIcon":[32]}]]],["bs-chip-informative.cjs",[[769,"bs-chip-informative",{"color":[1],"size":[1],"disabled":[516],"hasIcon":[32]}]]],["bs-chip-input.cjs",[[785,"bs-chip-input",{"size":[1],"selected":[1540],"disabled":[516],"hasIcon":[32],"labelText":[32]}]]],["bs-composer.cjs",[[785,"bs-composer",{"variant":[1],"placeholder":[1],"value":[1025],"state":[513],"ariaLabel":[1,"aria-label"],"hasAttachments":[32]}]]],["bs-composer-status-banner.cjs",[[769,"bs-composer-status-banner",{"type":[1],"message":[1],"showIcon":[4,"show-icon"],"showButton":[4,"show-button"],"actionLabel":[1,"action-label"],"allowClose":[4,"allow-close"]}]]],["bs-data-table.cjs",[[513,"bs-data-table",{"columns":[16],"rows":[16],"sortColumn":[1,"sort-column"],"sortDirection":[1,"sort-direction"],"selectable":[4],"cellRenderer":[16],"selectedIds":[32],"clearSelection":[64]}]]],["bs-inline-tab.cjs",[[769,"bs-inline-tab",{"selected":[516],"disabled":[4],"ariaLabel":[1,"aria-label"],"value":[513],"tabbable":[4],"hasIcon":[32],"hasLabel":[32]}]]],["bs-navigation-drawer-item.cjs",[[769,"bs-navigation-drawer-item",{"selected":[516],"expandable":[4],"expanded":[1540],"nested":[4],"ariaLabel":[1,"aria-label"],"collapsed":[516],"hasIcon":[32]}]]],["bs-progress.cjs",[[513,"bs-progress",{"type":[1],"size":[1],"strokeWidth":[2,"stroke-width"],"value":[2],"showLabel":[4,"show-label"],"ariaLabel":[1,"aria-label"]}]]],["bs-radio.cjs",[[849,"bs-radio",{"name":[513],"value":[1],"checked":[1540],"disabled":[516],"groupHasChecked":[32]},[[4,"bsChange","onAnyRadioChange"]],{"checked":[{"onCheckedChange":0}],"value":[{"onCheckedChange":0}]}]]],["bs-slider.cjs",[[593,"bs-slider",{"type":[1],"name":[513],"showLabel":[4,"show-label"],"label":[1],"min":[2],"max":[2],"step":[2],"value":[1026],"valueStart":[1026,"value-start"],"valueEnd":[1026,"value-end"],"segments":[2],"disabled":[516],"ariaLabel":[1,"aria-label"]},null,{"value":[{"syncFormValue":0}],"valueStart":[{"syncFormValue":0}],"valueEnd":[{"syncFormValue":0}],"type":[{"syncFormValue":0}]}]]],["bs-source-link.cjs",[[769,"bs-source-link",{"fileName":[1,"file-name"],"sourceName":[1,"source-name"],"version":[1],"href":[1],"target":[1]}]]],["bs-stepper.cjs",[[769,"bs-stepper",{"currentStep":[2,"current-step"],"direction":[513],"showDescription":[4,"show-description"]},null,{"currentStep":[{"onPropChange":0}],"direction":[{"onPropChange":0}],"showDescription":[{"onPropChange":0}]}]]],["bs-stepper-step.cjs",[[513,"bs-stepper-step",{"name":[1],"description":[1],"error":[4],"disabled":[4],"computedState":[1,"computed-state"],"direction":[1],"showDescription":[4,"show-description"]}]]],["bs-switch.cjs",[[849,"bs-switch",{"name":[513],"value":[1],"size":[1],"checked":[1540],"disabled":[516],"ariaLabel":[1,"aria-label"]},null,{"checked":[{"syncFormValue":0}],"value":[{"syncFormValue":0}]}]]],["bs-tab.cjs",[[769,"bs-tab",{"selected":[516],"disabled":[4],"iconPosition":[1,"icon-position"],"ariaLabel":[1,"aria-label"],"value":[513],"tabbable":[4],"hasIcon":[32],"hasLabel":[32],"hasBadge":[32]}]]],["bs-tabs.cjs",[[769,"bs-tabs",{"type":[1],"orientation":[1],"value":[1537]},[[0,"bsSelect","onTabSelect"],[0,"keydown","onKeyDown"]],{"value":[{"onValueChange":0}],"orientation":[{"syncRovingTabindex":0}]}]]],["bs-toast.cjs",[[769,"bs-toast",{"state":[1]}]]],["bs-menu-item.cjs",[[769,"bs-menu-item",{"disabled":[4],"hasIcon":[32]}]]],["bs-pagination.cjs",[[513,"bs-pagination",{"totalPages":[2,"total-pages"],"currentPage":[1538,"current-page"],"siblingCount":[2,"sibling-count"],"previousLabel":[1,"previous-label"],"nextLabel":[1,"next-label"]}]]],["bs-progress-linear.cjs",[[769,"bs-progress-linear",{"type":[1],"size":[1],"value":[2],"showLabel":[4,"show-label"],"ariaLabel":[1,"aria-label"],"hasLabelContent":[32]}]]],["bs-tooltip.cjs",[[769,"bs-tooltip",{"placement":[1]}]]],["bs-logo.cjs",[[513,"bs-logo",{"variant":[1],"background":[1],"src":[1],"alt":[1]}]]],["bs-menu.cjs",[[257,"bs-menu",null,[[0,"keydown","onKeyDown"]]]]],["bs-button.cjs",[[849,"bs-button",{"variant":[1],"size":[1],"disabled":[4],"type":[1],"ariaLabel":[1,"aria-label"],"hasIcon":[32],"hasEndIcon":[32],"hasLabel":[32]}]]],["bs-icon-button.cjs",[[849,"bs-icon-button",{"variant":[1],"size":[1],"disabled":[4],"type":[1],"ariaLabel":[1,"aria-label"]}]]]], options);
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
exports.setNonce = index.setNonce;
|
|
@@ -59,4 +59,67 @@ describe('bs-breadcrumb', () => {
|
|
|
59
59
|
expect(smFontSize).not.toBe(mdFontSize);
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
|
+
describe('bsNavigate', () => {
|
|
63
|
+
// Dispatches a synthetic click and reports whether the component's own handlers (or a consumer's
|
|
64
|
+
// bsNavigate listener) prevented it. A window-level guard then blocks the native navigation in
|
|
65
|
+
// every case -- an un-prevented click on an <a> really navigates the test page, which the test
|
|
66
|
+
// runner's frame does not survive.
|
|
67
|
+
const click = (el, init = {}) => {
|
|
68
|
+
const ev = new MouseEvent('click', { bubbles: true, cancelable: true, composed: true, button: 0, ...init });
|
|
69
|
+
let prevented = false;
|
|
70
|
+
const guard = (e) => {
|
|
71
|
+
prevented = e.defaultPrevented;
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
};
|
|
74
|
+
window.addEventListener('click', guard, { once: true });
|
|
75
|
+
el.dispatchEvent(ev);
|
|
76
|
+
window.removeEventListener('click', guard);
|
|
77
|
+
return { prevented };
|
|
78
|
+
};
|
|
79
|
+
const link = (root) => root.shadowRoot.querySelector('a');
|
|
80
|
+
it('a plain click emits bsNavigate with the href, as a cancelable event', async () => {
|
|
81
|
+
const { root, spyOnEvent } = await render(h("bs-breadcrumb", { href: "#crumb-a" }, "Home"));
|
|
82
|
+
const spy = spyOnEvent('bsNavigate');
|
|
83
|
+
click(link(root));
|
|
84
|
+
expect(spy).toHaveReceivedEventTimes(1);
|
|
85
|
+
expect(spy).toHaveReceivedEventDetail({ href: '#crumb-a' });
|
|
86
|
+
expect(spy.lastEvent.cancelable).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
it('without a listener the native navigation still happens (click is not prevented)', async () => {
|
|
89
|
+
const { root } = await render(h("bs-breadcrumb", { href: "#crumb-b" }, "Home"));
|
|
90
|
+
expect(click(link(root)).prevented).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
it('preventDefault() in a bsNavigate listener cancels the native navigation', async () => {
|
|
93
|
+
const { root } = await render(h("bs-breadcrumb", { href: "#crumb-c" }, "Home"));
|
|
94
|
+
root.addEventListener('bsNavigate', e => e.preventDefault());
|
|
95
|
+
expect(click(link(root)).prevented).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
for (const [name, init] of [
|
|
98
|
+
['ctrl', { ctrlKey: true }],
|
|
99
|
+
['meta', { metaKey: true }],
|
|
100
|
+
['shift', { shiftKey: true }],
|
|
101
|
+
['alt', { altKey: true }],
|
|
102
|
+
['middle button', { button: 1 }],
|
|
103
|
+
]) {
|
|
104
|
+
it(`a ${name}-click does not emit bsNavigate (left to the browser)`, async () => {
|
|
105
|
+
const { root, spyOnEvent } = await render(h("bs-breadcrumb", { href: "#crumb-d" }, "Home"));
|
|
106
|
+
const spy = spyOnEvent('bsNavigate');
|
|
107
|
+
click(link(root), init);
|
|
108
|
+
expect(spy).not.toHaveReceivedEvent();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
it('bubbles to the surrounding <bs-breadcrumbs>, so one listener covers every crumb', async () => {
|
|
112
|
+
const { root } = await render(h("bs-breadcrumbs", null, h("bs-breadcrumb", { href: "#one" }, "One"), h("bs-breadcrumb", { href: "#two" }, "Two")));
|
|
113
|
+
const seen = [];
|
|
114
|
+
root.addEventListener('bsNavigate', (e) => seen.push(e.detail.href));
|
|
115
|
+
const crumbs = root.querySelectorAll('bs-breadcrumb');
|
|
116
|
+
click(link(crumbs[0]));
|
|
117
|
+
click(link(crumbs[1]));
|
|
118
|
+
expect(seen).toEqual(['#one', '#two']);
|
|
119
|
+
});
|
|
120
|
+
it('the current crumb is never a link, so it never emits', async () => {
|
|
121
|
+
const { root } = await render(h("bs-breadcrumb", { current: true, href: "#now" }, "Now"));
|
|
122
|
+
expect(root.shadowRoot.querySelector('a')).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
62
125
|
});
|
|
@@ -42,8 +42,21 @@ export class BsBreadcrumb {
|
|
|
42
42
|
* size is unchanged, since Figma didn't specify a `sm` icon size and guessing one risked drifting
|
|
43
43
|
* off-brand. Reflected so consumers/CSS can target `bs-breadcrumb[size="sm"]`. */
|
|
44
44
|
size = 'md';
|
|
45
|
+
/** Fires when the crumb's link is clicked, before the browser navigates. **Cancelable**: call
|
|
46
|
+
* `event.preventDefault()` to stop the native navigation and route it yourself (an SPA router).
|
|
47
|
+
* Detail is `{ href }`. Only a plain primary-button click emits it -- ctrl/cmd/shift/alt-clicks
|
|
48
|
+
* and middle-clicks (open in a new tab/window) are left to the browser. Bubbles and is composed,
|
|
49
|
+
* so one listener on the surrounding `<bs-breadcrumbs>` covers every crumb. */
|
|
50
|
+
bsNavigate;
|
|
51
|
+
onLinkClick = (ev) => {
|
|
52
|
+
if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey)
|
|
53
|
+
return;
|
|
54
|
+
const navigate = this.bsNavigate.emit({ href: this.href });
|
|
55
|
+
if (navigate.defaultPrevented)
|
|
56
|
+
ev.preventDefault();
|
|
57
|
+
};
|
|
45
58
|
render() {
|
|
46
|
-
return (h(Host, { key: '
|
|
59
|
+
return (h(Host, { key: '92b78d6b9eeb98420ed6a42496d632d4d437ea78', role: "listitem" }, h("span", { key: '0fbd23cacfecc6b5ea75c250d65875a1087913f7', part: "separator", class: "bs-breadcrumb__separator", "aria-hidden": "true" }, h(CaretRightIcon, { key: '2799c66de6d3291622bd3dea39e4c1f7f97a1bc9' })), this.current ? (h("span", { part: "current", class: "bs-breadcrumb__current", "aria-current": "page" }, h("slot", null))) : this.href ? (h("a", { part: "link", class: "bs-breadcrumb__link", href: this.href, onClick: this.onLinkClick }, h("slot", null))) : (h("span", { part: "link", class: "bs-breadcrumb__link" }, h("slot", null)))));
|
|
47
60
|
}
|
|
48
61
|
static get is() { return "bs-breadcrumb"; }
|
|
49
62
|
static get encapsulation() { return "shadow"; }
|
|
@@ -126,5 +139,23 @@ export class BsBreadcrumb {
|
|
|
126
139
|
}
|
|
127
140
|
};
|
|
128
141
|
}
|
|
142
|
+
static get events() {
|
|
143
|
+
return [{
|
|
144
|
+
"method": "bsNavigate",
|
|
145
|
+
"name": "bsNavigate",
|
|
146
|
+
"bubbles": true,
|
|
147
|
+
"cancelable": true,
|
|
148
|
+
"composed": true,
|
|
149
|
+
"docs": {
|
|
150
|
+
"tags": [],
|
|
151
|
+
"text": "Fires when the crumb's link is clicked, before the browser navigates. **Cancelable**: call\n`event.preventDefault()` to stop the native navigation and route it yourself (an SPA router).\nDetail is `{ href }`. Only a plain primary-button click emits it -- ctrl/cmd/shift/alt-clicks\nand middle-clicks (open in a new tab/window) are left to the browser. Bubbles and is composed,\nso one listener on the surrounding `<bs-breadcrumbs>` covers every crumb."
|
|
152
|
+
},
|
|
153
|
+
"complexType": {
|
|
154
|
+
"original": "{ href: string }",
|
|
155
|
+
"resolved": "{ href: string; }",
|
|
156
|
+
"references": {}
|
|
157
|
+
}
|
|
158
|
+
}];
|
|
159
|
+
}
|
|
129
160
|
}
|
|
130
161
|
const CaretRightIcon = () => (h("svg", { viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" }, h("path", { d: "M6 3L11 8L6 13", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" })));
|
|
@@ -115,4 +115,49 @@ describe('bs-breadcrumb-overflow', () => {
|
|
|
115
115
|
expect(root).toEqualAttribute('size', 'sm');
|
|
116
116
|
});
|
|
117
117
|
});
|
|
118
|
+
describe('bsNavigate', () => {
|
|
119
|
+
const open = async (items) => {
|
|
120
|
+
const { root, waitForChanges, spyOnEvent } = await render(h("bs-breadcrumb-overflow", null));
|
|
121
|
+
root.items = items;
|
|
122
|
+
await waitForChanges();
|
|
123
|
+
root.shadowRoot.querySelector('[part="ellipsis"]').click();
|
|
124
|
+
await waitForChanges();
|
|
125
|
+
return { root, spy: spyOnEvent('bsNavigate') };
|
|
126
|
+
};
|
|
127
|
+
// Dispatches a synthetic click and reports whether the component's own handlers (or a consumer's
|
|
128
|
+
// bsNavigate listener) prevented it. A window-level guard then blocks the native navigation in
|
|
129
|
+
// every case -- an un-prevented click on an <a> really navigates the test page, which the test
|
|
130
|
+
// runner's frame does not survive.
|
|
131
|
+
const click = (el, init = {}) => {
|
|
132
|
+
const ev = new MouseEvent('click', { bubbles: true, cancelable: true, composed: true, button: 0, ...init });
|
|
133
|
+
let prevented = false;
|
|
134
|
+
const guard = (e) => {
|
|
135
|
+
prevented = e.defaultPrevented;
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
};
|
|
138
|
+
window.addEventListener('click', guard, { once: true });
|
|
139
|
+
el.dispatchEvent(ev);
|
|
140
|
+
window.removeEventListener('click', guard);
|
|
141
|
+
return { prevented };
|
|
142
|
+
};
|
|
143
|
+
it('clicking a popup link emits a cancelable bsNavigate with that link\'s href', async () => {
|
|
144
|
+
const { root, spy } = await open([
|
|
145
|
+
{ label: 'A', href: '#ov-a' },
|
|
146
|
+
{ label: 'B', href: '#ov-b' },
|
|
147
|
+
]);
|
|
148
|
+
const links = root.shadowRoot.querySelectorAll('a[part="link"]');
|
|
149
|
+
click(links[1]);
|
|
150
|
+
expect(spy).toHaveReceivedEventTimes(1);
|
|
151
|
+
expect(spy).toHaveReceivedEventDetail({ href: '#ov-b' });
|
|
152
|
+
expect(spy.lastEvent.cancelable).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
it('preventDefault() cancels the native navigation; a ctrl-click does not emit', async () => {
|
|
155
|
+
const { root, spy } = await open([{ label: 'A', href: '#ov-c' }]);
|
|
156
|
+
root.addEventListener('bsNavigate', e => e.preventDefault());
|
|
157
|
+
const link = root.shadowRoot.querySelector('a[part="link"]');
|
|
158
|
+
expect(click(link).prevented).toBe(true);
|
|
159
|
+
click(link, { ctrlKey: true });
|
|
160
|
+
expect(spy).toHaveReceivedEventTimes(1);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
118
163
|
});
|
package/dist/collection/components/bs-breadcrumb/bs-breadcrumb-overflow/bs-breadcrumb-overflow.js
CHANGED
|
@@ -54,6 +54,17 @@ export class BsBreadcrumbOverflow {
|
|
|
54
54
|
size = 'md';
|
|
55
55
|
/** Whether the overflow popup is open. */
|
|
56
56
|
open = false;
|
|
57
|
+
/** Fires when one of the popup's links is clicked, before the browser navigates. **Cancelable**
|
|
58
|
+
* -- see `bs-breadcrumb`'s `bsNavigate`, which this mirrors (same detail, same modified-click
|
|
59
|
+
* rule). Detail is `{ href }`. */
|
|
60
|
+
bsNavigate;
|
|
61
|
+
onItemClick = (href) => (ev) => {
|
|
62
|
+
if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey)
|
|
63
|
+
return;
|
|
64
|
+
const navigate = this.bsNavigate.emit({ href });
|
|
65
|
+
if (navigate.defaultPrevented)
|
|
66
|
+
ev.preventDefault();
|
|
67
|
+
};
|
|
57
68
|
onEllipsisToggle = () => {
|
|
58
69
|
this.open = !this.open;
|
|
59
70
|
};
|
|
@@ -77,7 +88,7 @@ export class BsBreadcrumbOverflow {
|
|
|
77
88
|
}
|
|
78
89
|
}
|
|
79
90
|
render() {
|
|
80
|
-
return (h(Host, { key: '
|
|
91
|
+
return (h(Host, { key: '4dd958f3f99f42072bb314654fc5a0856d8e41c4', role: "listitem" }, h("span", { key: 'ab7d01467e99b810b62e71d7ed289316470529f5', part: "separator", class: "bs-breadcrumb-overflow__separator", "aria-hidden": "true" }, h(CaretRightIcon, { key: '6590de37057e39c4c914fde3dc3490e17566ad66' })), h("span", { key: '63081f592180f605716221c80dbb877acf926d48', class: "bs-breadcrumb-overflow__wrapper" }, h("button", { key: '0ec87f47561dc329269b1fefef0ccb76dae4cdb4', type: "button", part: "ellipsis", class: "bs-breadcrumb-overflow__ellipsis", "aria-haspopup": "menu", "aria-expanded": String(this.open), "aria-label": this.overflowLabel, onClick: this.onEllipsisToggle }, h(DotsThreeIcon, { key: '6da3c5bee9f532264e2e58227dccc4dfc3786782' })), this.open && this.items.length > 0 && (h("bs-menu", { key: '66f772f0f53090e960d2a9b08e1283989dfc02f8', part: "menu", class: "bs-breadcrumb-overflow__menu" }, this.items.map(item => item.href ? (h("a", { part: "link", class: "bs-breadcrumb-overflow__link", href: item.href, onClick: this.onItemClick(item.href) }, item.label)) : (h("span", { part: "link", class: "bs-breadcrumb-overflow__link" }, item.label))))))));
|
|
81
92
|
}
|
|
82
93
|
static get is() { return "bs-breadcrumb-overflow"; }
|
|
83
94
|
static get encapsulation() { return "shadow"; }
|
|
@@ -171,6 +182,24 @@ export class BsBreadcrumbOverflow {
|
|
|
171
182
|
"open": {}
|
|
172
183
|
};
|
|
173
184
|
}
|
|
185
|
+
static get events() {
|
|
186
|
+
return [{
|
|
187
|
+
"method": "bsNavigate",
|
|
188
|
+
"name": "bsNavigate",
|
|
189
|
+
"bubbles": true,
|
|
190
|
+
"cancelable": true,
|
|
191
|
+
"composed": true,
|
|
192
|
+
"docs": {
|
|
193
|
+
"tags": [],
|
|
194
|
+
"text": "Fires when one of the popup's links is clicked, before the browser navigates. **Cancelable**\n-- see `bs-breadcrumb`'s `bsNavigate`, which this mirrors (same detail, same modified-click\nrule). Detail is `{ href }`."
|
|
195
|
+
},
|
|
196
|
+
"complexType": {
|
|
197
|
+
"original": "{ href: string }",
|
|
198
|
+
"resolved": "{ href: string; }",
|
|
199
|
+
"references": {}
|
|
200
|
+
}
|
|
201
|
+
}];
|
|
202
|
+
}
|
|
174
203
|
static get elementRef() { return "el"; }
|
|
175
204
|
static get listeners() {
|
|
176
205
|
return [{
|
|
@@ -80,4 +80,24 @@ describe('bs-icon-button', () => {
|
|
|
80
80
|
expect(iconWrapper.querySelector('slot')).not.toBeNull();
|
|
81
81
|
expect(root.querySelector('svg.my-icon')).not.toBeNull();
|
|
82
82
|
});
|
|
83
|
+
describe('icon-font glyphs', () => {
|
|
84
|
+
// Issue: a slotted icon-font glyph (<i class="ph-bold ...">) ignored the size prop, because
|
|
85
|
+
// glyphs are sized by font-size and only width/height were being set.
|
|
86
|
+
for (const [size, px] of [
|
|
87
|
+
['xs', 8],
|
|
88
|
+
['sm', 16],
|
|
89
|
+
['md', 20],
|
|
90
|
+
['lg', 24],
|
|
91
|
+
]) {
|
|
92
|
+
it(`size="${size}" sizes a slotted glyph to ${px}px via font-size`, async () => {
|
|
93
|
+
const { root } = await render(h("bs-icon-button", { size: size, ariaLabel: "Bell" }, h("i", { class: "glyph" }, "A")));
|
|
94
|
+
const glyph = root.querySelector('i.glyph');
|
|
95
|
+
expect(getComputedStyle(glyph).fontSize).toBe(`${px}px`);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
it('does not override a font-size the consumer sets on the glyph themselves', async () => {
|
|
99
|
+
const { root } = await render(h("bs-icon-button", { size: "md", ariaLabel: "Bell" }, h("i", { class: "glyph", style: { fontSize: '30px' } }, "A")));
|
|
100
|
+
expect(getComputedStyle(root.querySelector('i.glyph')).fontSize).toBe('30px');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
83
103
|
});
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* @prop --bs-icon-button-radius: Corner radius, size="sm"/"md"/"lg". Aliased to --bs-border-radius-100.
|
|
3
3
|
* @prop --bs-icon-button-radius-xs: Corner radius, size="xs" -- a smaller radius than --bs-icon-button-radius, since --bs-border-radius-100 (8px) on a 16px box would look nearly circular. Aliased to --bs-border-radius-50.
|
|
4
4
|
* @prop --bs-icon-button-size-xs: Container width/height at size="xs". 16px.
|
|
5
|
-
* @prop --bs-icon-button-icon-size-xs: Slotted icon width/height at size="xs". 8px.
|
|
5
|
+
* @prop --bs-icon-button-icon-size-xs: Slotted icon width/height (and font-size, for icon-font glyphs) at size="xs". 8px.
|
|
6
6
|
* @prop --bs-icon-button-padding-xs: Padding on all sides, size="xs" only -- distinct from --bs-icon-button-padding, since 16px/8px icon math (4px) differs from the sm/md/lg constant (12px). Aliased to --bs-spacing-50.
|
|
7
7
|
* @prop --bs-icon-button-size-sm: Container width/height at size="sm". 40px.
|
|
8
8
|
* @prop --bs-icon-button-size-md: Container width/height at size="md". 44px.
|
|
9
9
|
* @prop --bs-icon-button-size-lg: Container width/height at size="lg". 48px.
|
|
10
|
-
* @prop --bs-icon-button-icon-size-sm: Slotted icon width/height at size="sm". 16px.
|
|
11
|
-
* @prop --bs-icon-button-icon-size-md: Slotted icon width/height at size="md". 20px.
|
|
12
|
-
* @prop --bs-icon-button-icon-size-lg: Slotted icon width/height at size="lg". 24px.
|
|
10
|
+
* @prop --bs-icon-button-icon-size-sm: Slotted icon width/height (and font-size, for icon-font glyphs) at size="sm". 16px.
|
|
11
|
+
* @prop --bs-icon-button-icon-size-md: Slotted icon width/height (and font-size, for icon-font glyphs) at size="md". 20px.
|
|
12
|
+
* @prop --bs-icon-button-icon-size-lg: Slotted icon width/height (and font-size, for icon-font glyphs) at size="lg". 24px.
|
|
13
13
|
* @prop --bs-icon-button-padding: Padding on all sides, all sizes (constant -- centers the icon
|
|
14
14
|
* inside the container at every size). Aliased to --bs-spacing-150.
|
|
15
15
|
* @prop --bs-icon-button-primary-default: Background, variant="primary", default. Aliased to --bs-color-primary-default.
|
|
@@ -161,6 +161,9 @@
|
|
|
161
161
|
.bs-icon-button--xs .bs-icon-button__icon {
|
|
162
162
|
width: var(--bs-icon-button-icon-size-xs);
|
|
163
163
|
height: var(--bs-icon-button-icon-size-xs);
|
|
164
|
+
/* Icon-font glyphs (e.g. <i class="ph-bold ph-bell">) are sized by font-size, not width/height,
|
|
165
|
+
so they follow the same token as SVG icons do. */
|
|
166
|
+
font-size: var(--bs-icon-button-icon-size-xs);
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
.bs-icon-button--sm {
|
|
@@ -170,6 +173,9 @@
|
|
|
170
173
|
.bs-icon-button--sm .bs-icon-button__icon {
|
|
171
174
|
width: var(--bs-icon-button-icon-size-sm);
|
|
172
175
|
height: var(--bs-icon-button-icon-size-sm);
|
|
176
|
+
/* Icon-font glyphs (e.g. <i class="ph-bold ph-bell">) are sized by font-size, not width/height,
|
|
177
|
+
so they follow the same token as SVG icons do. */
|
|
178
|
+
font-size: var(--bs-icon-button-icon-size-sm);
|
|
173
179
|
}
|
|
174
180
|
|
|
175
181
|
.bs-icon-button--md {
|
|
@@ -179,6 +185,9 @@
|
|
|
179
185
|
.bs-icon-button--md .bs-icon-button__icon {
|
|
180
186
|
width: var(--bs-icon-button-icon-size-md);
|
|
181
187
|
height: var(--bs-icon-button-icon-size-md);
|
|
188
|
+
/* Icon-font glyphs (e.g. <i class="ph-bold ph-bell">) are sized by font-size, not width/height,
|
|
189
|
+
so they follow the same token as SVG icons do. */
|
|
190
|
+
font-size: var(--bs-icon-button-icon-size-md);
|
|
182
191
|
}
|
|
183
192
|
|
|
184
193
|
.bs-icon-button--lg {
|
|
@@ -188,6 +197,9 @@
|
|
|
188
197
|
.bs-icon-button--lg .bs-icon-button__icon {
|
|
189
198
|
width: var(--bs-icon-button-icon-size-lg);
|
|
190
199
|
height: var(--bs-icon-button-icon-size-lg);
|
|
200
|
+
/* Icon-font glyphs (e.g. <i class="ph-bold ph-bell">) are sized by font-size, not width/height,
|
|
201
|
+
so they follow the same token as SVG icons do. */
|
|
202
|
+
font-size: var(--bs-icon-button-icon-size-lg);
|
|
191
203
|
}
|
|
192
204
|
|
|
193
205
|
.bs-icon-button__icon {
|
|
@@ -195,12 +207,17 @@
|
|
|
195
207
|
align-items: center;
|
|
196
208
|
justify-content: center;
|
|
197
209
|
color: inherit;
|
|
210
|
+
line-height: 1;
|
|
198
211
|
}
|
|
199
212
|
|
|
200
213
|
.bs-icon-button__icon ::slotted(*) {
|
|
201
214
|
display: block;
|
|
202
215
|
width: 100%;
|
|
203
216
|
height: 100%;
|
|
217
|
+
/* Make a slotted icon font glyph take the wrapper's font-size (above) rather than whatever the
|
|
218
|
+
page's inherited size happens to be. */
|
|
219
|
+
font-size: inherit;
|
|
220
|
+
line-height: 1;
|
|
204
221
|
}
|
|
205
222
|
|
|
206
223
|
.bs-icon-button--primary {
|
|
@@ -20,7 +20,8 @@ import { h } from "@stencil/core";
|
|
|
20
20
|
* just conditionally, unlike `bs-button`) -- there's no visible label text to fall back on for an
|
|
21
21
|
* accessible name.
|
|
22
22
|
*
|
|
23
|
-
* @slot - Default slot: the icon content. Exactly one icon, no label
|
|
23
|
+
* @slot - Default slot: the icon content. Exactly one icon, no label -- an inline SVG, or an icon-font
|
|
24
|
+
* glyph (sized by `font-size`, which this component sets from the `size` prop).
|
|
24
25
|
* @part icon - The icon wrapper element.
|
|
25
26
|
*/
|
|
26
27
|
// Note: this component's CSS Custom Properties (--bs-icon-button-*) are documented via @prop
|
|
@@ -61,7 +62,7 @@ export class BsIconButton {
|
|
|
61
62
|
form.reset();
|
|
62
63
|
};
|
|
63
64
|
render() {
|
|
64
|
-
return (h("button", { key: '
|
|
65
|
+
return (h("button", { key: '538cd121df25e20ea7e21a7b5269e73d275f8bff', class: `bs-icon-button bs-icon-button--${this.variant} bs-icon-button--${this.size}`, type: this.type, disabled: this.disabled, "aria-label": this.ariaLabel, onClick: this.onClick }, h("span", { key: '35548fe082134edb85dbdcb0caf820ec3b5233c2', part: "icon", class: "bs-icon-button__icon" }, h("slot", { key: '7f16ef5f2c08fc5d2bb852bd5c36598be5aa1175' }))));
|
|
65
66
|
}
|
|
66
67
|
static get is() { return "bs-icon-button"; }
|
|
67
68
|
static get encapsulation() { return "shadow"; }
|
|
@@ -42,6 +42,16 @@ export const Sizes = {
|
|
|
42
42
|
</div>
|
|
43
43
|
`,
|
|
44
44
|
};
|
|
45
|
+
// An icon-font glyph in the slot (here a plain text glyph, so the story needs no icon-font
|
|
46
|
+
// dependency) scales with `size` exactly like the SVG above -- no manual font-size needed.
|
|
47
|
+
export const IconFontGlyph = {
|
|
48
|
+
name: 'Icon-font glyph',
|
|
49
|
+
render: args => html `
|
|
50
|
+
<div style="display:flex; gap:12px; align-items:center;">
|
|
51
|
+
${['xs', 'sm', 'md', 'lg'].map(size => html `<bs-icon-button size=${size} aria-label=${args.ariaLabel}><i style="font-style: normal;">★</i></bs-icon-button>`)}
|
|
52
|
+
</div>
|
|
53
|
+
`,
|
|
54
|
+
};
|
|
45
55
|
export const Disabled = {
|
|
46
56
|
args: { disabled: true },
|
|
47
57
|
};
|
|
@@ -99,6 +99,25 @@ describe('bs-data-table', () => {
|
|
|
99
99
|
expect(cell.querySelector('img')).toBeNull();
|
|
100
100
|
expect(cell.textContent).toBe('<img src=x onerror="window.__xss = true">');
|
|
101
101
|
});
|
|
102
|
+
it('renders rich cell content: a link and a web component become real elements', async () => {
|
|
103
|
+
const { root, waitForChanges } = await render(h("bs-data-table", { columns: columns, rows: rows, cellRenderer: (row) => `<a href="/users/${row.id}">Open</a><bs-badge variant="success">Active</bs-badge>` }));
|
|
104
|
+
await waitForChanges();
|
|
105
|
+
const cell = root.shadowRoot.querySelector('tbody td');
|
|
106
|
+
expect(cell.querySelector('a')).toEqualAttribute('href', '/users/1');
|
|
107
|
+
const badge = cell.querySelector('bs-badge');
|
|
108
|
+
expect(badge).not.toBeNull();
|
|
109
|
+
expect(badge.shadowRoot).not.toBeNull(); // upgraded: it is a live component, not inert text
|
|
110
|
+
});
|
|
111
|
+
it('applies cellRenderer to every cell, and does NOT escape what it returns (the caller must)', async () => {
|
|
112
|
+
const unsafeRows = [{ id: 1, name: '<img src=x onerror="window.__xss2 = true">', status: 'Active' }];
|
|
113
|
+
const unsafe = await render(h("bs-data-table", { columns: columns, rows: unsafeRows, cellRenderer: (row, col) => String(row[col.key]) }));
|
|
114
|
+
expect(unsafe.root.shadowRoot.querySelector('tbody td').querySelector('img')).not.toBeNull();
|
|
115
|
+
const esc = (v) => String(v).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
116
|
+
const safe = await render(h("bs-data-table", { columns: columns, rows: unsafeRows, cellRenderer: (row, col) => esc(row[col.key]) }));
|
|
117
|
+
const cell = safe.root.shadowRoot.querySelector('tbody td');
|
|
118
|
+
expect(cell.querySelector('img')).toBeNull();
|
|
119
|
+
expect(cell.textContent).toBe('<img src=x onerror="window.__xss2 = true">');
|
|
120
|
+
});
|
|
102
121
|
it('still allows cellRenderer to render trusted custom HTML when explicitly supplied', async () => {
|
|
103
122
|
const { root } = await render(h("bs-data-table", { columns: columns, rows: rows, cellRenderer: () => '<button type="button">Edit</button>' }));
|
|
104
123
|
const cell = root.shadowRoot.querySelector('tbody td');
|
|
@@ -29,8 +29,22 @@ export class BsDataTable {
|
|
|
29
29
|
sortColumn;
|
|
30
30
|
sortDirection = 'asc';
|
|
31
31
|
selectable = false;
|
|
32
|
-
/** Optional custom cell renderer,
|
|
33
|
-
*
|
|
32
|
+
/** Optional custom cell renderer, for rich cells such as a status badge, a link or an actions
|
|
33
|
+
* column. It returns an **HTML string** that is inserted into the cell as real markup
|
|
34
|
+
* (`innerHTML`), so links, buttons, `<img>` and other web components (e.g. `<bs-badge>`) all render.
|
|
35
|
+
*
|
|
36
|
+
* **When set it renders every cell, not just the special ones** -- return the plain value for
|
|
37
|
+
* ordinary columns too. And **the returned string is not escaped or sanitized** (same trust
|
|
38
|
+
* model as React's `dangerouslySetInnerHTML`): escape any untrusted row data you put in it, plain
|
|
39
|
+
* columns included, or it will be interpreted as HTML. Without a `cellRenderer`, values are
|
|
40
|
+
* rendered as escaped text.
|
|
41
|
+
*
|
|
42
|
+
* The markup is static HTML -- attach behavior yourself, e.g. one `click` listener on the table
|
|
43
|
+
* that reads `event.composedPath()[0]` (events from inside the shadow root are retargeted to the
|
|
44
|
+
* table itself).
|
|
45
|
+
*
|
|
46
|
+
* Function props are JS-property-only, same as array props -- there is no attribute equivalent.
|
|
47
|
+
* Falls back to the raw cell value (as escaped text) when unset. */
|
|
34
48
|
cellRenderer;
|
|
35
49
|
bsSort;
|
|
36
50
|
bsRowSelect;
|
|
@@ -71,11 +85,11 @@ export class BsDataTable {
|
|
|
71
85
|
}
|
|
72
86
|
}
|
|
73
87
|
render() {
|
|
74
|
-
return (h("table", { key: '
|
|
88
|
+
return (h("table", { key: '7ea79a70148513512cdba2f8cb8d5a5c7ae09706', part: "table", class: "bs-data-table" }, h("thead", { key: 'e9cf73cbba196e9caf4b03a837866a5370c13e19', part: "head" }, h("tr", { key: 'a33fbd57f1925e34c6e7ce292b6945d2ee5c6113' }, this.selectable && (h("th", { key: '876948b66e54ae68d6aa6148c962781223fa0441', class: "bs-data-table__select-col" }, this.rows.length > 0 && (h("input", { key: 'cf8f26258bcee486190e3ac4b3d5ce4715784d5f', type: "checkbox", "aria-label": "Select all rows", checked: this.selectedIds.size > 0 && this.selectedIds.size === this.rows.length, indeterminate: this.selectedIds.size > 0 && this.selectedIds.size < this.rows.length, onChange: this.onSelectAllChange })))), this.columns.map(column => {
|
|
75
89
|
const isSorted = this.sortColumn === column.key;
|
|
76
90
|
const ariaSort = column.sortable ? (isSorted ? (this.sortDirection === 'asc' ? 'ascending' : 'descending') : 'none') : undefined;
|
|
77
91
|
return (h("th", { part: "header-cell", class: "bs-data-table__header-cell", "aria-sort": ariaSort }, column.sortable ? (h("button", { type: "button", class: "bs-data-table__sort-button", onClick: () => this.onHeaderClick(column) }, column.label, isSorted && h("span", { class: "bs-data-table__sort-indicator" }, this.sortDirection === 'asc' ? ' ▲' : ' ▼'))) : (column.label)));
|
|
78
|
-
}))), h("tbody", { key: '
|
|
92
|
+
}))), h("tbody", { key: '3a0c2ce6fea27797b855ee2b3c724d0e99471d28', part: "body" }, this.rows.map(row => (h("tr", { part: "row", class: "bs-data-table__row" }, this.selectable && (h("td", { class: "bs-data-table__select-col" }, h("input", { type: "checkbox", checked: this.selectedIds.has(row.id), onChange: ev => this.onRowCheckboxChange(row, ev) }))), this.columns.map(column => this.cellRenderer ? (
|
|
79
93
|
// cellRenderer is a developer-supplied, trusted function (JS-property-only, never
|
|
80
94
|
// derived from untrusted input) -- its HTML output is intentionally not escaped,
|
|
81
95
|
// same trust model as React's dangerouslySetInnerHTML.
|
|
@@ -228,7 +242,7 @@ export class BsDataTable {
|
|
|
228
242
|
"optional": true,
|
|
229
243
|
"docs": {
|
|
230
244
|
"tags": [],
|
|
231
|
-
"text": "Optional custom cell renderer, e.g. for
|
|
245
|
+
"text": "Optional custom cell renderer, for rich cells such as a status badge, a link or an actions\ncolumn. It returns an **HTML string** that is inserted into the cell as real markup\n(`innerHTML`), so links, buttons, `<img>` and other web components (e.g. `<bs-badge>`) all render.\n\n**When set it renders every cell, not just the special ones** -- return the plain value for\nordinary columns too. And **the returned string is not escaped or sanitized** (same trust\nmodel as React's `dangerouslySetInnerHTML`): escape any untrusted row data you put in it, plain\ncolumns included, or it will be interpreted as HTML. Without a `cellRenderer`, values are\nrendered as escaped text.\n\nThe markup is static HTML -- attach behavior yourself, e.g. one `click` listener on the table\nthat reads `event.composedPath()[0]` (events from inside the shadow root are retargeted to the\ntable itself).\n\nFunction props are JS-property-only, same as array props -- there is no attribute equivalent.\nFalls back to the raw cell value (as escaped text) when unset."
|
|
232
246
|
},
|
|
233
247
|
"getter": false,
|
|
234
248
|
"setter": false
|