@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 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.setAttribute("role", "dialog");
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.modal && transformedAncestor == null;
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
- updateAttribute(this.#$pop, name, newVal);
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.ariaLabel = newVal;
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", "width", "orientation"];
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="presentation"><slot></slot></div>';
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", "listbox");
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
- updateAttribute(this.#$search, "placeholder", newVal);
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
- updateAttribute(this.#$search, "placeholder", placeholder);
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
- const isSearchActive = hasClass(this.#$search, "active");
15524
- if (isSearchActive) {
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.#onFocus();
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
  }
@@ -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;
@@ -1,6 +1,6 @@
1
1
  import "../tooltip/index.js";
2
2
  import "../icon/index.js";
3
- import { updateAttribute, getAttribute, getIntegerAttribute, updateIntegerAttribute } from "../utils/dom.js";
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", "width", "orientation"];
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
  }
@@ -1,6 +1,15 @@
1
- import type { TSinchTooltipProps } from '../tooltip/types';
2
- import type { NectaryComponentReactByType, NectaryComponentVanillaByType, NectaryComponentReact, NectaryComponentVanilla } from '../types';
3
- export type TSinchHelpTooltipProps = TSinchTooltipProps;
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
@@ -301,7 +301,7 @@ class Input extends NectaryElement {
301
301
  break;
302
302
  }
303
303
  case "aria-label": {
304
- this.#$input.ariaLabel = newVal;
304
+ updateAttribute(this.#$input, "aria-label", newVal);
305
305
  break;
306
306
  }
307
307
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectary/components",
3
- "version": "5.42.1",
3
+ "version": "5.42.3",
4
4
  "files": [
5
5
  "**/*/*.css",
6
6
  "**/*/*.json",
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.setAttribute("role", "dialog");
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.modal && transformedAncestor == null;
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();
@@ -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
  }
@@ -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 */
@@ -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
  }
@@ -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="presentation"><slot></slot></div>';
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", "listbox");
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
- updateAttribute(this.#$search, "placeholder", newVal);
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
- updateAttribute(this.#$search, "placeholder", placeholder);
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
- const isSearchActive = hasClass(this.#$search, "active");
225
- if (isSearchActive) {
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.#onFocus();
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
  }
@@ -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
- updateAttribute(this.#$pop, name, newVal);
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;
@@ -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;