@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.
Files changed (100) hide show
  1. package/custom-elements.json +139 -11
  2. package/dist/brandsync-wc/brandsync-wc.esm.js +1 -1
  3. package/dist/brandsync-wc/p-0eed6813.entry.js +1 -0
  4. package/dist/brandsync-wc/{p-ee7da23d.entry.js → p-107b1c5d.entry.js} +1 -1
  5. package/dist/brandsync-wc/p-2181378f.entry.js +1 -0
  6. package/dist/brandsync-wc/p-27bbed83.entry.js +1 -0
  7. package/dist/brandsync-wc/p-36bf37d9.entry.js +1 -0
  8. package/dist/brandsync-wc/p-3b64e3cd.entry.js +1 -0
  9. package/dist/brandsync-wc/p-6fd7dd6a.entry.js +1 -0
  10. package/dist/brandsync-wc/p-9f6fe37d.entry.js +1 -0
  11. package/dist/brandsync-wc/{p-03b08508.entry.js → p-c3f72584.entry.js} +1 -1
  12. package/dist/brandsync-wc/p-fc240aa0.entry.js +1 -0
  13. package/dist/cjs/brandsync-wc.cjs.js +1 -1
  14. package/dist/cjs/bs-breadcrumb-overflow.cjs.entry.js +13 -1
  15. package/dist/cjs/bs-breadcrumb.cjs.entry.js +15 -1
  16. package/dist/cjs/bs-data-table.cjs.entry.js +18 -4
  17. package/dist/cjs/bs-icon-button.cjs.entry.js +2 -2
  18. package/dist/cjs/bs-inline-tab.cjs.entry.js +5 -1
  19. package/dist/cjs/bs-navigation-drawer-item.cjs.entry.js +1 -1
  20. package/dist/cjs/bs-navigation-drawer.cjs.entry.js +118 -5
  21. package/dist/cjs/bs-navigation-header.cjs.entry.js +1 -1
  22. package/dist/cjs/bs-tab.cjs.entry.js +5 -1
  23. package/dist/cjs/bs-tabs.cjs.entry.js +47 -2
  24. package/dist/cjs/loader.cjs.js +1 -1
  25. package/dist/collection/components/bs-breadcrumb/bs-breadcrumb/bs-breadcrumb.cmp.test.js +63 -0
  26. package/dist/collection/components/bs-breadcrumb/bs-breadcrumb/bs-breadcrumb.js +32 -1
  27. package/dist/collection/components/bs-breadcrumb/bs-breadcrumb-overflow/bs-breadcrumb-overflow.cmp.test.js +45 -0
  28. package/dist/collection/components/bs-breadcrumb/bs-breadcrumb-overflow/bs-breadcrumb-overflow.js +30 -1
  29. package/dist/collection/components/bs-button/bs-icon-button/bs-icon-button.cmp.test.js +20 -0
  30. package/dist/collection/components/bs-button/bs-icon-button/bs-icon-button.css +21 -4
  31. package/dist/collection/components/bs-button/bs-icon-button/bs-icon-button.js +3 -2
  32. package/dist/collection/components/bs-button/bs-icon-button/bs-icon-button.stories.js +10 -0
  33. package/dist/collection/components/bs-data-table/bs-data-table.cmp.test.js +19 -0
  34. package/dist/collection/components/bs-data-table/bs-data-table.js +19 -5
  35. package/dist/collection/components/bs-data-table/bs-data-table.stories.js +8 -1
  36. package/dist/collection/components/bs-logo/bs-logo.cmp.test.js +52 -3
  37. package/dist/collection/components/bs-menu/bs-menu.stories.js +54 -0
  38. package/dist/collection/components/bs-tab/bs-inline-tab/bs-inline-tab.js +24 -1
  39. package/dist/collection/components/bs-tab/bs-tab/bs-tab.js +24 -1
  40. package/dist/collection/components/bs-tab/bs-tabs/bs-tabs.cmp.test.js +88 -0
  41. package/dist/collection/components/bs-tab/bs-tabs/bs-tabs.js +83 -2
  42. package/dist/collection/components/bs-tab/bs-tabs/bs-tabs.stories.js +24 -0
  43. package/dist/collection/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer/bs-navigation-drawer.cmp.test.js +173 -5
  44. package/dist/collection/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer/bs-navigation-drawer.css +106 -34
  45. package/dist/collection/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer/bs-navigation-drawer.js +181 -5
  46. package/dist/collection/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer/bs-navigation-drawer.stories.js +48 -0
  47. package/dist/collection/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer-item/bs-navigation-drawer-item.js +1 -1
  48. package/dist/collection/components/ui-shell/bs-navigation-header/bs-navigation-header/bs-navigation-header.cmp.test.js +68 -0
  49. package/dist/collection/components/ui-shell/bs-navigation-header/bs-navigation-header/bs-navigation-header.css +31 -17
  50. package/dist/components/bs-attachment.js +1 -1
  51. package/dist/components/bs-breadcrumb-overflow.js +1 -1
  52. package/dist/components/bs-breadcrumb.js +1 -1
  53. package/dist/components/bs-carousel.js +1 -1
  54. package/dist/components/bs-data-table.js +1 -1
  55. package/dist/components/bs-dialog.js +1 -1
  56. package/dist/components/bs-file-upload-item.js +1 -1
  57. package/dist/components/bs-icon-button.js +1 -1
  58. package/dist/components/bs-inline-tab.js +1 -1
  59. package/dist/components/bs-navigation-drawer-item.js +1 -1
  60. package/dist/components/bs-navigation-drawer.js +1 -1
  61. package/dist/components/bs-navigation-header.js +1 -1
  62. package/dist/components/bs-snackbar.js +1 -1
  63. package/dist/components/bs-tab.js +1 -1
  64. package/dist/components/bs-tabs.js +1 -1
  65. package/dist/components/p-BoeorHNv.js +1 -0
  66. package/dist/esm/brandsync-wc.js +1 -1
  67. package/dist/esm/bs-breadcrumb-overflow.entry.js +14 -2
  68. package/dist/esm/bs-breadcrumb.entry.js +16 -2
  69. package/dist/esm/bs-data-table.entry.js +18 -4
  70. package/dist/esm/bs-icon-button.entry.js +2 -2
  71. package/dist/esm/bs-inline-tab.entry.js +5 -1
  72. package/dist/esm/bs-navigation-drawer-item.entry.js +1 -1
  73. package/dist/esm/bs-navigation-drawer.entry.js +119 -6
  74. package/dist/esm/bs-navigation-header.entry.js +1 -1
  75. package/dist/esm/bs-tab.entry.js +5 -1
  76. package/dist/esm/bs-tabs.entry.js +48 -3
  77. package/dist/esm/loader.js +1 -1
  78. package/dist/types/components/bs-breadcrumb/bs-breadcrumb/bs-breadcrumb.d.ts +10 -0
  79. package/dist/types/components/bs-breadcrumb/bs-breadcrumb-overflow/bs-breadcrumb-overflow.d.ts +8 -0
  80. package/dist/types/components/bs-button/bs-icon-button/bs-icon-button.d.ts +2 -1
  81. package/dist/types/components/bs-button/bs-icon-button/bs-icon-button.stories.d.ts +1 -0
  82. package/dist/types/components/bs-data-table/bs-data-table.d.ts +16 -2
  83. package/dist/types/components/bs-menu/bs-menu.stories.d.ts +1 -0
  84. package/dist/types/components/bs-tab/bs-inline-tab/bs-inline-tab.d.ts +4 -0
  85. package/dist/types/components/bs-tab/bs-tab/bs-tab.d.ts +4 -0
  86. package/dist/types/components/bs-tab/bs-tabs/bs-tabs.d.ts +17 -0
  87. package/dist/types/components/bs-tab/bs-tabs/bs-tabs.stories.d.ts +1 -0
  88. package/dist/types/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer/bs-navigation-drawer.d.ts +50 -1
  89. package/dist/types/components/ui-shell/bs-navigation-drawer/bs-navigation-drawer/bs-navigation-drawer.stories.d.ts +2 -0
  90. package/dist/types/components.d.ts +130 -2
  91. package/package.json +1 -1
  92. package/dist/brandsync-wc/p-002d258d.entry.js +0 -1
  93. package/dist/brandsync-wc/p-153da845.entry.js +0 -1
  94. package/dist/brandsync-wc/p-2e7a1082.entry.js +0 -1
  95. package/dist/brandsync-wc/p-612cf79c.entry.js +0 -1
  96. package/dist/brandsync-wc/p-652d4068.entry.js +0 -1
  97. package/dist/brandsync-wc/p-bc69a96c.entry.js +0 -1
  98. package/dist/brandsync-wc/p-bf53ad52.entry.js +0 -1
  99. package/dist/brandsync-wc/p-c7f4c114.entry.js +0 -1
  100. 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.el.querySelectorAll(':scope > bs-tab, :scope > bs-inline-tab');
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: '81b3b46d54d4acc074b0c59d0da5e3e971e7b62e', part: "tablist", role: "tablist", "aria-orientation": this.orientation, class: `bs-tabs bs-tabs--${this.type} ${vertical ? 'bs-tabs--vertical' : ''}` }, index.h("slot", { key: 'ba296a99062629de3e331cd7de1ebc41548c77f9', onSlotchange: () => this.syncRovingTabindex() })));
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
  }]
@@ -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"]},null,{"collapsed":[{"onCollapsedChange":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"],"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"],"tabbable":[4],"hasIcon":[32],"hasLabel":[32],"hasBadge":[32]}]]],["bs-tabs.cjs",[[769,"bs-tabs",{"type":[1],"orientation":[1]},[[0,"bsSelect","onTabSelect"],[0,"keydown","onKeyDown"]],{"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);
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: 'f9c93eed4d4844e03cdfc50c9efbee238c188257', role: "listitem" }, h("span", { key: 'e0f26700582662cd4269bd786a9c81fd8031716e', part: "separator", class: "bs-breadcrumb__separator", "aria-hidden": "true" }, h(CaretRightIcon, { key: '77e8ed02ffd367e9b7ca39869ab41a95759450e4' })), 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 }, h("slot", null))) : (h("span", { part: "link", class: "bs-breadcrumb__link" }, h("slot", null)))));
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
  });
@@ -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: '6219cabef08760aa3940d9a4e12d6e5ecd21e407', role: "listitem" }, h("span", { key: '59261696d02c49b1ea81b29d822d6d42c4fbafd5', part: "separator", class: "bs-breadcrumb-overflow__separator", "aria-hidden": "true" }, h(CaretRightIcon, { key: '62921ade525de21d7e74e732e0736c345fef87b7' })), h("span", { key: '0a8e9a9dc472ab9d1b63bdd497ceda12e04a736a', class: "bs-breadcrumb-overflow__wrapper" }, h("button", { key: '6930b4b341ee5b810eae5ffe32e33022402a01d3', 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: 'c9cf273fa51bac2bf11953d294184be641a47a9f' })), this.open && this.items.length > 0 && (h("bs-menu", { key: '1a1948a75569ad7639131d2982c8ebdf28027f85', 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 }, item.label)) : (h("span", { part: "link", class: "bs-breadcrumb-overflow__link" }, item.label))))))));
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: '1d0525e3ad0b90e723cab2c9cd0217046a905a5e', 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: '1c94ad4a861dde9e4942f6795a2a868161c0b4a1', part: "icon", class: "bs-icon-button__icon" }, h("slot", { key: '9cd3a6aa2d9cbfebaa1d0e70fc10448af4ef4429' }))));
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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[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, e.g. for an actions column. Function props are JS-property-only,
33
- * same as array props -- there is no attribute equivalent. Falls back to the raw cell value. */
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: 'a1a9d0c92eb2af12f8f9cab9ba4ae0994a10958a', part: "table", class: "bs-data-table" }, h("thead", { key: '2b51500b221465d750b55c31e00af94684367eb2', part: "head" }, h("tr", { key: '57aa25ca9b255213f155b5b30841e284d1eebaf8' }, this.selectable && (h("th", { key: '44b709a7cd1f2072934efe8fef44a63b2ed20d62', class: "bs-data-table__select-col" }, this.rows.length > 0 && (h("input", { key: '084d099453c53333d77c60786b7eb6bedfa21ef3', 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 => {
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: '0438897652b9f8909ff67ae9f7b22d0950f1e5e8', 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 ? (
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 an actions column. Function props are JS-property-only,\nsame as array props -- there is no attribute equivalent. Falls back to the raw cell value."
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