@nectary/components 5.42.1 → 5.42.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 +160 -23
- package/help-tooltip/index.d.ts +0 -2
- package/help-tooltip/index.js +2 -8
- package/help-tooltip/types.d.ts +12 -3
- package/input/index.js +1 -1
- package/package.json +1 -1
- package/pop/index.js +52 -3
- package/popover/index.d.ts +2 -0
- package/popover/index.js +24 -0
- package/popover/types.d.ts +2 -0
- package/select-menu/index.d.ts +7 -1
- package/select-menu/index.js +69 -12
- package/select-menu/types.d.ts +5 -1
- package/tooltip/index.js +14 -1
- package/tooltip/types.d.ts +4 -0
- package/types.d.ts +4 -0
package/bundle.js
CHANGED
|
@@ -3485,6 +3485,12 @@ class Pop extends NectaryElement {
|
|
|
3485
3485
|
targetStyleValue: null,
|
|
3486
3486
|
transformedAncestor: null
|
|
3487
3487
|
};
|
|
3488
|
+
static #dialogA11yAttrs = [
|
|
3489
|
+
"aria-label",
|
|
3490
|
+
"aria-labelledby",
|
|
3491
|
+
"aria-describedby",
|
|
3492
|
+
"aria-description"
|
|
3493
|
+
];
|
|
3488
3494
|
constructor() {
|
|
3489
3495
|
super();
|
|
3490
3496
|
const shadowRoot = this.attachShadow();
|
|
@@ -3515,7 +3521,7 @@ class Pop extends NectaryElement {
|
|
|
3515
3521
|
const { signal } = this.#controller;
|
|
3516
3522
|
this.#keydownContext.listen(signal);
|
|
3517
3523
|
this.#visibilityContext.listen(signal);
|
|
3518
|
-
this
|
|
3524
|
+
this.#syncDialogA11yAttrs();
|
|
3519
3525
|
this.#$dialog.addEventListener("cancel", this.#onCancel, { signal });
|
|
3520
3526
|
this.#$dialog.addEventListener("mousedown", this.#onBackdropMouseDown, { signal });
|
|
3521
3527
|
this.addEventListener("-close", this.#onCloseReactHandler, { signal });
|
|
@@ -3536,7 +3542,12 @@ class Pop extends NectaryElement {
|
|
|
3536
3542
|
static get observedAttributes() {
|
|
3537
3543
|
return [
|
|
3538
3544
|
"orientation",
|
|
3539
|
-
"open"
|
|
3545
|
+
"open",
|
|
3546
|
+
"modal",
|
|
3547
|
+
"aria-label",
|
|
3548
|
+
"aria-labelledby",
|
|
3549
|
+
"aria-describedby",
|
|
3550
|
+
"aria-description"
|
|
3540
3551
|
];
|
|
3541
3552
|
}
|
|
3542
3553
|
get allowScroll() {
|
|
@@ -3606,7 +3617,42 @@ class Pop extends NectaryElement {
|
|
|
3606
3617
|
}
|
|
3607
3618
|
break;
|
|
3608
3619
|
}
|
|
3620
|
+
case "modal": {
|
|
3621
|
+
if (this.#$dialog.open) {
|
|
3622
|
+
this.#syncDialogSemantics(this.#shouldUseModalSemantics());
|
|
3623
|
+
}
|
|
3624
|
+
break;
|
|
3625
|
+
}
|
|
3626
|
+
case "aria-label":
|
|
3627
|
+
case "aria-labelledby":
|
|
3628
|
+
case "aria-describedby":
|
|
3629
|
+
case "aria-description": {
|
|
3630
|
+
this.#syncDialogA11yAttrs();
|
|
3631
|
+
break;
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
#syncDialogA11yAttrs() {
|
|
3636
|
+
Pop.#dialogA11yAttrs.forEach((attrName) => {
|
|
3637
|
+
const attrVal = this.getAttribute(attrName);
|
|
3638
|
+
if (attrVal === null) {
|
|
3639
|
+
this.#$dialog.removeAttribute(attrName);
|
|
3640
|
+
} else {
|
|
3641
|
+
this.#$dialog.setAttribute(attrName, attrVal);
|
|
3642
|
+
}
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
3645
|
+
#syncDialogSemantics(useDialogSemantics) {
|
|
3646
|
+
if (useDialogSemantics) {
|
|
3647
|
+
this.setAttribute("role", "dialog");
|
|
3648
|
+
this.#$dialog.setAttribute("aria-modal", "true");
|
|
3649
|
+
return;
|
|
3609
3650
|
}
|
|
3651
|
+
this.removeAttribute("role");
|
|
3652
|
+
this.#$dialog.removeAttribute("aria-modal");
|
|
3653
|
+
}
|
|
3654
|
+
#shouldUseModalSemantics(transformedAncestor = this.#openSession.transformedAncestor) {
|
|
3655
|
+
return this.modal && transformedAncestor === null;
|
|
3610
3656
|
}
|
|
3611
3657
|
#getTargetRect() {
|
|
3612
3658
|
let item = getFirstSlotElement(this.#$targetSlot, true);
|
|
@@ -3682,7 +3728,7 @@ class Pop extends NectaryElement {
|
|
|
3682
3728
|
}
|
|
3683
3729
|
const transformedAncestor = getTransformedAncestor(this);
|
|
3684
3730
|
const effectiveAllowScroll = this.allowScroll || transformedAncestor != null;
|
|
3685
|
-
const shouldUseModal = this
|
|
3731
|
+
const shouldUseModal = this.#shouldUseModalSemantics(transformedAncestor);
|
|
3686
3732
|
const openAsModal = shouldUseModal || !effectiveAllowScroll;
|
|
3687
3733
|
this.#openSession = {
|
|
3688
3734
|
effectiveAllowScroll,
|
|
@@ -3690,6 +3736,8 @@ class Pop extends NectaryElement {
|
|
|
3690
3736
|
targetStyleValue: null,
|
|
3691
3737
|
transformedAncestor
|
|
3692
3738
|
};
|
|
3739
|
+
this.#syncDialogA11yAttrs();
|
|
3740
|
+
this.#syncDialogSemantics(shouldUseModal);
|
|
3693
3741
|
this.#$targetSlot.addEventListener("blur", this.#stopEventPropagation, true);
|
|
3694
3742
|
this.#$focus.setAttribute("tabindex", "-1");
|
|
3695
3743
|
this.#$focus.style.display = "block";
|
|
@@ -3800,6 +3848,7 @@ class Pop extends NectaryElement {
|
|
|
3800
3848
|
targetStyleValue: null,
|
|
3801
3849
|
transformedAncestor: null
|
|
3802
3850
|
};
|
|
3851
|
+
this.#syncDialogSemantics(false);
|
|
3803
3852
|
}
|
|
3804
3853
|
#onResize = () => {
|
|
3805
3854
|
this.#resizeThrottle.fn();
|
|
@@ -4166,6 +4215,7 @@ class Tooltip extends NectaryElement {
|
|
|
4166
4215
|
updateAttribute(this.#$pop, "orientation", getPopOrientation$1(this.orientation));
|
|
4167
4216
|
updateBooleanAttribute(this.#$pop, "hide-outside-viewport", !this.showOutsideViewport);
|
|
4168
4217
|
updateBooleanAttribute(this.#$pop, "disable-focus-restore", true);
|
|
4218
|
+
this.#updatePopAriaLabel();
|
|
4169
4219
|
this.#updateText();
|
|
4170
4220
|
}
|
|
4171
4221
|
disconnectedCallback() {
|
|
@@ -4211,6 +4261,7 @@ class Tooltip extends NectaryElement {
|
|
|
4211
4261
|
switch (name) {
|
|
4212
4262
|
case "text": {
|
|
4213
4263
|
this.#updateText();
|
|
4264
|
+
this.#updatePopAriaLabel();
|
|
4214
4265
|
break;
|
|
4215
4266
|
}
|
|
4216
4267
|
case "orientation": {
|
|
@@ -4233,7 +4284,11 @@ class Tooltip extends NectaryElement {
|
|
|
4233
4284
|
}
|
|
4234
4285
|
case "aria-label":
|
|
4235
4286
|
case "aria-description": {
|
|
4236
|
-
|
|
4287
|
+
if (name === "aria-label") {
|
|
4288
|
+
this.#updatePopAriaLabel();
|
|
4289
|
+
} else {
|
|
4290
|
+
updateAttribute(this.#$pop, name, newVal);
|
|
4291
|
+
}
|
|
4237
4292
|
break;
|
|
4238
4293
|
}
|
|
4239
4294
|
case "show-outside-viewport": {
|
|
@@ -4809,6 +4864,13 @@ class Tooltip extends NectaryElement {
|
|
|
4809
4864
|
this.#subscribeMouseEnterEvent();
|
|
4810
4865
|
}
|
|
4811
4866
|
}
|
|
4867
|
+
#updatePopAriaLabel() {
|
|
4868
|
+
const rawAriaLabel = getAttribute(this, "aria-label");
|
|
4869
|
+
const explicitAriaLabel = rawAriaLabel !== null && rawAriaLabel.trim().length > 0 ? rawAriaLabel.trim() : null;
|
|
4870
|
+
const fallbackAriaLabel = this.text ?? "";
|
|
4871
|
+
const ariaLabel = explicitAriaLabel ?? (fallbackAriaLabel.length === 0 ? null : fallbackAriaLabel);
|
|
4872
|
+
updateAttribute(this.#$pop, "aria-label", ariaLabel);
|
|
4873
|
+
}
|
|
4812
4874
|
#subscribeMouseEnterEvent() {
|
|
4813
4875
|
if (!this.isDomConnected || this.#isSubscribed) {
|
|
4814
4876
|
return;
|
|
@@ -6695,7 +6757,7 @@ class Input extends NectaryElement {
|
|
|
6695
6757
|
break;
|
|
6696
6758
|
}
|
|
6697
6759
|
case "aria-label": {
|
|
6698
|
-
this.#$input
|
|
6760
|
+
updateAttribute(this.#$input, "aria-label", newVal);
|
|
6699
6761
|
break;
|
|
6700
6762
|
}
|
|
6701
6763
|
}
|
|
@@ -7219,6 +7281,9 @@ const getPopOrientation = (orientation) => {
|
|
|
7219
7281
|
const templateHTML$S = '<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>';
|
|
7220
7282
|
const TIP_SIZE = 16;
|
|
7221
7283
|
const template$S = document.createElement("template");
|
|
7284
|
+
const isFocusableContent = (element) => {
|
|
7285
|
+
return typeof element?.focusOnOpen === "function";
|
|
7286
|
+
};
|
|
7222
7287
|
template$S.innerHTML = templateHTML$S;
|
|
7223
7288
|
class Popover extends NectaryElement {
|
|
7224
7289
|
#$pop;
|
|
@@ -7276,6 +7341,7 @@ class Popover extends NectaryElement {
|
|
|
7276
7341
|
"open",
|
|
7277
7342
|
"modal",
|
|
7278
7343
|
"allow-scroll",
|
|
7344
|
+
"focus-content-on-open",
|
|
7279
7345
|
"tip",
|
|
7280
7346
|
"aria-label",
|
|
7281
7347
|
"aria-description"
|
|
@@ -7306,6 +7372,10 @@ class Popover extends NectaryElement {
|
|
|
7306
7372
|
updateBooleanAttribute(this, name, isAttrTrue(newVal));
|
|
7307
7373
|
break;
|
|
7308
7374
|
}
|
|
7375
|
+
case "focus-content-on-open": {
|
|
7376
|
+
updateBooleanAttribute(this, name, isAttrTrue(newVal));
|
|
7377
|
+
break;
|
|
7378
|
+
}
|
|
7309
7379
|
case "modal":
|
|
7310
7380
|
case "open": {
|
|
7311
7381
|
updateAttribute(this.#$pop, name, newVal);
|
|
@@ -7335,6 +7405,12 @@ class Popover extends NectaryElement {
|
|
|
7335
7405
|
get allowScroll() {
|
|
7336
7406
|
return getBooleanAttribute(this, "allow-scroll");
|
|
7337
7407
|
}
|
|
7408
|
+
set focusContentOnOpen(shouldFocus) {
|
|
7409
|
+
updateBooleanAttribute(this, "focus-content-on-open", shouldFocus);
|
|
7410
|
+
}
|
|
7411
|
+
get focusContentOnOpen() {
|
|
7412
|
+
return getBooleanAttribute(this, "focus-content-on-open");
|
|
7413
|
+
}
|
|
7338
7414
|
set open(isOpen) {
|
|
7339
7415
|
updateBooleanAttribute(this, "open", isOpen);
|
|
7340
7416
|
}
|
|
@@ -7369,6 +7445,12 @@ class Popover extends NectaryElement {
|
|
|
7369
7445
|
}
|
|
7370
7446
|
return elements[0];
|
|
7371
7447
|
}
|
|
7448
|
+
#focusSlottedContentOnOpen() {
|
|
7449
|
+
const slottedContent = this.#getFirstAssignedElementInSlot(this.#$contentSlot);
|
|
7450
|
+
if (isFocusableContent(slottedContent)) {
|
|
7451
|
+
slottedContent.focusOnOpen();
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7372
7454
|
#onPopClose = () => {
|
|
7373
7455
|
this.#dispatchCloseEvent();
|
|
7374
7456
|
};
|
|
@@ -7385,6 +7467,10 @@ class Popover extends NectaryElement {
|
|
|
7385
7467
|
if (e.detail) {
|
|
7386
7468
|
this.#updateTipOrientation();
|
|
7387
7469
|
this.#updateContentMaxWidth();
|
|
7470
|
+
if (this.focusContentOnOpen) {
|
|
7471
|
+
this.#$content.getBoundingClientRect();
|
|
7472
|
+
this.#focusSlottedContentOnOpen();
|
|
7473
|
+
}
|
|
7388
7474
|
} else {
|
|
7389
7475
|
this.#resetTipOrientation();
|
|
7390
7476
|
}
|
|
@@ -9558,7 +9644,7 @@ class HelpTooltip extends NectaryElement {
|
|
|
9558
9644
|
this.#controller = null;
|
|
9559
9645
|
}
|
|
9560
9646
|
static get observedAttributes() {
|
|
9561
|
-
return ["aria-label", "text", "
|
|
9647
|
+
return ["aria-label", "text", "orientation"];
|
|
9562
9648
|
}
|
|
9563
9649
|
attributeChangedCallback(name, _, newVal) {
|
|
9564
9650
|
updateAttribute(this.#$tooltip, name, newVal);
|
|
@@ -9569,12 +9655,6 @@ class HelpTooltip extends NectaryElement {
|
|
|
9569
9655
|
set text(value) {
|
|
9570
9656
|
updateAttribute(this, "text", value);
|
|
9571
9657
|
}
|
|
9572
|
-
get width() {
|
|
9573
|
-
return getIntegerAttribute(this, "width");
|
|
9574
|
-
}
|
|
9575
|
-
set width(value) {
|
|
9576
|
-
updateIntegerAttribute(this, "width", value);
|
|
9577
|
-
}
|
|
9578
9658
|
get orientation() {
|
|
9579
9659
|
return getAttribute(this, "orientation", "top");
|
|
9580
9660
|
}
|
|
@@ -15309,8 +15389,9 @@ class SelectMenuOption extends NectaryElement {
|
|
|
15309
15389
|
}
|
|
15310
15390
|
defineCustomElement("sinch-select-menu-option", SelectMenuOption);
|
|
15311
15391
|
const isSelectMenuOption = (el) => el.localName === "sinch-select-menu-option";
|
|
15312
|
-
const templateHTML$h = '<style>:host{display:block;outline:0}#listbox{overflow-y:auto;max-height:var(--sinch-comp-select-menu-font-max-height)}#search{display:none;margin:10px}#search.active{display:block}#search-clear:not(.active){display:none}#not-found{display:flex;align-items:center;justify-content:center;width:100%;height:30px;margin-bottom:10px;pointer-events:none;user-select:none;--sinch-comp-text-font:var(--sinch-comp-select-menu-font-not-found-text);--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-not-found-text-initial)}#not-found:not(.active){display:none}::slotted(.hidden){display:none}::slotted(sinch-title){padding:8px 16px;--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-title-initial)}</style><sinch-input id="search" size="s" placeholder="Search"><sinch-icon icons-version="2" name="magnifying-glass" id="icon-search" slot="icon"></sinch-icon><sinch-button id="search-clear" slot="right"><sinch-icon icons-version="2" name="fa-xmark" slot="icon"></sinch-icon></sinch-button></sinch-input><div id="not-found"><sinch-text type="m">No results</sinch-text></div><div id="listbox" role="
|
|
15392
|
+
const templateHTML$h = '<style>:host{display:block;outline:0}#listbox{overflow-y:auto;max-height:var(--sinch-comp-select-menu-font-max-height)}#search{display:none;margin:10px}#search.active{display:block}#search-clear:not(.active){display:none}#not-found{display:flex;align-items:center;justify-content:center;width:100%;height:30px;margin-bottom:10px;pointer-events:none;user-select:none;--sinch-comp-text-font:var(--sinch-comp-select-menu-font-not-found-text);--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-not-found-text-initial)}#not-found:not(.active){display:none}::slotted(.hidden){display:none}::slotted(sinch-title){padding:8px 16px;--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-title-initial)}</style><sinch-input id="search" size="s" placeholder="Search" aria-label="Search"><sinch-icon icons-version="2" name="magnifying-glass" id="icon-search" slot="icon"></sinch-icon><sinch-button id="search-clear" slot="right"><sinch-icon icons-version="2" name="fa-xmark" slot="icon"></sinch-icon></sinch-button></sinch-input><div id="not-found"><sinch-text type="m">No results</sinch-text></div><div id="listbox" role="listbox" tabindex="-1"><slot></slot></div>';
|
|
15313
15393
|
const ITEM_HEIGHT = 40;
|
|
15394
|
+
const DEFAULT_SEARCH_LABEL = "Search";
|
|
15314
15395
|
const template$h = document.createElement("template");
|
|
15315
15396
|
template$h.innerHTML = templateHTML$h;
|
|
15316
15397
|
class SelectMenu extends NectaryElement {
|
|
@@ -15337,13 +15418,14 @@ class SelectMenu extends NectaryElement {
|
|
|
15337
15418
|
this.#searchDebounce = debounceTimeout(200)(this.#updateSearchValue);
|
|
15338
15419
|
}
|
|
15339
15420
|
connectedCallback() {
|
|
15421
|
+
super.connectedCallback();
|
|
15340
15422
|
this.#controller = new AbortController();
|
|
15341
15423
|
const options = {
|
|
15342
15424
|
signal: this.#controller.signal
|
|
15343
15425
|
};
|
|
15344
|
-
this.setAttribute("role", "
|
|
15345
|
-
this.#internals.role = "listbox";
|
|
15426
|
+
this.setAttribute("role", "group");
|
|
15346
15427
|
this.tabIndex = 0;
|
|
15428
|
+
this.#syncListboxLabel();
|
|
15347
15429
|
this.addEventListener("keydown", this.#onListboxKeyDown, options);
|
|
15348
15430
|
this.addEventListener("focus", this.#onFocus, options);
|
|
15349
15431
|
this.addEventListener("blur", this.#onListboxBlur, options);
|
|
@@ -15388,6 +15470,7 @@ class SelectMenu extends NectaryElement {
|
|
|
15388
15470
|
this.#searchDebounce.cancel();
|
|
15389
15471
|
this.#controller.abort();
|
|
15390
15472
|
this.#controller = null;
|
|
15473
|
+
super.disconnectedCallback();
|
|
15391
15474
|
}
|
|
15392
15475
|
formAssociatedCallback() {
|
|
15393
15476
|
setFormValue(this.#internals, CSVToFormData(this.name, this.value));
|
|
@@ -15412,6 +15495,8 @@ class SelectMenu extends NectaryElement {
|
|
|
15412
15495
|
"rows",
|
|
15413
15496
|
"multiple",
|
|
15414
15497
|
"searchable",
|
|
15498
|
+
"aria-label",
|
|
15499
|
+
"aria-labelledby",
|
|
15415
15500
|
// eslint-disable-next-line @nectary/observed-attribute-accessor -- baseline backlog: missing set searchValue
|
|
15416
15501
|
"search-value",
|
|
15417
15502
|
// eslint-disable-next-line @nectary/observed-attribute-accessor -- baseline backlog: missing set searchPlaceholder
|
|
@@ -15425,17 +15510,24 @@ class SelectMenu extends NectaryElement {
|
|
|
15425
15510
|
case "multiple": {
|
|
15426
15511
|
this.#onValueChange(this.value);
|
|
15427
15512
|
updateExplicitBooleanAttribute(
|
|
15428
|
-
this,
|
|
15513
|
+
this.#$listbox,
|
|
15429
15514
|
"aria-multiselectable",
|
|
15430
15515
|
isAttrTrue(newVal)
|
|
15431
15516
|
);
|
|
15432
|
-
this.#internals.ariaMultiSelectable = isAttrTrue(newVal).toString();
|
|
15433
15517
|
break;
|
|
15434
15518
|
}
|
|
15435
15519
|
case "searchable": {
|
|
15436
15520
|
this.#onOptionSlotChange();
|
|
15437
15521
|
break;
|
|
15438
15522
|
}
|
|
15523
|
+
case "aria-label": {
|
|
15524
|
+
this.#syncListboxLabel();
|
|
15525
|
+
break;
|
|
15526
|
+
}
|
|
15527
|
+
case "aria-labelledby": {
|
|
15528
|
+
this.#syncListboxLabel();
|
|
15529
|
+
break;
|
|
15530
|
+
}
|
|
15439
15531
|
case "search-autocomplete": {
|
|
15440
15532
|
updateAttribute(this.#$search, "autocomplete", newVal);
|
|
15441
15533
|
break;
|
|
@@ -15449,7 +15541,7 @@ class SelectMenu extends NectaryElement {
|
|
|
15449
15541
|
break;
|
|
15450
15542
|
}
|
|
15451
15543
|
case "search-placeholder": {
|
|
15452
|
-
|
|
15544
|
+
this.#updateSearchPlaceholder(newVal);
|
|
15453
15545
|
break;
|
|
15454
15546
|
}
|
|
15455
15547
|
case "search-value": {
|
|
@@ -15497,6 +15589,18 @@ class SelectMenu extends NectaryElement {
|
|
|
15497
15589
|
const searchableAttribute = this.getAttribute("searchable");
|
|
15498
15590
|
return searchableAttribute === null ? searchableAttribute : isAttrTrue(searchableAttribute);
|
|
15499
15591
|
}
|
|
15592
|
+
get "aria-label"() {
|
|
15593
|
+
return getAttribute(this, "aria-label");
|
|
15594
|
+
}
|
|
15595
|
+
set "aria-label"(value) {
|
|
15596
|
+
updateAttribute(this, "aria-label", value);
|
|
15597
|
+
}
|
|
15598
|
+
get "aria-labelledby"() {
|
|
15599
|
+
return getAttribute(this, "aria-labelledby");
|
|
15600
|
+
}
|
|
15601
|
+
set "aria-labelledby"(value) {
|
|
15602
|
+
updateAttribute(this, "aria-labelledby", value);
|
|
15603
|
+
}
|
|
15500
15604
|
set "search-autocomplete"(autocomplete) {
|
|
15501
15605
|
updateAttribute(this.#$search, "autocomplete", autocomplete);
|
|
15502
15606
|
}
|
|
@@ -15504,7 +15608,7 @@ class SelectMenu extends NectaryElement {
|
|
|
15504
15608
|
return getAttribute(this.#$search, "autocomplete", "");
|
|
15505
15609
|
}
|
|
15506
15610
|
set "search-placeholder"(placeholder) {
|
|
15507
|
-
|
|
15611
|
+
this.#updateSearchPlaceholder(placeholder);
|
|
15508
15612
|
}
|
|
15509
15613
|
get "search-placeholder"() {
|
|
15510
15614
|
return getAttribute(this.#$search, "placeholder", "");
|
|
@@ -15519,12 +15623,22 @@ class SelectMenu extends NectaryElement {
|
|
|
15519
15623
|
get focusable() {
|
|
15520
15624
|
return true;
|
|
15521
15625
|
}
|
|
15626
|
+
focusOnOpen() {
|
|
15627
|
+
this.#focusActiveSearch();
|
|
15628
|
+
}
|
|
15522
15629
|
#onFocus = () => {
|
|
15523
|
-
|
|
15524
|
-
|
|
15525
|
-
this.#$search.focus();
|
|
15630
|
+
if (getDeepActiveElement(this.ownerDocument) !== this) {
|
|
15631
|
+
return;
|
|
15526
15632
|
}
|
|
15633
|
+
this.#focusActiveSearch();
|
|
15527
15634
|
};
|
|
15635
|
+
#focusActiveSearch() {
|
|
15636
|
+
if (!this.isDomConnected || !hasClass(this.#$search, "active")) {
|
|
15637
|
+
return;
|
|
15638
|
+
}
|
|
15639
|
+
this.#$search.getBoundingClientRect();
|
|
15640
|
+
this.#$search.focus();
|
|
15641
|
+
}
|
|
15528
15642
|
#onListboxBlur = () => {
|
|
15529
15643
|
this.#selectOption(null);
|
|
15530
15644
|
};
|
|
@@ -15589,7 +15703,7 @@ class SelectMenu extends NectaryElement {
|
|
|
15589
15703
|
#onContextVisibility = (e) => {
|
|
15590
15704
|
if (e.detail) {
|
|
15591
15705
|
this.#selectOption(this.#findCheckedOption());
|
|
15592
|
-
this.#
|
|
15706
|
+
this.#focusActiveSearch();
|
|
15593
15707
|
} else {
|
|
15594
15708
|
this.#selectOption(null);
|
|
15595
15709
|
}
|
|
@@ -15627,7 +15741,30 @@ class SelectMenu extends NectaryElement {
|
|
|
15627
15741
|
this.#dispatchChangeEvent(option);
|
|
15628
15742
|
}
|
|
15629
15743
|
};
|
|
15744
|
+
#syncListboxLabel = () => {
|
|
15745
|
+
const labelledby = getAttribute(this, "aria-labelledby");
|
|
15746
|
+
const label = labelledby === null ? getAttribute(this, "aria-label") : null;
|
|
15747
|
+
updateAttribute(this.#$listbox, "aria-labelledby", labelledby);
|
|
15748
|
+
updateAttribute(this.#$listbox, "aria-label", label);
|
|
15749
|
+
};
|
|
15750
|
+
#updateSearchPlaceholder(placeholder) {
|
|
15751
|
+
const currentPlaceholder = getAttribute(this.#$search, "placeholder");
|
|
15752
|
+
const currentLabel = getAttribute(this.#$search, "aria-label");
|
|
15753
|
+
const isManagedLabel = currentLabel === null || currentLabel === DEFAULT_SEARCH_LABEL || currentLabel === currentPlaceholder;
|
|
15754
|
+
updateAttribute(this.#$search, "placeholder", placeholder);
|
|
15755
|
+
if (isManagedLabel) {
|
|
15756
|
+
updateAttribute(this.#$search, "aria-label", placeholder === null || placeholder === "" ? DEFAULT_SEARCH_LABEL : placeholder);
|
|
15757
|
+
}
|
|
15758
|
+
}
|
|
15759
|
+
#syncListboxChildren = () => {
|
|
15760
|
+
for (const $element of this.#$optionSlot.assignedElements()) {
|
|
15761
|
+
if (!isSelectMenuOption($element) && getAttribute($element, "aria-hidden") !== "true") {
|
|
15762
|
+
updateAttribute($element, "aria-hidden", "true");
|
|
15763
|
+
}
|
|
15764
|
+
}
|
|
15765
|
+
};
|
|
15630
15766
|
#onOptionSlotChange = () => {
|
|
15767
|
+
this.#syncListboxChildren();
|
|
15631
15768
|
if (this.hasAttribute("rows")) {
|
|
15632
15769
|
this.#updateListboxMaxHeight();
|
|
15633
15770
|
}
|
package/help-tooltip/index.d.ts
CHANGED
|
@@ -11,8 +11,6 @@ export declare class HelpTooltip extends NectaryElement {
|
|
|
11
11
|
attributeChangedCallback(name: string, _: string | null, newVal: string | null): void;
|
|
12
12
|
get text(): string;
|
|
13
13
|
set text(value: string);
|
|
14
|
-
get width(): number | undefined;
|
|
15
|
-
set width(value: number | undefined);
|
|
16
14
|
get orientation(): string;
|
|
17
15
|
set orientation(value: string);
|
|
18
16
|
get footprintRect(): import("../types").TRect;
|
package/help-tooltip/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "../tooltip/index.js";
|
|
2
2
|
import "../icon/index.js";
|
|
3
|
-
import { updateAttribute, getAttribute
|
|
3
|
+
import { updateAttribute, getAttribute } from "../utils/dom.js";
|
|
4
4
|
import { defineCustomElement, NectaryElement } from "../utils/element.js";
|
|
5
5
|
import { getReactEventHandler } from "../utils/get-react-event-handler.js";
|
|
6
6
|
const templateHTML = '<style>:host{display:contents}#icon{--sinch-global-size-icon:18px}</style><sinch-tooltip type="fast"><sinch-icon icons-version="2" name="circle-question" id="icon"></sinch-icon></sinch-tooltip>';
|
|
@@ -30,7 +30,7 @@ class HelpTooltip extends NectaryElement {
|
|
|
30
30
|
this.#controller = null;
|
|
31
31
|
}
|
|
32
32
|
static get observedAttributes() {
|
|
33
|
-
return ["aria-label", "text", "
|
|
33
|
+
return ["aria-label", "text", "orientation"];
|
|
34
34
|
}
|
|
35
35
|
attributeChangedCallback(name, _, newVal) {
|
|
36
36
|
updateAttribute(this.#$tooltip, name, newVal);
|
|
@@ -41,12 +41,6 @@ class HelpTooltip extends NectaryElement {
|
|
|
41
41
|
set text(value) {
|
|
42
42
|
updateAttribute(this, "text", value);
|
|
43
43
|
}
|
|
44
|
-
get width() {
|
|
45
|
-
return getIntegerAttribute(this, "width");
|
|
46
|
-
}
|
|
47
|
-
set width(value) {
|
|
48
|
-
updateIntegerAttribute(this, "width", value);
|
|
49
|
-
}
|
|
50
44
|
get orientation() {
|
|
51
45
|
return getAttribute(this, "orientation", "top");
|
|
52
46
|
}
|
package/help-tooltip/types.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { NectaryComponentReactByType, NectaryComponentVanillaByType, NectaryComponentReact, NectaryComponentVanilla } from '../types';
|
|
3
|
-
export type TSinchHelpTooltipProps =
|
|
1
|
+
import type { TSinchTooltipOrientation } from '../tooltip/types';
|
|
2
|
+
import type { NectaryComponentReactByType, NectaryComponentVanillaByType, NectaryComponentReact, NectaryComponentVanilla, TRect } from '../types';
|
|
3
|
+
export type TSinchHelpTooltipProps = {
|
|
4
|
+
/** Accessible name for the tooltip content */
|
|
5
|
+
'aria-label'?: string;
|
|
6
|
+
/** Text */
|
|
7
|
+
text: string;
|
|
8
|
+
/** Orientation, where it *points to* from origin */
|
|
9
|
+
orientation?: TSinchTooltipOrientation;
|
|
10
|
+
readonly footprintRect?: TRect;
|
|
11
|
+
readonly tooltipRect?: TRect;
|
|
12
|
+
};
|
|
4
13
|
export type TSinchHelpTooltipStyle = {
|
|
5
14
|
'--sinch-global-size-icon'?: string;
|
|
6
15
|
};
|
package/input/index.js
CHANGED
package/package.json
CHANGED
package/pop/index.js
CHANGED
|
@@ -35,6 +35,12 @@ class Pop extends NectaryElement {
|
|
|
35
35
|
targetStyleValue: null,
|
|
36
36
|
transformedAncestor: null
|
|
37
37
|
};
|
|
38
|
+
static #dialogA11yAttrs = [
|
|
39
|
+
"aria-label",
|
|
40
|
+
"aria-labelledby",
|
|
41
|
+
"aria-describedby",
|
|
42
|
+
"aria-description"
|
|
43
|
+
];
|
|
38
44
|
constructor() {
|
|
39
45
|
super();
|
|
40
46
|
const shadowRoot = this.attachShadow();
|
|
@@ -65,7 +71,7 @@ class Pop extends NectaryElement {
|
|
|
65
71
|
const { signal } = this.#controller;
|
|
66
72
|
this.#keydownContext.listen(signal);
|
|
67
73
|
this.#visibilityContext.listen(signal);
|
|
68
|
-
this
|
|
74
|
+
this.#syncDialogA11yAttrs();
|
|
69
75
|
this.#$dialog.addEventListener("cancel", this.#onCancel, { signal });
|
|
70
76
|
this.#$dialog.addEventListener("mousedown", this.#onBackdropMouseDown, { signal });
|
|
71
77
|
this.addEventListener("-close", this.#onCloseReactHandler, { signal });
|
|
@@ -86,7 +92,12 @@ class Pop extends NectaryElement {
|
|
|
86
92
|
static get observedAttributes() {
|
|
87
93
|
return [
|
|
88
94
|
"orientation",
|
|
89
|
-
"open"
|
|
95
|
+
"open",
|
|
96
|
+
"modal",
|
|
97
|
+
"aria-label",
|
|
98
|
+
"aria-labelledby",
|
|
99
|
+
"aria-describedby",
|
|
100
|
+
"aria-description"
|
|
90
101
|
];
|
|
91
102
|
}
|
|
92
103
|
get allowScroll() {
|
|
@@ -156,8 +167,43 @@ class Pop extends NectaryElement {
|
|
|
156
167
|
}
|
|
157
168
|
break;
|
|
158
169
|
}
|
|
170
|
+
case "modal": {
|
|
171
|
+
if (this.#$dialog.open) {
|
|
172
|
+
this.#syncDialogSemantics(this.#shouldUseModalSemantics());
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case "aria-label":
|
|
177
|
+
case "aria-labelledby":
|
|
178
|
+
case "aria-describedby":
|
|
179
|
+
case "aria-description": {
|
|
180
|
+
this.#syncDialogA11yAttrs();
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
159
183
|
}
|
|
160
184
|
}
|
|
185
|
+
#syncDialogA11yAttrs() {
|
|
186
|
+
Pop.#dialogA11yAttrs.forEach((attrName) => {
|
|
187
|
+
const attrVal = this.getAttribute(attrName);
|
|
188
|
+
if (attrVal === null) {
|
|
189
|
+
this.#$dialog.removeAttribute(attrName);
|
|
190
|
+
} else {
|
|
191
|
+
this.#$dialog.setAttribute(attrName, attrVal);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
#syncDialogSemantics(useDialogSemantics) {
|
|
196
|
+
if (useDialogSemantics) {
|
|
197
|
+
this.setAttribute("role", "dialog");
|
|
198
|
+
this.#$dialog.setAttribute("aria-modal", "true");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.removeAttribute("role");
|
|
202
|
+
this.#$dialog.removeAttribute("aria-modal");
|
|
203
|
+
}
|
|
204
|
+
#shouldUseModalSemantics(transformedAncestor = this.#openSession.transformedAncestor) {
|
|
205
|
+
return this.modal && transformedAncestor === null;
|
|
206
|
+
}
|
|
161
207
|
#getTargetRect() {
|
|
162
208
|
let item = getFirstSlotElement(this.#$targetSlot, true);
|
|
163
209
|
if (item === null && this.#$dialog.open) {
|
|
@@ -232,7 +278,7 @@ class Pop extends NectaryElement {
|
|
|
232
278
|
}
|
|
233
279
|
const transformedAncestor = getTransformedAncestor(this);
|
|
234
280
|
const effectiveAllowScroll = this.allowScroll || transformedAncestor != null;
|
|
235
|
-
const shouldUseModal = this
|
|
281
|
+
const shouldUseModal = this.#shouldUseModalSemantics(transformedAncestor);
|
|
236
282
|
const openAsModal = shouldUseModal || !effectiveAllowScroll;
|
|
237
283
|
this.#openSession = {
|
|
238
284
|
effectiveAllowScroll,
|
|
@@ -240,6 +286,8 @@ class Pop extends NectaryElement {
|
|
|
240
286
|
targetStyleValue: null,
|
|
241
287
|
transformedAncestor
|
|
242
288
|
};
|
|
289
|
+
this.#syncDialogA11yAttrs();
|
|
290
|
+
this.#syncDialogSemantics(shouldUseModal);
|
|
243
291
|
this.#$targetSlot.addEventListener("blur", this.#stopEventPropagation, true);
|
|
244
292
|
this.#$focus.setAttribute("tabindex", "-1");
|
|
245
293
|
this.#$focus.style.display = "block";
|
|
@@ -350,6 +398,7 @@ class Pop extends NectaryElement {
|
|
|
350
398
|
targetStyleValue: null,
|
|
351
399
|
transformedAncestor: null
|
|
352
400
|
};
|
|
401
|
+
this.#syncDialogSemantics(false);
|
|
353
402
|
}
|
|
354
403
|
#onResize = () => {
|
|
355
404
|
this.#resizeThrottle.fn();
|
package/popover/index.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare class Popover extends NectaryElement {
|
|
|
13
13
|
get modal(): boolean;
|
|
14
14
|
set allowScroll(allow: boolean);
|
|
15
15
|
get allowScroll(): boolean;
|
|
16
|
+
set focusContentOnOpen(shouldFocus: boolean);
|
|
17
|
+
get focusContentOnOpen(): boolean;
|
|
16
18
|
set open(isOpen: boolean);
|
|
17
19
|
get open(): boolean;
|
|
18
20
|
set tip(hasTip: boolean);
|
package/popover/index.js
CHANGED
|
@@ -8,6 +8,9 @@ import { getPopOrientation, orientationValues } from "./utils.js";
|
|
|
8
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
|
+
const isFocusableContent = (element) => {
|
|
12
|
+
return typeof element?.focusOnOpen === "function";
|
|
13
|
+
};
|
|
11
14
|
template.innerHTML = templateHTML;
|
|
12
15
|
class Popover extends NectaryElement {
|
|
13
16
|
#$pop;
|
|
@@ -65,6 +68,7 @@ class Popover extends NectaryElement {
|
|
|
65
68
|
"open",
|
|
66
69
|
"modal",
|
|
67
70
|
"allow-scroll",
|
|
71
|
+
"focus-content-on-open",
|
|
68
72
|
"tip",
|
|
69
73
|
"aria-label",
|
|
70
74
|
"aria-description"
|
|
@@ -95,6 +99,10 @@ class Popover extends NectaryElement {
|
|
|
95
99
|
updateBooleanAttribute(this, name, isAttrTrue(newVal));
|
|
96
100
|
break;
|
|
97
101
|
}
|
|
102
|
+
case "focus-content-on-open": {
|
|
103
|
+
updateBooleanAttribute(this, name, isAttrTrue(newVal));
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
98
106
|
case "modal":
|
|
99
107
|
case "open": {
|
|
100
108
|
updateAttribute(this.#$pop, name, newVal);
|
|
@@ -124,6 +132,12 @@ class Popover extends NectaryElement {
|
|
|
124
132
|
get allowScroll() {
|
|
125
133
|
return getBooleanAttribute(this, "allow-scroll");
|
|
126
134
|
}
|
|
135
|
+
set focusContentOnOpen(shouldFocus) {
|
|
136
|
+
updateBooleanAttribute(this, "focus-content-on-open", shouldFocus);
|
|
137
|
+
}
|
|
138
|
+
get focusContentOnOpen() {
|
|
139
|
+
return getBooleanAttribute(this, "focus-content-on-open");
|
|
140
|
+
}
|
|
127
141
|
set open(isOpen) {
|
|
128
142
|
updateBooleanAttribute(this, "open", isOpen);
|
|
129
143
|
}
|
|
@@ -158,6 +172,12 @@ class Popover extends NectaryElement {
|
|
|
158
172
|
}
|
|
159
173
|
return elements[0];
|
|
160
174
|
}
|
|
175
|
+
#focusSlottedContentOnOpen() {
|
|
176
|
+
const slottedContent = this.#getFirstAssignedElementInSlot(this.#$contentSlot);
|
|
177
|
+
if (isFocusableContent(slottedContent)) {
|
|
178
|
+
slottedContent.focusOnOpen();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
161
181
|
#onPopClose = () => {
|
|
162
182
|
this.#dispatchCloseEvent();
|
|
163
183
|
};
|
|
@@ -174,6 +194,10 @@ class Popover extends NectaryElement {
|
|
|
174
194
|
if (e.detail) {
|
|
175
195
|
this.#updateTipOrientation();
|
|
176
196
|
this.#updateContentMaxWidth();
|
|
197
|
+
if (this.focusContentOnOpen) {
|
|
198
|
+
this.#$content.getBoundingClientRect();
|
|
199
|
+
this.#focusSlottedContentOnOpen();
|
|
200
|
+
}
|
|
177
201
|
} else {
|
|
178
202
|
this.#resetTipOrientation();
|
|
179
203
|
}
|
package/popover/types.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export type TSinchPopoverOrientation = 'top-left' | 'top-right' | 'bottom-left'
|
|
|
3
3
|
export type TSinchPopoverProps = {
|
|
4
4
|
/** Allow scrolling of the page when popover is open */
|
|
5
5
|
'allow-scroll'?: boolean;
|
|
6
|
+
/** Let slotted content opt into handling focus when the popover opens */
|
|
7
|
+
'focus-content-on-open'?: boolean;
|
|
6
8
|
/** Open/close state */
|
|
7
9
|
open: boolean;
|
|
8
10
|
/** Orientation, where it *points to* from origin */
|
package/select-menu/index.d.ts
CHANGED
|
@@ -2,8 +2,9 @@ import '../input';
|
|
|
2
2
|
import '../icon';
|
|
3
3
|
import '../text';
|
|
4
4
|
import { NectaryElement } from '../utils';
|
|
5
|
+
import type { Focusable } from '../types';
|
|
5
6
|
export * from './types';
|
|
6
|
-
export declare class SelectMenu extends NectaryElement {
|
|
7
|
+
export declare class SelectMenu extends NectaryElement implements Focusable {
|
|
7
8
|
#private;
|
|
8
9
|
static formAssociated: boolean;
|
|
9
10
|
constructor();
|
|
@@ -24,6 +25,10 @@ export declare class SelectMenu extends NectaryElement {
|
|
|
24
25
|
get multiple(): boolean;
|
|
25
26
|
set searchable(isSearchable: boolean | null | undefined);
|
|
26
27
|
get searchable(): boolean | null | undefined;
|
|
28
|
+
get 'aria-label'(): string | null;
|
|
29
|
+
set 'aria-label'(value: string | null);
|
|
30
|
+
get 'aria-labelledby'(): string | null;
|
|
31
|
+
set 'aria-labelledby'(value: string | null);
|
|
27
32
|
set 'search-autocomplete'(autocomplete: string);
|
|
28
33
|
get 'search-autocomplete'(): string;
|
|
29
34
|
set 'search-placeholder'(placeholder: string);
|
|
@@ -31,4 +36,5 @@ export declare class SelectMenu extends NectaryElement {
|
|
|
31
36
|
set 'search-value'(value: string);
|
|
32
37
|
get 'search-value'(): string;
|
|
33
38
|
get focusable(): boolean;
|
|
39
|
+
focusOnOpen(): void;
|
|
34
40
|
}
|
package/select-menu/index.js
CHANGED
|
@@ -4,14 +4,15 @@ import "../text/index.js";
|
|
|
4
4
|
import { isSelectMenuOption } from "../select-menu-option/utils.js";
|
|
5
5
|
import { subscribeContext } from "../utils/context.js";
|
|
6
6
|
import { unpackCsv, getFirstCsvValue, updateCsv } from "../utils/csv.js";
|
|
7
|
-
import { getBooleanAttribute, updateAttribute, updateExplicitBooleanAttribute, isAttrTrue, getAttribute, updateIntegerAttribute, getIntegerAttribute, updateBooleanAttribute, hasClass, setClass, attrValueToPixels } from "../utils/dom.js";
|
|
7
|
+
import { getBooleanAttribute, updateAttribute, updateExplicitBooleanAttribute, isAttrTrue, getAttribute, updateIntegerAttribute, getIntegerAttribute, updateBooleanAttribute, getDeepActiveElement, hasClass, setClass, attrValueToPixels } from "../utils/dom.js";
|
|
8
8
|
import { defineCustomElement, NectaryElement } from "../utils/element.js";
|
|
9
9
|
import { debounceTimeout } from "../utils/debounce.js";
|
|
10
10
|
import { getReactEventHandler } from "../utils/get-react-event-handler.js";
|
|
11
11
|
import { isTargetEqual } from "../utils/event-target.js";
|
|
12
12
|
import { setFormValue, CSVToFormData } from "../utils/form.js";
|
|
13
|
-
const templateHTML = '<style>:host{display:block;outline:0}#listbox{overflow-y:auto;max-height:var(--sinch-comp-select-menu-font-max-height)}#search{display:none;margin:10px}#search.active{display:block}#search-clear:not(.active){display:none}#not-found{display:flex;align-items:center;justify-content:center;width:100%;height:30px;margin-bottom:10px;pointer-events:none;user-select:none;--sinch-comp-text-font:var(--sinch-comp-select-menu-font-not-found-text);--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-not-found-text-initial)}#not-found:not(.active){display:none}::slotted(.hidden){display:none}::slotted(sinch-title){padding:8px 16px;--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-title-initial)}</style><sinch-input id="search" size="s" placeholder="Search"><sinch-icon icons-version="2" name="magnifying-glass" id="icon-search" slot="icon"></sinch-icon><sinch-button id="search-clear" slot="right"><sinch-icon icons-version="2" name="fa-xmark" slot="icon"></sinch-icon></sinch-button></sinch-input><div id="not-found"><sinch-text type="m">No results</sinch-text></div><div id="listbox" role="
|
|
13
|
+
const templateHTML = '<style>:host{display:block;outline:0}#listbox{overflow-y:auto;max-height:var(--sinch-comp-select-menu-font-max-height)}#search{display:none;margin:10px}#search.active{display:block}#search-clear:not(.active){display:none}#not-found{display:flex;align-items:center;justify-content:center;width:100%;height:30px;margin-bottom:10px;pointer-events:none;user-select:none;--sinch-comp-text-font:var(--sinch-comp-select-menu-font-not-found-text);--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-not-found-text-initial)}#not-found:not(.active){display:none}::slotted(.hidden){display:none}::slotted(sinch-title){padding:8px 16px;--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-title-initial)}</style><sinch-input id="search" size="s" placeholder="Search" aria-label="Search"><sinch-icon icons-version="2" name="magnifying-glass" id="icon-search" slot="icon"></sinch-icon><sinch-button id="search-clear" slot="right"><sinch-icon icons-version="2" name="fa-xmark" slot="icon"></sinch-icon></sinch-button></sinch-input><div id="not-found"><sinch-text type="m">No results</sinch-text></div><div id="listbox" role="listbox" tabindex="-1"><slot></slot></div>';
|
|
14
14
|
const ITEM_HEIGHT = 40;
|
|
15
|
+
const DEFAULT_SEARCH_LABEL = "Search";
|
|
15
16
|
const template = document.createElement("template");
|
|
16
17
|
template.innerHTML = templateHTML;
|
|
17
18
|
class SelectMenu extends NectaryElement {
|
|
@@ -38,13 +39,14 @@ class SelectMenu extends NectaryElement {
|
|
|
38
39
|
this.#searchDebounce = debounceTimeout(200)(this.#updateSearchValue);
|
|
39
40
|
}
|
|
40
41
|
connectedCallback() {
|
|
42
|
+
super.connectedCallback();
|
|
41
43
|
this.#controller = new AbortController();
|
|
42
44
|
const options = {
|
|
43
45
|
signal: this.#controller.signal
|
|
44
46
|
};
|
|
45
|
-
this.setAttribute("role", "
|
|
46
|
-
this.#internals.role = "listbox";
|
|
47
|
+
this.setAttribute("role", "group");
|
|
47
48
|
this.tabIndex = 0;
|
|
49
|
+
this.#syncListboxLabel();
|
|
48
50
|
this.addEventListener("keydown", this.#onListboxKeyDown, options);
|
|
49
51
|
this.addEventListener("focus", this.#onFocus, options);
|
|
50
52
|
this.addEventListener("blur", this.#onListboxBlur, options);
|
|
@@ -89,6 +91,7 @@ class SelectMenu extends NectaryElement {
|
|
|
89
91
|
this.#searchDebounce.cancel();
|
|
90
92
|
this.#controller.abort();
|
|
91
93
|
this.#controller = null;
|
|
94
|
+
super.disconnectedCallback();
|
|
92
95
|
}
|
|
93
96
|
formAssociatedCallback() {
|
|
94
97
|
setFormValue(this.#internals, CSVToFormData(this.name, this.value));
|
|
@@ -113,6 +116,8 @@ class SelectMenu extends NectaryElement {
|
|
|
113
116
|
"rows",
|
|
114
117
|
"multiple",
|
|
115
118
|
"searchable",
|
|
119
|
+
"aria-label",
|
|
120
|
+
"aria-labelledby",
|
|
116
121
|
// eslint-disable-next-line @nectary/observed-attribute-accessor -- baseline backlog: missing set searchValue
|
|
117
122
|
"search-value",
|
|
118
123
|
// eslint-disable-next-line @nectary/observed-attribute-accessor -- baseline backlog: missing set searchPlaceholder
|
|
@@ -126,17 +131,24 @@ class SelectMenu extends NectaryElement {
|
|
|
126
131
|
case "multiple": {
|
|
127
132
|
this.#onValueChange(this.value);
|
|
128
133
|
updateExplicitBooleanAttribute(
|
|
129
|
-
this,
|
|
134
|
+
this.#$listbox,
|
|
130
135
|
"aria-multiselectable",
|
|
131
136
|
isAttrTrue(newVal)
|
|
132
137
|
);
|
|
133
|
-
this.#internals.ariaMultiSelectable = isAttrTrue(newVal).toString();
|
|
134
138
|
break;
|
|
135
139
|
}
|
|
136
140
|
case "searchable": {
|
|
137
141
|
this.#onOptionSlotChange();
|
|
138
142
|
break;
|
|
139
143
|
}
|
|
144
|
+
case "aria-label": {
|
|
145
|
+
this.#syncListboxLabel();
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case "aria-labelledby": {
|
|
149
|
+
this.#syncListboxLabel();
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
140
152
|
case "search-autocomplete": {
|
|
141
153
|
updateAttribute(this.#$search, "autocomplete", newVal);
|
|
142
154
|
break;
|
|
@@ -150,7 +162,7 @@ class SelectMenu extends NectaryElement {
|
|
|
150
162
|
break;
|
|
151
163
|
}
|
|
152
164
|
case "search-placeholder": {
|
|
153
|
-
|
|
165
|
+
this.#updateSearchPlaceholder(newVal);
|
|
154
166
|
break;
|
|
155
167
|
}
|
|
156
168
|
case "search-value": {
|
|
@@ -198,6 +210,18 @@ class SelectMenu extends NectaryElement {
|
|
|
198
210
|
const searchableAttribute = this.getAttribute("searchable");
|
|
199
211
|
return searchableAttribute === null ? searchableAttribute : isAttrTrue(searchableAttribute);
|
|
200
212
|
}
|
|
213
|
+
get "aria-label"() {
|
|
214
|
+
return getAttribute(this, "aria-label");
|
|
215
|
+
}
|
|
216
|
+
set "aria-label"(value) {
|
|
217
|
+
updateAttribute(this, "aria-label", value);
|
|
218
|
+
}
|
|
219
|
+
get "aria-labelledby"() {
|
|
220
|
+
return getAttribute(this, "aria-labelledby");
|
|
221
|
+
}
|
|
222
|
+
set "aria-labelledby"(value) {
|
|
223
|
+
updateAttribute(this, "aria-labelledby", value);
|
|
224
|
+
}
|
|
201
225
|
set "search-autocomplete"(autocomplete) {
|
|
202
226
|
updateAttribute(this.#$search, "autocomplete", autocomplete);
|
|
203
227
|
}
|
|
@@ -205,7 +229,7 @@ class SelectMenu extends NectaryElement {
|
|
|
205
229
|
return getAttribute(this.#$search, "autocomplete", "");
|
|
206
230
|
}
|
|
207
231
|
set "search-placeholder"(placeholder) {
|
|
208
|
-
|
|
232
|
+
this.#updateSearchPlaceholder(placeholder);
|
|
209
233
|
}
|
|
210
234
|
get "search-placeholder"() {
|
|
211
235
|
return getAttribute(this.#$search, "placeholder", "");
|
|
@@ -220,12 +244,22 @@ class SelectMenu extends NectaryElement {
|
|
|
220
244
|
get focusable() {
|
|
221
245
|
return true;
|
|
222
246
|
}
|
|
247
|
+
focusOnOpen() {
|
|
248
|
+
this.#focusActiveSearch();
|
|
249
|
+
}
|
|
223
250
|
#onFocus = () => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.#$search.focus();
|
|
251
|
+
if (getDeepActiveElement(this.ownerDocument) !== this) {
|
|
252
|
+
return;
|
|
227
253
|
}
|
|
254
|
+
this.#focusActiveSearch();
|
|
228
255
|
};
|
|
256
|
+
#focusActiveSearch() {
|
|
257
|
+
if (!this.isDomConnected || !hasClass(this.#$search, "active")) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this.#$search.getBoundingClientRect();
|
|
261
|
+
this.#$search.focus();
|
|
262
|
+
}
|
|
229
263
|
#onListboxBlur = () => {
|
|
230
264
|
this.#selectOption(null);
|
|
231
265
|
};
|
|
@@ -290,7 +324,7 @@ class SelectMenu extends NectaryElement {
|
|
|
290
324
|
#onContextVisibility = (e) => {
|
|
291
325
|
if (e.detail) {
|
|
292
326
|
this.#selectOption(this.#findCheckedOption());
|
|
293
|
-
this.#
|
|
327
|
+
this.#focusActiveSearch();
|
|
294
328
|
} else {
|
|
295
329
|
this.#selectOption(null);
|
|
296
330
|
}
|
|
@@ -328,7 +362,30 @@ class SelectMenu extends NectaryElement {
|
|
|
328
362
|
this.#dispatchChangeEvent(option);
|
|
329
363
|
}
|
|
330
364
|
};
|
|
365
|
+
#syncListboxLabel = () => {
|
|
366
|
+
const labelledby = getAttribute(this, "aria-labelledby");
|
|
367
|
+
const label = labelledby === null ? getAttribute(this, "aria-label") : null;
|
|
368
|
+
updateAttribute(this.#$listbox, "aria-labelledby", labelledby);
|
|
369
|
+
updateAttribute(this.#$listbox, "aria-label", label);
|
|
370
|
+
};
|
|
371
|
+
#updateSearchPlaceholder(placeholder) {
|
|
372
|
+
const currentPlaceholder = getAttribute(this.#$search, "placeholder");
|
|
373
|
+
const currentLabel = getAttribute(this.#$search, "aria-label");
|
|
374
|
+
const isManagedLabel = currentLabel === null || currentLabel === DEFAULT_SEARCH_LABEL || currentLabel === currentPlaceholder;
|
|
375
|
+
updateAttribute(this.#$search, "placeholder", placeholder);
|
|
376
|
+
if (isManagedLabel) {
|
|
377
|
+
updateAttribute(this.#$search, "aria-label", placeholder === null || placeholder === "" ? DEFAULT_SEARCH_LABEL : placeholder);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
#syncListboxChildren = () => {
|
|
381
|
+
for (const $element of this.#$optionSlot.assignedElements()) {
|
|
382
|
+
if (!isSelectMenuOption($element) && getAttribute($element, "aria-hidden") !== "true") {
|
|
383
|
+
updateAttribute($element, "aria-hidden", "true");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
331
387
|
#onOptionSlotChange = () => {
|
|
388
|
+
this.#syncListboxChildren();
|
|
332
389
|
if (this.hasAttribute("rows")) {
|
|
333
390
|
this.#updateListboxMaxHeight();
|
|
334
391
|
}
|
package/select-menu/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NectaryComponentReactByType, NectaryComponentVanillaByType, NectaryComponentReact, NectaryComponentVanilla } from '../types';
|
|
1
|
+
import type { Focusable, NectaryComponentReactByType, NectaryComponentVanillaByType, NectaryComponentReact, NectaryComponentVanilla } from '../types';
|
|
2
2
|
export type TSinchSelectMenuProps = {
|
|
3
3
|
/** Identification for uncontrolled form submissions */
|
|
4
4
|
name?: string;
|
|
@@ -18,6 +18,8 @@ export type TSinchSelectMenuProps = {
|
|
|
18
18
|
'search-placeholder'?: string;
|
|
19
19
|
/** Label that is used for a11y */
|
|
20
20
|
'aria-label': string;
|
|
21
|
+
/** Element ID reference that labels the listbox */
|
|
22
|
+
'aria-labelledby'?: string;
|
|
21
23
|
};
|
|
22
24
|
export type TSinchSelectMenuEvents = {
|
|
23
25
|
/** Change value handler */
|
|
@@ -33,10 +35,12 @@ export type TSinchSelectMenuStyle = {
|
|
|
33
35
|
'--sinch-comp-select-menu-font-not-found-text'?: string;
|
|
34
36
|
'--sinch-comp-select-menu-font-max-height'?: string;
|
|
35
37
|
};
|
|
38
|
+
export type TSinchSelectMenuMethods = Focusable;
|
|
36
39
|
export type TSinchSelectMenu = {
|
|
37
40
|
props: TSinchSelectMenuProps;
|
|
38
41
|
events: TSinchSelectMenuEvents;
|
|
39
42
|
style: TSinchSelectMenuStyle;
|
|
43
|
+
methods: TSinchSelectMenuMethods;
|
|
40
44
|
};
|
|
41
45
|
export type TSinchSelectMenuElement = NectaryComponentVanillaByType<TSinchSelectMenu>;
|
|
42
46
|
export type TSinchSelectMenuReact = NectaryComponentReactByType<TSinchSelectMenu>;
|
package/tooltip/index.js
CHANGED
|
@@ -94,6 +94,7 @@ class Tooltip extends NectaryElement {
|
|
|
94
94
|
updateAttribute(this.#$pop, "orientation", getPopOrientation(this.orientation));
|
|
95
95
|
updateBooleanAttribute(this.#$pop, "hide-outside-viewport", !this.showOutsideViewport);
|
|
96
96
|
updateBooleanAttribute(this.#$pop, "disable-focus-restore", true);
|
|
97
|
+
this.#updatePopAriaLabel();
|
|
97
98
|
this.#updateText();
|
|
98
99
|
}
|
|
99
100
|
disconnectedCallback() {
|
|
@@ -139,6 +140,7 @@ class Tooltip extends NectaryElement {
|
|
|
139
140
|
switch (name) {
|
|
140
141
|
case "text": {
|
|
141
142
|
this.#updateText();
|
|
143
|
+
this.#updatePopAriaLabel();
|
|
142
144
|
break;
|
|
143
145
|
}
|
|
144
146
|
case "orientation": {
|
|
@@ -161,7 +163,11 @@ class Tooltip extends NectaryElement {
|
|
|
161
163
|
}
|
|
162
164
|
case "aria-label":
|
|
163
165
|
case "aria-description": {
|
|
164
|
-
|
|
166
|
+
if (name === "aria-label") {
|
|
167
|
+
this.#updatePopAriaLabel();
|
|
168
|
+
} else {
|
|
169
|
+
updateAttribute(this.#$pop, name, newVal);
|
|
170
|
+
}
|
|
165
171
|
break;
|
|
166
172
|
}
|
|
167
173
|
case "show-outside-viewport": {
|
|
@@ -737,6 +743,13 @@ class Tooltip extends NectaryElement {
|
|
|
737
743
|
this.#subscribeMouseEnterEvent();
|
|
738
744
|
}
|
|
739
745
|
}
|
|
746
|
+
#updatePopAriaLabel() {
|
|
747
|
+
const rawAriaLabel = getAttribute(this, "aria-label");
|
|
748
|
+
const explicitAriaLabel = rawAriaLabel !== null && rawAriaLabel.trim().length > 0 ? rawAriaLabel.trim() : null;
|
|
749
|
+
const fallbackAriaLabel = this.text ?? "";
|
|
750
|
+
const ariaLabel = explicitAriaLabel ?? (fallbackAriaLabel.length === 0 ? null : fallbackAriaLabel);
|
|
751
|
+
updateAttribute(this.#$pop, "aria-label", ariaLabel);
|
|
752
|
+
}
|
|
740
753
|
#subscribeMouseEnterEvent() {
|
|
741
754
|
if (!this.isDomConnected || this.#isSubscribed) {
|
|
742
755
|
return;
|
package/tooltip/types.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ export type TSinchTooltipProps = {
|
|
|
20
20
|
* to true can have unexpected behavior in dialogs
|
|
21
21
|
*/
|
|
22
22
|
'allow-scroll'?: boolean;
|
|
23
|
+
/** Overrides the accessible name of the tooltip popup. Falls back to `text` when absent or empty. */
|
|
24
|
+
'aria-label'?: string;
|
|
25
|
+
/** Sets the aria-description on the tooltip popup */
|
|
26
|
+
'aria-description'?: string;
|
|
23
27
|
};
|
|
24
28
|
export type TSinchTooltipEvents = {
|
|
25
29
|
/** Show event handler */
|
package/types.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export type TRect = {
|
|
|
6
6
|
width: number;
|
|
7
7
|
height: number;
|
|
8
8
|
};
|
|
9
|
+
export interface Focusable {
|
|
10
|
+
/** Called by containers that let slotted content choose where focus lands after opening. */
|
|
11
|
+
focusOnOpen(): void;
|
|
12
|
+
}
|
|
9
13
|
export type NectaryComponentVanillaByType<T extends NectaryComponentMap[keyof NectaryComponentMap]> = Omit<HTMLElement, 'addEventListener' | 'removeEventListener'> & ExtendEventListeners<Required<SafeSelect<T, 'events'>>> & SetAttributes<Required<RemoveReadonly<SafeSelect<T, 'props'>>>> & Required<CamelCaseify<SafeSelect<T, 'props'>>> & Required<SafeSelect<T, 'methods'>>;
|
|
10
14
|
export type NectaryComponentReactByType<T extends NectaryComponentMap[keyof NectaryComponentMap]> = PatchReactEvents<WebComponentReactBaseProp<NectaryComponentVanillaByType<T>>, ReactifyEvents<SafeSelect<T, 'events'>>> & RemoveReadonly<SafeSelect<T, 'props'>> & {
|
|
11
15
|
style?: Partial<SafeSelect<T, 'style'>> & CSSProperties;
|