@nectary/components 5.42.1 → 5.42.2
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 +93 -12
- package/input/index.js +1 -1
- package/package.json +1 -1
- 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/types.d.ts +4 -0
package/bundle.js
CHANGED
|
@@ -6695,7 +6695,7 @@ class Input extends NectaryElement {
|
|
|
6695
6695
|
break;
|
|
6696
6696
|
}
|
|
6697
6697
|
case "aria-label": {
|
|
6698
|
-
this.#$input
|
|
6698
|
+
updateAttribute(this.#$input, "aria-label", newVal);
|
|
6699
6699
|
break;
|
|
6700
6700
|
}
|
|
6701
6701
|
}
|
|
@@ -7219,6 +7219,9 @@ const getPopOrientation = (orientation) => {
|
|
|
7219
7219
|
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
7220
|
const TIP_SIZE = 16;
|
|
7221
7221
|
const template$S = document.createElement("template");
|
|
7222
|
+
const isFocusableContent = (element) => {
|
|
7223
|
+
return typeof element?.focusOnOpen === "function";
|
|
7224
|
+
};
|
|
7222
7225
|
template$S.innerHTML = templateHTML$S;
|
|
7223
7226
|
class Popover extends NectaryElement {
|
|
7224
7227
|
#$pop;
|
|
@@ -7276,6 +7279,7 @@ class Popover extends NectaryElement {
|
|
|
7276
7279
|
"open",
|
|
7277
7280
|
"modal",
|
|
7278
7281
|
"allow-scroll",
|
|
7282
|
+
"focus-content-on-open",
|
|
7279
7283
|
"tip",
|
|
7280
7284
|
"aria-label",
|
|
7281
7285
|
"aria-description"
|
|
@@ -7306,6 +7310,10 @@ class Popover extends NectaryElement {
|
|
|
7306
7310
|
updateBooleanAttribute(this, name, isAttrTrue(newVal));
|
|
7307
7311
|
break;
|
|
7308
7312
|
}
|
|
7313
|
+
case "focus-content-on-open": {
|
|
7314
|
+
updateBooleanAttribute(this, name, isAttrTrue(newVal));
|
|
7315
|
+
break;
|
|
7316
|
+
}
|
|
7309
7317
|
case "modal":
|
|
7310
7318
|
case "open": {
|
|
7311
7319
|
updateAttribute(this.#$pop, name, newVal);
|
|
@@ -7335,6 +7343,12 @@ class Popover extends NectaryElement {
|
|
|
7335
7343
|
get allowScroll() {
|
|
7336
7344
|
return getBooleanAttribute(this, "allow-scroll");
|
|
7337
7345
|
}
|
|
7346
|
+
set focusContentOnOpen(shouldFocus) {
|
|
7347
|
+
updateBooleanAttribute(this, "focus-content-on-open", shouldFocus);
|
|
7348
|
+
}
|
|
7349
|
+
get focusContentOnOpen() {
|
|
7350
|
+
return getBooleanAttribute(this, "focus-content-on-open");
|
|
7351
|
+
}
|
|
7338
7352
|
set open(isOpen) {
|
|
7339
7353
|
updateBooleanAttribute(this, "open", isOpen);
|
|
7340
7354
|
}
|
|
@@ -7369,6 +7383,12 @@ class Popover extends NectaryElement {
|
|
|
7369
7383
|
}
|
|
7370
7384
|
return elements[0];
|
|
7371
7385
|
}
|
|
7386
|
+
#focusSlottedContentOnOpen() {
|
|
7387
|
+
const slottedContent = this.#getFirstAssignedElementInSlot(this.#$contentSlot);
|
|
7388
|
+
if (isFocusableContent(slottedContent)) {
|
|
7389
|
+
slottedContent.focusOnOpen();
|
|
7390
|
+
}
|
|
7391
|
+
}
|
|
7372
7392
|
#onPopClose = () => {
|
|
7373
7393
|
this.#dispatchCloseEvent();
|
|
7374
7394
|
};
|
|
@@ -7385,6 +7405,10 @@ class Popover extends NectaryElement {
|
|
|
7385
7405
|
if (e.detail) {
|
|
7386
7406
|
this.#updateTipOrientation();
|
|
7387
7407
|
this.#updateContentMaxWidth();
|
|
7408
|
+
if (this.focusContentOnOpen) {
|
|
7409
|
+
this.#$content.getBoundingClientRect();
|
|
7410
|
+
this.#focusSlottedContentOnOpen();
|
|
7411
|
+
}
|
|
7388
7412
|
} else {
|
|
7389
7413
|
this.#resetTipOrientation();
|
|
7390
7414
|
}
|
|
@@ -15309,8 +15333,9 @@ class SelectMenuOption extends NectaryElement {
|
|
|
15309
15333
|
}
|
|
15310
15334
|
defineCustomElement("sinch-select-menu-option", SelectMenuOption);
|
|
15311
15335
|
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="
|
|
15336
|
+
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
15337
|
const ITEM_HEIGHT = 40;
|
|
15338
|
+
const DEFAULT_SEARCH_LABEL = "Search";
|
|
15314
15339
|
const template$h = document.createElement("template");
|
|
15315
15340
|
template$h.innerHTML = templateHTML$h;
|
|
15316
15341
|
class SelectMenu extends NectaryElement {
|
|
@@ -15337,13 +15362,14 @@ class SelectMenu extends NectaryElement {
|
|
|
15337
15362
|
this.#searchDebounce = debounceTimeout(200)(this.#updateSearchValue);
|
|
15338
15363
|
}
|
|
15339
15364
|
connectedCallback() {
|
|
15365
|
+
super.connectedCallback();
|
|
15340
15366
|
this.#controller = new AbortController();
|
|
15341
15367
|
const options = {
|
|
15342
15368
|
signal: this.#controller.signal
|
|
15343
15369
|
};
|
|
15344
|
-
this.setAttribute("role", "
|
|
15345
|
-
this.#internals.role = "listbox";
|
|
15370
|
+
this.setAttribute("role", "group");
|
|
15346
15371
|
this.tabIndex = 0;
|
|
15372
|
+
this.#syncListboxLabel();
|
|
15347
15373
|
this.addEventListener("keydown", this.#onListboxKeyDown, options);
|
|
15348
15374
|
this.addEventListener("focus", this.#onFocus, options);
|
|
15349
15375
|
this.addEventListener("blur", this.#onListboxBlur, options);
|
|
@@ -15388,6 +15414,7 @@ class SelectMenu extends NectaryElement {
|
|
|
15388
15414
|
this.#searchDebounce.cancel();
|
|
15389
15415
|
this.#controller.abort();
|
|
15390
15416
|
this.#controller = null;
|
|
15417
|
+
super.disconnectedCallback();
|
|
15391
15418
|
}
|
|
15392
15419
|
formAssociatedCallback() {
|
|
15393
15420
|
setFormValue(this.#internals, CSVToFormData(this.name, this.value));
|
|
@@ -15412,6 +15439,8 @@ class SelectMenu extends NectaryElement {
|
|
|
15412
15439
|
"rows",
|
|
15413
15440
|
"multiple",
|
|
15414
15441
|
"searchable",
|
|
15442
|
+
"aria-label",
|
|
15443
|
+
"aria-labelledby",
|
|
15415
15444
|
// eslint-disable-next-line @nectary/observed-attribute-accessor -- baseline backlog: missing set searchValue
|
|
15416
15445
|
"search-value",
|
|
15417
15446
|
// eslint-disable-next-line @nectary/observed-attribute-accessor -- baseline backlog: missing set searchPlaceholder
|
|
@@ -15425,17 +15454,24 @@ class SelectMenu extends NectaryElement {
|
|
|
15425
15454
|
case "multiple": {
|
|
15426
15455
|
this.#onValueChange(this.value);
|
|
15427
15456
|
updateExplicitBooleanAttribute(
|
|
15428
|
-
this,
|
|
15457
|
+
this.#$listbox,
|
|
15429
15458
|
"aria-multiselectable",
|
|
15430
15459
|
isAttrTrue(newVal)
|
|
15431
15460
|
);
|
|
15432
|
-
this.#internals.ariaMultiSelectable = isAttrTrue(newVal).toString();
|
|
15433
15461
|
break;
|
|
15434
15462
|
}
|
|
15435
15463
|
case "searchable": {
|
|
15436
15464
|
this.#onOptionSlotChange();
|
|
15437
15465
|
break;
|
|
15438
15466
|
}
|
|
15467
|
+
case "aria-label": {
|
|
15468
|
+
this.#syncListboxLabel();
|
|
15469
|
+
break;
|
|
15470
|
+
}
|
|
15471
|
+
case "aria-labelledby": {
|
|
15472
|
+
this.#syncListboxLabel();
|
|
15473
|
+
break;
|
|
15474
|
+
}
|
|
15439
15475
|
case "search-autocomplete": {
|
|
15440
15476
|
updateAttribute(this.#$search, "autocomplete", newVal);
|
|
15441
15477
|
break;
|
|
@@ -15449,7 +15485,7 @@ class SelectMenu extends NectaryElement {
|
|
|
15449
15485
|
break;
|
|
15450
15486
|
}
|
|
15451
15487
|
case "search-placeholder": {
|
|
15452
|
-
|
|
15488
|
+
this.#updateSearchPlaceholder(newVal);
|
|
15453
15489
|
break;
|
|
15454
15490
|
}
|
|
15455
15491
|
case "search-value": {
|
|
@@ -15497,6 +15533,18 @@ class SelectMenu extends NectaryElement {
|
|
|
15497
15533
|
const searchableAttribute = this.getAttribute("searchable");
|
|
15498
15534
|
return searchableAttribute === null ? searchableAttribute : isAttrTrue(searchableAttribute);
|
|
15499
15535
|
}
|
|
15536
|
+
get "aria-label"() {
|
|
15537
|
+
return getAttribute(this, "aria-label");
|
|
15538
|
+
}
|
|
15539
|
+
set "aria-label"(value) {
|
|
15540
|
+
updateAttribute(this, "aria-label", value);
|
|
15541
|
+
}
|
|
15542
|
+
get "aria-labelledby"() {
|
|
15543
|
+
return getAttribute(this, "aria-labelledby");
|
|
15544
|
+
}
|
|
15545
|
+
set "aria-labelledby"(value) {
|
|
15546
|
+
updateAttribute(this, "aria-labelledby", value);
|
|
15547
|
+
}
|
|
15500
15548
|
set "search-autocomplete"(autocomplete) {
|
|
15501
15549
|
updateAttribute(this.#$search, "autocomplete", autocomplete);
|
|
15502
15550
|
}
|
|
@@ -15504,7 +15552,7 @@ class SelectMenu extends NectaryElement {
|
|
|
15504
15552
|
return getAttribute(this.#$search, "autocomplete", "");
|
|
15505
15553
|
}
|
|
15506
15554
|
set "search-placeholder"(placeholder) {
|
|
15507
|
-
|
|
15555
|
+
this.#updateSearchPlaceholder(placeholder);
|
|
15508
15556
|
}
|
|
15509
15557
|
get "search-placeholder"() {
|
|
15510
15558
|
return getAttribute(this.#$search, "placeholder", "");
|
|
@@ -15519,12 +15567,22 @@ class SelectMenu extends NectaryElement {
|
|
|
15519
15567
|
get focusable() {
|
|
15520
15568
|
return true;
|
|
15521
15569
|
}
|
|
15570
|
+
focusOnOpen() {
|
|
15571
|
+
this.#focusActiveSearch();
|
|
15572
|
+
}
|
|
15522
15573
|
#onFocus = () => {
|
|
15523
|
-
|
|
15524
|
-
|
|
15525
|
-
this.#$search.focus();
|
|
15574
|
+
if (getDeepActiveElement(this.ownerDocument) !== this) {
|
|
15575
|
+
return;
|
|
15526
15576
|
}
|
|
15577
|
+
this.#focusActiveSearch();
|
|
15527
15578
|
};
|
|
15579
|
+
#focusActiveSearch() {
|
|
15580
|
+
if (!this.isDomConnected || !hasClass(this.#$search, "active")) {
|
|
15581
|
+
return;
|
|
15582
|
+
}
|
|
15583
|
+
this.#$search.getBoundingClientRect();
|
|
15584
|
+
this.#$search.focus();
|
|
15585
|
+
}
|
|
15528
15586
|
#onListboxBlur = () => {
|
|
15529
15587
|
this.#selectOption(null);
|
|
15530
15588
|
};
|
|
@@ -15589,7 +15647,7 @@ class SelectMenu extends NectaryElement {
|
|
|
15589
15647
|
#onContextVisibility = (e) => {
|
|
15590
15648
|
if (e.detail) {
|
|
15591
15649
|
this.#selectOption(this.#findCheckedOption());
|
|
15592
|
-
this.#
|
|
15650
|
+
this.#focusActiveSearch();
|
|
15593
15651
|
} else {
|
|
15594
15652
|
this.#selectOption(null);
|
|
15595
15653
|
}
|
|
@@ -15627,7 +15685,30 @@ class SelectMenu extends NectaryElement {
|
|
|
15627
15685
|
this.#dispatchChangeEvent(option);
|
|
15628
15686
|
}
|
|
15629
15687
|
};
|
|
15688
|
+
#syncListboxLabel = () => {
|
|
15689
|
+
const labelledby = getAttribute(this, "aria-labelledby");
|
|
15690
|
+
const label = labelledby === null ? getAttribute(this, "aria-label") : null;
|
|
15691
|
+
updateAttribute(this.#$listbox, "aria-labelledby", labelledby);
|
|
15692
|
+
updateAttribute(this.#$listbox, "aria-label", label);
|
|
15693
|
+
};
|
|
15694
|
+
#updateSearchPlaceholder(placeholder) {
|
|
15695
|
+
const currentPlaceholder = getAttribute(this.#$search, "placeholder");
|
|
15696
|
+
const currentLabel = getAttribute(this.#$search, "aria-label");
|
|
15697
|
+
const isManagedLabel = currentLabel === null || currentLabel === DEFAULT_SEARCH_LABEL || currentLabel === currentPlaceholder;
|
|
15698
|
+
updateAttribute(this.#$search, "placeholder", placeholder);
|
|
15699
|
+
if (isManagedLabel) {
|
|
15700
|
+
updateAttribute(this.#$search, "aria-label", placeholder === null || placeholder === "" ? DEFAULT_SEARCH_LABEL : placeholder);
|
|
15701
|
+
}
|
|
15702
|
+
}
|
|
15703
|
+
#syncListboxChildren = () => {
|
|
15704
|
+
for (const $element of this.#$optionSlot.assignedElements()) {
|
|
15705
|
+
if (!isSelectMenuOption($element) && getAttribute($element, "aria-hidden") !== "true") {
|
|
15706
|
+
updateAttribute($element, "aria-hidden", "true");
|
|
15707
|
+
}
|
|
15708
|
+
}
|
|
15709
|
+
};
|
|
15630
15710
|
#onOptionSlotChange = () => {
|
|
15711
|
+
this.#syncListboxChildren();
|
|
15631
15712
|
if (this.hasAttribute("rows")) {
|
|
15632
15713
|
this.#updateListboxMaxHeight();
|
|
15633
15714
|
}
|
package/input/index.js
CHANGED
package/package.json
CHANGED
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/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;
|