rich-input 1.2.1 → 1.2.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.
@@ -283,6 +283,7 @@ TEMPLATE.innerHTML = `
283
283
  aria-autocomplete="list"
284
284
  aria-expanded="false"
285
285
  aria-haspopup="listbox"
286
+ aria-controls="ri-listbox"
286
287
  />
287
288
  <button part="clear-button" type="button" class="clear-button" aria-label="Clear search query" hidden>
288
289
  <svg viewBox="0 0 20 20" fill="currentColor">
@@ -296,13 +297,17 @@ TEMPLATE.innerHTML = `
296
297
  part="popover suggestions"
297
298
  class="popover"
298
299
  popover="manual"
299
- role="listbox"
300
- aria-label="Search suggestions"
301
300
  >
302
301
  <div part="suggestions-header" class="popover-header">
303
- <span part="suggestions-title" class="popover-title">Suggestions</span>
302
+ <span id="ri-listbox-label" part="suggestions-title" class="popover-title">Suggestions</span>
304
303
  </div>
305
- <ul part="suggestions-list" class="suggestions-list" role="presentation"></ul>
304
+ <ul
305
+ id="ri-listbox"
306
+ part="suggestions-list"
307
+ class="suggestions-list"
308
+ role="listbox"
309
+ aria-labelledby="ri-listbox-label"
310
+ ></ul>
306
311
  </div>
307
312
 
308
313
  <slot style="display: none;"></slot>
@@ -361,6 +366,8 @@ export class RichInput extends HTMLElement {
361
366
  'autofocus',
362
367
  'required',
363
368
  'highlight-quotes',
369
+ 'aria-label',
370
+ 'aria-labelledby',
364
371
  ];
365
372
  }
366
373
 
@@ -383,6 +390,7 @@ export class RichInput extends HTMLElement {
383
390
  editableDiv.setAttribute('aria-autocomplete', 'list');
384
391
  editableDiv.setAttribute('aria-expanded', 'false');
385
392
  editableDiv.setAttribute('aria-haspopup', 'listbox');
393
+ editableDiv.setAttribute('aria-controls', 'ri-listbox');
386
394
  editableDiv.setAttribute('spellcheck', 'false');
387
395
  editableDiv.setAttribute('tabindex', '0');
388
396
  existingInput.replaceWith(editableDiv);
@@ -416,6 +424,7 @@ export class RichInput extends HTMLElement {
416
424
  this._onClick = this._onClick.bind(this);
417
425
  this._onSelectionChange = this._onSelectionChange.bind(this);
418
426
  this._onClearClick = this._onClearClick.bind(this);
427
+ this._onClearMouseDown = (e) => e.preventDefault();
419
428
  this._onSlotChange = this._onSlotChange.bind(this);
420
429
  this._onGlobalClick = this._onGlobalClick.bind(this);
421
430
  this._onGlobalResizeOrScroll = this._onGlobalResizeOrScroll.bind(this);
@@ -453,6 +462,7 @@ export class RichInput extends HTMLElement {
453
462
  this._input.addEventListener('focus', this._onFocus);
454
463
  this._input.addEventListener('blur', this._onBlur);
455
464
  this._input.addEventListener('click', this._onClick);
465
+ this._clearBtn.addEventListener('mousedown', this._onClearMouseDown);
456
466
  this._clearBtn.addEventListener('click', this._onClearClick);
457
467
 
458
468
  // Global events
@@ -475,6 +485,7 @@ export class RichInput extends HTMLElement {
475
485
  this._internals.setFormValue(this._input.value);
476
486
  }
477
487
 
488
+ this._syncAccessibleName();
478
489
  this._updateClearButton();
479
490
  this.updateHighlights();
480
491
  }
@@ -494,6 +505,7 @@ export class RichInput extends HTMLElement {
494
505
  this._input.removeEventListener('focus', this._onFocus);
495
506
  this._input.removeEventListener('blur', this._onBlur);
496
507
  this._input.removeEventListener('click', this._onClick);
508
+ this._clearBtn.removeEventListener('mousedown', this._onClearMouseDown);
497
509
  this._clearBtn.removeEventListener('click', this._onClearClick);
498
510
 
499
511
  document.removeEventListener('click', this._onGlobalClick);
@@ -518,6 +530,9 @@ export class RichInput extends HTMLElement {
518
530
  }
519
531
  } else if (name === 'placeholder') {
520
532
  this._input.placeholder = newValue || '';
533
+ this._syncAccessibleName();
534
+ } else if (name === 'aria-label' || name === 'aria-labelledby') {
535
+ this._syncAccessibleName();
521
536
  } else if (name === 'disabled') {
522
537
  this._input.disabled = newValue !== null;
523
538
  } else if (name === 'readonly') {
@@ -535,6 +550,25 @@ export class RichInput extends HTMLElement {
535
550
  }
536
551
  }
537
552
 
553
+ _syncAccessibleName() {
554
+ if (!this._input) return;
555
+ if (this.hasAttribute('aria-label')) {
556
+ this._input.setAttribute('aria-label', this.getAttribute('aria-label'));
557
+ } else if (this._internals?.labels?.length > 0) {
558
+ const labelText = Array.from(this._internals.labels)
559
+ .map((l) => l.textContent.trim())
560
+ .filter(Boolean)
561
+ .join(' ');
562
+ if (labelText) {
563
+ this._input.setAttribute('aria-label', labelText);
564
+ }
565
+ } else if (this.hasAttribute('placeholder')) {
566
+ this._input.setAttribute('aria-label', this.getAttribute('placeholder'));
567
+ } else {
568
+ this._input.setAttribute('aria-label', 'Search');
569
+ }
570
+ }
571
+
538
572
  // --- Form Associated Lifecycle ---
539
573
  formResetCallback() {
540
574
  this.value = this.getAttribute('value') || '';
@@ -949,6 +983,7 @@ export class RichInput extends HTMLElement {
949
983
  this.updateHighlights();
950
984
  // Delay closing so click events on popover items can fire
951
985
  setTimeout(() => {
986
+ if (this._isFocused) return;
952
987
  this.hideSuggestions();
953
988
  this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
954
989
  }, 150);
@@ -1079,9 +1114,9 @@ export class RichInput extends HTMLElement {
1079
1114
  if (!this._isPopoverOpen()) {
1080
1115
  try {
1081
1116
  this._popover.showPopover();
1082
- this._input.setAttribute('aria-expanded', 'true');
1083
1117
  } catch (e) {}
1084
1118
  }
1119
+ this._input.setAttribute('aria-expanded', 'true');
1085
1120
 
1086
1121
  // Position popover at the start of the current OpaqueRange (or via mirror-div fallback when OpaqueRange is unsupported)
1087
1122
  const currentRange = this._getCurrentOpaqueRange();
@@ -1190,10 +1225,14 @@ export class RichInput extends HTMLElement {
1190
1225
  const isAct = i === this._selectedIndex;
1191
1226
  items[i].classList.toggle('active', isAct);
1192
1227
  items[i].setAttribute('aria-selected', isAct ? 'true' : 'false');
1228
+ items[i].setAttribute('part', `suggestion-item${isAct ? ' suggestion-item-active' : ''}`);
1193
1229
  if (isAct) {
1194
1230
  this._input.setAttribute('aria-activedescendant', items[i].id);
1195
1231
  }
1196
1232
  }
1233
+ if (this._selectedIndex === -1) {
1234
+ this._input.removeAttribute('aria-activedescendant');
1235
+ }
1197
1236
  }
1198
1237
 
1199
1238
  _applySelectedSuggestion() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rich-input",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "A rich input field with keyword-based autocomplete and in-input highlighting powered by <datalist>, OpaqueRange, and the Custom Highlight API",
5
5
  "type": "module",
6
6
  "main": "./index.js",