@nectary/components 5.42.0 → 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 +95 -13
- 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/utils/markdown.js +2 -1
package/bundle.js
CHANGED
|
@@ -1528,7 +1528,8 @@ const regStrikethrough = new RegExp("(?<!\\\\)~~(?<strike>.+?)(?<!\\\\)~~");
|
|
|
1528
1528
|
const regButtonPlaceholder = new RegExp("(?<!\\\\)\\[\\[(?<button>[a-zA-Z0-9_-]+)\\]\\]");
|
|
1529
1529
|
const regLink = new RegExp("(?<!\\\\)!?\\[(?<linktext>[^\\]]*?)\\]\\((?<linkhref>[^)]+?)\\)(\\{(?<linkattrs>[^)]+?)\\})?");
|
|
1530
1530
|
const regChip = new RegExp("(?<!\\\\)\\{\\{(?<chip>[a-zA-Z0-9_-]+)\\}\\}");
|
|
1531
|
-
const
|
|
1531
|
+
const regEmojiSeq = "(?![0-9#*])\\p{Emoji}(?:\\uFE0F|\\p{Emoji_Modifier})?\\u20E3?";
|
|
1532
|
+
const regEmoji = new RegExp(`(?<emoji>\\p{RI}\\p{RI}|${regEmojiSeq}(?:\\u200D${regEmojiSeq})*)`, "u");
|
|
1532
1533
|
const regUList = /^(?<indent>[\t ]*?)[*+-][\t ]+(?<ultext>.*?)[\t ]*?$/;
|
|
1533
1534
|
const regOList = /^(?<indent>[\t ]*?)\d+\.[\t ]+(?<oltext>.*?)[\t ]*?$/;
|
|
1534
1535
|
const regEscapedChars = /\\(?<escaped>[\\\*_\[\]`~\{\}])/;
|
|
@@ -6694,7 +6695,7 @@ class Input extends NectaryElement {
|
|
|
6694
6695
|
break;
|
|
6695
6696
|
}
|
|
6696
6697
|
case "aria-label": {
|
|
6697
|
-
this.#$input
|
|
6698
|
+
updateAttribute(this.#$input, "aria-label", newVal);
|
|
6698
6699
|
break;
|
|
6699
6700
|
}
|
|
6700
6701
|
}
|
|
@@ -7218,6 +7219,9 @@ const getPopOrientation = (orientation) => {
|
|
|
7218
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>';
|
|
7219
7220
|
const TIP_SIZE = 16;
|
|
7220
7221
|
const template$S = document.createElement("template");
|
|
7222
|
+
const isFocusableContent = (element) => {
|
|
7223
|
+
return typeof element?.focusOnOpen === "function";
|
|
7224
|
+
};
|
|
7221
7225
|
template$S.innerHTML = templateHTML$S;
|
|
7222
7226
|
class Popover extends NectaryElement {
|
|
7223
7227
|
#$pop;
|
|
@@ -7275,6 +7279,7 @@ class Popover extends NectaryElement {
|
|
|
7275
7279
|
"open",
|
|
7276
7280
|
"modal",
|
|
7277
7281
|
"allow-scroll",
|
|
7282
|
+
"focus-content-on-open",
|
|
7278
7283
|
"tip",
|
|
7279
7284
|
"aria-label",
|
|
7280
7285
|
"aria-description"
|
|
@@ -7305,6 +7310,10 @@ class Popover extends NectaryElement {
|
|
|
7305
7310
|
updateBooleanAttribute(this, name, isAttrTrue(newVal));
|
|
7306
7311
|
break;
|
|
7307
7312
|
}
|
|
7313
|
+
case "focus-content-on-open": {
|
|
7314
|
+
updateBooleanAttribute(this, name, isAttrTrue(newVal));
|
|
7315
|
+
break;
|
|
7316
|
+
}
|
|
7308
7317
|
case "modal":
|
|
7309
7318
|
case "open": {
|
|
7310
7319
|
updateAttribute(this.#$pop, name, newVal);
|
|
@@ -7334,6 +7343,12 @@ class Popover extends NectaryElement {
|
|
|
7334
7343
|
get allowScroll() {
|
|
7335
7344
|
return getBooleanAttribute(this, "allow-scroll");
|
|
7336
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
|
+
}
|
|
7337
7352
|
set open(isOpen) {
|
|
7338
7353
|
updateBooleanAttribute(this, "open", isOpen);
|
|
7339
7354
|
}
|
|
@@ -7368,6 +7383,12 @@ class Popover extends NectaryElement {
|
|
|
7368
7383
|
}
|
|
7369
7384
|
return elements[0];
|
|
7370
7385
|
}
|
|
7386
|
+
#focusSlottedContentOnOpen() {
|
|
7387
|
+
const slottedContent = this.#getFirstAssignedElementInSlot(this.#$contentSlot);
|
|
7388
|
+
if (isFocusableContent(slottedContent)) {
|
|
7389
|
+
slottedContent.focusOnOpen();
|
|
7390
|
+
}
|
|
7391
|
+
}
|
|
7371
7392
|
#onPopClose = () => {
|
|
7372
7393
|
this.#dispatchCloseEvent();
|
|
7373
7394
|
};
|
|
@@ -7384,6 +7405,10 @@ class Popover extends NectaryElement {
|
|
|
7384
7405
|
if (e.detail) {
|
|
7385
7406
|
this.#updateTipOrientation();
|
|
7386
7407
|
this.#updateContentMaxWidth();
|
|
7408
|
+
if (this.focusContentOnOpen) {
|
|
7409
|
+
this.#$content.getBoundingClientRect();
|
|
7410
|
+
this.#focusSlottedContentOnOpen();
|
|
7411
|
+
}
|
|
7387
7412
|
} else {
|
|
7388
7413
|
this.#resetTipOrientation();
|
|
7389
7414
|
}
|
|
@@ -15308,8 +15333,9 @@ class SelectMenuOption extends NectaryElement {
|
|
|
15308
15333
|
}
|
|
15309
15334
|
defineCustomElement("sinch-select-menu-option", SelectMenuOption);
|
|
15310
15335
|
const isSelectMenuOption = (el) => el.localName === "sinch-select-menu-option";
|
|
15311
|
-
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>';
|
|
15312
15337
|
const ITEM_HEIGHT = 40;
|
|
15338
|
+
const DEFAULT_SEARCH_LABEL = "Search";
|
|
15313
15339
|
const template$h = document.createElement("template");
|
|
15314
15340
|
template$h.innerHTML = templateHTML$h;
|
|
15315
15341
|
class SelectMenu extends NectaryElement {
|
|
@@ -15336,13 +15362,14 @@ class SelectMenu extends NectaryElement {
|
|
|
15336
15362
|
this.#searchDebounce = debounceTimeout(200)(this.#updateSearchValue);
|
|
15337
15363
|
}
|
|
15338
15364
|
connectedCallback() {
|
|
15365
|
+
super.connectedCallback();
|
|
15339
15366
|
this.#controller = new AbortController();
|
|
15340
15367
|
const options = {
|
|
15341
15368
|
signal: this.#controller.signal
|
|
15342
15369
|
};
|
|
15343
|
-
this.setAttribute("role", "
|
|
15344
|
-
this.#internals.role = "listbox";
|
|
15370
|
+
this.setAttribute("role", "group");
|
|
15345
15371
|
this.tabIndex = 0;
|
|
15372
|
+
this.#syncListboxLabel();
|
|
15346
15373
|
this.addEventListener("keydown", this.#onListboxKeyDown, options);
|
|
15347
15374
|
this.addEventListener("focus", this.#onFocus, options);
|
|
15348
15375
|
this.addEventListener("blur", this.#onListboxBlur, options);
|
|
@@ -15387,6 +15414,7 @@ class SelectMenu extends NectaryElement {
|
|
|
15387
15414
|
this.#searchDebounce.cancel();
|
|
15388
15415
|
this.#controller.abort();
|
|
15389
15416
|
this.#controller = null;
|
|
15417
|
+
super.disconnectedCallback();
|
|
15390
15418
|
}
|
|
15391
15419
|
formAssociatedCallback() {
|
|
15392
15420
|
setFormValue(this.#internals, CSVToFormData(this.name, this.value));
|
|
@@ -15411,6 +15439,8 @@ class SelectMenu extends NectaryElement {
|
|
|
15411
15439
|
"rows",
|
|
15412
15440
|
"multiple",
|
|
15413
15441
|
"searchable",
|
|
15442
|
+
"aria-label",
|
|
15443
|
+
"aria-labelledby",
|
|
15414
15444
|
// eslint-disable-next-line @nectary/observed-attribute-accessor -- baseline backlog: missing set searchValue
|
|
15415
15445
|
"search-value",
|
|
15416
15446
|
// eslint-disable-next-line @nectary/observed-attribute-accessor -- baseline backlog: missing set searchPlaceholder
|
|
@@ -15424,17 +15454,24 @@ class SelectMenu extends NectaryElement {
|
|
|
15424
15454
|
case "multiple": {
|
|
15425
15455
|
this.#onValueChange(this.value);
|
|
15426
15456
|
updateExplicitBooleanAttribute(
|
|
15427
|
-
this,
|
|
15457
|
+
this.#$listbox,
|
|
15428
15458
|
"aria-multiselectable",
|
|
15429
15459
|
isAttrTrue(newVal)
|
|
15430
15460
|
);
|
|
15431
|
-
this.#internals.ariaMultiSelectable = isAttrTrue(newVal).toString();
|
|
15432
15461
|
break;
|
|
15433
15462
|
}
|
|
15434
15463
|
case "searchable": {
|
|
15435
15464
|
this.#onOptionSlotChange();
|
|
15436
15465
|
break;
|
|
15437
15466
|
}
|
|
15467
|
+
case "aria-label": {
|
|
15468
|
+
this.#syncListboxLabel();
|
|
15469
|
+
break;
|
|
15470
|
+
}
|
|
15471
|
+
case "aria-labelledby": {
|
|
15472
|
+
this.#syncListboxLabel();
|
|
15473
|
+
break;
|
|
15474
|
+
}
|
|
15438
15475
|
case "search-autocomplete": {
|
|
15439
15476
|
updateAttribute(this.#$search, "autocomplete", newVal);
|
|
15440
15477
|
break;
|
|
@@ -15448,7 +15485,7 @@ class SelectMenu extends NectaryElement {
|
|
|
15448
15485
|
break;
|
|
15449
15486
|
}
|
|
15450
15487
|
case "search-placeholder": {
|
|
15451
|
-
|
|
15488
|
+
this.#updateSearchPlaceholder(newVal);
|
|
15452
15489
|
break;
|
|
15453
15490
|
}
|
|
15454
15491
|
case "search-value": {
|
|
@@ -15496,6 +15533,18 @@ class SelectMenu extends NectaryElement {
|
|
|
15496
15533
|
const searchableAttribute = this.getAttribute("searchable");
|
|
15497
15534
|
return searchableAttribute === null ? searchableAttribute : isAttrTrue(searchableAttribute);
|
|
15498
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
|
+
}
|
|
15499
15548
|
set "search-autocomplete"(autocomplete) {
|
|
15500
15549
|
updateAttribute(this.#$search, "autocomplete", autocomplete);
|
|
15501
15550
|
}
|
|
@@ -15503,7 +15552,7 @@ class SelectMenu extends NectaryElement {
|
|
|
15503
15552
|
return getAttribute(this.#$search, "autocomplete", "");
|
|
15504
15553
|
}
|
|
15505
15554
|
set "search-placeholder"(placeholder) {
|
|
15506
|
-
|
|
15555
|
+
this.#updateSearchPlaceholder(placeholder);
|
|
15507
15556
|
}
|
|
15508
15557
|
get "search-placeholder"() {
|
|
15509
15558
|
return getAttribute(this.#$search, "placeholder", "");
|
|
@@ -15518,12 +15567,22 @@ class SelectMenu extends NectaryElement {
|
|
|
15518
15567
|
get focusable() {
|
|
15519
15568
|
return true;
|
|
15520
15569
|
}
|
|
15570
|
+
focusOnOpen() {
|
|
15571
|
+
this.#focusActiveSearch();
|
|
15572
|
+
}
|
|
15521
15573
|
#onFocus = () => {
|
|
15522
|
-
|
|
15523
|
-
|
|
15524
|
-
this.#$search.focus();
|
|
15574
|
+
if (getDeepActiveElement(this.ownerDocument) !== this) {
|
|
15575
|
+
return;
|
|
15525
15576
|
}
|
|
15577
|
+
this.#focusActiveSearch();
|
|
15526
15578
|
};
|
|
15579
|
+
#focusActiveSearch() {
|
|
15580
|
+
if (!this.isDomConnected || !hasClass(this.#$search, "active")) {
|
|
15581
|
+
return;
|
|
15582
|
+
}
|
|
15583
|
+
this.#$search.getBoundingClientRect();
|
|
15584
|
+
this.#$search.focus();
|
|
15585
|
+
}
|
|
15527
15586
|
#onListboxBlur = () => {
|
|
15528
15587
|
this.#selectOption(null);
|
|
15529
15588
|
};
|
|
@@ -15588,7 +15647,7 @@ class SelectMenu extends NectaryElement {
|
|
|
15588
15647
|
#onContextVisibility = (e) => {
|
|
15589
15648
|
if (e.detail) {
|
|
15590
15649
|
this.#selectOption(this.#findCheckedOption());
|
|
15591
|
-
this.#
|
|
15650
|
+
this.#focusActiveSearch();
|
|
15592
15651
|
} else {
|
|
15593
15652
|
this.#selectOption(null);
|
|
15594
15653
|
}
|
|
@@ -15626,7 +15685,30 @@ class SelectMenu extends NectaryElement {
|
|
|
15626
15685
|
this.#dispatchChangeEvent(option);
|
|
15627
15686
|
}
|
|
15628
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
|
+
};
|
|
15629
15710
|
#onOptionSlotChange = () => {
|
|
15711
|
+
this.#syncListboxChildren();
|
|
15630
15712
|
if (this.hasAttribute("rows")) {
|
|
15631
15713
|
this.#updateListboxMaxHeight();
|
|
15632
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;
|
package/utils/markdown.js
CHANGED
|
@@ -11,7 +11,8 @@ const regStrikethrough = new RegExp("(?<!\\\\)~~(?<strike>.+?)(?<!\\\\)~~");
|
|
|
11
11
|
const regButtonPlaceholder = new RegExp("(?<!\\\\)\\[\\[(?<button>[a-zA-Z0-9_-]+)\\]\\]");
|
|
12
12
|
const regLink = new RegExp("(?<!\\\\)!?\\[(?<linktext>[^\\]]*?)\\]\\((?<linkhref>[^)]+?)\\)(\\{(?<linkattrs>[^)]+?)\\})?");
|
|
13
13
|
const regChip = new RegExp("(?<!\\\\)\\{\\{(?<chip>[a-zA-Z0-9_-]+)\\}\\}");
|
|
14
|
-
const
|
|
14
|
+
const regEmojiSeq = "(?![0-9#*])\\p{Emoji}(?:\\uFE0F|\\p{Emoji_Modifier})?\\u20E3?";
|
|
15
|
+
const regEmoji = new RegExp(`(?<emoji>\\p{RI}\\p{RI}|${regEmojiSeq}(?:\\u200D${regEmojiSeq})*)`, "u");
|
|
15
16
|
const regUList = /^(?<indent>[\t ]*?)[*+-][\t ]+(?<ultext>.*?)[\t ]*?$/;
|
|
16
17
|
const regOList = /^(?<indent>[\t ]*?)\d+\.[\t ]+(?<oltext>.*?)[\t ]*?$/;
|
|
17
18
|
const regEscapedChars = /\\(?<escaped>[\\\*_\[\]`~\{\}])/;
|