rich-input 1.2.0 → 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.
- package/README.md +7 -2
- package/components/rich-input.js +44 -5
- package/package.json +1 -1
- package/utils/query-parser.js +8 -9
package/README.md
CHANGED
|
@@ -86,6 +86,7 @@ Nest `<datalist>` elements inside `<rich-input>` to configure keywords and autoc
|
|
|
86
86
|
<option value="Kranky"></option>
|
|
87
87
|
<option value="Madhouse Records"></option>
|
|
88
88
|
<option value="Ninja Tune"></option>
|
|
89
|
+
<option value="Warp Records"></option>
|
|
89
90
|
<option value="We Play House Recordings"></option>
|
|
90
91
|
<option value="XL Recordings"></option>
|
|
91
92
|
</datalist>
|
|
@@ -149,6 +150,10 @@ Datalists can be added, updated, or removed dynamically at runtime; `<rich-input
|
|
|
149
150
|
<img src="assets/ninja-tune.jpg" height="50" width="50" alt="Ninja Tune Logo">
|
|
150
151
|
Ninja Tune
|
|
151
152
|
</option>
|
|
153
|
+
<option value="Warp Records">
|
|
154
|
+
<img src="assets/warp-records.png" height="50" width="50" alt="Warp Records Logo">
|
|
155
|
+
Warp Records
|
|
156
|
+
</option>
|
|
152
157
|
<option value="We Play House Recordings">
|
|
153
158
|
<img src="assets/we-play-house-recordings.jpg" height="50" width="50" alt="We Play House Recordings Logo">
|
|
154
159
|
We Play House Recordings
|
|
@@ -245,7 +250,7 @@ In browsers without `OpaqueRange` that support the CSS Custom Highlight API (suc
|
|
|
245
250
|
Values corresponding to configured keywords are registered into the global `CSS.highlights` registry and styled using standard CSS `::highlight(keyword)` pseudo-elements in your stylesheet:
|
|
246
251
|
|
|
247
252
|
```css
|
|
248
|
-
/* Style the value set in label:"
|
|
253
|
+
/* Style the value set in label:"Warp Records" */
|
|
249
254
|
::highlight(label) {
|
|
250
255
|
background-color: oklch(0.92 0.08 240);
|
|
251
256
|
color: oklch(0.28 0.14 240);
|
|
@@ -282,7 +287,7 @@ In browsers using the `[contenteditable]` fallback inside Shadow DOM (such as Sa
|
|
|
282
287
|
For self-contained widgets or instance-specific style overrides, `<rich-input>` also supports an optional embedded `<style>` block as a direct child, which is automatically injected into the shadow root:
|
|
283
288
|
|
|
284
289
|
```html
|
|
285
|
-
<rich-input value='artist:"Aphex Twin" label:"
|
|
290
|
+
<rich-input value='artist:"Aphex Twin" label:"Warp Records"'>
|
|
286
291
|
<style>
|
|
287
292
|
::highlight(label) {
|
|
288
293
|
background-color: #dbeafe;
|
package/components/rich-input.js
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|
package/utils/query-parser.js
CHANGED
|
@@ -181,8 +181,8 @@ export function getCaretContext(inputStr, caretPos, configuredKeywords) {
|
|
|
181
181
|
// 1. Caret is within a keyword token
|
|
182
182
|
if (activeToken.type === 'keyword') {
|
|
183
183
|
if (caretPos <= activeToken.colonIndex) {
|
|
184
|
-
// User is editing the keyword name
|
|
185
|
-
const query =
|
|
184
|
+
// User is editing the keyword name (filter based on full keyword name, not caret position)
|
|
185
|
+
const query = activeToken.keyword;
|
|
186
186
|
return {
|
|
187
187
|
mode: 'keyword',
|
|
188
188
|
query,
|
|
@@ -193,17 +193,16 @@ export function getCaretContext(inputStr, caretPos, configuredKeywords) {
|
|
|
193
193
|
tokens,
|
|
194
194
|
};
|
|
195
195
|
} else {
|
|
196
|
-
// User is editing the keyword value
|
|
196
|
+
// User is editing the keyword value (filter based on full value string, not caret position)
|
|
197
197
|
const isQuoted = activeToken.quoted;
|
|
198
|
-
const
|
|
199
|
-
const innerEnd = activeToken.innerEnd;
|
|
200
|
-
const valuePrefix = inputStr.slice(innerStart, Math.min(caretPos, innerEnd));
|
|
198
|
+
const value = activeToken.innerValue;
|
|
201
199
|
|
|
202
200
|
return {
|
|
203
201
|
mode: 'value',
|
|
204
202
|
keyword: activeToken.keyword,
|
|
205
203
|
keywordLower: activeToken.keywordLower,
|
|
206
|
-
valuePrefix,
|
|
204
|
+
valuePrefix: value,
|
|
205
|
+
query: value,
|
|
207
206
|
quoted: isQuoted,
|
|
208
207
|
quoteChar: activeToken.quoteChar || '"',
|
|
209
208
|
isClosed: activeToken.isClosed,
|
|
@@ -216,9 +215,9 @@ export function getCaretContext(inputStr, caretPos, configuredKeywords) {
|
|
|
216
215
|
}
|
|
217
216
|
}
|
|
218
217
|
|
|
219
|
-
// 2. Caret is within a plain text token
|
|
218
|
+
// 2. Caret is within a plain text token (filter based on full word, not caret position)
|
|
220
219
|
if (activeToken.type === 'text') {
|
|
221
|
-
const query =
|
|
220
|
+
const query = activeToken.raw;
|
|
222
221
|
return {
|
|
223
222
|
mode: 'keyword',
|
|
224
223
|
query,
|