@nectary/components 5.1.1 → 5.1.3

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
@@ -5731,6 +5731,8 @@ const orientationValues = [
5731
5731
  "top-right",
5732
5732
  "bottom-left",
5733
5733
  "bottom-right",
5734
+ "left",
5735
+ "right",
5734
5736
  "bottom",
5735
5737
  "top"
5736
5738
  ];
@@ -5741,9 +5743,15 @@ const getPopOrientation = (orientation) => {
5741
5743
  if (orientation === "bottom") {
5742
5744
  return "bottom-stretch";
5743
5745
  }
5746
+ if (orientation === "left") {
5747
+ return "center-left";
5748
+ }
5749
+ if (orientation === "right") {
5750
+ return "center-right";
5751
+ }
5744
5752
  return orientation;
5745
5753
  };
5746
- const templateHTML$K = '<style>:host{display:contents}#content-wrapper{position:relative;padding-top:4px;width:fit-content;min-width:100%}:host([tip]) #content-wrapper{padding-top:12px;filter:drop-shadow(var(--sinch-comp-popover-shadow))}:host([orientation^=top]) #content-wrapper{padding-top:0;padding-bottom:4px}:host([orientation^=top][tip]) #content-wrapper{padding-top:0;padding-bottom:12px}#content{background-color:var(--sinch-comp-popover-color-default-background-initial);border:1px solid var(--sinch-comp-popover-color-default-border-initial);border-radius:var(--sinch-comp-popover-shape-radius);box-shadow:var(--sinch-comp-popover-shadow);overflow:hidden}:host([tip]) #content{box-shadow:none}#tip{position:absolute;left:50%;top:13px;transform:translateX(-50%) rotate(180deg);transform-origin:top center;fill:var(--sinch-comp-popover-color-default-background-initial);stroke:var(--sinch-comp-popover-color-default-border-initial);stroke-linecap:square;pointer-events:none;display:none}:host([orientation^=top]) #tip{transform:translateX(-50%) rotate(0);top:calc(100% - 13px)}:host([tip]) #tip:not(.hidden){display:block}</style><sinch-pop id="pop" inset="4"><slot name="target" slot="target"></slot><div id="content-wrapper" slot="content"><div id="content"><slot name="content"></slot></div><svg id="tip" width="16" height="9" aria-hidden="true"><path d="m0 0 8 8 8 -8"/></svg></div></sinch-pop>';
5754
+ const templateHTML$K = '<style>:host{display:contents}#content-wrapper{position:relative;padding-top:4px;width:fit-content;min-width:100%}:host([tip]) #content-wrapper{padding-top:12px;filter:drop-shadow(var(--sinch-comp-popover-shadow))}:host([orientation^=top]) #content-wrapper{padding-top:0;padding-bottom:4px}:host([orientation=left]) #content-wrapper{padding-top:0;padding-right:4px}:host([orientation=right]) #content-wrapper{padding-top:0;padding-left:4px}:host([orientation^=top][tip]) #content-wrapper{padding-top:0;padding-bottom:12px}:host([orientation=left][tip]) #content-wrapper{padding-top:0;padding-right:12px}:host([orientation=right][tip]) #content-wrapper{padding-top:0;padding-left:12px}#content{background-color:var(--sinch-comp-popover-color-default-background-initial);border:1px solid var(--sinch-comp-popover-color-default-border-initial);border-radius:var(--sinch-comp-popover-shape-radius);box-shadow:var(--sinch-comp-popover-shadow);overflow:hidden}:host([tip]) #content{box-shadow:none}#tip{position:absolute;left:50%;top:13px;transform:translateX(-50%) rotate(180deg);transform-origin:top center;fill:var(--sinch-comp-popover-color-default-background-initial);stroke:var(--sinch-comp-popover-color-default-border-initial);stroke-linecap:square;pointer-events:none;display:none}:host([orientation^=top]) #tip{transform:translateX(-50%) rotate(0);top:calc(100% - 13px)}:host([orientation=left]) #tip{transform:translateX(-50%) rotate(-90deg);top:calc(50%);left:calc(100% - 13px)}:host([orientation=right]) #tip{transform:translateX(-50%) rotate(90deg);top:calc(50%);left:13px}:host([tip]) #tip:not(.hidden){display:block}</style><sinch-pop id="pop" inset="4"><slot name="target" slot="target"></slot><div id="content-wrapper" slot="content"><div id="content"><slot name="content"></slot></div><svg id="tip" width="16" height="9" aria-hidden="true"><path d="m0 0 8 8 8 -8"/></svg></div></sinch-pop>';
5747
5755
  const TIP_SIZE = 16;
5748
5756
  const template$K = document.createElement("template");
5749
5757
  template$K.innerHTML = templateHTML$K;
@@ -5912,16 +5920,23 @@ class Popover extends NectaryElement {
5912
5920
  const orientation = this.orientation;
5913
5921
  const targetRect = this.#$pop.footprintRect;
5914
5922
  const contentRect = this.#$content.getBoundingClientRect();
5915
- const diffX = targetRect.x - contentRect.x;
5916
- let desiredX = diffX + targetRect.width / 2;
5917
- if (orientation === "bottom-left" || orientation === "top-left") {
5918
- desiredX = Math.max(desiredX, contentRect.width * 0.75);
5919
- }
5920
- if (orientation === "bottom-right" || orientation === "top-right") {
5921
- desiredX = Math.min(desiredX, contentRect.width * 0.25);
5923
+ if (orientation.startsWith("top") || orientation.startsWith("bottom")) {
5924
+ const diffX = targetRect.x - contentRect.x;
5925
+ let desiredX = diffX + targetRect.width / 2;
5926
+ if (orientation === "bottom-left" || orientation === "top-left") {
5927
+ desiredX = Math.max(desiredX, contentRect.width * 0.75);
5928
+ }
5929
+ if (orientation === "bottom-right" || orientation === "top-right") {
5930
+ desiredX = Math.min(desiredX, contentRect.width * 0.25);
5931
+ }
5932
+ const xPos = Math.max(TIP_SIZE, Math.min(desiredX, contentRect.width - TIP_SIZE));
5933
+ this.#$tip.style.left = `${xPos}px`;
5934
+ } else if (orientation.startsWith("left") || orientation.startsWith("right")) {
5935
+ const diffY = targetRect.y - contentRect.y;
5936
+ const desiredY = diffY + targetRect.height / 2;
5937
+ const yPos = Math.max(TIP_SIZE, Math.min(desiredY, contentRect.height - TIP_SIZE));
5938
+ this.#$tip.style.top = `${yPos}px`;
5922
5939
  }
5923
- const xPos = Math.max(TIP_SIZE, Math.min(desiredX, contentRect.width - TIP_SIZE));
5924
- this.#$tip.style.left = `${xPos}px`;
5925
5940
  setClass(this.#$tip, "hidden", rectOverlap(targetRect, contentRect));
5926
5941
  };
5927
5942
  // Prevent content from overflowing the viewport
@@ -10479,12 +10494,80 @@ class SegmentedControlOption extends NectaryElement {
10479
10494
  };
10480
10495
  }
10481
10496
  defineCustomElement("sinch-segmented-control-option", SegmentedControlOption);
10497
+ function getActualActiveElement() {
10498
+ let activeElement = document.activeElement;
10499
+ while (activeElement?.shadowRoot?.activeElement != null) {
10500
+ activeElement = activeElement.shadowRoot.activeElement;
10501
+ }
10502
+ return activeElement;
10503
+ }
10504
+ function createKeyboardNavigation() {
10505
+ return {
10506
+ navigateToNextOption(enabledOptions, forward) {
10507
+ const optionsLength = enabledOptions.length;
10508
+ if (optionsLength === 0) {
10509
+ return;
10510
+ }
10511
+ const currentIndex = enabledOptions.findIndex((option) => option === getActualActiveElement());
10512
+ let nextIndex;
10513
+ if (currentIndex !== -1) {
10514
+ if (forward) {
10515
+ nextIndex = (currentIndex + 1) % optionsLength;
10516
+ } else {
10517
+ nextIndex = currentIndex === 0 ? optionsLength - 1 : currentIndex - 1;
10518
+ }
10519
+ } else {
10520
+ nextIndex = forward ? 0 : optionsLength - 1;
10521
+ }
10522
+ this.navigateToOption(enabledOptions, nextIndex);
10523
+ },
10524
+ navigateToOption(enabledOptions, index) {
10525
+ const optionsLength = enabledOptions.length;
10526
+ if (enabledOptions.length === 0 || index < 0 || index >= optionsLength) {
10527
+ return;
10528
+ }
10529
+ const option = enabledOptions[index];
10530
+ option.focus();
10531
+ },
10532
+ handleKeyboardNavigation(e, enabledOptions) {
10533
+ switch (e.code) {
10534
+ case "Space":
10535
+ case "Enter": {
10536
+ e.preventDefault();
10537
+ const target = getTargetByAttribute(e, "value");
10538
+ if (target !== null) {
10539
+ target.click();
10540
+ }
10541
+ break;
10542
+ }
10543
+ case "ArrowLeft":
10544
+ case "ArrowRight": {
10545
+ e.preventDefault();
10546
+ this.navigateToNextOption(enabledOptions, e.code === "ArrowRight");
10547
+ break;
10548
+ }
10549
+ case "Home": {
10550
+ e.preventDefault();
10551
+ this.navigateToOption(enabledOptions, 0);
10552
+ break;
10553
+ }
10554
+ case "End": {
10555
+ e.preventDefault();
10556
+ this.navigateToOption(enabledOptions, enabledOptions.length - 1);
10557
+ break;
10558
+ }
10559
+ }
10560
+ }
10561
+ };
10562
+ }
10482
10563
  const templateHTML$k = '<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>';
10483
10564
  const template$k = document.createElement("template");
10484
10565
  template$k.innerHTML = templateHTML$k;
10485
10566
  class SegmentedControl extends NectaryElement {
10486
10567
  #$slot;
10487
10568
  #controller = null;
10569
+ #enabledOptions = [];
10570
+ #keyboardNav = createKeyboardNavigation();
10488
10571
  constructor() {
10489
10572
  super();
10490
10573
  const shadowRoot = this.attachShadow();
@@ -10496,10 +10579,12 @@ class SegmentedControl extends NectaryElement {
10496
10579
  const { signal } = this.#controller;
10497
10580
  const options = { signal };
10498
10581
  this.setAttribute("role", "tablist");
10582
+ this.setAttribute("aria-orientation", "horizontal");
10499
10583
  this.#$slot.addEventListener("slotchange", this.#onSlotChange, options);
10500
10584
  this.#$slot.addEventListener("click", this.#onOptionClick, options);
10501
10585
  this.#$slot.addEventListener("keydown", this.#onOptionKeydown, options);
10502
10586
  this.addEventListener("-change", this.#onChangeReactHandler);
10587
+ this.#updateEnabledOptions();
10503
10588
  }
10504
10589
  disconnectedCallback() {
10505
10590
  this.#controller.abort();
@@ -10522,8 +10607,14 @@ class SegmentedControl extends NectaryElement {
10522
10607
  get value() {
10523
10608
  return getAttribute(this, "value", "");
10524
10609
  }
10610
+ #updateEnabledOptions = () => {
10611
+ this.#enabledOptions = Array.from(this.#$slot.assignedElements()).filter(
10612
+ (option) => !getBooleanAttribute(option, "disabled")
10613
+ );
10614
+ };
10525
10615
  #onSlotChange = () => {
10526
10616
  this.#onValueChange(this.value);
10617
+ this.#updateEnabledOptions();
10527
10618
  };
10528
10619
  #onOptionClick = (e) => {
10529
10620
  const target = getTargetByAttribute(e, "value");
@@ -10536,16 +10627,7 @@ class SegmentedControl extends NectaryElement {
10536
10627
  );
10537
10628
  };
10538
10629
  #onOptionKeydown = (e) => {
10539
- switch (e.code) {
10540
- case "Space":
10541
- case "Enter": {
10542
- e.preventDefault();
10543
- const target = getTargetByAttribute(e, "value");
10544
- if (target !== null) {
10545
- target.click();
10546
- }
10547
- }
10548
- }
10630
+ this.#keyboardNav.handleKeyboardNavigation(e, this.#enabledOptions);
10549
10631
  };
10550
10632
  #onValueChange(value) {
10551
10633
  for (const $option of this.#$slot.assignedElements()) {
@@ -10641,6 +10723,8 @@ template$i.innerHTML = templateHTML$i;
10641
10723
  class SegmentedIconControl extends NectaryElement {
10642
10724
  #$slot;
10643
10725
  #controller = null;
10726
+ #keyboardNav = createKeyboardNavigation();
10727
+ #enabledOptions = [];
10644
10728
  constructor() {
10645
10729
  super();
10646
10730
  const shadowRoot = this.attachShadow();
@@ -10652,10 +10736,12 @@ class SegmentedIconControl extends NectaryElement {
10652
10736
  const { signal } = this.#controller;
10653
10737
  const options = { signal };
10654
10738
  this.setAttribute("role", "tablist");
10739
+ this.setAttribute("aria-orientation", "horizontal");
10655
10740
  this.#$slot.addEventListener("slotchange", this.#onSlotChange, options);
10656
10741
  this.#$slot.addEventListener("click", this.#onOptionClick, options);
10657
10742
  this.#$slot.addEventListener("keydown", this.#onOptionKeydown, options);
10658
10743
  this.addEventListener("-change", this.#onChangeReactHandler, options);
10744
+ this.#updateEnabledOptions();
10659
10745
  }
10660
10746
  disconnectedCallback() {
10661
10747
  this.#controller.abort();
@@ -10684,8 +10770,14 @@ class SegmentedIconControl extends NectaryElement {
10684
10770
  get multiple() {
10685
10771
  return getBooleanAttribute(this, "multiple");
10686
10772
  }
10773
+ #updateEnabledOptions = () => {
10774
+ this.#enabledOptions = Array.from(this.#$slot.assignedElements()).filter(
10775
+ (option) => !getBooleanAttribute(option, "disabled")
10776
+ );
10777
+ };
10687
10778
  #onSlotChange = () => {
10688
10779
  this.#onValueChange(this.value);
10780
+ this.#updateEnabledOptions();
10689
10781
  };
10690
10782
  #onOptionClick = (e) => {
10691
10783
  const target = getTargetByAttribute(e, "value");
@@ -10699,16 +10791,7 @@ class SegmentedIconControl extends NectaryElement {
10699
10791
  );
10700
10792
  };
10701
10793
  #onOptionKeydown = (e) => {
10702
- switch (e.code) {
10703
- case "Space":
10704
- case "Enter": {
10705
- e.preventDefault();
10706
- const target = getTargetByAttribute(e, "value");
10707
- if (target !== null) {
10708
- target.click();
10709
- }
10710
- }
10711
- }
10794
+ this.#keyboardNav.handleKeyboardNavigation(e, this.#enabledOptions);
10712
10795
  };
10713
10796
  #onValueChange(csv) {
10714
10797
  if (this.multiple) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectary/components",
3
- "version": "5.1.1",
3
+ "version": "5.1.3",
4
4
  "files": [
5
5
  "**/*/*.css",
6
6
  "**/*/*.json",
package/popover/index.js CHANGED
@@ -5,7 +5,7 @@ import { defineCustomElement, NectaryElement } from "../utils/element.js";
5
5
  import { rectOverlap } from "../utils/rect.js";
6
6
  import { getReactEventHandler } from "../utils/get-react-event-handler.js";
7
7
  import { getPopOrientation, orientationValues } from "./utils.js";
8
- const templateHTML = '<style>:host{display:contents}#content-wrapper{position:relative;padding-top:4px;width:fit-content;min-width:100%}:host([tip]) #content-wrapper{padding-top:12px;filter:drop-shadow(var(--sinch-comp-popover-shadow))}:host([orientation^=top]) #content-wrapper{padding-top:0;padding-bottom:4px}:host([orientation^=top][tip]) #content-wrapper{padding-top:0;padding-bottom:12px}#content{background-color:var(--sinch-comp-popover-color-default-background-initial);border:1px solid var(--sinch-comp-popover-color-default-border-initial);border-radius:var(--sinch-comp-popover-shape-radius);box-shadow:var(--sinch-comp-popover-shadow);overflow:hidden}:host([tip]) #content{box-shadow:none}#tip{position:absolute;left:50%;top:13px;transform:translateX(-50%) rotate(180deg);transform-origin:top center;fill:var(--sinch-comp-popover-color-default-background-initial);stroke:var(--sinch-comp-popover-color-default-border-initial);stroke-linecap:square;pointer-events:none;display:none}:host([orientation^=top]) #tip{transform:translateX(-50%) rotate(0);top:calc(100% - 13px)}:host([tip]) #tip:not(.hidden){display:block}</style><sinch-pop id="pop" inset="4"><slot name="target" slot="target"></slot><div id="content-wrapper" slot="content"><div id="content"><slot name="content"></slot></div><svg id="tip" width="16" height="9" aria-hidden="true"><path d="m0 0 8 8 8 -8"/></svg></div></sinch-pop>';
8
+ const templateHTML = '<style>:host{display:contents}#content-wrapper{position:relative;padding-top:4px;width:fit-content;min-width:100%}:host([tip]) #content-wrapper{padding-top:12px;filter:drop-shadow(var(--sinch-comp-popover-shadow))}:host([orientation^=top]) #content-wrapper{padding-top:0;padding-bottom:4px}:host([orientation=left]) #content-wrapper{padding-top:0;padding-right:4px}:host([orientation=right]) #content-wrapper{padding-top:0;padding-left:4px}:host([orientation^=top][tip]) #content-wrapper{padding-top:0;padding-bottom:12px}:host([orientation=left][tip]) #content-wrapper{padding-top:0;padding-right:12px}:host([orientation=right][tip]) #content-wrapper{padding-top:0;padding-left:12px}#content{background-color:var(--sinch-comp-popover-color-default-background-initial);border:1px solid var(--sinch-comp-popover-color-default-border-initial);border-radius:var(--sinch-comp-popover-shape-radius);box-shadow:var(--sinch-comp-popover-shadow);overflow:hidden}:host([tip]) #content{box-shadow:none}#tip{position:absolute;left:50%;top:13px;transform:translateX(-50%) rotate(180deg);transform-origin:top center;fill:var(--sinch-comp-popover-color-default-background-initial);stroke:var(--sinch-comp-popover-color-default-border-initial);stroke-linecap:square;pointer-events:none;display:none}:host([orientation^=top]) #tip{transform:translateX(-50%) rotate(0);top:calc(100% - 13px)}:host([orientation=left]) #tip{transform:translateX(-50%) rotate(-90deg);top:calc(50%);left:calc(100% - 13px)}:host([orientation=right]) #tip{transform:translateX(-50%) rotate(90deg);top:calc(50%);left:13px}:host([tip]) #tip:not(.hidden){display:block}</style><sinch-pop id="pop" inset="4"><slot name="target" slot="target"></slot><div id="content-wrapper" slot="content"><div id="content"><slot name="content"></slot></div><svg id="tip" width="16" height="9" aria-hidden="true"><path d="m0 0 8 8 8 -8"/></svg></div></sinch-pop>';
9
9
  const TIP_SIZE = 16;
10
10
  const template = document.createElement("template");
11
11
  template.innerHTML = templateHTML;
@@ -174,16 +174,23 @@ class Popover extends NectaryElement {
174
174
  const orientation = this.orientation;
175
175
  const targetRect = this.#$pop.footprintRect;
176
176
  const contentRect = this.#$content.getBoundingClientRect();
177
- const diffX = targetRect.x - contentRect.x;
178
- let desiredX = diffX + targetRect.width / 2;
179
- if (orientation === "bottom-left" || orientation === "top-left") {
180
- desiredX = Math.max(desiredX, contentRect.width * 0.75);
181
- }
182
- if (orientation === "bottom-right" || orientation === "top-right") {
183
- desiredX = Math.min(desiredX, contentRect.width * 0.25);
177
+ if (orientation.startsWith("top") || orientation.startsWith("bottom")) {
178
+ const diffX = targetRect.x - contentRect.x;
179
+ let desiredX = diffX + targetRect.width / 2;
180
+ if (orientation === "bottom-left" || orientation === "top-left") {
181
+ desiredX = Math.max(desiredX, contentRect.width * 0.75);
182
+ }
183
+ if (orientation === "bottom-right" || orientation === "top-right") {
184
+ desiredX = Math.min(desiredX, contentRect.width * 0.25);
185
+ }
186
+ const xPos = Math.max(TIP_SIZE, Math.min(desiredX, contentRect.width - TIP_SIZE));
187
+ this.#$tip.style.left = `${xPos}px`;
188
+ } else if (orientation.startsWith("left") || orientation.startsWith("right")) {
189
+ const diffY = targetRect.y - contentRect.y;
190
+ const desiredY = diffY + targetRect.height / 2;
191
+ const yPos = Math.max(TIP_SIZE, Math.min(desiredY, contentRect.height - TIP_SIZE));
192
+ this.#$tip.style.top = `${yPos}px`;
184
193
  }
185
- const xPos = Math.max(TIP_SIZE, Math.min(desiredX, contentRect.width - TIP_SIZE));
186
- this.#$tip.style.left = `${xPos}px`;
187
194
  setClass(this.#$tip, "hidden", rectOverlap(targetRect, contentRect));
188
195
  };
189
196
  // Prevent content from overflowing the viewport
@@ -1,5 +1,5 @@
1
1
  import type { NectaryComponentReactByType, NectaryComponentVanillaByType, TRect, NectaryComponentReact, NectaryComponentVanilla } from '../types';
2
- export type TSinchPopoverOrientation = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'bottom' | 'top';
2
+ export type TSinchPopoverOrientation = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'bottom' | 'top' | 'left' | 'right';
3
3
  export type TSinchPopoverProps = {
4
4
  /** Open/close state */
5
5
  open: boolean;
package/popover/utils.js CHANGED
@@ -3,6 +3,8 @@ const orientationValues = [
3
3
  "top-right",
4
4
  "bottom-left",
5
5
  "bottom-right",
6
+ "left",
7
+ "right",
6
8
  "bottom",
7
9
  "top"
8
10
  ];
@@ -13,6 +15,12 @@ const getPopOrientation = (orientation) => {
13
15
  if (orientation === "bottom") {
14
16
  return "bottom-stretch";
15
17
  }
18
+ if (orientation === "left") {
19
+ return "center-left";
20
+ }
21
+ if (orientation === "right") {
22
+ return "center-right";
23
+ }
16
24
  return orientation;
17
25
  };
18
26
  export {
@@ -2,12 +2,15 @@ import { updateAttribute, getAttribute, getBooleanAttribute, updateBooleanAttrib
2
2
  import { defineCustomElement, NectaryElement } from "../utils/element.js";
3
3
  import { getReactEventHandler } from "../utils/get-react-event-handler.js";
4
4
  import { getTargetByAttribute } from "../utils/event-target.js";
5
+ import { createKeyboardNavigation } from "../utils/control-keyboard-navigation.js";
5
6
  const templateHTML = '<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>';
6
7
  const template = document.createElement("template");
7
8
  template.innerHTML = templateHTML;
8
9
  class SegmentedControl extends NectaryElement {
9
10
  #$slot;
10
11
  #controller = null;
12
+ #enabledOptions = [];
13
+ #keyboardNav = createKeyboardNavigation();
11
14
  constructor() {
12
15
  super();
13
16
  const shadowRoot = this.attachShadow();
@@ -19,10 +22,12 @@ class SegmentedControl extends NectaryElement {
19
22
  const { signal } = this.#controller;
20
23
  const options = { signal };
21
24
  this.setAttribute("role", "tablist");
25
+ this.setAttribute("aria-orientation", "horizontal");
22
26
  this.#$slot.addEventListener("slotchange", this.#onSlotChange, options);
23
27
  this.#$slot.addEventListener("click", this.#onOptionClick, options);
24
28
  this.#$slot.addEventListener("keydown", this.#onOptionKeydown, options);
25
29
  this.addEventListener("-change", this.#onChangeReactHandler);
30
+ this.#updateEnabledOptions();
26
31
  }
27
32
  disconnectedCallback() {
28
33
  this.#controller.abort();
@@ -45,8 +50,14 @@ class SegmentedControl extends NectaryElement {
45
50
  get value() {
46
51
  return getAttribute(this, "value", "");
47
52
  }
53
+ #updateEnabledOptions = () => {
54
+ this.#enabledOptions = Array.from(this.#$slot.assignedElements()).filter(
55
+ (option) => !getBooleanAttribute(option, "disabled")
56
+ );
57
+ };
48
58
  #onSlotChange = () => {
49
59
  this.#onValueChange(this.value);
60
+ this.#updateEnabledOptions();
50
61
  };
51
62
  #onOptionClick = (e) => {
52
63
  const target = getTargetByAttribute(e, "value");
@@ -59,16 +70,7 @@ class SegmentedControl extends NectaryElement {
59
70
  );
60
71
  };
61
72
  #onOptionKeydown = (e) => {
62
- switch (e.code) {
63
- case "Space":
64
- case "Enter": {
65
- e.preventDefault();
66
- const target = getTargetByAttribute(e, "value");
67
- if (target !== null) {
68
- target.click();
69
- }
70
- }
71
- }
73
+ this.#keyboardNav.handleKeyboardNavigation(e, this.#enabledOptions);
72
74
  };
73
75
  #onValueChange(value) {
74
76
  for (const $option of this.#$slot.assignedElements()) {
@@ -3,12 +3,15 @@ import { updateAttribute, getAttribute, updateBooleanAttribute, getBooleanAttrib
3
3
  import { defineCustomElement, NectaryElement } from "../utils/element.js";
4
4
  import { getReactEventHandler } from "../utils/get-react-event-handler.js";
5
5
  import { getTargetByAttribute } from "../utils/event-target.js";
6
+ import { createKeyboardNavigation } from "../utils/control-keyboard-navigation.js";
6
7
  const templateHTML = '<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>';
7
8
  const template = document.createElement("template");
8
9
  template.innerHTML = templateHTML;
9
10
  class SegmentedIconControl extends NectaryElement {
10
11
  #$slot;
11
12
  #controller = null;
13
+ #keyboardNav = createKeyboardNavigation();
14
+ #enabledOptions = [];
12
15
  constructor() {
13
16
  super();
14
17
  const shadowRoot = this.attachShadow();
@@ -20,10 +23,12 @@ class SegmentedIconControl extends NectaryElement {
20
23
  const { signal } = this.#controller;
21
24
  const options = { signal };
22
25
  this.setAttribute("role", "tablist");
26
+ this.setAttribute("aria-orientation", "horizontal");
23
27
  this.#$slot.addEventListener("slotchange", this.#onSlotChange, options);
24
28
  this.#$slot.addEventListener("click", this.#onOptionClick, options);
25
29
  this.#$slot.addEventListener("keydown", this.#onOptionKeydown, options);
26
30
  this.addEventListener("-change", this.#onChangeReactHandler, options);
31
+ this.#updateEnabledOptions();
27
32
  }
28
33
  disconnectedCallback() {
29
34
  this.#controller.abort();
@@ -52,8 +57,14 @@ class SegmentedIconControl extends NectaryElement {
52
57
  get multiple() {
53
58
  return getBooleanAttribute(this, "multiple");
54
59
  }
60
+ #updateEnabledOptions = () => {
61
+ this.#enabledOptions = Array.from(this.#$slot.assignedElements()).filter(
62
+ (option) => !getBooleanAttribute(option, "disabled")
63
+ );
64
+ };
55
65
  #onSlotChange = () => {
56
66
  this.#onValueChange(this.value);
67
+ this.#updateEnabledOptions();
57
68
  };
58
69
  #onOptionClick = (e) => {
59
70
  const target = getTargetByAttribute(e, "value");
@@ -67,16 +78,7 @@ class SegmentedIconControl extends NectaryElement {
67
78
  );
68
79
  };
69
80
  #onOptionKeydown = (e) => {
70
- switch (e.code) {
71
- case "Space":
72
- case "Enter": {
73
- e.preventDefault();
74
- const target = getTargetByAttribute(e, "value");
75
- if (target !== null) {
76
- target.click();
77
- }
78
- }
79
- }
81
+ this.#keyboardNav.handleKeyboardNavigation(e, this.#enabledOptions);
80
82
  };
81
83
  #onValueChange(csv) {
82
84
  if (this.multiple) {
@@ -0,0 +1,5 @@
1
+ export declare function createKeyboardNavigation(): {
2
+ navigateToNextOption(enabledOptions: Element[], forward: boolean): void;
3
+ navigateToOption(enabledOptions: Element[], index: number): void;
4
+ handleKeyboardNavigation(e: KeyboardEvent, enabledOptions: Element[]): void;
5
+ };
@@ -0,0 +1,70 @@
1
+ import { getTargetByAttribute } from "./event-target.js";
2
+ function getActualActiveElement() {
3
+ let activeElement = document.activeElement;
4
+ while (activeElement?.shadowRoot?.activeElement != null) {
5
+ activeElement = activeElement.shadowRoot.activeElement;
6
+ }
7
+ return activeElement;
8
+ }
9
+ function createKeyboardNavigation() {
10
+ return {
11
+ navigateToNextOption(enabledOptions, forward) {
12
+ const optionsLength = enabledOptions.length;
13
+ if (optionsLength === 0) {
14
+ return;
15
+ }
16
+ const currentIndex = enabledOptions.findIndex((option) => option === getActualActiveElement());
17
+ let nextIndex;
18
+ if (currentIndex !== -1) {
19
+ if (forward) {
20
+ nextIndex = (currentIndex + 1) % optionsLength;
21
+ } else {
22
+ nextIndex = currentIndex === 0 ? optionsLength - 1 : currentIndex - 1;
23
+ }
24
+ } else {
25
+ nextIndex = forward ? 0 : optionsLength - 1;
26
+ }
27
+ this.navigateToOption(enabledOptions, nextIndex);
28
+ },
29
+ navigateToOption(enabledOptions, index) {
30
+ const optionsLength = enabledOptions.length;
31
+ if (enabledOptions.length === 0 || index < 0 || index >= optionsLength) {
32
+ return;
33
+ }
34
+ const option = enabledOptions[index];
35
+ option.focus();
36
+ },
37
+ handleKeyboardNavigation(e, enabledOptions) {
38
+ switch (e.code) {
39
+ case "Space":
40
+ case "Enter": {
41
+ e.preventDefault();
42
+ const target = getTargetByAttribute(e, "value");
43
+ if (target !== null) {
44
+ target.click();
45
+ }
46
+ break;
47
+ }
48
+ case "ArrowLeft":
49
+ case "ArrowRight": {
50
+ e.preventDefault();
51
+ this.navigateToNextOption(enabledOptions, e.code === "ArrowRight");
52
+ break;
53
+ }
54
+ case "Home": {
55
+ e.preventDefault();
56
+ this.navigateToOption(enabledOptions, 0);
57
+ break;
58
+ }
59
+ case "End": {
60
+ e.preventDefault();
61
+ this.navigateToOption(enabledOptions, enabledOptions.length - 1);
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ };
67
+ }
68
+ export {
69
+ createKeyboardNavigation
70
+ };