@nectary/components 5.37.7 → 5.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bundle.js CHANGED
@@ -7378,12 +7378,80 @@ class Popover extends NectaryElement {
7378
7378
  };
7379
7379
  }
7380
7380
  defineCustomElement("sinch-popover", Popover);
7381
+ const isActivationKey = (key) => key === "Enter" || key === " ";
7382
+ function createKeyboardNavigation() {
7383
+ return {
7384
+ navigateToNextOption(enabledOptions, forward) {
7385
+ const optionsLength = enabledOptions.length;
7386
+ if (optionsLength === 0) {
7387
+ return;
7388
+ }
7389
+ const currentIndex = enabledOptions.findIndex((option) => option === getDeepActiveElement());
7390
+ let nextIndex;
7391
+ if (currentIndex !== -1) {
7392
+ if (forward) {
7393
+ nextIndex = (currentIndex + 1) % optionsLength;
7394
+ } else {
7395
+ nextIndex = currentIndex === 0 ? optionsLength - 1 : currentIndex - 1;
7396
+ }
7397
+ } else {
7398
+ nextIndex = forward ? 0 : optionsLength - 1;
7399
+ }
7400
+ this.navigateToOption(enabledOptions, nextIndex);
7401
+ },
7402
+ navigateToOption(enabledOptions, index) {
7403
+ const optionsLength = enabledOptions.length;
7404
+ if (enabledOptions.length === 0 || index < 0 || index >= optionsLength) {
7405
+ return;
7406
+ }
7407
+ const option = enabledOptions[index];
7408
+ option.focus();
7409
+ },
7410
+ handleKeyboardNavigation(e, enabledOptions) {
7411
+ switch (e.code) {
7412
+ case "Space":
7413
+ case "Enter": {
7414
+ e.preventDefault();
7415
+ const target = getTargetByAttribute(e, "value");
7416
+ if (target !== null) {
7417
+ target.click();
7418
+ }
7419
+ break;
7420
+ }
7421
+ case "ArrowLeft": {
7422
+ e.preventDefault();
7423
+ this.navigateToNextOption(enabledOptions, false);
7424
+ break;
7425
+ }
7426
+ case "ArrowRight": {
7427
+ e.preventDefault();
7428
+ this.navigateToNextOption(enabledOptions, true);
7429
+ break;
7430
+ }
7431
+ case "Home": {
7432
+ e.preventDefault();
7433
+ this.navigateToOption(enabledOptions, 0);
7434
+ break;
7435
+ }
7436
+ case "End": {
7437
+ e.preventDefault();
7438
+ this.navigateToOption(enabledOptions, enabledOptions.length - 1);
7439
+ break;
7440
+ }
7441
+ }
7442
+ }
7443
+ };
7444
+ }
7381
7445
  const templateHTML$P = '<style>:host{display:block}#wrapper{display:flex;width:100%;height:40px;border-bottom:1px solid var(--sinch-comp-tab-color-default-border-initial);box-sizing:border-box}</style><div id="wrapper"><slot></slot></div>';
7382
7446
  const template$P = document.createElement("template");
7383
7447
  template$P.innerHTML = templateHTML$P;
7384
7448
  class Tabs extends NectaryElement {
7385
7449
  #$slot;
7386
7450
  #controller = null;
7451
+ #enabledOptions = [];
7452
+ #keyboardNav = createKeyboardNavigation();
7453
+ #observer = null;
7454
+ #rovingValue = null;
7387
7455
  constructor() {
7388
7456
  super();
7389
7457
  const shadowRoot = this.attachShadow();
@@ -7393,14 +7461,28 @@ class Tabs extends NectaryElement {
7393
7461
  connectedCallback() {
7394
7462
  this.#controller = new AbortController();
7395
7463
  const { signal } = this.#controller;
7464
+ const options = { signal };
7396
7465
  this.setAttribute("role", "tablist");
7397
- this.#$slot.addEventListener("option-change", this.#onOptionChange, { signal });
7398
- this.#$slot.addEventListener("slotchange", this.#onSlotChange, { signal });
7399
- this.addEventListener("-change", this.#onChangeReactHandler, { signal });
7466
+ this.setAttribute("aria-orientation", "horizontal");
7467
+ this.#$slot.addEventListener("option-change", this.#onOptionChange, options);
7468
+ this.#$slot.addEventListener("slotchange", this.#onSlotChange, options);
7469
+ this.#$slot.addEventListener("focusin", this.#onOptionFocusIn, options);
7470
+ this.#$slot.addEventListener("keydown", this.#onOptionKeydown, options);
7471
+ this.addEventListener("-change", this.#onChangeReactHandler, options);
7472
+ this.#observer = new MutationObserver(this.#onOptionMutation);
7473
+ this.#observer.observe(this, {
7474
+ attributes: true,
7475
+ attributeFilter: ["disabled", "value"],
7476
+ childList: true,
7477
+ subtree: true
7478
+ });
7479
+ this.#syncOptions();
7400
7480
  }
7401
7481
  disconnectedCallback() {
7402
7482
  this.#controller.abort();
7403
7483
  this.#controller = null;
7484
+ this.#observer?.disconnect();
7485
+ this.#observer = null;
7404
7486
  }
7405
7487
  static get observedAttributes() {
7406
7488
  return ["value"];
@@ -7426,19 +7508,91 @@ class Tabs extends NectaryElement {
7426
7508
  }
7427
7509
  return null;
7428
7510
  }
7511
+ #getOptions() {
7512
+ return this.#$slot.assignedElements();
7513
+ }
7514
+ #updateEnabledOptions(options = this.#getOptions()) {
7515
+ this.#enabledOptions = options.filter((option) => !getBooleanAttribute(option, "disabled"));
7516
+ }
7517
+ #getFocusableOption() {
7518
+ if (this.#enabledOptions.length === 0) {
7519
+ return null;
7520
+ }
7521
+ if (this.#rovingValue !== null) {
7522
+ const rovingOption = this.#enabledOptions.find(
7523
+ (option) => getAttribute(option, "value", "") === this.#rovingValue
7524
+ );
7525
+ if (rovingOption != null) {
7526
+ return rovingOption;
7527
+ }
7528
+ }
7529
+ const selectedOption = this.#enabledOptions.find(
7530
+ (option) => getAttribute(option, "value", "") === this.value
7531
+ );
7532
+ return selectedOption ?? this.#enabledOptions[0];
7533
+ }
7534
+ #syncOptions() {
7535
+ const options = this.#getOptions();
7536
+ this.#updateEnabledOptions(options);
7537
+ const focusableOption = this.#getFocusableOption();
7538
+ this.#rovingValue = focusableOption == null ? null : getAttribute(focusableOption, "value", "");
7539
+ for (const $option of options) {
7540
+ const isDisabled = getBooleanAttribute($option, "disabled");
7541
+ const isChecked = !isDisabled && this.value === getAttribute($option, "value", "");
7542
+ updateBooleanAttribute($option, "data-checked", isChecked);
7543
+ $option.tabIndex = !isDisabled && $option === focusableOption ? 0 : -1;
7544
+ }
7545
+ }
7429
7546
  #onSlotChange = () => {
7430
- this.#onValueChange(this.value);
7547
+ this.#syncOptions();
7431
7548
  };
7432
7549
  #onOptionChange = (e) => {
7433
7550
  e.stopPropagation();
7434
- this.#dispatchChangeEvent(e.detail);
7551
+ const value = e.detail;
7552
+ this.#rovingValue = value;
7553
+ this.#dispatchChangeEvent(value);
7435
7554
  };
7436
7555
  #onValueChange(value) {
7437
- for (const $option of this.#$slot.assignedElements()) {
7438
- const isChecked = !getBooleanAttribute($option, "disabled") && value === getAttribute($option, "value", "");
7439
- updateBooleanAttribute($option, "data-checked", isChecked);
7440
- }
7556
+ this.#rovingValue = value;
7557
+ this.#syncOptions();
7441
7558
  }
7559
+ #onOptionFocusIn = (e) => {
7560
+ const target = getTargetByAttribute(e, "value");
7561
+ if (target === null || getBooleanAttribute(target, "disabled")) {
7562
+ return;
7563
+ }
7564
+ this.#rovingValue = getAttribute(target, "value", "");
7565
+ this.#syncOptions();
7566
+ };
7567
+ #onOptionKeydown = (e) => {
7568
+ switch (e.code) {
7569
+ case "ArrowLeft": {
7570
+ e.preventDefault();
7571
+ this.#keyboardNav.navigateToNextOption(this.#enabledOptions, false);
7572
+ break;
7573
+ }
7574
+ case "ArrowRight": {
7575
+ e.preventDefault();
7576
+ this.#keyboardNav.navigateToNextOption(this.#enabledOptions, true);
7577
+ break;
7578
+ }
7579
+ case "Home": {
7580
+ e.preventDefault();
7581
+ this.#keyboardNav.navigateToOption(this.#enabledOptions, 0);
7582
+ break;
7583
+ }
7584
+ case "End": {
7585
+ e.preventDefault();
7586
+ this.#keyboardNav.navigateToOption(this.#enabledOptions, this.#enabledOptions.length - 1);
7587
+ break;
7588
+ }
7589
+ }
7590
+ };
7591
+ #onOptionMutation = (mutations) => {
7592
+ if (mutations.some((mutation) => mutation.target !== this)) {
7593
+ this.#syncOptions();
7594
+ }
7595
+ };
7442
7596
  #dispatchChangeEvent(value) {
7443
7597
  this.dispatchEvent(
7444
7598
  new CustomEvent("-change", { detail: value })
@@ -7450,25 +7604,26 @@ class Tabs extends NectaryElement {
7450
7604
  };
7451
7605
  }
7452
7606
  defineCustomElement("sinch-tabs", Tabs);
7453
- const templateHTML$O = '<style>:host{display:block;outline:0}#button{all:initial;position:relative;display:flex;flex-direction:column;padding:12px 16px 0;box-sizing:border-box;cursor:pointer;background-color:var(--sinch-comp-tab-color-default-background-initial);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);height:39px;--sinch-global-color-icon:var(--sinch-comp-tab-color-default-icon-initial);--sinch-global-size-icon:var(--sinch-comp-tab-size-icon)}#button:hover{background-color:var(--sinch-comp-tab-color-default-background-hover)}#button:focus-visible::after{content:"";position:absolute;inset:0;bottom:-3px;border:2px solid var(--sinch-comp-tab-color-default-outline-focus);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);pointer-events:none}#button:disabled{cursor:unset;pointer-events:none;--sinch-global-color-icon:var(--sinch-comp-tab-color-disabled-icon-initial)}:host([data-checked]) #button{--sinch-global-color-icon:var(--sinch-comp-tab-color-checked-icon-initial)}:host([data-checked]) #button::before{content:"";position:absolute;left:0;right:0;bottom:-1px;pointer-events:none;border-top:2px solid var(--sinch-comp-tab-color-checked-border-initial)}::slotted(*){display:block}</style><sinch-tooltip id="tooltip"><button id="button" tabindex="0"><slot name="icon"></slot></button></sinch-tooltip>';
7607
+ const templateHTML$O = '<style>:host{display:block;outline:0}#button{all:initial;position:relative;display:flex;flex-direction:column;padding:12px 16px 0;box-sizing:border-box;cursor:pointer;background-color:var(--sinch-comp-tab-color-default-background-initial);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);height:39px;--sinch-global-color-icon:var(--sinch-comp-tab-color-default-icon-initial);--sinch-global-size-icon:var(--sinch-comp-tab-size-icon)}:host([disabled]) #button{cursor:unset;pointer-events:none;--sinch-global-color-icon:var(--sinch-comp-tab-color-disabled-icon-initial)}:host([data-checked]) #button{--sinch-global-color-icon:var(--sinch-comp-tab-color-checked-icon-initial)}:host([data-checked]) #button::before{content:"";position:absolute;left:0;right:0;bottom:-1px;pointer-events:none;border-top:2px solid var(--sinch-comp-tab-color-checked-border-initial)}:host(:hover:not([disabled])) #button{background-color:var(--sinch-comp-tab-color-default-background-hover)}:host(:focus-visible) #button::after{content:"";position:absolute;inset:0;bottom:-3px;border:2px solid var(--sinch-comp-tab-color-default-outline-focus);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);pointer-events:none}::slotted(*){display:block;pointer-events:none}</style><sinch-tooltip id="tooltip"><div id="button"><slot name="icon"></slot></div></sinch-tooltip>';
7454
7608
  const template$O = document.createElement("template");
7455
7609
  template$O.innerHTML = templateHTML$O;
7456
7610
  class TabsIconOption extends NectaryElement {
7457
- #$button;
7458
7611
  #$tooltip;
7459
7612
  constructor() {
7460
7613
  super();
7461
- const shadowRoot = this.attachShadow({ delegatesFocus: true });
7614
+ const shadowRoot = this.attachShadow();
7462
7615
  shadowRoot.appendChild(template$O.content.cloneNode(true));
7463
- this.#$button = shadowRoot.querySelector("#button");
7464
7616
  this.#$tooltip = shadowRoot.querySelector("#tooltip");
7465
7617
  }
7466
7618
  connectedCallback() {
7467
7619
  this.setAttribute("role", "tab");
7468
- this.#$button.addEventListener("click", this.#onClick);
7620
+ this.addEventListener("click", this.#onClick);
7621
+ this.addEventListener("keydown", this.#onKeyDown);
7622
+ this.#updateTabIndex();
7469
7623
  }
7470
7624
  disconnectedCallback() {
7471
- this.#$button.removeEventListener("click", this.#onClick);
7625
+ this.removeEventListener("click", this.#onClick);
7626
+ this.removeEventListener("keydown", this.#onKeyDown);
7472
7627
  }
7473
7628
  static get observedAttributes() {
7474
7629
  return ["data-checked", "disabled", "aria-label"];
@@ -7496,7 +7651,8 @@ class TabsIconOption extends NectaryElement {
7496
7651
  }
7497
7652
  case "disabled": {
7498
7653
  const isDisabled = isAttrTrue(newVal);
7499
- this.#$button.disabled = isDisabled;
7654
+ this.#updateTabIndex();
7655
+ updateExplicitBooleanAttribute(this, "aria-disabled", isDisabled);
7500
7656
  updateBooleanAttribute(this, name, isDisabled);
7501
7657
  break;
7502
7658
  }
@@ -7510,17 +7666,32 @@ class TabsIconOption extends NectaryElement {
7510
7666
  return true;
7511
7667
  }
7512
7668
  focus() {
7513
- this.#$button.focus();
7669
+ HTMLElement.prototype.focus.call(this);
7514
7670
  }
7515
7671
  blur() {
7516
- this.#$button.blur();
7672
+ HTMLElement.prototype.blur.call(this);
7673
+ }
7674
+ #updateTabIndex() {
7675
+ if (this.disabled) {
7676
+ this.tabIndex = -1;
7677
+ }
7517
7678
  }
7518
7679
  #onClick = (e) => {
7680
+ if (this.disabled) {
7681
+ return;
7682
+ }
7519
7683
  e.stopPropagation();
7520
7684
  this.dispatchEvent(
7521
7685
  new CustomEvent("option-change", { bubbles: true, detail: this.value })
7522
7686
  );
7523
7687
  };
7688
+ #onKeyDown = (e) => {
7689
+ if (this.disabled || !isActivationKey(e.key)) {
7690
+ return;
7691
+ }
7692
+ e.preventDefault();
7693
+ this.#onClick(e);
7694
+ };
7524
7695
  }
7525
7696
  defineCustomElement("sinch-tabs-icon-option", TabsIconOption);
7526
7697
  const createDebounce = (delayFn, cancelFn) => (cb) => {
@@ -7857,13 +8028,14 @@ class EmojiPicker extends NectaryElement {
7857
8028
  }
7858
8029
  }
7859
8030
  defineCustomElement("sinch-emoji-picker", EmojiPicker);
7860
- const templateHTML$M = '<style>:host{display:block}#wrapper{display:flex;flex-direction:column;width:100%}#bottom,#top{display:flex;align-items:baseline}#top{height:24px;margin-bottom:2px}#top.empty{display:none}#additional,#invalid,#label,#optional{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#label{font:var(--sinch-comp-field-font-label);color:var(--sinch-comp-field-color-default-label-initial)}#optional{flex:1;font:var(--sinch-comp-field-font-optional);color:var(--sinch-comp-field-color-default-optional-initial);text-align:right}#additional{flex:1;text-align:right;font:var(--sinch-comp-field-font-additional);color:var(--sinch-comp-field-color-default-additional-initial);line-height:20px;margin-top:2px}#additional:empty{display:none}#invalid{font:var(--sinch-comp-field-font-invalid);color:var(--sinch-comp-field-color-invalid-text-initial);line-height:20px;margin-top:2px}#invalid:empty{display:none}#tooltip{align-self:center;margin:0 8px;display:flex}#tooltip.empty{display:none}:host([disabled]) #label{color:var(--sinch-comp-field-color-disabled-label-initial)}:host([disabled]) #additional{color:var(--sinch-comp-field-color-disabled-additional-initial)}:host([disabled]) #optional{color:var(--sinch-comp-field-color-disabled-optional-initial)}</style><div id="wrapper"><div id="top"><label id="label" for="input"></label><div id="tooltip"><slot name="tooltip"></slot></div><span id="optional"></span></div><slot name="input"></slot><div id="bottom"><div id="invalid"></div><div id="additional"></div></div></div>';
8031
+ const templateHTML$M = '<style>:host{display:block}#wrapper{display:flex;flex-direction:column;width:100%}#bottom,#top{display:flex;align-items:baseline}#top{height:24px;margin-bottom:2px}#top.empty{display:none}#additional,#invalid,#label,#optional{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#label{font:var(--sinch-comp-field-font-label);color:var(--sinch-comp-field-color-default-label-initial)}#optional{flex:1;font:var(--sinch-comp-field-font-optional);color:var(--sinch-comp-field-color-default-optional-initial);text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#optional:empty{display:none}#additional{flex:1;text-align:right;font:var(--sinch-comp-field-font-additional);color:var(--sinch-comp-field-color-default-additional-initial);line-height:20px;margin-top:2px}#additional:empty{display:none}#invalid{font:var(--sinch-comp-field-font-invalid);color:var(--sinch-comp-field-color-invalid-text-initial);line-height:20px;margin-top:2px}#invalid:empty{display:none}#tooltip{align-self:center;margin:0 8px;display:flex}#tooltip.empty{display:none}:host([disabled]) #label{color:var(--sinch-comp-field-color-disabled-label-initial)}:host([disabled]) #additional{color:var(--sinch-comp-field-color-disabled-additional-initial)}:host([disabled]) #optional{color:var(--sinch-comp-field-color-disabled-optional-initial)}</style><div id="wrapper"><div id="top"><label id="label" for="input"></label><div id="tooltip"><slot name="tooltip"></slot></div><sinch-tooltip id="optional-tooltip" allow-scroll><span id="optional"></span></sinch-tooltip></div><slot name="input"></slot><div id="bottom"><div id="invalid"></div><div id="additional"></div></div></div>';
7861
8032
  const template$M = document.createElement("template");
7862
8033
  template$M.innerHTML = templateHTML$M;
7863
8034
  class Field extends NectaryElement {
7864
8035
  topSection;
7865
8036
  #$label;
7866
8037
  #$optionalText;
8038
+ #$optionalTooltip;
7867
8039
  #$additionalText;
7868
8040
  #$invalidText;
7869
8041
  #$inputSlot;
@@ -7877,6 +8049,7 @@ class Field extends NectaryElement {
7877
8049
  this.topSection = shadowRoot.querySelector("#top");
7878
8050
  this.#$label = shadowRoot.querySelector("#label");
7879
8051
  this.#$optionalText = shadowRoot.querySelector("#optional");
8052
+ this.#$optionalTooltip = shadowRoot.querySelector("#optional-tooltip");
7880
8053
  this.#$additionalText = shadowRoot.querySelector("#additional");
7881
8054
  this.#$invalidText = shadowRoot.querySelector("#invalid");
7882
8055
  this.#$inputSlot = shadowRoot.querySelector('slot[name="input"]');
@@ -7900,6 +8073,7 @@ class Field extends NectaryElement {
7900
8073
  return [
7901
8074
  "label",
7902
8075
  "optionaltext",
8076
+ "optionaltooltip",
7903
8077
  "additionaltext",
7904
8078
  "invalidtext",
7905
8079
  "disabled"
@@ -7920,6 +8094,10 @@ class Field extends NectaryElement {
7920
8094
  this.#$optionalText.textContent = newVal;
7921
8095
  break;
7922
8096
  }
8097
+ case "optionaltooltip": {
8098
+ updateAttribute(this.#$optionalTooltip, "text", newVal);
8099
+ break;
8100
+ }
7923
8101
  case "additionaltext": {
7924
8102
  this.#$additionalText.textContent = newVal;
7925
8103
  break;
@@ -7950,6 +8128,12 @@ class Field extends NectaryElement {
7950
8128
  get optionalText() {
7951
8129
  return getAttribute(this, "optionaltext");
7952
8130
  }
8131
+ set optionalTooltip(value) {
8132
+ updateAttribute(this, "optionaltooltip", value);
8133
+ }
8134
+ get optionalTooltip() {
8135
+ return getAttribute(this, "optionaltooltip");
8136
+ }
7953
8137
  set additionalText(value) {
7954
8138
  updateAttribute(this, "additionaltext", value);
7955
8139
  }
@@ -13058,65 +13242,6 @@ class SegmentedControlOption extends NectaryElement {
13058
13242
  };
13059
13243
  }
13060
13244
  defineCustomElement("sinch-segmented-control-option", SegmentedControlOption);
13061
- function createKeyboardNavigation() {
13062
- return {
13063
- navigateToNextOption(enabledOptions, forward) {
13064
- const optionsLength = enabledOptions.length;
13065
- if (optionsLength === 0) {
13066
- return;
13067
- }
13068
- const currentIndex = enabledOptions.findIndex((option) => option === getDeepActiveElement());
13069
- let nextIndex;
13070
- if (currentIndex !== -1) {
13071
- if (forward) {
13072
- nextIndex = (currentIndex + 1) % optionsLength;
13073
- } else {
13074
- nextIndex = currentIndex === 0 ? optionsLength - 1 : currentIndex - 1;
13075
- }
13076
- } else {
13077
- nextIndex = forward ? 0 : optionsLength - 1;
13078
- }
13079
- this.navigateToOption(enabledOptions, nextIndex);
13080
- },
13081
- navigateToOption(enabledOptions, index) {
13082
- const optionsLength = enabledOptions.length;
13083
- if (enabledOptions.length === 0 || index < 0 || index >= optionsLength) {
13084
- return;
13085
- }
13086
- const option = enabledOptions[index];
13087
- option.focus();
13088
- },
13089
- handleKeyboardNavigation(e, enabledOptions) {
13090
- switch (e.code) {
13091
- case "Space":
13092
- case "Enter": {
13093
- e.preventDefault();
13094
- const target = getTargetByAttribute(e, "value");
13095
- if (target !== null) {
13096
- target.click();
13097
- }
13098
- break;
13099
- }
13100
- case "ArrowLeft":
13101
- case "ArrowRight": {
13102
- e.preventDefault();
13103
- this.navigateToNextOption(enabledOptions, e.code === "ArrowRight");
13104
- break;
13105
- }
13106
- case "Home": {
13107
- e.preventDefault();
13108
- this.navigateToOption(enabledOptions, 0);
13109
- break;
13110
- }
13111
- case "End": {
13112
- e.preventDefault();
13113
- this.navigateToOption(enabledOptions, enabledOptions.length - 1);
13114
- break;
13115
- }
13116
- }
13117
- }
13118
- };
13119
- }
13120
13245
  const templateHTML$m = '<style>:host{display:block;outline:0}#wrapper{display:flex;flex-direction:row;width:100%;box-sizing:border-box;position:relative;z-index:0}</style><div id="wrapper"><slot></slot></div>';
13121
13246
  const template$m = document.createElement("template");
13122
13247
  template$m.innerHTML = templateHTML$m;
@@ -14695,25 +14820,26 @@ class Table extends NectaryElement {
14695
14820
  }
14696
14821
  }
14697
14822
  defineCustomElement("sinch-table", Table);
14698
- const templateHTML$6 = '<style>:host{display:block}#button{all:initial;position:relative;display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:12px 16px;box-sizing:border-box;cursor:pointer;background-color:var(--sinch-comp-tab-color-default-background-initial);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);height:39px;--sinch-global-color-text:var(--sinch-comp-tab-color-default-text-initial);--sinch-global-color-icon:var(--sinch-comp-tab-color-default-icon-initial);--sinch-global-size-icon:var(--sinch-comp-tab-size-icon)}#button:hover{background-color:var(--sinch-comp-tab-color-default-background-hover)}#button:focus-visible::after{content:"";position:absolute;inset:0;bottom:-3px;border:2px solid var(--sinch-comp-tab-color-default-outline-focus);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);pointer-events:none}#button:disabled{cursor:unset;pointer-events:none;--sinch-global-color-text:var(--sinch-comp-tab-color-disabled-text-initial);--sinch-global-color-icon:var(--sinch-comp-tab-color-disabled-icon-initial)}:host([data-checked]) #button{--sinch-global-color-text:var(--sinch-comp-tab-color-checked-text-initial);--sinch-global-color-icon:var(--sinch-comp-tab-color-checked-icon-initial)}:host([data-checked]) #button::before{content:"";position:absolute;left:0;right:0;bottom:-1px;pointer-events:none;border-top:2px solid var(--sinch-comp-tab-color-checked-border-initial)}#text{flex-shrink:1;flex-basis:auto;min-width:0;--sinch-comp-text-font:var(--sinch-comp-tab-font-label)}::slotted(*){display:block}</style><button id="button" tabindex="0"><slot name="icon"></slot><sinch-text id="text" type="m" ellipsis></sinch-text></button>';
14823
+ const templateHTML$6 = '<style>:host{display:block;outline:0}#button{all:initial;position:relative;display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:12px 16px;box-sizing:border-box;cursor:pointer;background-color:var(--sinch-comp-tab-color-default-background-initial);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);height:39px;--sinch-global-color-text:var(--sinch-comp-tab-color-default-text-initial);--sinch-global-color-icon:var(--sinch-comp-tab-color-default-icon-initial);--sinch-global-size-icon:var(--sinch-comp-tab-size-icon)}:host([disabled]) #button{cursor:unset;pointer-events:none;--sinch-global-color-text:var(--sinch-comp-tab-color-disabled-text-initial);--sinch-global-color-icon:var(--sinch-comp-tab-color-disabled-icon-initial)}:host([data-checked]) #button{--sinch-global-color-text:var(--sinch-comp-tab-color-checked-text-initial);--sinch-global-color-icon:var(--sinch-comp-tab-color-checked-icon-initial)}:host([data-checked]) #button::before{content:"";position:absolute;left:0;right:0;bottom:-1px;pointer-events:none;border-top:2px solid var(--sinch-comp-tab-color-checked-border-initial)}:host(:hover:not([disabled])) #button{background-color:var(--sinch-comp-tab-color-default-background-hover)}:host(:focus-visible) #button::after{content:"";position:absolute;inset:0;bottom:-3px;border:2px solid var(--sinch-comp-tab-color-default-outline-focus);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);pointer-events:none}#text{flex-shrink:1;flex-basis:auto;min-width:0;--sinch-comp-text-font:var(--sinch-comp-tab-font-label)}::slotted(*){display:block;pointer-events:none}</style><div id="button"><slot name="icon"></slot><sinch-text id="text" type="m" ellipsis></sinch-text></div>';
14699
14824
  const template$6 = document.createElement("template");
14700
14825
  template$6.innerHTML = templateHTML$6;
14701
14826
  class TabsOption extends NectaryElement {
14702
- #$button;
14703
14827
  #$text;
14704
14828
  constructor() {
14705
14829
  super();
14706
- const shadowRoot = this.attachShadow({ delegatesFocus: true });
14830
+ const shadowRoot = this.attachShadow();
14707
14831
  shadowRoot.appendChild(template$6.content.cloneNode(true));
14708
- this.#$button = shadowRoot.querySelector("#button");
14709
14832
  this.#$text = shadowRoot.querySelector("#text");
14710
14833
  }
14711
14834
  connectedCallback() {
14712
14835
  this.setAttribute("role", "tab");
14713
14836
  this.addEventListener("click", this.#onClick);
14837
+ this.addEventListener("keydown", this.#onKeyDown);
14838
+ this.#updateTabIndex();
14714
14839
  }
14715
14840
  disconnectedCallback() {
14716
14841
  this.removeEventListener("click", this.#onClick);
14842
+ this.removeEventListener("keydown", this.#onKeyDown);
14717
14843
  }
14718
14844
  static get observedAttributes() {
14719
14845
  return ["data-checked", "disabled", "text"];
@@ -14733,7 +14859,8 @@ class TabsOption extends NectaryElement {
14733
14859
  }
14734
14860
  case "disabled": {
14735
14861
  const isDisabled = isAttrTrue(newVal);
14736
- this.#$button.disabled = isDisabled;
14862
+ this.#updateTabIndex();
14863
+ updateExplicitBooleanAttribute(this, "aria-disabled", isDisabled);
14737
14864
  updateBooleanAttribute(this, name, isDisabled);
14738
14865
  break;
14739
14866
  }
@@ -14761,17 +14888,32 @@ class TabsOption extends NectaryElement {
14761
14888
  return true;
14762
14889
  }
14763
14890
  focus() {
14764
- this.#$button.focus();
14891
+ HTMLElement.prototype.focus.call(this);
14765
14892
  }
14766
14893
  blur() {
14767
- this.#$button.blur();
14894
+ HTMLElement.prototype.blur.call(this);
14895
+ }
14896
+ #updateTabIndex() {
14897
+ if (this.disabled) {
14898
+ this.tabIndex = -1;
14899
+ }
14768
14900
  }
14769
14901
  #onClick = (e) => {
14902
+ if (this.disabled) {
14903
+ return;
14904
+ }
14770
14905
  e.stopPropagation();
14771
14906
  this.dispatchEvent(
14772
14907
  new CustomEvent("option-change", { bubbles: true, detail: this.value })
14773
14908
  );
14774
14909
  };
14910
+ #onKeyDown = (e) => {
14911
+ if (this.disabled || !isActivationKey(e.key)) {
14912
+ return;
14913
+ }
14914
+ e.preventDefault();
14915
+ this.#onClick(e);
14916
+ };
14775
14917
  }
14776
14918
  defineCustomElement("sinch-tabs-option", TabsOption);
14777
14919
  const templateHTML$5 = '<style>:host{display:inline-block;vertical-align:middle;outline:0}:host([ellipsis]){display:inline}#wrapper{display:flex;flex-direction:row;align-items:center;gap:4px;width:100%;height:var(--sinch-comp-tag-size-container-m);padding:0 9px;border:1px solid var(--sinch-comp-tag-border);border-radius:var(--sinch-comp-tag-shape-radius);background-color:var(--sinch-comp-tag-color-default-background);box-sizing:border-box;user-select:none;--sinch-global-color-text:var(--sinch-comp-tag-color-default-foreground);--sinch-global-color-icon:var(--sinch-comp-tag-color-default-foreground);--sinch-global-size-icon:var(--sinch-comp-tag-size-icon-m)}:host([small]) #wrapper{height:var(--sinch-comp-tag-size-container-s);padding:0 8px;--sinch-global-size-icon:var(--sinch-comp-tag-size-icon-s)}#text{flex:1;--sinch-comp-text-font:var(--sinch-comp-tag-font-size-m-label)}:host([small]) #text{--sinch-comp-text-font:var(--sinch-comp-tag-font-size-s-label)}::slotted(*){margin-left:-4px}</style><sinch-tooltip id="tooltip" type="fast"><div id="wrapper"><slot name="icon"></slot><sinch-text id="text" type="s" ellipsis></sinch-text></div></sinch-tooltip>';
package/field/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import '../tooltip';
1
2
  import { NectaryElement } from '../utils';
2
3
  export * from './types';
3
4
  export declare class Field extends NectaryElement {
@@ -13,6 +14,8 @@ export declare class Field extends NectaryElement {
13
14
  get label(): string | null;
14
15
  set optionalText(value: string | null);
15
16
  get optionalText(): string | null;
17
+ set optionalTooltip(value: string | null);
18
+ get optionalTooltip(): string | null;
16
19
  set additionalText(value: string | null);
17
20
  get additionalText(): string | null;
18
21
  set invalidText(value: string | null);
package/field/index.js CHANGED
@@ -1,13 +1,15 @@
1
+ import "../tooltip/index.js";
1
2
  import { getAttribute, setClass, isAttrEqual, updateBooleanAttribute, isAttrTrue, updateAttribute, getBooleanAttribute } from "../utils/dom.js";
2
3
  import { defineCustomElement, NectaryElement } from "../utils/element.js";
3
4
  import { getFirstSlotElement } from "../utils/slot.js";
4
- const templateHTML = '<style>:host{display:block}#wrapper{display:flex;flex-direction:column;width:100%}#bottom,#top{display:flex;align-items:baseline}#top{height:24px;margin-bottom:2px}#top.empty{display:none}#additional,#invalid,#label,#optional{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#label{font:var(--sinch-comp-field-font-label);color:var(--sinch-comp-field-color-default-label-initial)}#optional{flex:1;font:var(--sinch-comp-field-font-optional);color:var(--sinch-comp-field-color-default-optional-initial);text-align:right}#additional{flex:1;text-align:right;font:var(--sinch-comp-field-font-additional);color:var(--sinch-comp-field-color-default-additional-initial);line-height:20px;margin-top:2px}#additional:empty{display:none}#invalid{font:var(--sinch-comp-field-font-invalid);color:var(--sinch-comp-field-color-invalid-text-initial);line-height:20px;margin-top:2px}#invalid:empty{display:none}#tooltip{align-self:center;margin:0 8px;display:flex}#tooltip.empty{display:none}:host([disabled]) #label{color:var(--sinch-comp-field-color-disabled-label-initial)}:host([disabled]) #additional{color:var(--sinch-comp-field-color-disabled-additional-initial)}:host([disabled]) #optional{color:var(--sinch-comp-field-color-disabled-optional-initial)}</style><div id="wrapper"><div id="top"><label id="label" for="input"></label><div id="tooltip"><slot name="tooltip"></slot></div><span id="optional"></span></div><slot name="input"></slot><div id="bottom"><div id="invalid"></div><div id="additional"></div></div></div>';
5
+ const templateHTML = '<style>:host{display:block}#wrapper{display:flex;flex-direction:column;width:100%}#bottom,#top{display:flex;align-items:baseline}#top{height:24px;margin-bottom:2px}#top.empty{display:none}#additional,#invalid,#label,#optional{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#label{font:var(--sinch-comp-field-font-label);color:var(--sinch-comp-field-color-default-label-initial)}#optional{flex:1;font:var(--sinch-comp-field-font-optional);color:var(--sinch-comp-field-color-default-optional-initial);text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#optional:empty{display:none}#additional{flex:1;text-align:right;font:var(--sinch-comp-field-font-additional);color:var(--sinch-comp-field-color-default-additional-initial);line-height:20px;margin-top:2px}#additional:empty{display:none}#invalid{font:var(--sinch-comp-field-font-invalid);color:var(--sinch-comp-field-color-invalid-text-initial);line-height:20px;margin-top:2px}#invalid:empty{display:none}#tooltip{align-self:center;margin:0 8px;display:flex}#tooltip.empty{display:none}:host([disabled]) #label{color:var(--sinch-comp-field-color-disabled-label-initial)}:host([disabled]) #additional{color:var(--sinch-comp-field-color-disabled-additional-initial)}:host([disabled]) #optional{color:var(--sinch-comp-field-color-disabled-optional-initial)}</style><div id="wrapper"><div id="top"><label id="label" for="input"></label><div id="tooltip"><slot name="tooltip"></slot></div><sinch-tooltip id="optional-tooltip" allow-scroll><span id="optional"></span></sinch-tooltip></div><slot name="input"></slot><div id="bottom"><div id="invalid"></div><div id="additional"></div></div></div>';
5
6
  const template = document.createElement("template");
6
7
  template.innerHTML = templateHTML;
7
8
  class Field extends NectaryElement {
8
9
  topSection;
9
10
  #$label;
10
11
  #$optionalText;
12
+ #$optionalTooltip;
11
13
  #$additionalText;
12
14
  #$invalidText;
13
15
  #$inputSlot;
@@ -21,6 +23,7 @@ class Field extends NectaryElement {
21
23
  this.topSection = shadowRoot.querySelector("#top");
22
24
  this.#$label = shadowRoot.querySelector("#label");
23
25
  this.#$optionalText = shadowRoot.querySelector("#optional");
26
+ this.#$optionalTooltip = shadowRoot.querySelector("#optional-tooltip");
24
27
  this.#$additionalText = shadowRoot.querySelector("#additional");
25
28
  this.#$invalidText = shadowRoot.querySelector("#invalid");
26
29
  this.#$inputSlot = shadowRoot.querySelector('slot[name="input"]');
@@ -44,6 +47,7 @@ class Field extends NectaryElement {
44
47
  return [
45
48
  "label",
46
49
  "optionaltext",
50
+ "optionaltooltip",
47
51
  "additionaltext",
48
52
  "invalidtext",
49
53
  "disabled"
@@ -64,6 +68,10 @@ class Field extends NectaryElement {
64
68
  this.#$optionalText.textContent = newVal;
65
69
  break;
66
70
  }
71
+ case "optionaltooltip": {
72
+ updateAttribute(this.#$optionalTooltip, "text", newVal);
73
+ break;
74
+ }
67
75
  case "additionaltext": {
68
76
  this.#$additionalText.textContent = newVal;
69
77
  break;
@@ -94,6 +102,12 @@ class Field extends NectaryElement {
94
102
  get optionalText() {
95
103
  return getAttribute(this, "optionaltext");
96
104
  }
105
+ set optionalTooltip(value) {
106
+ updateAttribute(this, "optionaltooltip", value);
107
+ }
108
+ get optionalTooltip() {
109
+ return getAttribute(this, "optionaltooltip");
110
+ }
97
111
  set additionalText(value) {
98
112
  updateAttribute(this, "additionaltext", value);
99
113
  }
package/field/types.d.ts CHANGED
@@ -4,6 +4,8 @@ export type TSinchFieldProps = {
4
4
  label?: string;
5
5
  /** Optional text */
6
6
  optionalText?: string;
7
+ /** Tooltip text for optional text */
8
+ optionalTooltip?: string;
7
9
  /** Additional text */
8
10
  additionalText?: string;
9
11
  /** Invalid text, controls the overall invalid state of the text field */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectary/components",
3
- "version": "5.37.7",
3
+ "version": "5.38.0",
4
4
  "files": [
5
5
  "**/*/*.css",
6
6
  "**/*/*.json",
package/tabs/index.js CHANGED
@@ -2,12 +2,18 @@ import { updateAttribute, getAttribute, getBooleanAttribute, updateBooleanAttrib
2
2
  import { defineCustomElement, NectaryElement } from "../utils/element.js";
3
3
  import { getRect } from "../utils/rect.js";
4
4
  import { getReactEventHandler } from "../utils/get-react-event-handler.js";
5
+ import { getTargetByAttribute } from "../utils/event-target.js";
6
+ import { createKeyboardNavigation } from "../utils/control-keyboard-navigation.js";
5
7
  const templateHTML = '<style>:host{display:block}#wrapper{display:flex;width:100%;height:40px;border-bottom:1px solid var(--sinch-comp-tab-color-default-border-initial);box-sizing:border-box}</style><div id="wrapper"><slot></slot></div>';
6
8
  const template = document.createElement("template");
7
9
  template.innerHTML = templateHTML;
8
10
  class Tabs extends NectaryElement {
9
11
  #$slot;
10
12
  #controller = null;
13
+ #enabledOptions = [];
14
+ #keyboardNav = createKeyboardNavigation();
15
+ #observer = null;
16
+ #rovingValue = null;
11
17
  constructor() {
12
18
  super();
13
19
  const shadowRoot = this.attachShadow();
@@ -17,14 +23,28 @@ class Tabs extends NectaryElement {
17
23
  connectedCallback() {
18
24
  this.#controller = new AbortController();
19
25
  const { signal } = this.#controller;
26
+ const options = { signal };
20
27
  this.setAttribute("role", "tablist");
21
- this.#$slot.addEventListener("option-change", this.#onOptionChange, { signal });
22
- this.#$slot.addEventListener("slotchange", this.#onSlotChange, { signal });
23
- this.addEventListener("-change", this.#onChangeReactHandler, { signal });
28
+ this.setAttribute("aria-orientation", "horizontal");
29
+ this.#$slot.addEventListener("option-change", this.#onOptionChange, options);
30
+ this.#$slot.addEventListener("slotchange", this.#onSlotChange, options);
31
+ this.#$slot.addEventListener("focusin", this.#onOptionFocusIn, options);
32
+ this.#$slot.addEventListener("keydown", this.#onOptionKeydown, options);
33
+ this.addEventListener("-change", this.#onChangeReactHandler, options);
34
+ this.#observer = new MutationObserver(this.#onOptionMutation);
35
+ this.#observer.observe(this, {
36
+ attributes: true,
37
+ attributeFilter: ["disabled", "value"],
38
+ childList: true,
39
+ subtree: true
40
+ });
41
+ this.#syncOptions();
24
42
  }
25
43
  disconnectedCallback() {
26
44
  this.#controller.abort();
27
45
  this.#controller = null;
46
+ this.#observer?.disconnect();
47
+ this.#observer = null;
28
48
  }
29
49
  static get observedAttributes() {
30
50
  return ["value"];
@@ -50,19 +70,91 @@ class Tabs extends NectaryElement {
50
70
  }
51
71
  return null;
52
72
  }
73
+ #getOptions() {
74
+ return this.#$slot.assignedElements();
75
+ }
76
+ #updateEnabledOptions(options = this.#getOptions()) {
77
+ this.#enabledOptions = options.filter((option) => !getBooleanAttribute(option, "disabled"));
78
+ }
79
+ #getFocusableOption() {
80
+ if (this.#enabledOptions.length === 0) {
81
+ return null;
82
+ }
83
+ if (this.#rovingValue !== null) {
84
+ const rovingOption = this.#enabledOptions.find(
85
+ (option) => getAttribute(option, "value", "") === this.#rovingValue
86
+ );
87
+ if (rovingOption != null) {
88
+ return rovingOption;
89
+ }
90
+ }
91
+ const selectedOption = this.#enabledOptions.find(
92
+ (option) => getAttribute(option, "value", "") === this.value
93
+ );
94
+ return selectedOption ?? this.#enabledOptions[0];
95
+ }
96
+ #syncOptions() {
97
+ const options = this.#getOptions();
98
+ this.#updateEnabledOptions(options);
99
+ const focusableOption = this.#getFocusableOption();
100
+ this.#rovingValue = focusableOption == null ? null : getAttribute(focusableOption, "value", "");
101
+ for (const $option of options) {
102
+ const isDisabled = getBooleanAttribute($option, "disabled");
103
+ const isChecked = !isDisabled && this.value === getAttribute($option, "value", "");
104
+ updateBooleanAttribute($option, "data-checked", isChecked);
105
+ $option.tabIndex = !isDisabled && $option === focusableOption ? 0 : -1;
106
+ }
107
+ }
53
108
  #onSlotChange = () => {
54
- this.#onValueChange(this.value);
109
+ this.#syncOptions();
55
110
  };
56
111
  #onOptionChange = (e) => {
57
112
  e.stopPropagation();
58
- this.#dispatchChangeEvent(e.detail);
113
+ const value = e.detail;
114
+ this.#rovingValue = value;
115
+ this.#dispatchChangeEvent(value);
59
116
  };
60
117
  #onValueChange(value) {
61
- for (const $option of this.#$slot.assignedElements()) {
62
- const isChecked = !getBooleanAttribute($option, "disabled") && value === getAttribute($option, "value", "");
63
- updateBooleanAttribute($option, "data-checked", isChecked);
64
- }
118
+ this.#rovingValue = value;
119
+ this.#syncOptions();
65
120
  }
121
+ #onOptionFocusIn = (e) => {
122
+ const target = getTargetByAttribute(e, "value");
123
+ if (target === null || getBooleanAttribute(target, "disabled")) {
124
+ return;
125
+ }
126
+ this.#rovingValue = getAttribute(target, "value", "");
127
+ this.#syncOptions();
128
+ };
129
+ #onOptionKeydown = (e) => {
130
+ switch (e.code) {
131
+ case "ArrowLeft": {
132
+ e.preventDefault();
133
+ this.#keyboardNav.navigateToNextOption(this.#enabledOptions, false);
134
+ break;
135
+ }
136
+ case "ArrowRight": {
137
+ e.preventDefault();
138
+ this.#keyboardNav.navigateToNextOption(this.#enabledOptions, true);
139
+ break;
140
+ }
141
+ case "Home": {
142
+ e.preventDefault();
143
+ this.#keyboardNav.navigateToOption(this.#enabledOptions, 0);
144
+ break;
145
+ }
146
+ case "End": {
147
+ e.preventDefault();
148
+ this.#keyboardNav.navigateToOption(this.#enabledOptions, this.#enabledOptions.length - 1);
149
+ break;
150
+ }
151
+ }
152
+ };
153
+ #onOptionMutation = (mutations) => {
154
+ if (mutations.some((mutation) => mutation.target !== this)) {
155
+ this.#syncOptions();
156
+ }
157
+ };
66
158
  #dispatchChangeEvent(value) {
67
159
  this.dispatchEvent(
68
160
  new CustomEvent("-change", { detail: value })
@@ -1,24 +1,26 @@
1
- import { updateAttribute, getAttribute, updateBooleanAttribute, getBooleanAttribute, isAttrEqual, isAttrTrue, updateExplicitBooleanAttribute } from "../utils/dom.js";
1
+ import { updateAttribute, getAttribute, updateBooleanAttribute, getBooleanAttribute, isAttrEqual, updateExplicitBooleanAttribute, isAttrTrue } from "../utils/dom.js";
2
2
  import { defineCustomElement, NectaryElement } from "../utils/element.js";
3
- const templateHTML = '<style>:host{display:block;outline:0}#button{all:initial;position:relative;display:flex;flex-direction:column;padding:12px 16px 0;box-sizing:border-box;cursor:pointer;background-color:var(--sinch-comp-tab-color-default-background-initial);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);height:39px;--sinch-global-color-icon:var(--sinch-comp-tab-color-default-icon-initial);--sinch-global-size-icon:var(--sinch-comp-tab-size-icon)}#button:hover{background-color:var(--sinch-comp-tab-color-default-background-hover)}#button:focus-visible::after{content:"";position:absolute;inset:0;bottom:-3px;border:2px solid var(--sinch-comp-tab-color-default-outline-focus);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);pointer-events:none}#button:disabled{cursor:unset;pointer-events:none;--sinch-global-color-icon:var(--sinch-comp-tab-color-disabled-icon-initial)}:host([data-checked]) #button{--sinch-global-color-icon:var(--sinch-comp-tab-color-checked-icon-initial)}:host([data-checked]) #button::before{content:"";position:absolute;left:0;right:0;bottom:-1px;pointer-events:none;border-top:2px solid var(--sinch-comp-tab-color-checked-border-initial)}::slotted(*){display:block}</style><sinch-tooltip id="tooltip"><button id="button" tabindex="0"><slot name="icon"></slot></button></sinch-tooltip>';
3
+ import { isActivationKey } from "../utils/control-keyboard-navigation.js";
4
+ const templateHTML = '<style>:host{display:block;outline:0}#button{all:initial;position:relative;display:flex;flex-direction:column;padding:12px 16px 0;box-sizing:border-box;cursor:pointer;background-color:var(--sinch-comp-tab-color-default-background-initial);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);height:39px;--sinch-global-color-icon:var(--sinch-comp-tab-color-default-icon-initial);--sinch-global-size-icon:var(--sinch-comp-tab-size-icon)}:host([disabled]) #button{cursor:unset;pointer-events:none;--sinch-global-color-icon:var(--sinch-comp-tab-color-disabled-icon-initial)}:host([data-checked]) #button{--sinch-global-color-icon:var(--sinch-comp-tab-color-checked-icon-initial)}:host([data-checked]) #button::before{content:"";position:absolute;left:0;right:0;bottom:-1px;pointer-events:none;border-top:2px solid var(--sinch-comp-tab-color-checked-border-initial)}:host(:hover:not([disabled])) #button{background-color:var(--sinch-comp-tab-color-default-background-hover)}:host(:focus-visible) #button::after{content:"";position:absolute;inset:0;bottom:-3px;border:2px solid var(--sinch-comp-tab-color-default-outline-focus);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);pointer-events:none}::slotted(*){display:block;pointer-events:none}</style><sinch-tooltip id="tooltip"><div id="button"><slot name="icon"></slot></div></sinch-tooltip>';
4
5
  const template = document.createElement("template");
5
6
  template.innerHTML = templateHTML;
6
7
  class TabsIconOption extends NectaryElement {
7
- #$button;
8
8
  #$tooltip;
9
9
  constructor() {
10
10
  super();
11
- const shadowRoot = this.attachShadow({ delegatesFocus: true });
11
+ const shadowRoot = this.attachShadow();
12
12
  shadowRoot.appendChild(template.content.cloneNode(true));
13
- this.#$button = shadowRoot.querySelector("#button");
14
13
  this.#$tooltip = shadowRoot.querySelector("#tooltip");
15
14
  }
16
15
  connectedCallback() {
17
16
  this.setAttribute("role", "tab");
18
- this.#$button.addEventListener("click", this.#onClick);
17
+ this.addEventListener("click", this.#onClick);
18
+ this.addEventListener("keydown", this.#onKeyDown);
19
+ this.#updateTabIndex();
19
20
  }
20
21
  disconnectedCallback() {
21
- this.#$button.removeEventListener("click", this.#onClick);
22
+ this.removeEventListener("click", this.#onClick);
23
+ this.removeEventListener("keydown", this.#onKeyDown);
22
24
  }
23
25
  static get observedAttributes() {
24
26
  return ["data-checked", "disabled", "aria-label"];
@@ -46,7 +48,8 @@ class TabsIconOption extends NectaryElement {
46
48
  }
47
49
  case "disabled": {
48
50
  const isDisabled = isAttrTrue(newVal);
49
- this.#$button.disabled = isDisabled;
51
+ this.#updateTabIndex();
52
+ updateExplicitBooleanAttribute(this, "aria-disabled", isDisabled);
50
53
  updateBooleanAttribute(this, name, isDisabled);
51
54
  break;
52
55
  }
@@ -60,17 +63,32 @@ class TabsIconOption extends NectaryElement {
60
63
  return true;
61
64
  }
62
65
  focus() {
63
- this.#$button.focus();
66
+ HTMLElement.prototype.focus.call(this);
64
67
  }
65
68
  blur() {
66
- this.#$button.blur();
69
+ HTMLElement.prototype.blur.call(this);
70
+ }
71
+ #updateTabIndex() {
72
+ if (this.disabled) {
73
+ this.tabIndex = -1;
74
+ }
67
75
  }
68
76
  #onClick = (e) => {
77
+ if (this.disabled) {
78
+ return;
79
+ }
69
80
  e.stopPropagation();
70
81
  this.dispatchEvent(
71
82
  new CustomEvent("option-change", { bubbles: true, detail: this.value })
72
83
  );
73
84
  };
85
+ #onKeyDown = (e) => {
86
+ if (this.disabled || !isActivationKey(e.key)) {
87
+ return;
88
+ }
89
+ e.preventDefault();
90
+ this.#onClick(e);
91
+ };
74
92
  }
75
93
  defineCustomElement("sinch-tabs-icon-option", TabsIconOption);
76
94
  export {
@@ -1,25 +1,27 @@
1
1
  import "../text/index.js";
2
- import { isAttrEqual, isAttrTrue, updateBooleanAttribute, updateExplicitBooleanAttribute, updateAttribute, getAttribute, getBooleanAttribute } from "../utils/dom.js";
2
+ import { isAttrEqual, updateExplicitBooleanAttribute, updateBooleanAttribute, isAttrTrue, updateAttribute, getAttribute, getBooleanAttribute } from "../utils/dom.js";
3
3
  import { defineCustomElement, NectaryElement } from "../utils/element.js";
4
- const templateHTML = '<style>:host{display:block}#button{all:initial;position:relative;display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:12px 16px;box-sizing:border-box;cursor:pointer;background-color:var(--sinch-comp-tab-color-default-background-initial);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);height:39px;--sinch-global-color-text:var(--sinch-comp-tab-color-default-text-initial);--sinch-global-color-icon:var(--sinch-comp-tab-color-default-icon-initial);--sinch-global-size-icon:var(--sinch-comp-tab-size-icon)}#button:hover{background-color:var(--sinch-comp-tab-color-default-background-hover)}#button:focus-visible::after{content:"";position:absolute;inset:0;bottom:-3px;border:2px solid var(--sinch-comp-tab-color-default-outline-focus);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);pointer-events:none}#button:disabled{cursor:unset;pointer-events:none;--sinch-global-color-text:var(--sinch-comp-tab-color-disabled-text-initial);--sinch-global-color-icon:var(--sinch-comp-tab-color-disabled-icon-initial)}:host([data-checked]) #button{--sinch-global-color-text:var(--sinch-comp-tab-color-checked-text-initial);--sinch-global-color-icon:var(--sinch-comp-tab-color-checked-icon-initial)}:host([data-checked]) #button::before{content:"";position:absolute;left:0;right:0;bottom:-1px;pointer-events:none;border-top:2px solid var(--sinch-comp-tab-color-checked-border-initial)}#text{flex-shrink:1;flex-basis:auto;min-width:0;--sinch-comp-text-font:var(--sinch-comp-tab-font-label)}::slotted(*){display:block}</style><button id="button" tabindex="0"><slot name="icon"></slot><sinch-text id="text" type="m" ellipsis></sinch-text></button>';
4
+ import { isActivationKey } from "../utils/control-keyboard-navigation.js";
5
+ const templateHTML = '<style>:host{display:block;outline:0}#button{all:initial;position:relative;display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:12px 16px;box-sizing:border-box;cursor:pointer;background-color:var(--sinch-comp-tab-color-default-background-initial);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);height:39px;--sinch-global-color-text:var(--sinch-comp-tab-color-default-text-initial);--sinch-global-color-icon:var(--sinch-comp-tab-color-default-icon-initial);--sinch-global-size-icon:var(--sinch-comp-tab-size-icon)}:host([disabled]) #button{cursor:unset;pointer-events:none;--sinch-global-color-text:var(--sinch-comp-tab-color-disabled-text-initial);--sinch-global-color-icon:var(--sinch-comp-tab-color-disabled-icon-initial)}:host([data-checked]) #button{--sinch-global-color-text:var(--sinch-comp-tab-color-checked-text-initial);--sinch-global-color-icon:var(--sinch-comp-tab-color-checked-icon-initial)}:host([data-checked]) #button::before{content:"";position:absolute;left:0;right:0;bottom:-1px;pointer-events:none;border-top:2px solid var(--sinch-comp-tab-color-checked-border-initial)}:host(:hover:not([disabled])) #button{background-color:var(--sinch-comp-tab-color-default-background-hover)}:host(:focus-visible) #button::after{content:"";position:absolute;inset:0;bottom:-3px;border:2px solid var(--sinch-comp-tab-color-default-outline-focus);border-top-left-radius:var(--sinch-comp-tab-shape-radius);border-top-right-radius:var(--sinch-comp-tab-shape-radius);pointer-events:none}#text{flex-shrink:1;flex-basis:auto;min-width:0;--sinch-comp-text-font:var(--sinch-comp-tab-font-label)}::slotted(*){display:block;pointer-events:none}</style><div id="button"><slot name="icon"></slot><sinch-text id="text" type="m" ellipsis></sinch-text></div>';
5
6
  const template = document.createElement("template");
6
7
  template.innerHTML = templateHTML;
7
8
  class TabsOption extends NectaryElement {
8
- #$button;
9
9
  #$text;
10
10
  constructor() {
11
11
  super();
12
- const shadowRoot = this.attachShadow({ delegatesFocus: true });
12
+ const shadowRoot = this.attachShadow();
13
13
  shadowRoot.appendChild(template.content.cloneNode(true));
14
- this.#$button = shadowRoot.querySelector("#button");
15
14
  this.#$text = shadowRoot.querySelector("#text");
16
15
  }
17
16
  connectedCallback() {
18
17
  this.setAttribute("role", "tab");
19
18
  this.addEventListener("click", this.#onClick);
19
+ this.addEventListener("keydown", this.#onKeyDown);
20
+ this.#updateTabIndex();
20
21
  }
21
22
  disconnectedCallback() {
22
23
  this.removeEventListener("click", this.#onClick);
24
+ this.removeEventListener("keydown", this.#onKeyDown);
23
25
  }
24
26
  static get observedAttributes() {
25
27
  return ["data-checked", "disabled", "text"];
@@ -39,7 +41,8 @@ class TabsOption extends NectaryElement {
39
41
  }
40
42
  case "disabled": {
41
43
  const isDisabled = isAttrTrue(newVal);
42
- this.#$button.disabled = isDisabled;
44
+ this.#updateTabIndex();
45
+ updateExplicitBooleanAttribute(this, "aria-disabled", isDisabled);
43
46
  updateBooleanAttribute(this, name, isDisabled);
44
47
  break;
45
48
  }
@@ -67,17 +70,32 @@ class TabsOption extends NectaryElement {
67
70
  return true;
68
71
  }
69
72
  focus() {
70
- this.#$button.focus();
73
+ HTMLElement.prototype.focus.call(this);
71
74
  }
72
75
  blur() {
73
- this.#$button.blur();
76
+ HTMLElement.prototype.blur.call(this);
77
+ }
78
+ #updateTabIndex() {
79
+ if (this.disabled) {
80
+ this.tabIndex = -1;
81
+ }
74
82
  }
75
83
  #onClick = (e) => {
84
+ if (this.disabled) {
85
+ return;
86
+ }
76
87
  e.stopPropagation();
77
88
  this.dispatchEvent(
78
89
  new CustomEvent("option-change", { bubbles: true, detail: this.value })
79
90
  );
80
91
  };
92
+ #onKeyDown = (e) => {
93
+ if (this.disabled || !isActivationKey(e.key)) {
94
+ return;
95
+ }
96
+ e.preventDefault();
97
+ this.#onClick(e);
98
+ };
81
99
  }
82
100
  defineCustomElement("sinch-tabs-option", TabsOption);
83
101
  export {
@@ -1,3 +1,4 @@
1
+ export declare const isActivationKey: (key: string) => key is " " | "Enter";
1
2
  export declare function createKeyboardNavigation(): {
2
3
  navigateToNextOption(enabledOptions: Element[], forward: boolean): void;
3
4
  navigateToOption(enabledOptions: Element[], index: number): void;
@@ -1,5 +1,6 @@
1
1
  import { getDeepActiveElement } from "./dom.js";
2
2
  import { getTargetByAttribute } from "./event-target.js";
3
+ const isActivationKey = (key) => key === "Enter" || key === " ";
3
4
  function createKeyboardNavigation() {
4
5
  return {
5
6
  navigateToNextOption(enabledOptions, forward) {
@@ -39,10 +40,14 @@ function createKeyboardNavigation() {
39
40
  }
40
41
  break;
41
42
  }
42
- case "ArrowLeft":
43
+ case "ArrowLeft": {
44
+ e.preventDefault();
45
+ this.navigateToNextOption(enabledOptions, false);
46
+ break;
47
+ }
43
48
  case "ArrowRight": {
44
49
  e.preventDefault();
45
- this.navigateToNextOption(enabledOptions, e.code === "ArrowRight");
50
+ this.navigateToNextOption(enabledOptions, true);
46
51
  break;
47
52
  }
48
53
  case "Home": {
@@ -60,5 +65,6 @@ function createKeyboardNavigation() {
60
65
  };
61
66
  }
62
67
  export {
63
- createKeyboardNavigation
68
+ createKeyboardNavigation,
69
+ isActivationKey
64
70
  };
package/utils/index.d.ts CHANGED
@@ -9,4 +9,5 @@ export * from './debounce';
9
9
  export * from './get-react-event-handler';
10
10
  export * from './markdown';
11
11
  export * from './event-target';
12
+ export { isActivationKey } from './control-keyboard-navigation';
12
13
  export * from './uid';
package/utils/index.js CHANGED
@@ -9,6 +9,7 @@ import { debounceAnimationFrame, debounceTimeout } from "./debounce.js";
9
9
  import { getReactEventHandler } from "./get-react-event-handler.js";
10
10
  import { isEmojiString, parseMarkdown } from "./markdown.js";
11
11
  import { getTargetAttribute, getTargetByAttribute, getTargetIndexInParent, isTargetEqual } from "./event-target.js";
12
+ import { isActivationKey } from "./control-keyboard-navigation.js";
12
13
  import { getUid } from "./uid.js";
13
14
  export {
14
15
  CSV_DELIMITER,
@@ -42,6 +43,7 @@ export {
42
43
  getTransformedAncestor,
43
44
  getUid,
44
45
  hasClass,
46
+ isActivationKey,
45
47
  isAttrEqual,
46
48
  isAttrTrue,
47
49
  isElementFocused,