@limetech/lime-elements 40.21.0 → 40.21.1
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 +10 -0
- package/dist/cjs/limel-badge_7.cjs.entry.js +10 -0
- package/dist/cjs/limel-file-viewer.cjs.entry.js +162 -0
- package/dist/cjs/limel-picker.cjs.entry.js +68 -25
- package/dist/collection/components/input-field/input-field.js +10 -0
- package/dist/collection/components/picker/picker.js +67 -24
- package/dist/esm/limel-badge_7.entry.js +10 -0
- package/dist/esm/limel-file-viewer.entry.js +162 -0
- package/dist/esm/limel-picker.entry.js +69 -26
- package/dist/lime-elements/lime-elements.esm.js +1 -1
- package/dist/lime-elements/p-68bbb8dd.entry.js +1 -0
- package/dist/lime-elements/{p-7ab726c2.entry.js → p-7af83877.entry.js} +1 -1
- package/dist/lime-elements/p-9f0575fd.entry.js +159 -0
- package/dist/types/components/input-field/input-field.d.ts +1 -0
- package/dist/types/components/picker/picker.d.ts +10 -2
- package/package.json +2 -2
- package/dist/lime-elements/p-a807a45f.entry.js +0 -159
- package/dist/lime-elements/p-cd479c23.entry.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## [40.21.1](https://github.com/Lundalogik/lime-elements/compare/v40.21.0...v40.21.1) (2026-11-04)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
* **input-field:** give the focus back to the field with the up arrow ([e706469](https://github.com/Lundalogik/lime-elements/commit/e706469dbab4f84ff171e761aa77ecbf6be52933))
|
|
7
|
+
* **picker:** give the focus back to the field with the up arrow ([d25ca36](https://github.com/Lundalogik/lime-elements/commit/d25ca36e2b2558739a7541d86b3504b1234a2c69))
|
|
8
|
+
* **picker:** keep the suggestions open while one of them has the focus ([552f736](https://github.com/Lundalogik/lime-elements/commit/552f73688f21fc68780fdbfa0c03bf892b9fae7c))
|
|
9
|
+
* **picker:** move into the suggestions when an arrow key comes early ([5bea664](https://github.com/Lundalogik/lime-elements/commit/5bea664878e68bcb4597dd1241ce71d4d96bc792))
|
|
10
|
+
|
|
1
11
|
## [40.21.0](https://github.com/Lundalogik/lime-elements/compare/v40.20.1...v40.21.0) (2026-11-03)
|
|
2
12
|
|
|
3
13
|
### Features
|
|
@@ -642,10 +642,20 @@ const InputField = class {
|
|
|
642
642
|
this.showCompletions = false;
|
|
643
643
|
return;
|
|
644
644
|
}
|
|
645
|
+
if (event.key === keycodes.ARROW_UP && this.isKeyPressedOnFirstOption(event)) {
|
|
646
|
+
event.preventDefault();
|
|
647
|
+
this.setFocus();
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
645
650
|
if ([keycodes.TAB, keycodes.ENTER].includes(event.key) || typeahead.isTextEditingKey(event)) {
|
|
646
651
|
this.setFocus();
|
|
647
652
|
}
|
|
648
653
|
};
|
|
654
|
+
this.isKeyPressedOnFirstOption = (event) => {
|
|
655
|
+
const list = event.currentTarget;
|
|
656
|
+
const firstOption = list.shadowRoot.querySelector('.mdc-deprecated-list-item');
|
|
657
|
+
return !!firstOption && event.composedPath().includes(firstOption);
|
|
658
|
+
};
|
|
649
659
|
this.handleCloseMenu = () => {
|
|
650
660
|
this.showCompletions = false;
|
|
651
661
|
};
|
|
@@ -5850,6 +5850,164 @@ function base64ArrayBuffer(arrayBuffer) {
|
|
|
5850
5850
|
return base64;
|
|
5851
5851
|
}
|
|
5852
5852
|
|
|
5853
|
+
// Parser for the OTP-Token header of draft-goto-otp-token-01, an Internet-Draft that expires
|
|
5854
|
+
// on 25 March 2027. By then check for a newer revision of the draft or a published RFC, the
|
|
5855
|
+
// header name and its parameters are still open questions in the draft.
|
|
5856
|
+
//
|
|
5857
|
+
// The header value is a structured field item (RFC 9651): a String holding the passcode,
|
|
5858
|
+
// with an `origin` parameter naming the origin the passcode is bound to. Unknown parameters
|
|
5859
|
+
// are ignored, but they still have to be valid, otherwise the whole field fails to parse.
|
|
5860
|
+
// RFC 9651 lets a parser limit the size of a field. A passcode and an origin take well under
|
|
5861
|
+
// a kilobyte, the limit keeps the regular expressions below away from megabyte inputs
|
|
5862
|
+
const MAX_FIELD_LENGTH = 8192;
|
|
5863
|
+
// The bare item syntax of RFC 9651 4.2.3.1, keyed by the first character of the item. An
|
|
5864
|
+
// Integer has at most 15 digits, a Decimal at most 12 before and 1 to 3 after the dot, and a
|
|
5865
|
+
// Date is an Integer. Whatever follows a match is left for the caller, which only accepts a
|
|
5866
|
+
// parameter separator or the end of the field, so a 16th digit or a 4th decimal fails there
|
|
5867
|
+
const NUMBER = /-?(?:\d{1,12}\.\d{1,3}|\d{1,15})/y;
|
|
5868
|
+
const STRING = /"((?:[\x20\x21\x23-\x5b\x5d-\x7e]|\\["\\])*)"/y;
|
|
5869
|
+
const TOKEN = /[A-Za-z*][!#$%&'*+\-.^_`|~0-9A-Za-z:/]*/y;
|
|
5870
|
+
const BYTE_SEQUENCE = /:[A-Za-z0-9+/=]*:/y;
|
|
5871
|
+
const BOOLEAN = /\?[01]/y;
|
|
5872
|
+
const DATE = /@-?\d{1,15}/y;
|
|
5873
|
+
const DISPLAY_STRING = /%"((?:[\x20\x21\x23\x24\x26-\x7e]|%[0-9a-f]{2})*)"/y;
|
|
5874
|
+
const KEY = /[a-z*][a-z0-9_\-.*]*/y;
|
|
5875
|
+
const SPACES = / */y;
|
|
5876
|
+
// Matches the sticky expression at the cursor and moves past it, returns null if it does not match
|
|
5877
|
+
function match(cursor, re) {
|
|
5878
|
+
re.lastIndex = cursor.pos;
|
|
5879
|
+
const result = re.exec(cursor.input);
|
|
5880
|
+
if (result) {
|
|
5881
|
+
cursor.pos = re.lastIndex;
|
|
5882
|
+
}
|
|
5883
|
+
return result;
|
|
5884
|
+
}
|
|
5885
|
+
// RFC 9651 4.2.5, returns the unescaped String or null if there is no valid String at the cursor
|
|
5886
|
+
function parseString(cursor) {
|
|
5887
|
+
const result = match(cursor, STRING);
|
|
5888
|
+
return result && result[1].replace(/\\(["\\])/g, '$1');
|
|
5889
|
+
}
|
|
5890
|
+
// RFC 9651 4.2.10. The percent encoded bytes must be valid UTF-8, which decodeURIComponent
|
|
5891
|
+
// checks as it decodes them
|
|
5892
|
+
function isDisplayString(cursor) {
|
|
5893
|
+
const result = match(cursor, DISPLAY_STRING);
|
|
5894
|
+
if (!result) {
|
|
5895
|
+
return false;
|
|
5896
|
+
}
|
|
5897
|
+
try {
|
|
5898
|
+
return true;
|
|
5899
|
+
}
|
|
5900
|
+
catch {
|
|
5901
|
+
return false;
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
// RFC 9651 4.2.3.1. Returns the value of a String, true for any other valid bare item and
|
|
5905
|
+
// false for an invalid one
|
|
5906
|
+
function parseBareItem(cursor) {
|
|
5907
|
+
const c = cursor.input.charAt(cursor.pos);
|
|
5908
|
+
switch (c) {
|
|
5909
|
+
case '"':
|
|
5910
|
+
return parseString(cursor) ?? false;
|
|
5911
|
+
case ':':
|
|
5912
|
+
return !!match(cursor, BYTE_SEQUENCE);
|
|
5913
|
+
case '?':
|
|
5914
|
+
return !!match(cursor, BOOLEAN);
|
|
5915
|
+
case '@':
|
|
5916
|
+
return !!match(cursor, DATE);
|
|
5917
|
+
case '%':
|
|
5918
|
+
return isDisplayString(cursor);
|
|
5919
|
+
}
|
|
5920
|
+
return !!match(cursor, c === '-' || (c >= '0' && c <= '9') ? NUMBER : TOKEN);
|
|
5921
|
+
}
|
|
5922
|
+
/**
|
|
5923
|
+
* Parses a structured field item (RFC 9651 4.2 with field_type "item") whose bare item is a
|
|
5924
|
+
* String. Returns the String and the String parameters, or null if the value does not parse
|
|
5925
|
+
* or the bare item is of another type. A parameter of any other type is stored as true
|
|
5926
|
+
* @internal
|
|
5927
|
+
*/
|
|
5928
|
+
function parseStringItem(value) {
|
|
5929
|
+
const cursor = { input: value, pos: 0 };
|
|
5930
|
+
match(cursor, SPACES);
|
|
5931
|
+
const item = parseString(cursor);
|
|
5932
|
+
if (item === null) {
|
|
5933
|
+
return null;
|
|
5934
|
+
}
|
|
5935
|
+
// a later parameter with the same key replaces the earlier one
|
|
5936
|
+
const params = new Map();
|
|
5937
|
+
while (cursor.input[cursor.pos] === ';') {
|
|
5938
|
+
cursor.pos++;
|
|
5939
|
+
match(cursor, SPACES);
|
|
5940
|
+
const key = match(cursor, KEY);
|
|
5941
|
+
if (!key) {
|
|
5942
|
+
return null;
|
|
5943
|
+
}
|
|
5944
|
+
let paramValue = true;
|
|
5945
|
+
if (cursor.input[cursor.pos] === '=') {
|
|
5946
|
+
cursor.pos++;
|
|
5947
|
+
paramValue = parseBareItem(cursor);
|
|
5948
|
+
if (paramValue === false) {
|
|
5949
|
+
return null;
|
|
5950
|
+
}
|
|
5951
|
+
}
|
|
5952
|
+
params.set(key[0], paramValue);
|
|
5953
|
+
}
|
|
5954
|
+
match(cursor, SPACES);
|
|
5955
|
+
return cursor.pos === cursor.input.length ? { value: item, params } : null;
|
|
5956
|
+
}
|
|
5957
|
+
/**
|
|
5958
|
+
* Checks that the origin is the canonical ASCII serialization of a potentially trustworthy
|
|
5959
|
+
* origin, as the draft requires: https, or http on a loopback host
|
|
5960
|
+
* @internal
|
|
5961
|
+
*/
|
|
5962
|
+
function isOtpOrigin(origin) {
|
|
5963
|
+
let url;
|
|
5964
|
+
try {
|
|
5965
|
+
url = new URL(origin);
|
|
5966
|
+
}
|
|
5967
|
+
catch {
|
|
5968
|
+
return false;
|
|
5969
|
+
}
|
|
5970
|
+
// the URL parser lowercases the host, converts it to punycode and drops the default port, so
|
|
5971
|
+
// a value that is not canonical, or has a path, userinfo or an opaque origin, does not match
|
|
5972
|
+
// its own origin. The WHATWG parser lets characters such as a quote or a brace into a host,
|
|
5973
|
+
// which the origin grammar (RFC 6454) does not allow, so only a DNS name, an IPv4 address or a
|
|
5974
|
+
// bracketed IPv6 address is accepted
|
|
5975
|
+
if (url.origin !== origin || !/^([a-z0-9_-]+\.)*[a-z0-9_-]+$|^\[[0-9a-f:.]+\]$/.test(url.hostname)) {
|
|
5976
|
+
return false;
|
|
5977
|
+
}
|
|
5978
|
+
const isLoopback = /^(localhost|.+\.localhost|127(\.\d+){3}|\[::1\])$/.test(url.hostname);
|
|
5979
|
+
return url.protocol === 'https:' || (url.protocol === 'http:' && isLoopback);
|
|
5980
|
+
}
|
|
5981
|
+
/**
|
|
5982
|
+
* Returns the one-time passcode from the OTP-Token header of a message, following the
|
|
5983
|
+
* receiver steps of the draft (section 3.2), or undefined if the message has no valid one.
|
|
5984
|
+
* Only the header of the message itself counts, the draft has an OTP-Token in a MIME part
|
|
5985
|
+
* ignored, and more than one OTP-Token header makes the message have none
|
|
5986
|
+
* @internal
|
|
5987
|
+
*/
|
|
5988
|
+
function getOtp(headers) {
|
|
5989
|
+
let otpHeader;
|
|
5990
|
+
for (const header of headers) {
|
|
5991
|
+
if (header.key === 'otp-token') {
|
|
5992
|
+
if (otpHeader) {
|
|
5993
|
+
return undefined;
|
|
5994
|
+
}
|
|
5995
|
+
otpHeader = header;
|
|
5996
|
+
}
|
|
5997
|
+
}
|
|
5998
|
+
if (!otpHeader || otpHeader.value.length > MAX_FIELD_LENGTH) {
|
|
5999
|
+
return undefined;
|
|
6000
|
+
}
|
|
6001
|
+
// the header value is already unfolded, which the draft requires before the structured field parse
|
|
6002
|
+
const parsed = parseStringItem(otpHeader.value);
|
|
6003
|
+
const origin = parsed?.params.get('origin');
|
|
6004
|
+
// an empty passcode is valid syntax, but there is nothing in it to fill in
|
|
6005
|
+
if (!parsed?.value || typeof origin !== 'string' || !isOtpOrigin(origin)) {
|
|
6006
|
+
return undefined;
|
|
6007
|
+
}
|
|
6008
|
+
return { code: parsed.value, origin };
|
|
6009
|
+
}
|
|
6010
|
+
|
|
5853
6011
|
function isReadableStream(value) {
|
|
5854
6012
|
return !!value && typeof value.getReader === 'function';
|
|
5855
6013
|
}
|
|
@@ -6425,6 +6583,10 @@ class PostalMime {
|
|
|
6425
6583
|
// enforce ISO format if seems to be a valid date
|
|
6426
6584
|
message.date = date.toString() === 'Invalid Date' ? dateHeader.value : date.toISOString();
|
|
6427
6585
|
}
|
|
6586
|
+
const otp = getOtp(headers);
|
|
6587
|
+
if (otp) {
|
|
6588
|
+
message.otp = otp;
|
|
6589
|
+
}
|
|
6428
6590
|
if (this.textContent.html) {
|
|
6429
6591
|
message.html = this.textContent.html;
|
|
6430
6592
|
}
|
|
@@ -19,6 +19,8 @@ const pickerCss = () => `@charset "UTF-8";:host{position:relative;display:block}
|
|
|
19
19
|
const SEARCH_DEBOUNCE = 300;
|
|
20
20
|
const CHIP_SET_TAG_NAME = 'limel-chip-set';
|
|
21
21
|
const DEFAULT_SEARCHER_MAX_RESULTS = 20;
|
|
22
|
+
const MAX_FRAMES_TO_WAIT_FOR_LIST = 20;
|
|
23
|
+
const ENABLED_LIST_ITEM = '.mdc-deprecated-list-item:not(.mdc-deprecated-list-item--disabled)';
|
|
22
24
|
const Picker = class {
|
|
23
25
|
constructor(hostRef) {
|
|
24
26
|
index.registerInstance(this, hostRef);
|
|
@@ -95,7 +97,8 @@ const Picker = class {
|
|
|
95
97
|
this.chips = [];
|
|
96
98
|
// Should NOT be decorated with State(), since this
|
|
97
99
|
// should not trigger a re-render by itself.
|
|
98
|
-
this.
|
|
100
|
+
this.isEditing = false;
|
|
101
|
+
this.suggestionHasFocus = false;
|
|
99
102
|
this.suggestionsMessage = liveMessage.createLiveMessage(() => this.getSuggestionsMessage());
|
|
100
103
|
this.getValueId = (item) => {
|
|
101
104
|
const value = item.value;
|
|
@@ -179,6 +182,7 @@ const Picker = class {
|
|
|
179
182
|
this.suggestionsMessage.update();
|
|
180
183
|
}
|
|
181
184
|
disconnectedCallback() {
|
|
185
|
+
this.forgetSuggestionFocus();
|
|
182
186
|
this.debouncedSearch.cancel();
|
|
183
187
|
this.suggestionsMessage.update.cancel();
|
|
184
188
|
}
|
|
@@ -193,9 +197,10 @@ const Picker = class {
|
|
|
193
197
|
this.host.setAttribute('tabindex', '0');
|
|
194
198
|
}
|
|
195
199
|
async componentWillUpdate() {
|
|
196
|
-
this.
|
|
200
|
+
this.isEditing = false;
|
|
197
201
|
if (this.chipSet) {
|
|
198
|
-
this.
|
|
202
|
+
this.isEditing =
|
|
203
|
+
this.suggestionHasFocus || (await this.chipSet.getEditMode());
|
|
199
204
|
}
|
|
200
205
|
}
|
|
201
206
|
render() {
|
|
@@ -204,7 +209,7 @@ const Picker = class {
|
|
|
204
209
|
if (!this.multiple) {
|
|
205
210
|
props.maxItems = 1;
|
|
206
211
|
}
|
|
207
|
-
return (index.h(index.Host, { key: '
|
|
212
|
+
return (index.h(index.Host, { key: '1670bff5822f930a3eca69ad2a016cea978b3822' }, index.h("limel-chip-set", Object.assign({ key: '173a7facd5271992e792ce6a56a34a8890c9135f', 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.isEditing, suggestionListId: this.canOpenList() ? this.portalId : undefined, suggestionListOpen: this.suggestionsEngaged && dropdownContent.length > 0 }, props)), this.renderPortal(dropdownContent), index.h("span", { key: 'c151104c5059aadc3cfa526267184b663e87bebd', class: "suggestions-message", "aria-live": "polite", ref: this.suggestionsMessage.setElement })));
|
|
208
213
|
}
|
|
209
214
|
onChangeValue() {
|
|
210
215
|
this.chips = this.createChips(this.value);
|
|
@@ -277,7 +282,7 @@ const Picker = class {
|
|
|
277
282
|
return !this.readonly && !this.isFull();
|
|
278
283
|
}
|
|
279
284
|
shouldShowDropDownContent() {
|
|
280
|
-
return this.canOpenList() && !!this.
|
|
285
|
+
return this.canOpenList() && !!this.isEditing;
|
|
281
286
|
}
|
|
282
287
|
getDropdownContent() {
|
|
283
288
|
var _a;
|
|
@@ -346,10 +351,20 @@ const Picker = class {
|
|
|
346
351
|
this.chipSet.setFocus();
|
|
347
352
|
return;
|
|
348
353
|
}
|
|
354
|
+
if (event.key === keycodes.ARROW_UP && this.isKeyPressedOnFirstItem(event)) {
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
this.chipSet.setFocus();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
349
359
|
if ([keycodes.TAB, keycodes.ENTER].includes(event.key) || typeahead.isTextEditingKey(event)) {
|
|
350
360
|
this.chipSet.setFocus();
|
|
351
361
|
}
|
|
352
362
|
}
|
|
363
|
+
isKeyPressedOnFirstItem(event) {
|
|
364
|
+
const list = event.currentTarget;
|
|
365
|
+
const firstItem = list.shadowRoot.querySelector(ENABLED_LIST_ITEM);
|
|
366
|
+
return !!firstItem && event.composedPath().includes(firstItem);
|
|
367
|
+
}
|
|
353
368
|
renderPortal(content = []) {
|
|
354
369
|
const dropdownZIndex = getComputedStyle(this.host).getPropertyValue('--dropdown-z-index');
|
|
355
370
|
return (index.h("limel-portal", { visible: content.length > 0, containerId: this.portalId, inheritParentWidth: true, containerStyle: { 'z-index': dropdownZIndex } }, index.h("limel-menu-surface", { open: content.length > 0, allowClicksElement: this.host, style: {
|
|
@@ -383,6 +398,7 @@ const Picker = class {
|
|
|
383
398
|
const query = event.detail;
|
|
384
399
|
this.textValue = query;
|
|
385
400
|
this.suggestionsEngaged = true;
|
|
401
|
+
this.forgetSuggestionFocus();
|
|
386
402
|
this.debouncedSearch(query);
|
|
387
403
|
// If the search-query is an empty string, bypass debouncing.
|
|
388
404
|
if (query === '') {
|
|
@@ -432,6 +448,7 @@ const Picker = class {
|
|
|
432
448
|
if (!event.detail) {
|
|
433
449
|
return;
|
|
434
450
|
}
|
|
451
|
+
this.suggestionHasFocus = false;
|
|
435
452
|
this.action.emit(event.detail.value);
|
|
436
453
|
this.items = [];
|
|
437
454
|
}
|
|
@@ -440,6 +457,7 @@ const Picker = class {
|
|
|
440
457
|
* Prevent focus if the picker has a value and does not support multiple values
|
|
441
458
|
*/
|
|
442
459
|
handleInputFieldFocus() {
|
|
460
|
+
this.forgetSuggestionFocus();
|
|
443
461
|
const query = this.textValue;
|
|
444
462
|
this.debouncedSearch(query);
|
|
445
463
|
}
|
|
@@ -477,7 +495,6 @@ const Picker = class {
|
|
|
477
495
|
* @param event - event
|
|
478
496
|
*/
|
|
479
497
|
handleInputKeyDown(event) {
|
|
480
|
-
var _a;
|
|
481
498
|
this.suggestionsEngaged = true;
|
|
482
499
|
const isUp = event.key === keycodes.ARROW_UP;
|
|
483
500
|
const isDown = event.key === keycodes.ARROW_DOWN;
|
|
@@ -491,28 +508,47 @@ const Picker = class {
|
|
|
491
508
|
if (!isUp && !isDown) {
|
|
492
509
|
return;
|
|
493
510
|
}
|
|
494
|
-
const
|
|
495
|
-
if (
|
|
511
|
+
const position = isDown ? 'first' : 'last';
|
|
512
|
+
if (this.focusSuggestion(position)) {
|
|
513
|
+
event.preventDefault();
|
|
496
514
|
return;
|
|
497
515
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
516
|
+
this.suggestionToFocusWhenShown = position;
|
|
517
|
+
}
|
|
518
|
+
focusSuggestion(position) {
|
|
519
|
+
var _a;
|
|
520
|
+
const list = (_a = document.querySelector(` #${this.portalId} limel-list[data-suggestions]`)) !== null && _a !== void 0 ? _a : document.querySelector(` #${this.portalId} limel-list`);
|
|
521
|
+
if (!list) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
// Match by class only (not `:first-child`) so a leading
|
|
525
|
+
// `ListSeparator` doesn't shadow the first focusable item.
|
|
526
|
+
const items = list.shadowRoot.querySelectorAll(ENABLED_LIST_ITEM);
|
|
527
|
+
const item = position === 'first' ? items[0] : [...items].at(-1);
|
|
528
|
+
item === null || item === void 0 ? void 0 : item.focus();
|
|
529
|
+
this.suggestionHasFocus =
|
|
530
|
+
!!item && list.shadowRoot.activeElement === item;
|
|
531
|
+
return this.suggestionHasFocus;
|
|
532
|
+
}
|
|
533
|
+
focusSuggestionWhenShown(position) {
|
|
534
|
+
cancelAnimationFrame(this.suggestionFocusFrame);
|
|
535
|
+
let frames = 0;
|
|
536
|
+
const focusSuggestion = () => {
|
|
537
|
+
if (this.focusSuggestion(position)) {
|
|
503
538
|
return;
|
|
504
539
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
540
|
+
frames += 1;
|
|
541
|
+
if (frames < MAX_FRAMES_TO_WAIT_FOR_LIST) {
|
|
542
|
+
this.suggestionFocusFrame =
|
|
543
|
+
requestAnimationFrame(focusSuggestion);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
this.suggestionFocusFrame = requestAnimationFrame(focusSuggestion);
|
|
547
|
+
}
|
|
548
|
+
forgetSuggestionFocus() {
|
|
549
|
+
this.suggestionHasFocus = false;
|
|
550
|
+
this.suggestionToFocusWhenShown = undefined;
|
|
551
|
+
cancelAnimationFrame(this.suggestionFocusFrame);
|
|
516
552
|
}
|
|
517
553
|
handleSearchResult(query, result) {
|
|
518
554
|
var _a;
|
|
@@ -531,6 +567,11 @@ const Picker = class {
|
|
|
531
567
|
}
|
|
532
568
|
this.items = this.prependSearchHeader(query, nextItems);
|
|
533
569
|
this.loading = false;
|
|
570
|
+
const position = this.suggestionToFocusWhenShown;
|
|
571
|
+
this.suggestionToFocusWhenShown = undefined;
|
|
572
|
+
if (position && this.hasPickableItems(nextItems)) {
|
|
573
|
+
this.focusSuggestionWhenShown(position);
|
|
574
|
+
}
|
|
534
575
|
}
|
|
535
576
|
hasPickableItems(items) {
|
|
536
577
|
return items.some((item) => !('separator' in item));
|
|
@@ -553,7 +594,8 @@ const Picker = class {
|
|
|
553
594
|
/**
|
|
554
595
|
* Shared prelude for any flow that ends the current search session:
|
|
555
596
|
* wipe the chip-set's visible text, reset the picker's `textValue`,
|
|
556
|
-
*
|
|
597
|
+
* cancel any in-flight debounced search, and forget that a suggestion
|
|
598
|
+
* has the focus, or that an arrow key is waiting for the suggestions.
|
|
557
599
|
*
|
|
558
600
|
* Used by `clearInputField` (which then drops the dropdown
|
|
559
601
|
* entirely) and `resetSearchToDefault` (which re-runs the searcher
|
|
@@ -563,6 +605,7 @@ const Picker = class {
|
|
|
563
605
|
this.chipSet.emptyInput();
|
|
564
606
|
this.textValue = '';
|
|
565
607
|
this.debouncedSearch.cancel();
|
|
608
|
+
this.forgetSuggestionFocus();
|
|
566
609
|
}
|
|
567
610
|
clearInputField() {
|
|
568
611
|
this.clearTextValue();
|
|
@@ -524,10 +524,20 @@ export class InputField {
|
|
|
524
524
|
this.showCompletions = false;
|
|
525
525
|
return;
|
|
526
526
|
}
|
|
527
|
+
if (event.key === ARROW_UP && this.isKeyPressedOnFirstOption(event)) {
|
|
528
|
+
event.preventDefault();
|
|
529
|
+
this.setFocus();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
527
532
|
if ([TAB, ENTER].includes(event.key) || isTextEditingKey(event)) {
|
|
528
533
|
this.setFocus();
|
|
529
534
|
}
|
|
530
535
|
};
|
|
536
|
+
this.isKeyPressedOnFirstOption = (event) => {
|
|
537
|
+
const list = event.currentTarget;
|
|
538
|
+
const firstOption = list.shadowRoot.querySelector('.mdc-deprecated-list-item');
|
|
539
|
+
return !!firstOption && event.composedPath().includes(firstOption);
|
|
540
|
+
};
|
|
531
541
|
this.handleCloseMenu = () => {
|
|
532
542
|
this.showCompletions = false;
|
|
533
543
|
};
|
|
@@ -10,6 +10,8 @@ import { debounce } from "lodash-es";
|
|
|
10
10
|
const SEARCH_DEBOUNCE = 300;
|
|
11
11
|
const CHIP_SET_TAG_NAME = 'limel-chip-set';
|
|
12
12
|
const DEFAULT_SEARCHER_MAX_RESULTS = 20;
|
|
13
|
+
const MAX_FRAMES_TO_WAIT_FOR_LIST = 20;
|
|
14
|
+
const ENABLED_LIST_ITEM = '.mdc-deprecated-list-item:not(.mdc-deprecated-list-item--disabled)';
|
|
13
15
|
/**
|
|
14
16
|
* @exampleComponent limel-example-picker-basic
|
|
15
17
|
* @exampleComponent limel-example-picker-multiple
|
|
@@ -98,7 +100,8 @@ export class Picker {
|
|
|
98
100
|
this.chips = [];
|
|
99
101
|
// Should NOT be decorated with State(), since this
|
|
100
102
|
// should not trigger a re-render by itself.
|
|
101
|
-
this.
|
|
103
|
+
this.isEditing = false;
|
|
104
|
+
this.suggestionHasFocus = false;
|
|
102
105
|
this.suggestionsMessage = createLiveMessage(() => this.getSuggestionsMessage());
|
|
103
106
|
this.getValueId = (item) => {
|
|
104
107
|
const value = item.value;
|
|
@@ -182,6 +185,7 @@ export class Picker {
|
|
|
182
185
|
this.suggestionsMessage.update();
|
|
183
186
|
}
|
|
184
187
|
disconnectedCallback() {
|
|
188
|
+
this.forgetSuggestionFocus();
|
|
185
189
|
this.debouncedSearch.cancel();
|
|
186
190
|
this.suggestionsMessage.update.cancel();
|
|
187
191
|
}
|
|
@@ -196,9 +200,10 @@ export class Picker {
|
|
|
196
200
|
this.host.setAttribute('tabindex', '0');
|
|
197
201
|
}
|
|
198
202
|
async componentWillUpdate() {
|
|
199
|
-
this.
|
|
203
|
+
this.isEditing = false;
|
|
200
204
|
if (this.chipSet) {
|
|
201
|
-
this.
|
|
205
|
+
this.isEditing =
|
|
206
|
+
this.suggestionHasFocus || (await this.chipSet.getEditMode());
|
|
202
207
|
}
|
|
203
208
|
}
|
|
204
209
|
render() {
|
|
@@ -207,7 +212,7 @@ export class Picker {
|
|
|
207
212
|
if (!this.multiple) {
|
|
208
213
|
props.maxItems = 1;
|
|
209
214
|
}
|
|
210
|
-
return (h(Host, { key: '
|
|
215
|
+
return (h(Host, { key: '1670bff5822f930a3eca69ad2a016cea978b3822' }, h("limel-chip-set", Object.assign({ key: '173a7facd5271992e792ce6a56a34a8890c9135f', 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.isEditing, suggestionListId: this.canOpenList() ? this.portalId : undefined, suggestionListOpen: this.suggestionsEngaged && dropdownContent.length > 0 }, props)), this.renderPortal(dropdownContent), h("span", { key: 'c151104c5059aadc3cfa526267184b663e87bebd', class: "suggestions-message", "aria-live": "polite", ref: this.suggestionsMessage.setElement })));
|
|
211
216
|
}
|
|
212
217
|
onChangeValue() {
|
|
213
218
|
this.chips = this.createChips(this.value);
|
|
@@ -280,7 +285,7 @@ export class Picker {
|
|
|
280
285
|
return !this.readonly && !this.isFull();
|
|
281
286
|
}
|
|
282
287
|
shouldShowDropDownContent() {
|
|
283
|
-
return this.canOpenList() && !!this.
|
|
288
|
+
return this.canOpenList() && !!this.isEditing;
|
|
284
289
|
}
|
|
285
290
|
getDropdownContent() {
|
|
286
291
|
var _a;
|
|
@@ -349,10 +354,20 @@ export class Picker {
|
|
|
349
354
|
this.chipSet.setFocus();
|
|
350
355
|
return;
|
|
351
356
|
}
|
|
357
|
+
if (event.key === ARROW_UP && this.isKeyPressedOnFirstItem(event)) {
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
this.chipSet.setFocus();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
352
362
|
if ([TAB, ENTER].includes(event.key) || isTextEditingKey(event)) {
|
|
353
363
|
this.chipSet.setFocus();
|
|
354
364
|
}
|
|
355
365
|
}
|
|
366
|
+
isKeyPressedOnFirstItem(event) {
|
|
367
|
+
const list = event.currentTarget;
|
|
368
|
+
const firstItem = list.shadowRoot.querySelector(ENABLED_LIST_ITEM);
|
|
369
|
+
return !!firstItem && event.composedPath().includes(firstItem);
|
|
370
|
+
}
|
|
356
371
|
renderPortal(content = []) {
|
|
357
372
|
const dropdownZIndex = getComputedStyle(this.host).getPropertyValue('--dropdown-z-index');
|
|
358
373
|
return (h("limel-portal", { visible: content.length > 0, containerId: this.portalId, inheritParentWidth: true, containerStyle: { 'z-index': dropdownZIndex } }, h("limel-menu-surface", { open: content.length > 0, allowClicksElement: this.host, style: {
|
|
@@ -386,6 +401,7 @@ export class Picker {
|
|
|
386
401
|
const query = event.detail;
|
|
387
402
|
this.textValue = query;
|
|
388
403
|
this.suggestionsEngaged = true;
|
|
404
|
+
this.forgetSuggestionFocus();
|
|
389
405
|
this.debouncedSearch(query);
|
|
390
406
|
// If the search-query is an empty string, bypass debouncing.
|
|
391
407
|
if (query === '') {
|
|
@@ -435,6 +451,7 @@ export class Picker {
|
|
|
435
451
|
if (!event.detail) {
|
|
436
452
|
return;
|
|
437
453
|
}
|
|
454
|
+
this.suggestionHasFocus = false;
|
|
438
455
|
this.action.emit(event.detail.value);
|
|
439
456
|
this.items = [];
|
|
440
457
|
}
|
|
@@ -443,6 +460,7 @@ export class Picker {
|
|
|
443
460
|
* Prevent focus if the picker has a value and does not support multiple values
|
|
444
461
|
*/
|
|
445
462
|
handleInputFieldFocus() {
|
|
463
|
+
this.forgetSuggestionFocus();
|
|
446
464
|
const query = this.textValue;
|
|
447
465
|
this.debouncedSearch(query);
|
|
448
466
|
}
|
|
@@ -480,7 +498,6 @@ export class Picker {
|
|
|
480
498
|
* @param event - event
|
|
481
499
|
*/
|
|
482
500
|
handleInputKeyDown(event) {
|
|
483
|
-
var _a;
|
|
484
501
|
this.suggestionsEngaged = true;
|
|
485
502
|
const isUp = event.key === ARROW_UP;
|
|
486
503
|
const isDown = event.key === ARROW_DOWN;
|
|
@@ -494,28 +511,47 @@ export class Picker {
|
|
|
494
511
|
if (!isUp && !isDown) {
|
|
495
512
|
return;
|
|
496
513
|
}
|
|
514
|
+
const position = isDown ? 'first' : 'last';
|
|
515
|
+
if (this.focusSuggestion(position)) {
|
|
516
|
+
event.preventDefault();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
this.suggestionToFocusWhenShown = position;
|
|
520
|
+
}
|
|
521
|
+
focusSuggestion(position) {
|
|
522
|
+
var _a;
|
|
497
523
|
const list = (_a = document.querySelector(` #${this.portalId} limel-list[data-suggestions]`)) !== null && _a !== void 0 ? _a : document.querySelector(` #${this.portalId} limel-list`);
|
|
498
524
|
if (!list) {
|
|
499
|
-
return;
|
|
525
|
+
return false;
|
|
500
526
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
527
|
+
// Match by class only (not `:first-child`) so a leading
|
|
528
|
+
// `ListSeparator` doesn't shadow the first focusable item.
|
|
529
|
+
const items = list.shadowRoot.querySelectorAll(ENABLED_LIST_ITEM);
|
|
530
|
+
const item = position === 'first' ? items[0] : [...items].at(-1);
|
|
531
|
+
item === null || item === void 0 ? void 0 : item.focus();
|
|
532
|
+
this.suggestionHasFocus =
|
|
533
|
+
!!item && list.shadowRoot.activeElement === item;
|
|
534
|
+
return this.suggestionHasFocus;
|
|
535
|
+
}
|
|
536
|
+
focusSuggestionWhenShown(position) {
|
|
537
|
+
cancelAnimationFrame(this.suggestionFocusFrame);
|
|
538
|
+
let frames = 0;
|
|
539
|
+
const focusSuggestion = () => {
|
|
540
|
+
if (this.focusSuggestion(position)) {
|
|
506
541
|
return;
|
|
507
542
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
543
|
+
frames += 1;
|
|
544
|
+
if (frames < MAX_FRAMES_TO_WAIT_FOR_LIST) {
|
|
545
|
+
this.suggestionFocusFrame =
|
|
546
|
+
requestAnimationFrame(focusSuggestion);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
this.suggestionFocusFrame = requestAnimationFrame(focusSuggestion);
|
|
550
|
+
}
|
|
551
|
+
forgetSuggestionFocus() {
|
|
552
|
+
this.suggestionHasFocus = false;
|
|
553
|
+
this.suggestionToFocusWhenShown = undefined;
|
|
554
|
+
cancelAnimationFrame(this.suggestionFocusFrame);
|
|
519
555
|
}
|
|
520
556
|
handleSearchResult(query, result) {
|
|
521
557
|
var _a;
|
|
@@ -534,6 +570,11 @@ export class Picker {
|
|
|
534
570
|
}
|
|
535
571
|
this.items = this.prependSearchHeader(query, nextItems);
|
|
536
572
|
this.loading = false;
|
|
573
|
+
const position = this.suggestionToFocusWhenShown;
|
|
574
|
+
this.suggestionToFocusWhenShown = undefined;
|
|
575
|
+
if (position && this.hasPickableItems(nextItems)) {
|
|
576
|
+
this.focusSuggestionWhenShown(position);
|
|
577
|
+
}
|
|
537
578
|
}
|
|
538
579
|
hasPickableItems(items) {
|
|
539
580
|
return items.some((item) => !('separator' in item));
|
|
@@ -556,7 +597,8 @@ export class Picker {
|
|
|
556
597
|
/**
|
|
557
598
|
* Shared prelude for any flow that ends the current search session:
|
|
558
599
|
* wipe the chip-set's visible text, reset the picker's `textValue`,
|
|
559
|
-
*
|
|
600
|
+
* cancel any in-flight debounced search, and forget that a suggestion
|
|
601
|
+
* has the focus, or that an arrow key is waiting for the suggestions.
|
|
560
602
|
*
|
|
561
603
|
* Used by `clearInputField` (which then drops the dropdown
|
|
562
604
|
* entirely) and `resetSearchToDefault` (which re-runs the searcher
|
|
@@ -566,6 +608,7 @@ export class Picker {
|
|
|
566
608
|
this.chipSet.emptyInput();
|
|
567
609
|
this.textValue = '';
|
|
568
610
|
this.debouncedSearch.cancel();
|
|
611
|
+
this.forgetSuggestionFocus();
|
|
569
612
|
}
|
|
570
613
|
clearInputField() {
|
|
571
614
|
this.clearTextValue();
|
|
@@ -640,10 +640,20 @@ const InputField = class {
|
|
|
640
640
|
this.showCompletions = false;
|
|
641
641
|
return;
|
|
642
642
|
}
|
|
643
|
+
if (event.key === ARROW_UP && this.isKeyPressedOnFirstOption(event)) {
|
|
644
|
+
event.preventDefault();
|
|
645
|
+
this.setFocus();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
643
648
|
if ([TAB, ENTER].includes(event.key) || isTextEditingKey(event)) {
|
|
644
649
|
this.setFocus();
|
|
645
650
|
}
|
|
646
651
|
};
|
|
652
|
+
this.isKeyPressedOnFirstOption = (event) => {
|
|
653
|
+
const list = event.currentTarget;
|
|
654
|
+
const firstOption = list.shadowRoot.querySelector('.mdc-deprecated-list-item');
|
|
655
|
+
return !!firstOption && event.composedPath().includes(firstOption);
|
|
656
|
+
};
|
|
647
657
|
this.handleCloseMenu = () => {
|
|
648
658
|
this.showCompletions = false;
|
|
649
659
|
};
|