@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 +113 -30
- package/package.json +1 -1
- package/popover/index.js +17 -10
- package/popover/types.d.ts +1 -1
- package/popover/utils.js +8 -0
- package/segmented-control/index.js +12 -10
- package/segmented-icon-control/index.js +12 -10
- package/utils/control-keyboard-navigation.d.ts +5 -0
- package/utils/control-keyboard-navigation.js +70 -0
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
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
package/popover/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
};
|