@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
|
@@ -44,11 +44,18 @@ export const WithCustomCellRenderer = {
|
|
|
44
44
|
const table = document.createElement('bs-data-table');
|
|
45
45
|
table.columns = [...columns, { key: 'actions', label: 'Actions' }];
|
|
46
46
|
table.rows = rows;
|
|
47
|
+
// cellRenderer renders EVERY cell and its output is not escaped, so plain columns are escaped
|
|
48
|
+
// here too -- copying `String(row[column.key])` unescaped would make untrusted row data HTML.
|
|
49
|
+
const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
47
50
|
table.cellRenderer = (row, column) => {
|
|
48
51
|
if (column.key === 'actions') {
|
|
49
52
|
return `<button type="button">Edit</button>`;
|
|
50
53
|
}
|
|
51
|
-
|
|
54
|
+
if (column.key === 'status') {
|
|
55
|
+
// Rich content: a web component in the cell.
|
|
56
|
+
return `<bs-badge variant="${row.status === 'Active' ? 'success' : 'neutral'}">${escapeHtml(row.status)}</bs-badge>`;
|
|
57
|
+
}
|
|
58
|
+
return escapeHtml(row[column.key]);
|
|
52
59
|
};
|
|
53
60
|
return table;
|
|
54
61
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { render, h, describe, it, expect } from "@stencil/vitest";
|
|
2
|
+
import { vi } from "vitest";
|
|
2
3
|
describe('bs-logo', () => {
|
|
3
4
|
it('renders role="img" and aria-label="BrandSync"', async () => {
|
|
4
5
|
const { root } = await render(h("bs-logo", null));
|
|
@@ -37,9 +38,9 @@ describe('bs-logo', () => {
|
|
|
37
38
|
expect(getComputedStyle(darkAsset).display).toBe('none');
|
|
38
39
|
});
|
|
39
40
|
it('shows the dark asset and hides the light one when an ambient dark theme sets --bs-logo-asset-*-display', async () => {
|
|
40
|
-
// Simulates what src/global/base.css's [data-theme="dark"] rule sets
|
|
41
|
-
//
|
|
42
|
-
//
|
|
41
|
+
// Simulates what src/global/base.css's [data-theme="dark"] rule sets, by setting the same
|
|
42
|
+
// custom properties directly on the render stage container. (The real selector is exercised
|
|
43
|
+
// in the "follows the real [data-theme="dark"] attribute" tests below.) CSS custom
|
|
43
44
|
// properties inherit through the shadow boundary regardless of where they're defined.
|
|
44
45
|
const { root } = await render(h("bs-logo", null), {
|
|
45
46
|
stageAttrs: { style: '--bs-logo-asset-light-display: none; --bs-logo-asset-dark-display: block;' },
|
|
@@ -50,6 +51,54 @@ describe('bs-logo', () => {
|
|
|
50
51
|
expect(getComputedStyle(darkAsset).display).not.toBe('none');
|
|
51
52
|
});
|
|
52
53
|
});
|
|
54
|
+
// The tests above set the --bs-logo-asset-*-display variables by hand. These exercise the real
|
|
55
|
+
// `[data-theme="dark"]` rule from src/global/base.css (loaded by vitest-setup.ts), i.e. what a
|
|
56
|
+
// consuming app actually does -- including WHERE the attribute may sit (issue: dark logo on a dark
|
|
57
|
+
// theme).
|
|
58
|
+
describe('background="auto" follows the real [data-theme="dark"] attribute', () => {
|
|
59
|
+
const shown = (root) => ({
|
|
60
|
+
light: getComputedStyle(root.shadowRoot.querySelector('.bs-logo__asset--light')).display !== 'none',
|
|
61
|
+
dark: getComputedStyle(root.shadowRoot.querySelector('.bs-logo__asset--dark')).display !== 'none',
|
|
62
|
+
});
|
|
63
|
+
it('flips to the dark asset when data-theme="dark" is on <html>', async () => {
|
|
64
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
65
|
+
try {
|
|
66
|
+
const { root } = await render(h("bs-logo", null));
|
|
67
|
+
expect(shown(root)).toEqual({ light: false, dark: true });
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
document.documentElement.removeAttribute('data-theme');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
it('flips to the dark asset when data-theme="dark" is on <body>', async () => {
|
|
74
|
+
document.body.setAttribute('data-theme', 'dark');
|
|
75
|
+
try {
|
|
76
|
+
const { root } = await render(h("bs-logo", null));
|
|
77
|
+
expect(shown(root)).toEqual({ light: false, dark: true });
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
document.body.removeAttribute('data-theme');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
it('flips to the dark asset when data-theme="dark" is on any ancestor wrapper', async () => {
|
|
84
|
+
const { root } = await render(h("div", { "data-theme": "dark" }, h("bs-logo", null)));
|
|
85
|
+
expect(shown(root.querySelector('bs-logo'))).toEqual({ light: false, dark: true });
|
|
86
|
+
});
|
|
87
|
+
it('does NOT flip for a logo outside the data-theme="dark" element', async () => {
|
|
88
|
+
const { root } = await render(h("div", null, h("div", { "data-theme": "dark" }), h("bs-logo", null)));
|
|
89
|
+
expect(shown(root.querySelector('bs-logo'))).toEqual({ light: true, dark: false });
|
|
90
|
+
});
|
|
91
|
+
it('does not follow prefers-color-scheme (only the data-theme attribute)', async () => {
|
|
92
|
+
const spy = vi.spyOn(window, 'matchMedia').mockImplementation((q) => ({ matches: q.includes('prefers-color-scheme: dark'), media: q, addEventListener: () => undefined, removeEventListener: () => undefined, addListener: () => undefined, removeListener: () => undefined, onchange: null, dispatchEvent: () => false }));
|
|
93
|
+
try {
|
|
94
|
+
const { root } = await render(h("bs-logo", null));
|
|
95
|
+
expect(shown(root)).toEqual({ light: true, dark: false });
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
spy.mockRestore();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
53
102
|
describe('background="light"/"dark" (explicit)', () => {
|
|
54
103
|
it('pins the light asset even when an ambient dark theme is set', async () => {
|
|
55
104
|
const { root } = await render(h("bs-logo", { background: "light" }), {
|
|
@@ -13,3 +13,57 @@ const meta = {
|
|
|
13
13
|
};
|
|
14
14
|
export default meta;
|
|
15
15
|
export const Default = {};
|
|
16
|
+
// `bs-menu` is only the popup surface: it does not open, close or position itself. This story is the
|
|
17
|
+
// recipe for turning it into a dropdown with the pieces the library already has (a button, the menu,
|
|
18
|
+
// and ~25 lines of wiring). The same code is shown in bs-menu.mdx.
|
|
19
|
+
export const DropdownRecipe = {
|
|
20
|
+
name: 'Dropdown (recipe)',
|
|
21
|
+
render: () => {
|
|
22
|
+
const host = document.createElement('div');
|
|
23
|
+
host.style.minHeight = '220px';
|
|
24
|
+
host.innerHTML = `
|
|
25
|
+
<div class="dropdown" style="position: relative; display: inline-block;">
|
|
26
|
+
<bs-button aria-haspopup="menu" aria-expanded="false">Actions</bs-button>
|
|
27
|
+
<bs-menu style="display: none; position: absolute; top: calc(100% + var(--bs-spacing-50)); inset-inline-start: 0; z-index: var(--bs-z-index-popover); width: 210px;">
|
|
28
|
+
<bs-menu-item>Edit</bs-menu-item>
|
|
29
|
+
<bs-menu-item>Duplicate</bs-menu-item>
|
|
30
|
+
<bs-menu-item disabled>Archive</bs-menu-item>
|
|
31
|
+
<bs-menu-item>Delete</bs-menu-item>
|
|
32
|
+
</bs-menu>
|
|
33
|
+
</div>`;
|
|
34
|
+
const wrapper = host.querySelector('.dropdown');
|
|
35
|
+
const trigger = wrapper.querySelector('bs-button');
|
|
36
|
+
const menu = wrapper.querySelector('bs-menu');
|
|
37
|
+
const onDocumentClick = (ev) => {
|
|
38
|
+
// composedPath() so a click inside the shadow DOM of the button / menu items counts as inside
|
|
39
|
+
if (!ev.composedPath().includes(wrapper))
|
|
40
|
+
setOpen(false);
|
|
41
|
+
};
|
|
42
|
+
function setOpen(open, restoreFocus = false) {
|
|
43
|
+
menu.style.display = open ? 'block' : 'none';
|
|
44
|
+
trigger.setAttribute('aria-expanded', String(open));
|
|
45
|
+
if (open) {
|
|
46
|
+
document.addEventListener('click', onDocumentClick);
|
|
47
|
+
// bs-menu handles Up/Down/Home/End once focus is inside; put it on the first enabled item
|
|
48
|
+
menu.querySelector('bs-menu-item:not([disabled])')?.shadowRoot?.querySelector('button')?.focus();
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
document.removeEventListener('click', onDocumentClick);
|
|
52
|
+
if (restoreFocus)
|
|
53
|
+
trigger.focus();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
trigger.addEventListener('click', () => setOpen(menu.style.display === 'none'));
|
|
57
|
+
wrapper.addEventListener('keydown', (ev) => {
|
|
58
|
+
if (ev.key === 'Escape')
|
|
59
|
+
setOpen(false, true);
|
|
60
|
+
});
|
|
61
|
+
wrapper.addEventListener('focusout', (ev) => {
|
|
62
|
+
// Tab (or anything else) moving focus out of the whole dropdown closes it
|
|
63
|
+
if (menu.style.display !== 'none' && !wrapper.contains(ev.relatedTarget))
|
|
64
|
+
setOpen(false);
|
|
65
|
+
});
|
|
66
|
+
menu.addEventListener('bsSelect', () => setOpen(false, true));
|
|
67
|
+
return host;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -48,6 +48,10 @@ export class BsInlineTab {
|
|
|
48
48
|
* violation). Stencil reflects this camelCase prop to the `aria-label` HTML attribute
|
|
49
49
|
* automatically, same as `bs-tab`'s identical prop. */
|
|
50
50
|
ariaLabel = null;
|
|
51
|
+
/** Identifies this tab within a parent `bs-tabs`: the group's `value` is the selected tab's
|
|
52
|
+
* `value`, and `bsChange` reports it. Optional -- a tab without one is identified by its
|
|
53
|
+
* zero-based position in the group (`"0"`, `"1"`...). */
|
|
54
|
+
value;
|
|
51
55
|
/** Set by a parent `bs-tabs` to implement roving tabindex (only one tab in the group is in the
|
|
52
56
|
* Tab order; arrow keys move between them). Left `undefined` for a standalone tab, which stays
|
|
53
57
|
* a normal tab stop. */
|
|
@@ -90,7 +94,7 @@ export class BsInlineTab {
|
|
|
90
94
|
};
|
|
91
95
|
render() {
|
|
92
96
|
const iconOnly = this.hasIcon && !this.hasLabel;
|
|
93
|
-
return (h("button", { key: '
|
|
97
|
+
return (h("button", { key: '0d6927b0b39a7d84add287e6473681e5052b5f2d', type: "button", part: "tab", role: "tab", class: `bs-inline-tab ${this.selected ? 'bs-inline-tab--selected' : ''} ${this.disabled ? 'bs-inline-tab--disabled' : ''} ${iconOnly ? 'bs-inline-tab--icon-only' : ''} ${this.hasIcon ? 'bs-inline-tab--has-icon' : ''}`, disabled: this.disabled, "aria-selected": String(this.selected), "aria-disabled": this.disabled ? 'true' : undefined, "aria-label": this.ariaLabel, tabIndex: this.tabbable === undefined ? undefined : this.tabbable ? 0 : -1, onClick: this.onClick }, h("span", { key: '866660f0f79927cd018df42286719f2f48262cbb', part: "icon", class: `bs-inline-tab__icon ${this.hasIcon ? 'bs-inline-tab__icon--has-content' : ''}` }, h("slot", { key: '58e11c8a0a57646644cb0b7c950d73f5cee52310', name: "icon", onSlotchange: this.onIconSlotchange })), h("span", { key: '38d2e1df2b4e494f68d3c4b9ea34d7a317401953', part: "label", class: `bs-inline-tab__label ${this.hasLabel ? 'bs-inline-tab__label--has-content' : ''}` }, h("slot", { key: '51fb6ec04479b18af787bca4e64f8105a0b19311', onSlotchange: this.onLabelSlotchange }))));
|
|
94
98
|
}
|
|
95
99
|
static get is() { return "bs-inline-tab"; }
|
|
96
100
|
static get encapsulation() { return "shadow"; }
|
|
@@ -166,6 +170,25 @@ export class BsInlineTab {
|
|
|
166
170
|
"attribute": "aria-label",
|
|
167
171
|
"defaultValue": "null"
|
|
168
172
|
},
|
|
173
|
+
"value": {
|
|
174
|
+
"type": "string",
|
|
175
|
+
"mutable": false,
|
|
176
|
+
"complexType": {
|
|
177
|
+
"original": "string",
|
|
178
|
+
"resolved": "string",
|
|
179
|
+
"references": {}
|
|
180
|
+
},
|
|
181
|
+
"required": false,
|
|
182
|
+
"optional": true,
|
|
183
|
+
"docs": {
|
|
184
|
+
"tags": [],
|
|
185
|
+
"text": "Identifies this tab within a parent `bs-tabs`: the group's `value` is the selected tab's\n`value`, and `bsChange` reports it. Optional -- a tab without one is identified by its\nzero-based position in the group (`\"0\"`, `\"1\"`...)."
|
|
186
|
+
},
|
|
187
|
+
"getter": false,
|
|
188
|
+
"setter": false,
|
|
189
|
+
"reflect": true,
|
|
190
|
+
"attribute": "value"
|
|
191
|
+
},
|
|
169
192
|
"tabbable": {
|
|
170
193
|
"type": "boolean",
|
|
171
194
|
"mutable": false,
|
|
@@ -53,6 +53,10 @@ export class BsTab {
|
|
|
53
53
|
* violation). Stencil reflects this camelCase prop to the `aria-label` HTML attribute
|
|
54
54
|
* automatically, same as `bs-button`'s identical prop. */
|
|
55
55
|
ariaLabel = null;
|
|
56
|
+
/** Identifies this tab within a parent `bs-tabs`: the group's `value` is the selected tab's
|
|
57
|
+
* `value`, and `bsChange` reports it. Optional -- a tab without one is identified by its
|
|
58
|
+
* zero-based position in the group (`"0"`, `"1"`...). */
|
|
59
|
+
value;
|
|
56
60
|
/** Set by a parent `bs-tabs` to implement roving tabindex (only one tab in the group is in the
|
|
57
61
|
* Tab order; arrow keys move between them). Left `undefined` for a standalone tab, which stays
|
|
58
62
|
* a normal tab stop. */
|
|
@@ -103,7 +107,7 @@ export class BsTab {
|
|
|
103
107
|
const iconOnly = this.hasIcon && !this.hasLabel;
|
|
104
108
|
const vertical = this.hasIcon && this.hasLabel && this.iconPosition === 'top';
|
|
105
109
|
const showBadge = this.hasBadge && !iconOnly && !vertical;
|
|
106
|
-
return (h("button", { key: '
|
|
110
|
+
return (h("button", { key: '383cf44dfcf1feb5404e6a392318c8ce5a8a1178', type: "button", part: "tab", role: "tab", class: `bs-tab ${this.selected ? 'bs-tab--selected' : ''} ${this.disabled ? 'bs-tab--disabled' : ''} ${iconOnly ? 'bs-tab--icon-only' : ''} ${vertical ? 'bs-tab--vertical' : ''}`, disabled: this.disabled, "aria-selected": String(this.selected), "aria-disabled": this.disabled ? 'true' : undefined, "aria-label": this.ariaLabel, tabIndex: this.tabbable === undefined ? undefined : this.tabbable ? 0 : -1, onClick: this.onClick }, h("span", { key: '8b3a6b9ee663c2957c206024478ee76abf4fb905', part: "icon", class: `bs-tab__icon ${this.hasIcon ? 'bs-tab__icon--has-content' : ''}` }, h("slot", { key: 'b46fd73bac532345e747bb214f8c91f32b04810c', name: "icon", onSlotchange: this.onIconSlotchange })), h("span", { key: '9d3e69f4dc96e20a327ebccdbdf934f07a4892c0', part: "label", class: `bs-tab__label ${this.hasLabel ? 'bs-tab__label--has-content' : ''}` }, h("slot", { key: 'e724d44bec76845b64e8e0ebb5c883dc47ff11a3', onSlotchange: this.onLabelSlotchange })), h("span", { key: '1cb4e5a8056ebac8b1b098c350c4a5d89bd2968a', part: "badge", class: `bs-tab__badge ${showBadge ? 'bs-tab__badge--has-content' : ''}` }, h("slot", { key: '46a6bbf2c8702645b85062012294eedf3facceac', name: "badge", onSlotchange: this.onBadgeSlotchange })), h("span", { key: '38845ccafeddfe79af64c3e291197e88bbced150', part: "indicator", class: "bs-tab__indicator" })));
|
|
107
111
|
}
|
|
108
112
|
static get is() { return "bs-tab"; }
|
|
109
113
|
static get encapsulation() { return "shadow"; }
|
|
@@ -205,6 +209,25 @@ export class BsTab {
|
|
|
205
209
|
"attribute": "aria-label",
|
|
206
210
|
"defaultValue": "null"
|
|
207
211
|
},
|
|
212
|
+
"value": {
|
|
213
|
+
"type": "string",
|
|
214
|
+
"mutable": false,
|
|
215
|
+
"complexType": {
|
|
216
|
+
"original": "string",
|
|
217
|
+
"resolved": "string",
|
|
218
|
+
"references": {}
|
|
219
|
+
},
|
|
220
|
+
"required": false,
|
|
221
|
+
"optional": true,
|
|
222
|
+
"docs": {
|
|
223
|
+
"tags": [],
|
|
224
|
+
"text": "Identifies this tab within a parent `bs-tabs`: the group's `value` is the selected tab's\n`value`, and `bsChange` reports it. Optional -- a tab without one is identified by its\nzero-based position in the group (`\"0\"`, `\"1\"`...)."
|
|
225
|
+
},
|
|
226
|
+
"getter": false,
|
|
227
|
+
"setter": false,
|
|
228
|
+
"reflect": true,
|
|
229
|
+
"attribute": "value"
|
|
230
|
+
},
|
|
208
231
|
"tabbable": {
|
|
209
232
|
"type": "boolean",
|
|
210
233
|
"mutable": false,
|
|
@@ -58,4 +58,92 @@ describe('bs-tabs', () => {
|
|
|
58
58
|
expect(first.selected).toBe(true);
|
|
59
59
|
expect(second.selected).toBe(false);
|
|
60
60
|
});
|
|
61
|
+
describe('value and bsChange', () => {
|
|
62
|
+
const btn = (el) => el.shadowRoot.querySelector('button');
|
|
63
|
+
const key = (el, k) => el.dispatchEvent(new KeyboardEvent('keydown', { key: k, bubbles: true, composed: true, cancelable: true }));
|
|
64
|
+
const group = () => (h("bs-tabs", null, h("bs-tab", { value: "overview" }, "Overview"), h("bs-tab", { value: "details" }, "Details"), h("bs-tab", { value: "history" }, "History")));
|
|
65
|
+
it('a click selects the tab, updates the group value and emits bsChange with it', async () => {
|
|
66
|
+
const { root, waitForChanges, spyOnEvent } = await render(group());
|
|
67
|
+
const spy = spyOnEvent('bsChange');
|
|
68
|
+
const tabs = Array.from(root.querySelectorAll('bs-tab'));
|
|
69
|
+
btn(tabs[1]).click();
|
|
70
|
+
await waitForChanges();
|
|
71
|
+
expect(root.value).toBe('details');
|
|
72
|
+
expect(root.getAttribute('value')).toBe('details'); // reflected
|
|
73
|
+
expect(tabs[1].selected).toBe(true);
|
|
74
|
+
expect(spy).toHaveReceivedEventTimes(1);
|
|
75
|
+
expect(spy).toHaveReceivedEventDetail({ value: 'details' });
|
|
76
|
+
});
|
|
77
|
+
it('does not emit again when the already-selected tab is clicked', async () => {
|
|
78
|
+
const { root, waitForChanges, spyOnEvent } = await render(group());
|
|
79
|
+
const spy = spyOnEvent('bsChange');
|
|
80
|
+
const tabs = Array.from(root.querySelectorAll('bs-tab'));
|
|
81
|
+
btn(tabs[1]).click();
|
|
82
|
+
await waitForChanges();
|
|
83
|
+
btn(tabs[1]).click();
|
|
84
|
+
await waitForChanges();
|
|
85
|
+
expect(spy).toHaveReceivedEventTimes(1);
|
|
86
|
+
});
|
|
87
|
+
it('a tab without a value is identified by its zero-based index as a string', async () => {
|
|
88
|
+
const { root, waitForChanges, spyOnEvent } = await render(h("bs-tabs", null, h("bs-tab", null, "One"), h("bs-tab", null, "Two")));
|
|
89
|
+
const spy = spyOnEvent('bsChange');
|
|
90
|
+
btn(root.querySelectorAll('bs-tab')[1]).click();
|
|
91
|
+
await waitForChanges();
|
|
92
|
+
expect(spy).toHaveReceivedEventDetail({ value: '1' });
|
|
93
|
+
expect(root.value).toBe('1');
|
|
94
|
+
});
|
|
95
|
+
it('an initial value selects the matching tab on load', async () => {
|
|
96
|
+
const { root, waitForChanges } = await render(h("bs-tabs", { value: "history" }, h("bs-tab", { value: "overview" }, "Overview"), h("bs-tab", { value: "details" }, "Details"), h("bs-tab", { value: "history" }, "History")));
|
|
97
|
+
await waitForChanges();
|
|
98
|
+
const tabs = Array.from(root.querySelectorAll('bs-tab'));
|
|
99
|
+
expect(tabs.map(t => t.selected)).toEqual([false, false, true]);
|
|
100
|
+
// roving tabindex follows the selection
|
|
101
|
+
expect(btn(tabs[2]).tabIndex).toBe(0);
|
|
102
|
+
expect(btn(tabs[0]).tabIndex).toBe(-1);
|
|
103
|
+
});
|
|
104
|
+
it('setting value from outside selects the matching tab and does NOT emit bsChange', async () => {
|
|
105
|
+
const { root, waitForChanges, spyOnEvent } = await render(group());
|
|
106
|
+
const spy = spyOnEvent('bsChange');
|
|
107
|
+
root.value = 'details';
|
|
108
|
+
await waitForChanges();
|
|
109
|
+
const tabs = Array.from(root.querySelectorAll('bs-tab'));
|
|
110
|
+
expect(tabs.map(t => t.selected)).toEqual([false, true, false]);
|
|
111
|
+
expect(spy).not.toHaveReceivedEvent();
|
|
112
|
+
});
|
|
113
|
+
it('with no value set, reports the tab the consumer already marked selected (without emitting)', async () => {
|
|
114
|
+
const { root, waitForChanges, spyOnEvent } = await render(h("bs-tabs", null, h("bs-tab", { value: "a" }, "A"), h("bs-tab", { value: "b", selected: true }, "B")));
|
|
115
|
+
const spy = spyOnEvent('bsChange');
|
|
116
|
+
await waitForChanges();
|
|
117
|
+
expect(root.value).toBe('b');
|
|
118
|
+
expect(spy).not.toHaveReceivedEvent();
|
|
119
|
+
});
|
|
120
|
+
it('keyboard navigation emits bsChange too (arrow keys select the next tab)', async () => {
|
|
121
|
+
const { root, waitForChanges, spyOnEvent } = await render(group());
|
|
122
|
+
const spy = spyOnEvent('bsChange');
|
|
123
|
+
const tabs = Array.from(root.querySelectorAll('bs-tab'));
|
|
124
|
+
btn(tabs[0]).click();
|
|
125
|
+
await waitForChanges();
|
|
126
|
+
key(btn(tabs[0]), 'ArrowRight');
|
|
127
|
+
await waitForChanges();
|
|
128
|
+
expect(spy).toHaveReceivedEventDetail({ value: 'details' });
|
|
129
|
+
});
|
|
130
|
+
it('still emits bsSelect on the tab itself', async () => {
|
|
131
|
+
const { root, waitForChanges, spyOnEvent } = await render(group());
|
|
132
|
+
const spy = spyOnEvent('bsSelect');
|
|
133
|
+
btn(root.querySelectorAll('bs-tab')[2]).click();
|
|
134
|
+
await waitForChanges();
|
|
135
|
+
expect(spy).toHaveReceivedEventTimes(1);
|
|
136
|
+
});
|
|
137
|
+
it('works for bs-inline-tab groups', async () => {
|
|
138
|
+
const { root, waitForChanges, spyOnEvent } = await render(h("bs-tabs", { type: "bs-inline-tab" }, h("bs-inline-tab", { value: "x" }, "X"), h("bs-inline-tab", { value: "y" }, "Y")));
|
|
139
|
+
const spy = spyOnEvent('bsChange');
|
|
140
|
+
btn(root.querySelectorAll('bs-inline-tab')[1]).click();
|
|
141
|
+
await waitForChanges();
|
|
142
|
+
expect(spy).toHaveReceivedEventDetail({ value: 'y' });
|
|
143
|
+
});
|
|
144
|
+
it('a tab\'s value prop is reflected as an attribute', async () => {
|
|
145
|
+
const { root } = await render(group());
|
|
146
|
+
expect(root.querySelector('bs-tab').getAttribute('value')).toBe('overview');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
61
149
|
});
|
|
@@ -35,6 +35,16 @@ export class BsTabs {
|
|
|
35
35
|
/** Layout direction of the tab bar itself. Sets `aria-orientation` on the internal wrapper and
|
|
36
36
|
* switches `flex-direction`. */
|
|
37
37
|
orientation = 'horizontal';
|
|
38
|
+
/** The selected tab's `value`. Mutable and reflected: it follows the user's selection, and
|
|
39
|
+
* setting it from outside (attribute or property) selects the matching tab -- without emitting
|
|
40
|
+
* `bsChange`, which only fires for user-driven changes. A tab's value is its own `value` prop,
|
|
41
|
+
* or, for a tab without one, its zero-based position in the group as a string (`"0"`, `"1"`...).
|
|
42
|
+
* If unset, it is read from whichever tab is already `selected` once the group loads. */
|
|
43
|
+
value;
|
|
44
|
+
/** Fires when the user selects a different tab (click, or arrow/Home/End), with the new
|
|
45
|
+
* `value`. Does not fire when `value` is set programmatically. `bsSelect` on the individual tab
|
|
46
|
+
* still fires as before. */
|
|
47
|
+
bsChange;
|
|
38
48
|
/**
|
|
39
49
|
* Catches `bsSelect` bubbling up from any slotted `<bs-tab>`/`<bs-inline-tab>` (Stencil's
|
|
40
50
|
* `@Event()` defaults to `bubbles: true, composed: true`, and `@Listen` without a target listens
|
|
@@ -43,15 +53,46 @@ export class BsTabs {
|
|
|
43
53
|
* `if (this.disabled) return;`), so no special-casing is needed here.
|
|
44
54
|
*/
|
|
45
55
|
onTabSelect(ev) {
|
|
46
|
-
const tabs = this.
|
|
56
|
+
const tabs = this.tabs();
|
|
47
57
|
tabs.forEach(tab => {
|
|
48
58
|
tab.selected = tab === ev.target;
|
|
49
59
|
});
|
|
50
60
|
this.syncRovingTabindex();
|
|
61
|
+
const index = tabs.findIndex(tab => tab === ev.target);
|
|
62
|
+
if (index === -1)
|
|
63
|
+
return;
|
|
64
|
+
const value = this.tabValue(tabs[index], index);
|
|
65
|
+
if (value !== this.value) {
|
|
66
|
+
this.value = value;
|
|
67
|
+
this.bsChange.emit({ value });
|
|
68
|
+
}
|
|
51
69
|
}
|
|
52
70
|
componentDidLoad() {
|
|
71
|
+
if (this.value !== undefined) {
|
|
72
|
+
this.applyValue();
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Uncontrolled: report whichever tab the consumer already marked `selected`.
|
|
76
|
+
const tabs = this.tabs();
|
|
77
|
+
const index = tabs.findIndex(t => t.selected);
|
|
78
|
+
if (index !== -1)
|
|
79
|
+
this.value = this.tabValue(tabs[index], index);
|
|
80
|
+
}
|
|
53
81
|
this.syncRovingTabindex();
|
|
54
82
|
}
|
|
83
|
+
tabValue(tab, index) {
|
|
84
|
+
return tab.value ?? String(index);
|
|
85
|
+
}
|
|
86
|
+
/** Selects the tab whose value matches `this.value` (and deselects the rest). */
|
|
87
|
+
applyValue() {
|
|
88
|
+
this.tabs().forEach((tab, i) => {
|
|
89
|
+
tab.selected = this.tabValue(tab, i) === this.value;
|
|
90
|
+
});
|
|
91
|
+
this.syncRovingTabindex();
|
|
92
|
+
}
|
|
93
|
+
onValueChange() {
|
|
94
|
+
this.applyValue();
|
|
95
|
+
}
|
|
55
96
|
syncRovingTabindex() {
|
|
56
97
|
const tabs = this.tabs();
|
|
57
98
|
const anySelected = tabs.some(t => t.selected);
|
|
@@ -94,7 +135,7 @@ export class BsTabs {
|
|
|
94
135
|
}
|
|
95
136
|
render() {
|
|
96
137
|
const vertical = this.orientation === 'vertical';
|
|
97
|
-
return (h("div", { key: '
|
|
138
|
+
return (h("div", { key: '251521d36dcf4d6960df344285bc9a060ff87abb', part: "tablist", role: "tablist", "aria-orientation": this.orientation, class: `bs-tabs bs-tabs--${this.type} ${vertical ? 'bs-tabs--vertical' : ''}` }, h("slot", { key: '02216f048eaedb13f00891c30e1c49d15a509c1d', onSlotchange: () => this.syncRovingTabindex() })));
|
|
98
139
|
}
|
|
99
140
|
static get is() { return "bs-tabs"; }
|
|
100
141
|
static get encapsulation() { return "shadow"; }
|
|
@@ -161,12 +202,52 @@ export class BsTabs {
|
|
|
161
202
|
"reflect": false,
|
|
162
203
|
"attribute": "orientation",
|
|
163
204
|
"defaultValue": "'horizontal'"
|
|
205
|
+
},
|
|
206
|
+
"value": {
|
|
207
|
+
"type": "string",
|
|
208
|
+
"mutable": true,
|
|
209
|
+
"complexType": {
|
|
210
|
+
"original": "string",
|
|
211
|
+
"resolved": "string",
|
|
212
|
+
"references": {}
|
|
213
|
+
},
|
|
214
|
+
"required": false,
|
|
215
|
+
"optional": true,
|
|
216
|
+
"docs": {
|
|
217
|
+
"tags": [],
|
|
218
|
+
"text": "The selected tab's `value`. Mutable and reflected: it follows the user's selection, and\nsetting it from outside (attribute or property) selects the matching tab -- without emitting\n`bsChange`, which only fires for user-driven changes. A tab's value is its own `value` prop,\nor, for a tab without one, its zero-based position in the group as a string (`\"0\"`, `\"1\"`...).\nIf unset, it is read from whichever tab is already `selected` once the group loads."
|
|
219
|
+
},
|
|
220
|
+
"getter": false,
|
|
221
|
+
"setter": false,
|
|
222
|
+
"reflect": true,
|
|
223
|
+
"attribute": "value"
|
|
164
224
|
}
|
|
165
225
|
};
|
|
166
226
|
}
|
|
227
|
+
static get events() {
|
|
228
|
+
return [{
|
|
229
|
+
"method": "bsChange",
|
|
230
|
+
"name": "bsChange",
|
|
231
|
+
"bubbles": true,
|
|
232
|
+
"cancelable": true,
|
|
233
|
+
"composed": true,
|
|
234
|
+
"docs": {
|
|
235
|
+
"tags": [],
|
|
236
|
+
"text": "Fires when the user selects a different tab (click, or arrow/Home/End), with the new\n`value`. Does not fire when `value` is set programmatically. `bsSelect` on the individual tab\nstill fires as before."
|
|
237
|
+
},
|
|
238
|
+
"complexType": {
|
|
239
|
+
"original": "{ value: string }",
|
|
240
|
+
"resolved": "{ value: string; }",
|
|
241
|
+
"references": {}
|
|
242
|
+
}
|
|
243
|
+
}];
|
|
244
|
+
}
|
|
167
245
|
static get elementRef() { return "el"; }
|
|
168
246
|
static get watchers() {
|
|
169
247
|
return [{
|
|
248
|
+
"propName": "value",
|
|
249
|
+
"methodName": "onValueChange"
|
|
250
|
+
}, {
|
|
170
251
|
"propName": "orientation",
|
|
171
252
|
"methodName": "syncRovingTabindex"
|
|
172
253
|
}];
|
|
@@ -37,3 +37,27 @@ export const InlineTabGroup = {
|
|
|
37
37
|
export const Vertical = {
|
|
38
38
|
args: { orientation: 'vertical' },
|
|
39
39
|
};
|
|
40
|
+
// The group's `value` follows the selection, and `bsChange` reports user-driven changes: the panel
|
|
41
|
+
// below switches in response to it, with no per-tab selection wiring.
|
|
42
|
+
export const WithValueAndPanels = {
|
|
43
|
+
name: 'With value and panels',
|
|
44
|
+
render: () => {
|
|
45
|
+
const panels = {
|
|
46
|
+
overview: 'Overview content',
|
|
47
|
+
details: 'Details content',
|
|
48
|
+
history: 'History content',
|
|
49
|
+
};
|
|
50
|
+
const onChange = (ev) => {
|
|
51
|
+
const panel = ev.currentTarget.nextElementSibling;
|
|
52
|
+
panel.textContent = panels[ev.detail.value];
|
|
53
|
+
};
|
|
54
|
+
return html `
|
|
55
|
+
<bs-tabs value="overview" @bsChange=${onChange}>
|
|
56
|
+
<bs-tab value="overview">Overview</bs-tab>
|
|
57
|
+
<bs-tab value="details">Details</bs-tab>
|
|
58
|
+
<bs-tab value="history">History</bs-tab>
|
|
59
|
+
</bs-tabs>
|
|
60
|
+
<p role="tabpanel" style="margin: 16px 0 0;">Overview content</p>
|
|
61
|
+
`;
|
|
62
|
+
},
|
|
63
|
+
};
|