@limetech/lime-elements 40.18.1 → 40.18.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/CHANGELOG.md +19 -0
- package/dist/cjs/limel-breadcrumbs_6.cjs.entry.js +68 -29
- package/dist/cjs/limel-callout.cjs.entry.js +1 -1
- package/dist/cjs/limel-helper-line_3.cjs.entry.js +9 -19
- package/dist/cjs/limel-picker.cjs.entry.js +9 -14
- package/dist/cjs/limel-select.cjs.entry.js +9 -168
- package/dist/cjs/typeahead-CnyIjog_.js +194 -0
- package/dist/collection/components/callout/callout.css +1 -0
- package/dist/collection/components/input-field/input-field.js +68 -29
- package/dist/collection/components/list/list.js +9 -19
- package/dist/collection/components/picker/picker.js +9 -14
- package/dist/collection/util/typeahead.js +26 -0
- package/dist/esm/limel-breadcrumbs_6.entry.js +69 -30
- package/dist/esm/limel-callout.entry.js +1 -1
- package/dist/esm/limel-helper-line_3.entry.js +9 -19
- package/dist/esm/limel-picker.entry.js +9 -14
- package/dist/esm/limel-select.entry.js +1 -160
- package/dist/esm/typeahead-DOlnT3wh.js +188 -0
- package/dist/lime-elements/lime-elements.esm.js +1 -1
- package/dist/lime-elements/p-70272ea2.entry.js +1 -0
- package/dist/lime-elements/p-93a2d3b3.entry.js +159 -0
- package/dist/lime-elements/p-a2dc6645.entry.js +1 -0
- package/dist/lime-elements/p-c1678d3a.entry.js +23 -0
- package/dist/lime-elements/p-ea11f365.entry.js +1 -0
- package/dist/lime-elements/p-fYs7n6yY.js +1 -0
- package/dist/types/components/input-field/input-field.d.ts +6 -6
- package/dist/types/util/typeahead.d.ts +16 -0
- package/package.json +1 -1
- package/dist/lime-elements/p-27d7c255.entry.js +0 -159
- package/dist/lime-elements/p-3c861ca8.entry.js +0 -23
- package/dist/lime-elements/p-5f82ea0b.entry.js +0 -1
- package/dist/lime-elements/p-6aae3f58.entry.js +0 -1
- package/dist/lime-elements/p-7af16e1d.entry.js +0 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var keycodes = require('./keycodes-BQ_AERli.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* These helpers implement "type to jump", the behavior a native `<select>` has:
|
|
7
|
+
* typing characters moves to the option whose text starts with those
|
|
8
|
+
* characters. Characters accumulate into a short-lived buffer, so typing
|
|
9
|
+
* `n`, `e` finds "Netherlands" instead of re-matching on `n` every time.
|
|
10
|
+
*
|
|
11
|
+
* The matching itself is pure and stateless — all the state lives in a
|
|
12
|
+
* `TypeaheadBuffer`, which owns the timer that discards the buffer once the
|
|
13
|
+
* user stops typing.
|
|
14
|
+
*
|
|
15
|
+
* `isTextEditingKey` is for the other side of a list: a field with a list
|
|
16
|
+
* under it uses it to tell which keys belong to the field.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* How long a buffer of typed characters is kept before it is discarded, in
|
|
20
|
+
* milliseconds. Roughly matches a native `<select>`.
|
|
21
|
+
*/
|
|
22
|
+
const TYPEAHEAD_BUFFER_TIMEOUT = 1000;
|
|
23
|
+
/**
|
|
24
|
+
* Returned by `findTypeaheadMatch` when nothing matched.
|
|
25
|
+
*/
|
|
26
|
+
const NO_TYPEAHEAD_MATCH = -1;
|
|
27
|
+
/**
|
|
28
|
+
* Whether a keyboard event should be treated as a typed character.
|
|
29
|
+
*
|
|
30
|
+
* The space bar counts as a character here. Whether a *bare* space should
|
|
31
|
+
* start a buffer, or keep whatever meaning it has in the consuming component,
|
|
32
|
+
* is the consumer's decision.
|
|
33
|
+
*
|
|
34
|
+
* `shiftKey` is allowed, since capital letters are typed with it, and
|
|
35
|
+
* `event.repeat` is allowed, since holding a key down cycles through matches
|
|
36
|
+
* in a native `<select>` too.
|
|
37
|
+
*
|
|
38
|
+
* @param event - the keyboard event to inspect
|
|
39
|
+
* @returns `true` when the event represents a typed character
|
|
40
|
+
*/
|
|
41
|
+
function isTypeaheadKey(event) {
|
|
42
|
+
return (event.key.length === 1 &&
|
|
43
|
+
!event.altKey &&
|
|
44
|
+
!event.ctrlKey &&
|
|
45
|
+
!event.metaKey &&
|
|
46
|
+
!event.isComposing);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Find the candidate to move to for a given buffer of typed characters.
|
|
50
|
+
*
|
|
51
|
+
* Two different searches are used, mirroring a native `<select>`:
|
|
52
|
+
* - When the buffer is a single character, possibly repeated (`b`, `bb`,
|
|
53
|
+
* `bbb`), only that character is matched and the search starts *after*
|
|
54
|
+
* `currentIndex`. Pressing the same key repeatedly therefore cycles through
|
|
55
|
+
* all candidates starting with it.
|
|
56
|
+
* - Otherwise the whole buffer is matched and the search starts *at*
|
|
57
|
+
* `currentIndex`, so continuing to type on an already-matching candidate
|
|
58
|
+
* keeps it rather than jumping to the next one.
|
|
59
|
+
*
|
|
60
|
+
* Both searches wrap around the end of the list.
|
|
61
|
+
*
|
|
62
|
+
* @param candidates - the candidates to search, index-aligned with the rows
|
|
63
|
+
* they are rendered as. Use `null` for rows that can never match, such as
|
|
64
|
+
* separators, so that they still occupy their index.
|
|
65
|
+
* @param buffer - the characters typed so far
|
|
66
|
+
* @param currentIndex - the index to search from. Anything outside
|
|
67
|
+
* `candidates` is treated as "no current candidate", and the search starts
|
|
68
|
+
* from the beginning.
|
|
69
|
+
* @returns the index of the matching candidate, or `NO_TYPEAHEAD_MATCH`
|
|
70
|
+
*/
|
|
71
|
+
function findTypeaheadMatch(candidates, buffer, currentIndex) {
|
|
72
|
+
const count = candidates.length;
|
|
73
|
+
const query = buffer.toLowerCase();
|
|
74
|
+
if (!query || count === 0) {
|
|
75
|
+
return NO_TYPEAHEAD_MATCH;
|
|
76
|
+
}
|
|
77
|
+
const characters = [...query];
|
|
78
|
+
const cycles = isSingleDistinctCharacter(characters);
|
|
79
|
+
const needle = cycles ? characters[0] : query;
|
|
80
|
+
// Cycling starts one past the current candidate, so that pressing the same
|
|
81
|
+
// key again advances. Matching several characters starts at it, so that
|
|
82
|
+
// continuing to type keeps a candidate that already matches.
|
|
83
|
+
const offset = cycles ? 1 : 0;
|
|
84
|
+
const hasCurrent = currentIndex >= 0 && currentIndex < count;
|
|
85
|
+
const start = hasCurrent ? (currentIndex + offset) % count : 0;
|
|
86
|
+
for (let step = 0; step < count; step += 1) {
|
|
87
|
+
const index = (start + step) % count;
|
|
88
|
+
if (candidateMatches(candidates[index], needle)) {
|
|
89
|
+
return index;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return NO_TYPEAHEAD_MATCH;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Whether a string consists of a single character, possibly repeated. Also
|
|
96
|
+
* `true` for one character on its own, which is what makes a single key press
|
|
97
|
+
* advance past the current candidate rather than re-matching it.
|
|
98
|
+
*
|
|
99
|
+
* Takes the characters already split apart, so that the caller and this
|
|
100
|
+
* function agree on what "a character" is for anything outside the basic
|
|
101
|
+
* multilingual plane.
|
|
102
|
+
*
|
|
103
|
+
* @param characters - the characters of the string
|
|
104
|
+
* @returns `true` when every character is the same
|
|
105
|
+
*/
|
|
106
|
+
function isSingleDistinctCharacter(characters) {
|
|
107
|
+
return characters.every((character) => character === characters[0]);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* @param candidate - the candidate to test, or `null` for a row that can
|
|
111
|
+
* never match
|
|
112
|
+
* @param needle - the lower cased characters to match
|
|
113
|
+
* @returns `true` when the candidate starts with the given characters
|
|
114
|
+
*/
|
|
115
|
+
function candidateMatches(candidate, needle) {
|
|
116
|
+
var _a;
|
|
117
|
+
if (!candidate || candidate.disabled) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const text = (_a = candidate.text) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
|
|
121
|
+
return !!text && text.startsWith(needle);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Accumulates typed characters, and discards them again once the user stops
|
|
125
|
+
* typing for `TYPEAHEAD_BUFFER_TIMEOUT`.
|
|
126
|
+
*/
|
|
127
|
+
class TypeaheadBuffer {
|
|
128
|
+
constructor(timeout = TYPEAHEAD_BUFFER_TIMEOUT) {
|
|
129
|
+
this.buffer = '';
|
|
130
|
+
this.timeout = timeout;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The characters typed so far.
|
|
134
|
+
*/
|
|
135
|
+
get value() {
|
|
136
|
+
return this.buffer;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether nothing has been typed, or the buffer has been discarded.
|
|
140
|
+
*/
|
|
141
|
+
get isEmpty() {
|
|
142
|
+
return this.buffer === '';
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Append a character, and restart the timer that discards the buffer.
|
|
146
|
+
*
|
|
147
|
+
* @param character - the character to append
|
|
148
|
+
* @returns the buffer, including the appended character
|
|
149
|
+
*/
|
|
150
|
+
append(character) {
|
|
151
|
+
this.buffer += character;
|
|
152
|
+
clearTimeout(this.resetTimeoutId);
|
|
153
|
+
this.resetTimeoutId = setTimeout(() => {
|
|
154
|
+
this.clear();
|
|
155
|
+
}, this.timeout);
|
|
156
|
+
return this.buffer;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Discard the buffer, and cancel the timer that would have discarded it.
|
|
160
|
+
*/
|
|
161
|
+
clear() {
|
|
162
|
+
this.buffer = '';
|
|
163
|
+
clearTimeout(this.resetTimeoutId);
|
|
164
|
+
this.resetTimeoutId = undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Whether a keyboard event edits text: a typed character, or Backspace or
|
|
169
|
+
* Delete. A bare space is left out, since on an option of a list it selects
|
|
170
|
+
* the option.
|
|
171
|
+
*
|
|
172
|
+
* A character typed with Option, or with AltGr, which is reported as Ctrl and
|
|
173
|
+
* Alt together, counts, since many layouts type `@` and `$` that way. A
|
|
174
|
+
* shortcut with Ctrl or Meta does not.
|
|
175
|
+
*
|
|
176
|
+
* @param event - the keyboard event to inspect
|
|
177
|
+
* @returns `true` when the event edits text
|
|
178
|
+
*/
|
|
179
|
+
function isTextEditingKey(event) {
|
|
180
|
+
if (event.key === keycodes.BACKSPACE || event.key === keycodes.DELETE) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
const isCharacter = event.key.length === 1 &&
|
|
184
|
+
event.key !== keycodes.SPACEBAR &&
|
|
185
|
+
!event.metaKey &&
|
|
186
|
+
!event.isComposing;
|
|
187
|
+
return isCharacter && (!event.ctrlKey || event.altKey);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
exports.NO_TYPEAHEAD_MATCH = NO_TYPEAHEAD_MATCH;
|
|
191
|
+
exports.TypeaheadBuffer = TypeaheadBuffer;
|
|
192
|
+
exports.findTypeaheadMatch = findTypeaheadMatch;
|
|
193
|
+
exports.isTextEditingKey = isTextEditingKey;
|
|
194
|
+
exports.isTypeaheadKey = isTypeaheadKey;
|
|
@@ -4,9 +4,11 @@ import { debounce } from "lodash-es";
|
|
|
4
4
|
import { ARROW_DOWN, ARROW_UP, ENTER, ESCAPE, SPACE, TAB, } from "../../util/keycodes";
|
|
5
5
|
import { getHref, getTarget, getRel } from "../../util/link-helper";
|
|
6
6
|
import { createRandomString } from "../../util/random-string";
|
|
7
|
+
import { isTextEditingKey } from "../../util/typeahead";
|
|
7
8
|
import { globalConfig } from "../../global/config";
|
|
8
9
|
const CHANGE_EVENT_DEBOUNCE_TIMEOUT = 300;
|
|
9
10
|
const RESIZE_HANDLER_DEBOUNCE_TIMEOUT = 100;
|
|
11
|
+
const MAX_FRAMES_TO_WAIT_FOR_LIST = 20;
|
|
10
12
|
/**
|
|
11
13
|
* @exampleComponent limel-example-input-field-text
|
|
12
14
|
* @exampleComponent limel-example-input-field-placeholder
|
|
@@ -367,41 +369,64 @@ export class InputField {
|
|
|
367
369
|
}
|
|
368
370
|
return (h("span", { class: "lime-formatted-input lime-looks-like-input-value", "aria-hidden": this.readonly ? 'true' : undefined }, renderValue));
|
|
369
371
|
};
|
|
370
|
-
/**
|
|
371
|
-
* Key handler for the input field
|
|
372
|
-
* Will change focus to the first/last item in the dropdown list to enable selection with the keyboard
|
|
373
|
-
*
|
|
374
|
-
* @param event - event
|
|
375
|
-
*/
|
|
376
372
|
this.onKeyDown = (event) => {
|
|
377
|
-
|
|
378
|
-
|
|
373
|
+
if (event.key === ESCAPE) {
|
|
374
|
+
this.closeListWithEscape(event);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const isTab = event.key === TAB &&
|
|
379
378
|
!event.altKey &&
|
|
380
379
|
!event.metaKey &&
|
|
381
|
-
!event.
|
|
382
|
-
|
|
383
|
-
const isDown = event.key === ARROW_DOWN;
|
|
384
|
-
if (event.key === TAB && event.shiftKey) {
|
|
380
|
+
!event.ctrlKey;
|
|
381
|
+
if (isTab) {
|
|
385
382
|
this.showCompletions = false;
|
|
386
|
-
}
|
|
387
|
-
if (!isForwardTab && !isUp && !isDown) {
|
|
388
383
|
return;
|
|
389
384
|
}
|
|
390
|
-
const
|
|
391
|
-
|
|
385
|
+
const wasOpen = this.isSuggestionListOpen();
|
|
386
|
+
this.showCompletions = true;
|
|
387
|
+
if (event.key === ARROW_DOWN || event.key === ARROW_UP) {
|
|
388
|
+
this.moveIntoList(event, wasOpen);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
this.moveIntoList = (event, wasOpen) => {
|
|
392
|
+
var _a;
|
|
393
|
+
if (!this.isSuggestionListOpen()) {
|
|
392
394
|
return;
|
|
393
395
|
}
|
|
394
396
|
event.preventDefault();
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
397
|
+
const position = event.key === ARROW_DOWN ? 'first' : 'last';
|
|
398
|
+
if (wasOpen) {
|
|
399
|
+
(_a = this.getListOption(position)) === null || _a === void 0 ? void 0 : _a.focus();
|
|
398
400
|
return;
|
|
399
401
|
}
|
|
400
|
-
|
|
402
|
+
this.focusOptionWhenShown(position);
|
|
403
|
+
};
|
|
404
|
+
this.getListOption = (position) => {
|
|
405
|
+
const list = document.querySelector(` #${this.portalId} limel-list`);
|
|
406
|
+
return list === null || list === void 0 ? void 0 : list.shadowRoot.querySelector(`.mdc-deprecated-list-item:${position}-child`);
|
|
407
|
+
};
|
|
408
|
+
this.focusOptionWhenShown = (position) => {
|
|
409
|
+
let frames = 0;
|
|
410
|
+
const focusOption = () => {
|
|
411
|
+
const option = this.getListOption(position);
|
|
412
|
+
if ((option === null || option === void 0 ? void 0 : option.getClientRects().length) > 0) {
|
|
413
|
+
option.focus();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
frames += 1;
|
|
417
|
+
if (frames < MAX_FRAMES_TO_WAIT_FOR_LIST) {
|
|
418
|
+
requestAnimationFrame(focusOption);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
requestAnimationFrame(focusOption);
|
|
422
|
+
};
|
|
423
|
+
this.closeListWithEscape = (event) => {
|
|
424
|
+
if (!this.isSuggestionListOpen()) {
|
|
401
425
|
return;
|
|
402
426
|
}
|
|
403
|
-
|
|
404
|
-
|
|
427
|
+
event.preventDefault();
|
|
428
|
+
event.stopPropagation();
|
|
429
|
+
this.showCompletions = false;
|
|
405
430
|
};
|
|
406
431
|
this.handleCompletionChange = (event) => {
|
|
407
432
|
event.stopPropagation();
|
|
@@ -418,10 +443,18 @@ export class InputField {
|
|
|
418
443
|
this.changeEmitter(event.detail.text);
|
|
419
444
|
this.changeEmitter.flush();
|
|
420
445
|
};
|
|
446
|
+
this.hasSuggestions = () => {
|
|
447
|
+
return (!this.readonly &&
|
|
448
|
+
this.type !== 'textarea' &&
|
|
449
|
+
this.completions.length > 0);
|
|
450
|
+
};
|
|
451
|
+
this.isSuggestionListOpen = () => {
|
|
452
|
+
return (this.hasSuggestions() &&
|
|
453
|
+
this.showCompletions &&
|
|
454
|
+
this.filterCompletions(this.getCurrentValue()).length > 0);
|
|
455
|
+
};
|
|
421
456
|
this.renderAutocompleteList = () => {
|
|
422
|
-
if (this.
|
|
423
|
-
this.type === 'textarea' ||
|
|
424
|
-
this.completions.length === 0) {
|
|
457
|
+
if (!this.hasSuggestions()) {
|
|
425
458
|
return;
|
|
426
459
|
}
|
|
427
460
|
const dropdownZIndex = getComputedStyle(this.limelInputField).getPropertyValue('--dropdown-z-index');
|
|
@@ -439,8 +472,14 @@ export class InputField {
|
|
|
439
472
|
return (h("limel-list", { onChange: this.handleCompletionChange, onKeyDown: this.handleKeyDownInDropdown, type: "selectable", items: filteredCompletions }));
|
|
440
473
|
};
|
|
441
474
|
this.handleKeyDownInDropdown = (event) => {
|
|
442
|
-
|
|
443
|
-
|
|
475
|
+
if (event.key === ESCAPE) {
|
|
476
|
+
event.preventDefault();
|
|
477
|
+
event.stopPropagation();
|
|
478
|
+
this.setFocus();
|
|
479
|
+
this.showCompletions = false;
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if ([TAB, ENTER].includes(event.key) || isTextEditingKey(event)) {
|
|
444
483
|
this.setFocus();
|
|
445
484
|
}
|
|
446
485
|
};
|
|
@@ -598,10 +637,10 @@ export class InputField {
|
|
|
598
637
|
if (this.isInvalid()) {
|
|
599
638
|
properties['aria-invalid'] = 'true';
|
|
600
639
|
}
|
|
601
|
-
if (this.
|
|
640
|
+
if (this.hasSuggestions()) {
|
|
602
641
|
properties['aria-controls'] = this.portalId;
|
|
603
642
|
}
|
|
604
|
-
return (h(Host, { key: '
|
|
643
|
+
return (h(Host, { key: 'e09ee9e3102d6a15d0806602c65361001cd20c63' }, h("limel-notched-outline", { key: 'c2301d9fd1af41e8b4396dc5f2978e51b260fe2f', labelId: this.inputId, labelElementId: this.labelElementId, label: this.label, required: this.required, invalid: this.invalid || this.isInvalid(), disabled: this.disabled, readonly: this.readonly, hasValue: !!this.value, hasLeadingIcon: !!this.leadingIcon }, h("label", { key: '69e02e5df31a6ce10743c3c8a9e78c5765794a18', slot: "content", class: this.getContainerClassList() }, this.renderLeadingIcon(), this.renderPrefix(), this.renderFormattedNumber(), this.renderInput(properties), this.renderSuffix(), this.renderTextarea(properties), this.renderTrailingLinkOrButton())), this.renderHelperLine(), this.renderAutocompleteList()));
|
|
605
644
|
}
|
|
606
645
|
valueWatcher(newValue) {
|
|
607
646
|
if (!this.mdcTextField || this.changeWaiting) {
|
|
@@ -50,26 +50,16 @@ export class List {
|
|
|
50
50
|
this.listElement.removeEventListener('mousedown', this.handleItemMouseDown);
|
|
51
51
|
this.listElement.addEventListener('mousedown', this.handleItemMouseDown);
|
|
52
52
|
this.mdcList = new MDCList(element);
|
|
53
|
-
//
|
|
54
|
-
//
|
|
53
|
+
// MDC's typeahead (`hasTypeahead`) is left off, so that typed
|
|
54
|
+
// characters are not taken from a field with a list under it. The list
|
|
55
|
+
// test in `list.e2e.tsx` fails if it is turned on. `limel-select` has
|
|
56
|
+
// its own typeahead, see `src/util/typeahead.ts`.
|
|
55
57
|
//
|
|
56
|
-
// MDC
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// Do not "fix" this by adding MDC's class name to `limel-list-item`.
|
|
62
|
-
// That revives MDC's typeahead along with three behaviors that cannot
|
|
63
|
-
// be configured from the outside: a hard coded 300ms buffer, `Enter`
|
|
64
|
-
// being ignored for as long as that buffer lives (its `notifyAction`
|
|
65
|
-
// is guarded by `isTypeaheadInProgress`), and the space bar being
|
|
66
|
-
// excluded from matches. It would also collide with `limel-menu`,
|
|
67
|
-
// where single characters are used as item hotkeys.
|
|
68
|
-
//
|
|
69
|
-
// `limel-select` implements its own typeahead instead, see
|
|
70
|
-
// `src/util/typeahead.ts`. It intercepts typed characters in the
|
|
71
|
-
// capture phase on this very element, so they never reach MDC.
|
|
72
|
-
this.mdcList.hasTypeahead = true;
|
|
58
|
+
// Adding MDC's class name to `limel-list-item` does nothing while it is
|
|
59
|
+
// off. Turning it on as well would bring its 300ms buffer, `Enter`
|
|
60
|
+
// being ignored while the buffer lives and the space bar being left out
|
|
61
|
+
// of matches, none of which can be configured, and it would collide
|
|
62
|
+
// with the single characters that `limel-menu` uses as hotkeys.
|
|
73
63
|
};
|
|
74
64
|
/**
|
|
75
65
|
* Focus the pressed list item directly, without scrolling.
|
|
@@ -3,6 +3,7 @@ import { h, } from "@stencil/core";
|
|
|
3
3
|
import { isDescendant } from "../../util/dom";
|
|
4
4
|
import { ARROW_DOWN, ARROW_UP, ENTER, ESCAPE, TAB } from "../../util/keycodes";
|
|
5
5
|
import { createRandomString } from "../../util/random-string";
|
|
6
|
+
import { isTextEditingKey } from "../../util/typeahead";
|
|
6
7
|
import { getIconFillColor, getIconName } from "../icon/get-icon-props";
|
|
7
8
|
import { debounce } from "lodash-es";
|
|
8
9
|
const SEARCH_DEBOUNCE = 300;
|
|
@@ -199,7 +200,7 @@ export class Picker {
|
|
|
199
200
|
props.maxItems = 1;
|
|
200
201
|
}
|
|
201
202
|
return [
|
|
202
|
-
h("limel-chip-set", Object.assign({ key: '
|
|
203
|
+
h("limel-chip-set", Object.assign({ key: 'c3f4fd001d63a287a8a9f447f7c9df65279fbc34', type: "input", inputType: "search", label: this.label, helperText: this.helperText, leadingIcon: this.leadingIcon, value: this.chips, disabled: this.disabled, invalid: this.invalid, delimiter: this.renderDelimiter(), readonly: this.readonly, required: this.required, searchLabel: this.searchLabel, language: this.language, onInput: this.handleTextInput, onKeyDown: this.handleInputKeyDown, onChange: this.handleChange, onInteract: this.handleInteract, onStartEdit: this.handleInputFieldFocus, onStopEdit: this.handleStopEditAndBlur, emptyInputOnBlur: false, emptyInputOnChange: false, clearAllButton: this.multiple && !this.chipSetEditMode }, props)),
|
|
203
204
|
this.renderDropdown(),
|
|
204
205
|
];
|
|
205
206
|
}
|
|
@@ -247,7 +248,7 @@ export class Picker {
|
|
|
247
248
|
'is-on-top': this.actionPosition === 'top',
|
|
248
249
|
'is-at-bottom': this.actionPosition === 'bottom',
|
|
249
250
|
'has-position-sticky': this.actionScrollBehavior === 'sticky',
|
|
250
|
-
}, badgeIcons: true, type: 'selectable', onChange: this.handleActionListChange, items: this.actions.map(this.removeUnusedPropertiesOnAction) }),
|
|
251
|
+
}, badgeIcons: true, type: 'selectable', onChange: this.handleActionListChange, onKeyDown: this.onListKeyDown, items: this.actions.map(this.removeUnusedPropertiesOnAction) }),
|
|
251
252
|
];
|
|
252
253
|
}
|
|
253
254
|
removeUnusedPropertiesOnAction(action) {
|
|
@@ -311,7 +312,7 @@ export class Picker {
|
|
|
311
312
|
return h("p", { style: style }, text);
|
|
312
313
|
}
|
|
313
314
|
renderListResult() {
|
|
314
|
-
return (h("limel-list", { badgeIcons: this.badgeIcons, onChange: this.handleListChange, onKeyDown: this.onListKeyDown, type: "selectable", items: this.items }));
|
|
315
|
+
return (h("limel-list", { "data-suggestions": "", badgeIcons: this.badgeIcons, onChange: this.handleListChange, onKeyDown: this.onListKeyDown, type: "selectable", items: this.items }));
|
|
315
316
|
}
|
|
316
317
|
onListKeyDown(event) {
|
|
317
318
|
if (event.key === ESCAPE) {
|
|
@@ -323,7 +324,7 @@ export class Picker {
|
|
|
323
324
|
this.chipSet.setFocus();
|
|
324
325
|
return;
|
|
325
326
|
}
|
|
326
|
-
if ([TAB, ENTER].includes(event.key)) {
|
|
327
|
+
if ([TAB, ENTER].includes(event.key) || isTextEditingKey(event)) {
|
|
327
328
|
this.chipSet.setFocus();
|
|
328
329
|
}
|
|
329
330
|
}
|
|
@@ -452,10 +453,7 @@ export class Picker {
|
|
|
452
453
|
* @param event - event
|
|
453
454
|
*/
|
|
454
455
|
handleInputKeyDown(event) {
|
|
455
|
-
|
|
456
|
-
!event.altKey &&
|
|
457
|
-
!event.metaKey &&
|
|
458
|
-
!event.shiftKey;
|
|
456
|
+
var _a;
|
|
459
457
|
const isUp = event.key === ARROW_UP;
|
|
460
458
|
const isDown = event.key === ARROW_DOWN;
|
|
461
459
|
const isEscape = event.key === ESCAPE;
|
|
@@ -465,14 +463,14 @@ export class Picker {
|
|
|
465
463
|
this.handleEscape();
|
|
466
464
|
return;
|
|
467
465
|
}
|
|
468
|
-
if (!
|
|
466
|
+
if (!isUp && !isDown) {
|
|
469
467
|
return;
|
|
470
468
|
}
|
|
471
|
-
const list = document.querySelector(` #${this.portalId} limel-list`);
|
|
469
|
+
const list = (_a = document.querySelector(` #${this.portalId} limel-list[data-suggestions]`)) !== null && _a !== void 0 ? _a : document.querySelector(` #${this.portalId} limel-list`);
|
|
472
470
|
if (!list) {
|
|
473
471
|
return;
|
|
474
472
|
}
|
|
475
|
-
if (
|
|
473
|
+
if (isDown) {
|
|
476
474
|
// Match by class only (not `:first-child`) so a leading
|
|
477
475
|
// `ListSeparator` doesn't shadow the first focusable item.
|
|
478
476
|
const listElement = list.shadowRoot.querySelector('.mdc-deprecated-list-item');
|
|
@@ -483,9 +481,6 @@ export class Picker {
|
|
|
483
481
|
listElement.focus();
|
|
484
482
|
return;
|
|
485
483
|
}
|
|
486
|
-
if (!isUp) {
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
484
|
const listItems = list.shadowRoot.querySelectorAll('.mdc-deprecated-list-item');
|
|
490
485
|
const listElement = [...listItems].at(-1);
|
|
491
486
|
if (!listElement) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BACKSPACE, DELETE, SPACEBAR } from "./keycodes";
|
|
1
2
|
/**
|
|
2
3
|
* These helpers implement "type to jump", the behavior a native `<select>` has:
|
|
3
4
|
* typing characters moves to the option whose text starts with those
|
|
@@ -7,6 +8,9 @@
|
|
|
7
8
|
* The matching itself is pure and stateless — all the state lives in a
|
|
8
9
|
* `TypeaheadBuffer`, which owns the timer that discards the buffer once the
|
|
9
10
|
* user stops typing.
|
|
11
|
+
*
|
|
12
|
+
* `isTextEditingKey` is for the other side of a list: a field with a list
|
|
13
|
+
* under it uses it to tell which keys belong to the field.
|
|
10
14
|
*/
|
|
11
15
|
/**
|
|
12
16
|
* How long a buffer of typed characters is kept before it is discarded, in
|
|
@@ -157,3 +161,25 @@ export class TypeaheadBuffer {
|
|
|
157
161
|
this.resetTimeoutId = undefined;
|
|
158
162
|
}
|
|
159
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Whether a keyboard event edits text: a typed character, or Backspace or
|
|
166
|
+
* Delete. A bare space is left out, since on an option of a list it selects
|
|
167
|
+
* the option.
|
|
168
|
+
*
|
|
169
|
+
* A character typed with Option, or with AltGr, which is reported as Ctrl and
|
|
170
|
+
* Alt together, counts, since many layouts type `@` and `$` that way. A
|
|
171
|
+
* shortcut with Ctrl or Meta does not.
|
|
172
|
+
*
|
|
173
|
+
* @param event - the keyboard event to inspect
|
|
174
|
+
* @returns `true` when the event edits text
|
|
175
|
+
*/
|
|
176
|
+
export function isTextEditingKey(event) {
|
|
177
|
+
if (event.key === BACKSPACE || event.key === DELETE) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
const isCharacter = event.key.length === 1 &&
|
|
181
|
+
event.key !== SPACEBAR &&
|
|
182
|
+
!event.metaKey &&
|
|
183
|
+
!event.isComposing;
|
|
184
|
+
return isCharacter && (!event.ctrlKey || event.altKey);
|
|
185
|
+
}
|