@brandsync/wc 1.15.0 → 1.16.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/REFERENCE.md +82 -0
  3. package/custom-elements.json +1293 -443
  4. package/dist/brandsync-wc/brandsync-wc.esm.js +1 -1
  5. package/dist/brandsync-wc/{p-87d44938.entry.js → p-0159f0da.entry.js} +1 -1
  6. package/dist/brandsync-wc/p-0228a584.entry.js +1 -0
  7. package/dist/brandsync-wc/{p-6df48031.entry.js → p-15f4c343.entry.js} +1 -1
  8. package/dist/brandsync-wc/{p-3c21d0bf.entry.js → p-298158da.entry.js} +1 -1
  9. package/dist/brandsync-wc/{p-1b4c0971.entry.js → p-38fe9d07.entry.js} +1 -1
  10. package/dist/brandsync-wc/{p-bc97ece8.entry.js → p-42b79d95.entry.js} +1 -1
  11. package/dist/brandsync-wc/{p-b9c0b481.entry.js → p-521d0fc2.entry.js} +1 -1
  12. package/dist/brandsync-wc/{p-1bf8fbbb.entry.js → p-67629178.entry.js} +1 -1
  13. package/dist/brandsync-wc/{p-e294f698.entry.js → p-76748795.entry.js} +1 -1
  14. package/dist/brandsync-wc/p-7dc59e0b.entry.js +1 -0
  15. package/dist/brandsync-wc/p-949a4270.entry.js +1 -0
  16. package/dist/brandsync-wc/{p-7a072e64.entry.js → p-a12691d8.entry.js} +1 -1
  17. package/dist/brandsync-wc/{p-ac9472ec.entry.js → p-cc4c8c1d.entry.js} +1 -1
  18. package/dist/brandsync-wc/p-e9afb553.entry.js +1 -0
  19. package/dist/cjs/brandsync-wc.cjs.js +1 -1
  20. package/dist/cjs/bs-inline-tab.cjs.entry.js +1 -1
  21. package/dist/cjs/bs-input_3.cjs.entry.js +2 -2
  22. package/dist/cjs/bs-list-item.cjs.entry.js +66 -0
  23. package/dist/cjs/bs-list.cjs.entry.js +103 -0
  24. package/dist/cjs/bs-message-list.cjs.entry.js +1 -1
  25. package/dist/cjs/bs-navigation-drawer-item.cjs.entry.js +1 -1
  26. package/dist/cjs/bs-navigation-drawer.cjs.entry.js +2 -2
  27. package/dist/cjs/bs-pagination.cjs.entry.js +1 -1
  28. package/dist/cjs/bs-radio.cjs.entry.js +1 -1
  29. package/dist/cjs/bs-source-link.cjs.entry.js +2 -2
  30. package/dist/cjs/bs-stepper.cjs.entry.js +1 -1
  31. package/dist/cjs/bs-tooltip.cjs.entry.js +1 -1
  32. package/dist/cjs/bs-tree-item.cjs.entry.js +94 -0
  33. package/dist/cjs/bs-tree.cjs.entry.js +197 -0
  34. package/dist/cjs/loader.cjs.js +1 -1
  35. package/dist/collection/collection-manifest.json +4 -0
  36. package/dist/collection/components/bs-list/bs-list.cmp.test.js +99 -0
  37. package/dist/collection/components/bs-list/bs-list.css +15 -0
  38. package/dist/collection/components/bs-list/bs-list.js +241 -0
  39. package/dist/collection/components/bs-list/bs-list.stories.js +43 -0
  40. package/dist/collection/components/bs-list-item/bs-list-item.cmp.test.js +137 -0
  41. package/dist/collection/components/bs-list-item/bs-list-item.css +139 -0
  42. package/dist/collection/components/bs-list-item/bs-list-item.js +343 -0
  43. package/dist/collection/components/bs-list-item/bs-list-item.stories.js +86 -0
  44. package/dist/collection/components/bs-menu/bs-menu.js +1 -1
  45. package/dist/collection/components/bs-menu-item/bs-menu-item.cmp.test.js +6 -0
  46. package/dist/collection/components/bs-menu-item/bs-menu-item.js +1 -1
  47. package/dist/collection/components/bs-message-list/bs-message-list.js +1 -1
  48. package/dist/collection/components/bs-pagination/bs-pagination.js +1 -1
  49. package/dist/collection/components/bs-radio/bs-radio.js +1 -1
  50. package/dist/collection/components/bs-source-link/bs-source-link.js +2 -2
  51. package/dist/collection/components/bs-stepper/bs-stepper/bs-stepper.js +1 -1
  52. package/dist/collection/components/bs-tab/bs-inline-tab/bs-inline-tab.js +1 -1
  53. package/dist/collection/components/bs-tooltip/bs-tooltip.js +1 -1
  54. package/dist/collection/components/bs-tree/bs-tree.cmp.test.js +166 -0
  55. package/dist/collection/components/bs-tree/bs-tree.css +8 -0
  56. package/dist/collection/components/bs-tree/bs-tree.js +354 -0
  57. package/dist/collection/components/bs-tree/bs-tree.stories.js +34 -0
  58. package/dist/collection/components/bs-tree-item/bs-tree-item.cmp.test.js +187 -0
  59. package/dist/collection/components/bs-tree-item/bs-tree-item.css +161 -0
  60. package/dist/collection/components/bs-tree-item/bs-tree-item.js +305 -0
  61. package/dist/collection/components/bs-tree-item/bs-tree-item.stories.js +54 -0
  62. package/dist/collection/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer/bs-navigation-drawer.js +2 -2
  63. package/dist/collection/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer-item/bs-navigation-drawer-item.js +1 -1
  64. package/dist/components/bs-breadcrumb-overflow.js +1 -1
  65. package/dist/components/bs-carousel.js +1 -1
  66. package/dist/components/bs-chatbot-header.js +1 -1
  67. package/dist/components/bs-citation-preview.js +1 -1
  68. package/dist/components/bs-datepicker.js +1 -1
  69. package/dist/components/bs-inline-tab.js +1 -1
  70. package/dist/components/bs-input.js +1 -1
  71. package/dist/components/bs-list-item.d.ts +11 -0
  72. package/dist/components/bs-list-item.js +1 -0
  73. package/dist/components/bs-list.d.ts +11 -0
  74. package/dist/components/bs-list.js +1 -0
  75. package/dist/components/bs-menu-item.js +1 -1
  76. package/dist/components/bs-menu.js +1 -1
  77. package/dist/components/bs-message-list.js +1 -1
  78. package/dist/components/bs-navigation-drawer-item.js +1 -1
  79. package/dist/components/bs-navigation-drawer.js +1 -1
  80. package/dist/components/bs-pagination.js +1 -1
  81. package/dist/components/bs-radio.js +1 -1
  82. package/dist/components/bs-session-timeout.js +1 -1
  83. package/dist/components/bs-source-link.js +1 -1
  84. package/dist/components/bs-stepper.js +1 -1
  85. package/dist/components/bs-timepicker.js +1 -1
  86. package/dist/components/bs-tooltip-trigger.js +1 -1
  87. package/dist/components/bs-tooltip.js +1 -1
  88. package/dist/components/bs-tree-item.d.ts +11 -0
  89. package/dist/components/bs-tree-item.js +1 -0
  90. package/dist/components/bs-tree.d.ts +11 -0
  91. package/dist/components/bs-tree.js +1 -0
  92. package/dist/components/p-BaJEv6rL.js +1 -0
  93. package/dist/components/{p-BvYkZcOU.js → p-Bb1-2M8K.js} +1 -1
  94. package/dist/components/{p-Bqe4waKJ.js → p-BdkE4auv.js} +1 -1
  95. package/dist/components/{p-ElcsxZSh.js → p-C93ymWm7.js} +1 -1
  96. package/dist/components/{p-ByKcHsj0.js → p-DFKc3lLA.js} +1 -1
  97. package/dist/components/{p-B3l4WpgS.js → p-J9cxcDhY.js} +1 -1
  98. package/dist/esm/brandsync-wc.js +1 -1
  99. package/dist/esm/bs-inline-tab.entry.js +1 -1
  100. package/dist/esm/bs-input_3.entry.js +2 -2
  101. package/dist/esm/bs-list-item.entry.js +64 -0
  102. package/dist/esm/bs-list.entry.js +101 -0
  103. package/dist/esm/bs-message-list.entry.js +1 -1
  104. package/dist/esm/bs-navigation-drawer-item.entry.js +1 -1
  105. package/dist/esm/bs-navigation-drawer.entry.js +2 -2
  106. package/dist/esm/bs-pagination.entry.js +1 -1
  107. package/dist/esm/bs-radio.entry.js +1 -1
  108. package/dist/esm/bs-source-link.entry.js +2 -2
  109. package/dist/esm/bs-stepper.entry.js +1 -1
  110. package/dist/esm/bs-tooltip.entry.js +1 -1
  111. package/dist/esm/bs-tree-item.entry.js +92 -0
  112. package/dist/esm/bs-tree.entry.js +195 -0
  113. package/dist/esm/loader.js +1 -1
  114. package/dist/types/components/bs-list/bs-list.cmp.test.d.ts +1 -0
  115. package/dist/types/components/bs-list/bs-list.d.ts +42 -0
  116. package/dist/types/components/bs-list/bs-list.stories.d.ts +13 -0
  117. package/dist/types/components/bs-list-item/bs-list-item.cmp.test.d.ts +1 -0
  118. package/dist/types/components/bs-list-item/bs-list-item.d.ts +85 -0
  119. package/dist/types/components/bs-list-item/bs-list-item.stories.d.ts +22 -0
  120. package/dist/types/components/bs-tree/bs-tree.cmp.test.d.ts +1 -0
  121. package/dist/types/components/bs-tree/bs-tree.d.ts +53 -0
  122. package/dist/types/components/bs-tree/bs-tree.stories.d.ts +11 -0
  123. package/dist/types/components/bs-tree-item/bs-tree-item.cmp.test.d.ts +1 -0
  124. package/dist/types/components/bs-tree-item/bs-tree-item.d.ts +60 -0
  125. package/dist/types/components/bs-tree-item/bs-tree-item.stories.d.ts +19 -0
  126. package/dist/types/components.d.ts +675 -0
  127. package/package.json +1 -1
  128. package/reference.d.ts +12 -0
  129. package/reference.json +250 -0
  130. package/dist/components/p-Mf1jorXP.js +0 -1
@@ -0,0 +1,99 @@
1
+ import { render, h, describe, it, expect } from "@stencil/vitest";
2
+ import { runAxeCheck } from "../../test/helpers/conventions";
3
+ const mountList = (props = {}) => render(h("bs-list", { ariaLabel: "Things", ...props }, h("bs-list-item", { value: "a", label: "A", description: "first" }), h("bs-list-item", { value: "b", label: "B", description: "second" }), h("bs-list-item", { value: "c", label: "C", description: "third", disabled: true }), h("bs-list-item", { value: "d", label: "D", description: "fourth" })));
4
+ const itemOf = (root, value) => Array.from(root.querySelectorAll("bs-list-item")).find(i => i.value === value);
5
+ describe('bs-list', () => {
6
+ it('non-selectable: list/listitem roles, items not interactive', async () => {
7
+ const { root, waitForChanges } = await mountList();
8
+ await waitForChanges();
9
+ expect(root.shadowRoot.querySelector('[part="list"]')).toEqualAttribute('role', 'list');
10
+ expect(root.shadowRoot.querySelector('[part="list"]')).toEqualAttribute('aria-label', 'Things');
11
+ const a = itemOf(root, 'a');
12
+ expect(a).toEqualAttribute('role', 'listitem');
13
+ expect(a.hasAttribute('tabindex')).toBe(false);
14
+ let n = 0;
15
+ root.addEventListener('bsChange', () => n++);
16
+ a.click();
17
+ expect(n).toBe(0);
18
+ });
19
+ it('selectable: listbox/option roles with aria-selected and roving tabindex', async () => {
20
+ const { root, waitForChanges } = await mountList({ selectable: true, value: 'b' });
21
+ await waitForChanges();
22
+ expect(root.shadowRoot.querySelector('[part="list"]')).toEqualAttribute('role', 'listbox');
23
+ expect(itemOf(root, 'a')).toEqualAttribute('role', 'option');
24
+ expect(itemOf(root, 'b')).toEqualAttribute('aria-selected', 'true');
25
+ expect(itemOf(root, 'a')).toEqualAttribute('aria-selected', 'false');
26
+ expect(itemOf(root, 'b')).toEqualAttribute('tabindex', '0');
27
+ expect(itemOf(root, 'a')).toEqualAttribute('tabindex', '-1');
28
+ expect(itemOf(root, 'c').hasAttribute('tabindex')).toBe(false);
29
+ });
30
+ it('click selects, deselects the previous item, updates value and emits bsChange', async () => {
31
+ const { root, waitForChanges } = await mountList({ selectable: true, value: 'a' });
32
+ const got = [];
33
+ root.addEventListener('bsChange', (e) => got.push(e.detail));
34
+ itemOf(root, 'd').click();
35
+ await waitForChanges();
36
+ expect(got).toEqual(['d']);
37
+ expect(root.value).toBe('d');
38
+ expect(itemOf(root, 'd').hasAttribute('selected')).toBe(true);
39
+ expect(itemOf(root, 'a').hasAttribute('selected')).toBe(false);
40
+ itemOf(root, 'c').click(); // disabled
41
+ await waitForChanges();
42
+ expect(got).toEqual(['d']);
43
+ });
44
+ it('setting value programmatically selects without emitting', async () => {
45
+ const { root, waitForChanges } = await mountList({ selectable: true });
46
+ let n = 0;
47
+ root.addEventListener('bsChange', () => n++);
48
+ root.value = 'b';
49
+ await waitForChanges();
50
+ expect(itemOf(root, 'b').hasAttribute('selected')).toBe(true);
51
+ expect(n).toBe(0);
52
+ });
53
+ it('arrow keys, Home and End move focus and skip disabled items; Enter/Space select', async () => {
54
+ const { root, waitForChanges } = await mountList({ selectable: true });
55
+ await waitForChanges();
56
+ const key = (el, k) => el.dispatchEvent(new KeyboardEvent('keydown', { key: k, bubbles: true, cancelable: true }));
57
+ itemOf(root, 'a').focus();
58
+ key(itemOf(root, 'a'), 'ArrowDown');
59
+ expect(document.activeElement).toBe(itemOf(root, 'b'));
60
+ key(itemOf(root, 'b'), 'ArrowDown'); // skips disabled c
61
+ expect(document.activeElement).toBe(itemOf(root, 'd'));
62
+ key(itemOf(root, 'd'), 'ArrowUp');
63
+ expect(document.activeElement).toBe(itemOf(root, 'b'));
64
+ key(itemOf(root, 'b'), 'End');
65
+ expect(document.activeElement).toBe(itemOf(root, 'd'));
66
+ key(itemOf(root, 'd'), 'Home');
67
+ expect(document.activeElement).toBe(itemOf(root, 'a'));
68
+ expect(itemOf(root, 'a')).toEqualAttribute('tabindex', '0');
69
+ key(itemOf(root, 'a'), 'ArrowDown');
70
+ key(itemOf(root, 'b'), 'Enter');
71
+ await waitForChanges();
72
+ expect(root.value).toBe('b');
73
+ key(itemOf(root, 'b'), 'ArrowDown');
74
+ key(itemOf(root, 'd'), ' ');
75
+ await waitForChanges();
76
+ expect(root.value).toBe('d');
77
+ });
78
+ it('handles items added later', async () => {
79
+ const { root, waitForChanges } = await mountList({ selectable: true });
80
+ const extra = document.createElement('bs-list-item');
81
+ extra.setAttribute('value', 'e');
82
+ root.appendChild(extra);
83
+ await waitForChanges();
84
+ await new Promise(r => setTimeout(r, 50));
85
+ expect(extra).toEqualAttribute('role', 'option');
86
+ });
87
+ it('dividers draw a 1px separator between rows only', async () => {
88
+ const { root, waitForChanges } = await mountList({ dividers: true });
89
+ await waitForChanges();
90
+ expect(getComputedStyle(itemOf(root, 'a')).borderBottomWidth).toBe('1px');
91
+ expect(getComputedStyle(itemOf(root, 'd')).borderBottomWidth).toBe('0px');
92
+ });
93
+ it('has no axe violations (plain and selectable)', async () => {
94
+ const { root } = await mountList();
95
+ await runAxeCheck(root);
96
+ const { root: r2 } = await mountList({ selectable: true, value: 'a' });
97
+ await runAxeCheck(r2);
98
+ });
99
+ });
@@ -0,0 +1,15 @@
1
+ :host {
2
+ display: block;
3
+ --bs-list-gap: 0;
4
+ --bs-list-divider-color: var(--bs-border-neutral-container);
5
+ }
6
+
7
+ .bs-list {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: var(--bs-list-gap);
11
+ }
12
+
13
+ :host([dividers]) ::slotted(bs-list-item:not(:last-child)) {
14
+ border-bottom: 1px solid var(--bs-list-divider-color);
15
+ }
@@ -0,0 +1,241 @@
1
+ import { h } from "@stencil/core";
2
+ /**
3
+ * A vertical list of `bs-list-item` rows, optionally single-select.
4
+ *
5
+ * ## When to use
6
+ * - Records, people, files or options shown as rows with leading media and descriptions.
7
+ * - `selectable` for a single-choice list (e.g. a conversation list next to a detail pane).
8
+ *
9
+ * ## When not to use
10
+ * - A popup list of actions -- use `bs-menu`.
11
+ * - Navigation between app sections -- use `bs-navigation-drawer`.
12
+ * - Multi-column, sortable or paged data -- use `bs-data-table`.
13
+ *
14
+ * Built from `bs-list-item` children (light DOM; the list never moves nodes).
15
+ *
16
+ * @slot - The `bs-list-item` rows.
17
+ * @part list - The list container.
18
+ * @prop --bs-list-gap - Gap between rows. Defaults to `0`.
19
+ * @prop --bs-list-divider-color - Divider colour when `dividers` is set. Aliased to `--bs-border-neutral-container`.
20
+ */
21
+ export class BsList {
22
+ el;
23
+ /** Single-selection mode: role="listbox", rows are `option`s with roving tabindex. */
24
+ selectable = false;
25
+ /** `value` of the selected item (selectable mode). */
26
+ value = null;
27
+ /** Draws a 1px separator between rows. */
28
+ dividers = false;
29
+ /** Accessible name of the list. */
30
+ ariaLabel = null;
31
+ /** Fires with the new `value` when the user changes the selection. */
32
+ bsChange;
33
+ onChange() {
34
+ this.sync();
35
+ }
36
+ componentDidLoad() {
37
+ this.sync();
38
+ }
39
+ items() {
40
+ return Array.from(this.el.children).filter(c => c.tagName === 'BS-LIST-ITEM');
41
+ }
42
+ enabled() {
43
+ return this.items().filter(i => !i.disabled);
44
+ }
45
+ sync() {
46
+ const items = this.items();
47
+ items.forEach(i => {
48
+ i.selectionRole = this.selectable ? 'option' : 'listitem';
49
+ if (this.selectable)
50
+ i.selected = this.value != null && i.value === this.value;
51
+ i.removeAttribute('tabindex');
52
+ });
53
+ if (!this.selectable)
54
+ return;
55
+ const enabled = this.enabled();
56
+ const target = enabled.find(i => i.selected) || enabled[0];
57
+ enabled.forEach(i => i.setAttribute('tabindex', i === target ? '0' : '-1'));
58
+ }
59
+ rove(item) {
60
+ this.enabled().forEach(i => i.setAttribute('tabindex', i === item ? '0' : '-1'));
61
+ }
62
+ onItemClick(ev) {
63
+ if (!this.selectable)
64
+ return;
65
+ const item = ev.target;
66
+ if (item.disabled || !this.items().includes(item))
67
+ return;
68
+ ev.stopPropagation();
69
+ this.rove(item);
70
+ if (this.value === item.value)
71
+ return;
72
+ this.value = item.value;
73
+ this.sync();
74
+ this.rove(item);
75
+ this.bsChange.emit(this.value);
76
+ }
77
+ onKeyDown(ev) {
78
+ if (!this.selectable)
79
+ return;
80
+ const enabled = this.enabled();
81
+ const current = enabled.indexOf(ev.target);
82
+ if (current === -1)
83
+ return;
84
+ let next = -1;
85
+ if (ev.key === 'ArrowDown')
86
+ next = Math.min(current + 1, enabled.length - 1);
87
+ else if (ev.key === 'ArrowUp')
88
+ next = Math.max(current - 1, 0);
89
+ else if (ev.key === 'Home')
90
+ next = 0;
91
+ else if (ev.key === 'End')
92
+ next = enabled.length - 1;
93
+ if (next === -1)
94
+ return;
95
+ ev.preventDefault();
96
+ this.rove(enabled[next]);
97
+ enabled[next].focus();
98
+ }
99
+ render() {
100
+ return (h("div", { key: '0f3e4a90f79c77ab2a67e88c4480f0c5785e49eb', part: "list", class: "bs-list", role: this.selectable ? 'listbox' : 'list', "aria-label": this.ariaLabel ?? undefined }, h("slot", { key: '7b5618d45ad58385f3d27565bdfee3e4bb5eac2e', onSlotchange: () => this.sync() })));
101
+ }
102
+ static get is() { return "bs-list"; }
103
+ static get encapsulation() { return "shadow"; }
104
+ static get originalStyleUrls() {
105
+ return {
106
+ "$": ["bs-list.css"]
107
+ };
108
+ }
109
+ static get styleUrls() {
110
+ return {
111
+ "$": ["bs-list.css"]
112
+ };
113
+ }
114
+ static get properties() {
115
+ return {
116
+ "selectable": {
117
+ "type": "boolean",
118
+ "mutable": false,
119
+ "complexType": {
120
+ "original": "boolean",
121
+ "resolved": "boolean",
122
+ "references": {}
123
+ },
124
+ "required": false,
125
+ "optional": false,
126
+ "docs": {
127
+ "tags": [],
128
+ "text": "Single-selection mode: role=\"listbox\", rows are `option`s with roving tabindex."
129
+ },
130
+ "getter": false,
131
+ "setter": false,
132
+ "reflect": false,
133
+ "attribute": "selectable",
134
+ "defaultValue": "false"
135
+ },
136
+ "value": {
137
+ "type": "string",
138
+ "mutable": true,
139
+ "complexType": {
140
+ "original": "string | null",
141
+ "resolved": "string",
142
+ "references": {}
143
+ },
144
+ "required": false,
145
+ "optional": false,
146
+ "docs": {
147
+ "tags": [],
148
+ "text": "`value` of the selected item (selectable mode)."
149
+ },
150
+ "getter": false,
151
+ "setter": false,
152
+ "reflect": true,
153
+ "attribute": "value",
154
+ "defaultValue": "null"
155
+ },
156
+ "dividers": {
157
+ "type": "boolean",
158
+ "mutable": false,
159
+ "complexType": {
160
+ "original": "boolean",
161
+ "resolved": "boolean",
162
+ "references": {}
163
+ },
164
+ "required": false,
165
+ "optional": false,
166
+ "docs": {
167
+ "tags": [],
168
+ "text": "Draws a 1px separator between rows."
169
+ },
170
+ "getter": false,
171
+ "setter": false,
172
+ "reflect": true,
173
+ "attribute": "dividers",
174
+ "defaultValue": "false"
175
+ },
176
+ "ariaLabel": {
177
+ "type": "string",
178
+ "mutable": false,
179
+ "complexType": {
180
+ "original": "string | null",
181
+ "resolved": "string",
182
+ "references": {}
183
+ },
184
+ "required": false,
185
+ "optional": false,
186
+ "docs": {
187
+ "tags": [],
188
+ "text": "Accessible name of the list."
189
+ },
190
+ "getter": false,
191
+ "setter": false,
192
+ "reflect": false,
193
+ "attribute": "aria-label",
194
+ "defaultValue": "null"
195
+ }
196
+ };
197
+ }
198
+ static get events() {
199
+ return [{
200
+ "method": "bsChange",
201
+ "name": "bsChange",
202
+ "bubbles": true,
203
+ "cancelable": true,
204
+ "composed": true,
205
+ "docs": {
206
+ "tags": [],
207
+ "text": "Fires with the new `value` when the user changes the selection."
208
+ },
209
+ "complexType": {
210
+ "original": "string | null",
211
+ "resolved": "string",
212
+ "references": {}
213
+ }
214
+ }];
215
+ }
216
+ static get elementRef() { return "el"; }
217
+ static get watchers() {
218
+ return [{
219
+ "propName": "value",
220
+ "methodName": "onChange"
221
+ }, {
222
+ "propName": "selectable",
223
+ "methodName": "onChange"
224
+ }];
225
+ }
226
+ static get listeners() {
227
+ return [{
228
+ "name": "bsItemClick",
229
+ "method": "onItemClick",
230
+ "target": undefined,
231
+ "capture": false,
232
+ "passive": false
233
+ }, {
234
+ "name": "keydown",
235
+ "method": "onKeyDown",
236
+ "target": undefined,
237
+ "capture": false,
238
+ "passive": false
239
+ }];
240
+ }
241
+ }
@@ -0,0 +1,43 @@
1
+ import { html } from "lit";
2
+ import { componentDescription } from "../../stories-utils";
3
+ const meta = {
4
+ title: 'Components/bs-list',
5
+ parameters: { docs: { description: { component: componentDescription('bs-list') } } },
6
+ argTypes: {
7
+ selectable: { control: 'boolean' },
8
+ dividers: { control: 'boolean' },
9
+ value: { control: 'text' },
10
+ },
11
+ args: { selectable: false, dividers: false, value: '' },
12
+ render: args => html `
13
+ <bs-list style="width: 360px;" aria-label="Items" ?selectable=${args.selectable} ?dividers=${args.dividers} .value=${args.value || null}>
14
+ <bs-list-item value="a" label="First" description="Paragraph" show-arrow></bs-list-item>
15
+ <bs-list-item value="b" label="Second" description="Paragraph" support-text="Support text" show-arrow></bs-list-item>
16
+ <bs-list-item value="c" label="Third (disabled)" description="Paragraph" disabled></bs-list-item>
17
+ <bs-list-item value="d" label="Fourth" description="Paragraph" show-arrow></bs-list-item>
18
+ </bs-list>
19
+ `,
20
+ };
21
+ export default meta;
22
+ export const Default = {};
23
+ export const Selectable = {
24
+ parameters: { docs: { description: { story: 'Single selection with a controlled `value`.' } } },
25
+ args: { selectable: true, value: 'b' },
26
+ };
27
+ export const WithDividers = { name: 'With dividers', args: { dividers: true } };
28
+ export const AvatarList = {
29
+ name: 'Avatar list',
30
+ render: () => html `
31
+ <bs-list style="width: 360px;" aria-label="People" selectable value="sl">
32
+ <bs-list-item value="sl" label="Sam Lee" description="Designer">
33
+ <bs-avatar slot="leading" type="initials" initials="SL" size="lg" aria-label="Sam Lee"></bs-avatar>
34
+ </bs-list-item>
35
+ <bs-list-item value="ak" label="Alex Kim" description="Engineer">
36
+ <bs-avatar slot="leading" type="initials" initials="AK" size="lg" aria-label="Alex Kim"></bs-avatar>
37
+ </bs-list-item>
38
+ <bs-list-item value="jr" label="Jo Ray" description="Product manager">
39
+ <bs-avatar slot="leading" type="initials" initials="JR" size="lg" aria-label="Jo Ray"></bs-avatar>
40
+ </bs-list-item>
41
+ </bs-list>
42
+ `,
43
+ };
@@ -0,0 +1,137 @@
1
+ import { render, h, describe, it, expect } from "@stencil/vitest";
2
+ import { userEvent } from "vitest/browser";
3
+ import { assertShadowParts, runAxeCheck } from "../../test/helpers/conventions";
4
+ const row = (host) => host.shadowRoot.querySelector('[part="container"]');
5
+ /** Resolves a token to the colour the browser computes for it. */
6
+ const tokenColor = (token, prop = 'backgroundColor') => {
7
+ const p = document.createElement('div');
8
+ p.style[prop === 'color' ? 'color' : 'background'] = `var(${token})`;
9
+ document.body.appendChild(p);
10
+ const c = getComputedStyle(p)[prop];
11
+ p.remove();
12
+ return c;
13
+ };
14
+ const bg = (host) => getComputedStyle(row(host)).backgroundColor;
15
+ const settle = () => new Promise(r => setTimeout(r, 200));
16
+ describe('bs-list-item', () => {
17
+ it('has the Figma geometry', async () => {
18
+ const { root } = await render(h("bs-list-item", { label: "Label", description: "Paragraph" }));
19
+ const cs = getComputedStyle(row(root));
20
+ expect(cs.minHeight).toBe('80px');
21
+ expect(cs.paddingTop).toBe('12px');
22
+ expect(cs.paddingLeft).toBe('16px');
23
+ expect(cs.columnGap).toBe('16px');
24
+ expect(cs.borderRadius).toBe('8px');
25
+ expect(cs.alignItems).toBe('center');
26
+ expect(Math.round(root.getBoundingClientRect().height)).toBeGreaterThanOrEqual(80);
27
+ });
28
+ it('exposes its documented parts', async () => {
29
+ const { root } = await render(h("bs-list-item", { label: "L", description: "D", supportText: "S", showArrow: true }));
30
+ assertShadowParts(root.shadowRoot, ['container', 'leading', 'content', 'label', 'description', 'support-text', 'trailing']);
31
+ expect(root.shadowRoot.querySelector('[part="trailing"] svg')).not.toBeNull();
32
+ });
33
+ it('is a plain, role-less, non-focusable row when standalone', async () => {
34
+ const { root } = await render(h("bs-list-item", { label: "L" }));
35
+ expect(root.hasAttribute('role')).toBe(false);
36
+ expect(root.hasAttribute('tabindex')).toBe(false);
37
+ });
38
+ it('is a focusable button when interactive and emits bsItemClick on click/Enter/Space', async () => {
39
+ const { root, waitForChanges } = await render(h("bs-list-item", { label: "L", value: "v", interactive: true }));
40
+ expect(root).toEqualAttribute('role', 'button');
41
+ expect(root).toEqualAttribute('tabindex', '0');
42
+ const got = [];
43
+ root.addEventListener('bsItemClick', (e) => got.push(e.detail));
44
+ root.click();
45
+ root.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
46
+ root.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }));
47
+ await waitForChanges();
48
+ expect(got).toEqual(['v', 'v', 'v']);
49
+ });
50
+ it('does not emit for non-interactive rows', async () => {
51
+ const { root } = await render(h("bs-list-item", { label: "L", value: "v" }));
52
+ let n = 0;
53
+ root.addEventListener('bsItemClick', () => n++);
54
+ root.click();
55
+ expect(n).toBe(0);
56
+ });
57
+ it('resolves enabled and selected backgrounds to the surface tokens', async () => {
58
+ const { root, waitForChanges } = await render(h("bs-list-item", { label: "L", interactive: true }));
59
+ // The shared browser keeps its pointer where an earlier test left it, which can leave this row
60
+ // hovered (and mid-fade); move it away and let the transition finish before reading.
61
+ await userEvent.unhover(row(root));
62
+ await settle();
63
+ expect(bg(root)).toBe('rgba(0, 0, 0, 0)');
64
+ root.setAttribute('selected', '');
65
+ await waitForChanges();
66
+ await settle();
67
+ expect(bg(root)).toBe(tokenColor('--bs-surface-selected'));
68
+ });
69
+ it('resolves hovered backgrounds (unselected and selected)', async () => {
70
+ const { root, waitForChanges } = await render(h("bs-list-item", { label: "L", interactive: true }));
71
+ await userEvent.hover(root);
72
+ await settle();
73
+ expect(bg(root)).toBe(tokenColor('--bs-surface-hover'));
74
+ root.setAttribute('selected', '');
75
+ await waitForChanges();
76
+ await settle();
77
+ expect(bg(root)).toBe(tokenColor('--bs-surface-selected-hover'));
78
+ });
79
+ it('draws a focus ring and focus surface on keyboard focus', async () => {
80
+ const { root, waitForChanges } = await render(h("bs-list-item", { label: "L", interactive: true }));
81
+ await userEvent.keyboard('{Tab}');
82
+ await waitForChanges();
83
+ await settle();
84
+ const cs = getComputedStyle(row(root));
85
+ expect(cs.outlineStyle).toBe('solid');
86
+ expect(cs.outlineWidth).toBe('2px');
87
+ expect(cs.outlineOffset).toBe('2px');
88
+ expect(cs.outlineColor).toBe(tokenColor('--bs-border-neutral-focus', 'color'));
89
+ expect(cs.backgroundColor).toBe(tokenColor('--bs-surface-focus'));
90
+ });
91
+ it('disabled: surface, text colour, no focus, no event', async () => {
92
+ const { root, waitForChanges } = await render(h("bs-list-item", { label: "L", value: "v", interactive: true, disabled: true }));
93
+ await waitForChanges();
94
+ expect(bg(root)).toBe(tokenColor('--bs-surface-action-disabled'));
95
+ expect(getComputedStyle(row(root)).color).toBe(tokenColor('--bs-text-on-disabled', 'color'));
96
+ expect(root.hasAttribute('tabindex')).toBe(false);
97
+ expect(root).toEqualAttribute('aria-disabled', 'true');
98
+ let n = 0;
99
+ root.addEventListener('bsItemClick', () => n++);
100
+ root.click();
101
+ expect(n).toBe(0);
102
+ });
103
+ it('dims a disabled leading image to 50%', async () => {
104
+ const { root } = await render(h("bs-list-item", { label: "L", disabled: true }, h("img", { slot: "leading", alt: "", src: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'/>" })));
105
+ expect(getComputedStyle(root.querySelector('img')).opacity).toBe('0.5');
106
+ });
107
+ it('sizes a leading image 72x56', async () => {
108
+ const { root, waitForChanges } = await render(h("bs-list-item", { label: "L" }, h("img", { slot: "leading", alt: "", src: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'/>" })));
109
+ await waitForChanges();
110
+ const r = root.querySelector('img').getBoundingClientRect();
111
+ expect([r.width, r.height]).toEqual([72, 56]);
112
+ });
113
+ it('renders default-slot content instead of label/description', async () => {
114
+ const { root, waitForChanges } = await render(h("bs-list-item", { label: "L", description: "D" }, h("b", null, "Rich")));
115
+ await waitForChanges();
116
+ expect(root.shadowRoot.querySelector('[part="label"]')).toBeNull();
117
+ });
118
+ it('follows the text token in light and dark mode', async () => {
119
+ const { root } = await render(h("bs-list-item", { label: "L" }));
120
+ const light = getComputedStyle(row(root)).color;
121
+ expect(light).toBe(tokenColor('--bs-text-default', 'color'));
122
+ document.documentElement.setAttribute('data-theme', 'dark');
123
+ try {
124
+ await new Promise(r => setTimeout(r, 50));
125
+ const dark = getComputedStyle(row(root)).color;
126
+ expect(dark).not.toBe(light);
127
+ expect(dark).toBe(tokenColor('--bs-text-default', 'color'));
128
+ }
129
+ finally {
130
+ document.documentElement.removeAttribute('data-theme');
131
+ }
132
+ });
133
+ it('has no axe violations', async () => {
134
+ const { root } = await render(h("bs-list-item", { label: "Label", description: "Paragraph", showArrow: true }));
135
+ await runAxeCheck(root);
136
+ });
137
+ });
@@ -0,0 +1,139 @@
1
+ :host {
2
+ display: block;
3
+ width: 100%;
4
+ box-sizing: border-box;
5
+ outline: none;
6
+ color: var(--bs-list-item-color);
7
+ --bs-list-item-min-height: var(--bs-spacing-1000);
8
+ --bs-list-item-padding-y: var(--bs-spacing-150);
9
+ --bs-list-item-padding-x: var(--bs-spacing-200);
10
+ --bs-list-item-gap: var(--bs-spacing-200);
11
+ --bs-list-item-content-gap: var(--bs-spacing-25);
12
+ --bs-list-item-radius: var(--bs-border-radius-100);
13
+ --bs-list-item-bg: transparent;
14
+ --bs-list-item-bg-hover: var(--bs-surface-hover);
15
+ --bs-list-item-bg-pressed: var(--bs-surface-pressed);
16
+ --bs-list-item-bg-focus: var(--bs-surface-focus);
17
+ --bs-list-item-bg-selected: var(--bs-surface-selected);
18
+ --bs-list-item-bg-selected-hover: var(--bs-surface-selected-hover);
19
+ --bs-list-item-bg-selected-pressed: var(--bs-surface-selected-pressed);
20
+ --bs-list-item-bg-selected-focus: var(--bs-surface-selected-focus);
21
+ --bs-list-item-bg-disabled: var(--bs-surface-action-disabled);
22
+ --bs-list-item-color: var(--bs-text-default);
23
+ --bs-list-item-color-disabled: var(--bs-text-on-disabled);
24
+ --bs-list-item-focus-ring: var(--bs-border-neutral-focus);
25
+ --bs-list-item-image-width: 72px;
26
+ --bs-list-item-image-height: 56px;
27
+ }
28
+
29
+ .bs-list-item {
30
+ box-sizing: border-box;
31
+ display: flex;
32
+ align-items: center;
33
+ gap: var(--bs-list-item-gap);
34
+ width: 100%;
35
+ min-height: var(--bs-list-item-min-height);
36
+ padding: var(--bs-list-item-padding-y) var(--bs-list-item-padding-x);
37
+ border-radius: var(--bs-list-item-radius);
38
+ overflow: clip;
39
+ background: var(--bs-list-item-bg);
40
+ color: var(--bs-list-item-color);
41
+ font-family: inherit;
42
+ font-size: var(--bs-font-size-md);
43
+ line-height: calc(var(--bs-line-height-body-md) * 1px);
44
+ transition: background-color var(--bs-duration-fast) var(--bs-easing-standard);
45
+ }
46
+
47
+ :host([selected]) .bs-list-item {
48
+ background: var(--bs-list-item-bg-selected);
49
+ }
50
+
51
+ :host(.bs-list-item--interactive) {
52
+ cursor: pointer;
53
+ }
54
+
55
+ :host(.bs-list-item--interactive:hover) .bs-list-item {
56
+ background: var(--bs-list-item-bg-hover);
57
+ }
58
+ :host(.bs-list-item--interactive[selected]:hover) .bs-list-item {
59
+ background: var(--bs-list-item-bg-selected-hover);
60
+ }
61
+ :host(.bs-list-item--interactive:active) .bs-list-item {
62
+ background: var(--bs-list-item-bg-pressed);
63
+ }
64
+ :host(.bs-list-item--interactive[selected]:active) .bs-list-item {
65
+ background: var(--bs-list-item-bg-selected-pressed);
66
+ }
67
+
68
+ :host(:focus-visible) .bs-list-item {
69
+ background: var(--bs-list-item-bg-focus);
70
+ outline: 2px solid var(--bs-list-item-focus-ring);
71
+ outline-offset: 2px;
72
+ }
73
+ :host([selected]:focus-visible) .bs-list-item {
74
+ background: var(--bs-list-item-bg-selected-focus);
75
+ }
76
+
77
+ :host([disabled]) {
78
+ color: var(--bs-list-item-color-disabled);
79
+ cursor: default;
80
+ }
81
+ :host([disabled]) .bs-list-item {
82
+ background: var(--bs-list-item-bg-disabled);
83
+ color: var(--bs-list-item-color-disabled);
84
+ }
85
+ :host([disabled]) ::slotted(img),
86
+ :host([disabled]) ::slotted(bs-avatar) {
87
+ opacity: 0.5;
88
+ }
89
+
90
+ .bs-list-item__leading {
91
+ display: none;
92
+ flex-shrink: 0;
93
+ align-items: center;
94
+ }
95
+ .bs-list-item__leading--has-content {
96
+ display: flex;
97
+ }
98
+ ::slotted(img[slot='leading']) {
99
+ display: block;
100
+ width: var(--bs-list-item-image-width);
101
+ height: var(--bs-list-item-image-height);
102
+ border-radius: var(--bs-border-radius-100);
103
+ object-fit: cover;
104
+ }
105
+ ::slotted(svg[slot='leading']) {
106
+ width: 32px;
107
+ height: 32px;
108
+ }
109
+
110
+ .bs-list-item__content {
111
+ display: flex;
112
+ flex-direction: column;
113
+ gap: var(--bs-list-item-content-gap);
114
+ flex: 1 1 0;
115
+ min-width: 0;
116
+ }
117
+ .bs-list-item__label {
118
+ font-weight: var(--bs-font-weight-semibold);
119
+ }
120
+ .bs-list-item__description,
121
+ .bs-list-item__support {
122
+ font-weight: var(--bs-font-weight-regular);
123
+ }
124
+
125
+ .bs-list-item__trailing {
126
+ display: none;
127
+ flex-shrink: 0;
128
+ align-items: center;
129
+ height: 32px;
130
+ }
131
+ .bs-list-item__trailing--shown {
132
+ display: flex;
133
+ }
134
+
135
+ @media (prefers-reduced-motion: reduce) {
136
+ .bs-list-item {
137
+ transition: none;
138
+ }
139
+ }