@limetech/lime-elements 39.44.4 → 39.44.6
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 +15 -0
- package/dist/cjs/{keycodes-B75-uKOK.js → keycodes-k-EdzcMX.js} +4 -0
- package/dist/cjs/lime-elements.cjs.js +1 -1
- package/dist/cjs/limel-breadcrumbs_8.cjs.entry.js +20 -1
- package/dist/cjs/limel-chip-set.cjs.entry.js +1 -1
- package/dist/cjs/limel-chip_2.cjs.entry.js +1 -1
- package/dist/cjs/limel-file-viewer.cjs.entry.js +84 -8
- package/dist/cjs/limel-markdown.cjs.entry.js +2 -2
- package/dist/cjs/limel-picker.cjs.entry.js +1 -1
- package/dist/cjs/limel-popover_2.cjs.entry.js +1 -1
- package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js +1 -1
- package/dist/cjs/limel-select.cjs.entry.js +377 -13
- package/dist/cjs/limel-text-editor-link-menu.cjs.entry.js +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/{markdown-parser-F3YRMjcy.js → markdown-parser-ChnRAxpZ.js} +22 -6
- package/dist/collection/components/list/list.js +19 -0
- package/dist/collection/components/markdown/markdown-parser.js +5 -0
- package/dist/collection/components/markdown/markdown.js +2 -1
- package/dist/collection/components/markdown/sanitize-style.js +17 -6
- package/dist/collection/components/select/select.js +215 -12
- package/dist/collection/components/select/select.template.js +17 -3
- package/dist/collection/util/keycodes.js +3 -0
- package/dist/collection/util/typeahead.js +159 -0
- package/dist/esm/{keycodes-rI0IeKpx.js → keycodes-C4_9qUMP.js} +4 -1
- package/dist/esm/lime-elements.js +1 -1
- package/dist/esm/limel-breadcrumbs_8.entry.js +20 -1
- package/dist/esm/limel-chip-set.entry.js +1 -1
- package/dist/esm/limel-chip_2.entry.js +1 -1
- package/dist/esm/limel-file-viewer.entry.js +84 -8
- package/dist/esm/limel-markdown.entry.js +2 -2
- package/dist/esm/limel-picker.entry.js +1 -1
- package/dist/esm/limel-popover_2.entry.js +1 -1
- package/dist/esm/limel-prosemirror-adapter.entry.js +1 -1
- package/dist/esm/limel-select.entry.js +378 -14
- package/dist/esm/limel-text-editor-link-menu.entry.js +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/esm/{markdown-parser-Blt6ZFY0.js → markdown-parser-BSQKleVw.js} +22 -6
- package/dist/lime-elements/lime-elements.esm.js +1 -1
- package/dist/lime-elements/{p-a089b580.entry.js → p-03fd2181.entry.js} +1 -1
- package/dist/lime-elements/{p-fc43fb46.entry.js → p-12fe8fe1.entry.js} +6 -6
- package/dist/lime-elements/{p-bf3c78a8.entry.js → p-2b01c1f2.entry.js} +1 -1
- package/dist/lime-elements/{p-758939be.entry.js → p-2e5d5e79.entry.js} +1 -1
- package/dist/lime-elements/{p-224e80b5.entry.js → p-3ea159fd.entry.js} +1 -1
- package/dist/lime-elements/{p-4512a7b1.entry.js → p-845cfda7.entry.js} +1 -1
- package/dist/lime-elements/p-9152d7e1.entry.js +23 -0
- package/dist/lime-elements/{p-1e3cdfa1.entry.js → p-9a04a686.entry.js} +1 -1
- package/dist/lime-elements/{p-DnPeu2SH.js → p-B0qhUema.js} +1 -1
- package/dist/lime-elements/p-C4_9qUMP.js +1 -0
- package/dist/lime-elements/{p-6fbf20c6.entry.js → p-e8df7a2e.entry.js} +1 -1
- package/dist/lime-elements/p-f51b5651.entry.js +1 -0
- package/dist/types/components/markdown/markdown.d.ts +1 -0
- package/dist/types/components/markdown/sanitize-style.d.ts +9 -5
- package/dist/types/components/select/select.d.ts +76 -1
- package/dist/types/components/select/select.template.d.ts +21 -1
- package/dist/types/components.d.ts +40 -0
- package/dist/types/util/keycodes.d.ts +3 -0
- package/dist/types/util/typeahead.d.ts +101 -0
- package/package.json +3 -3
- package/dist/lime-elements/p-552d0733.entry.js +0 -1
- package/dist/lime-elements/p-8f4273e4.entry.js +0 -23
- package/dist/lime-elements/p-rI0IeKpx.js +0 -1
|
@@ -33792,26 +33792,31 @@ function sanitizeStyle(node) {
|
|
|
33792
33792
|
var _a;
|
|
33793
33793
|
if (node.tagName && ((_a = node.properties) === null || _a === void 0 ? void 0 : _a.style)) {
|
|
33794
33794
|
// Sanitize the 'style' attribute of the node.
|
|
33795
|
-
node.properties.style = sanitizeStyleValue(node.properties.style);
|
|
33795
|
+
node.properties.style = sanitizeStyleValue(node.properties.style, node.tagName);
|
|
33796
33796
|
}
|
|
33797
33797
|
}
|
|
33798
33798
|
/**
|
|
33799
33799
|
* Applies a whitelist to the CSS properties in the input string.
|
|
33800
33800
|
* Any CSS properties not in the whitelist will be removed.
|
|
33801
33801
|
*
|
|
33802
|
+
* A zero width is also removed from `table` elements. Some HTML sources
|
|
33803
|
+
* (e.g. Google Sheets) set `width: 0px` on the table element and define
|
|
33804
|
+
* the real column widths in `colgroup`; a zero width forces the table
|
|
33805
|
+
* down to its min-content size, crushing every column to its longest
|
|
33806
|
+
* word. A zero width on a table can only ever harm rendering, since
|
|
33807
|
+
* auto layout already floors the table at min-content.
|
|
33808
|
+
*
|
|
33802
33809
|
* @param styleValue - a string with CSS properties and values
|
|
33810
|
+
* @param tagName - tag name of the element the style belongs to
|
|
33803
33811
|
* @returns a sanitized version of the input string
|
|
33804
33812
|
*/
|
|
33805
|
-
|
|
33806
|
-
*
|
|
33807
|
-
* @param styleValue
|
|
33808
|
-
*/
|
|
33809
|
-
function sanitizeStyleValue(styleValue) {
|
|
33813
|
+
function sanitizeStyleValue(styleValue, tagName) {
|
|
33810
33814
|
try {
|
|
33811
33815
|
const css = StyleToObject(styleValue);
|
|
33812
33816
|
const normalizedCss = normalizeBackgroundColor(css);
|
|
33813
33817
|
return Object.entries(normalizedCss)
|
|
33814
33818
|
.filter(([key]) => allowedCssProperties.includes(key))
|
|
33819
|
+
.filter(([key, value]) => !isCollapsedTableWidth(tagName, key, value))
|
|
33815
33820
|
.map(([key, value]) => `${key}: ${value}`)
|
|
33816
33821
|
.join('; ');
|
|
33817
33822
|
}
|
|
@@ -33820,6 +33825,12 @@ function sanitizeStyleValue(styleValue) {
|
|
|
33820
33825
|
return '';
|
|
33821
33826
|
}
|
|
33822
33827
|
}
|
|
33828
|
+
function isCollapsedTableWidth(tagName, property, value) {
|
|
33829
|
+
return tagName === 'table' && property === 'width' && isZeroLength(value);
|
|
33830
|
+
}
|
|
33831
|
+
function isZeroLength(value) {
|
|
33832
|
+
return Number.parseFloat(value) === 0;
|
|
33833
|
+
}
|
|
33823
33834
|
/**
|
|
33824
33835
|
* Returns a copy of the input object with the `background` property removed.
|
|
33825
33836
|
* If the `background` property's value was a valid CSS color value, the
|
|
@@ -34119,6 +34130,11 @@ function getWhiteList(allowedComponents) {
|
|
|
34119
34130
|
// elements only.
|
|
34120
34131
|
clobberPrefix: '', strip: [...((_b = index$2.defaultSchema.strip) !== null && _b !== void 0 ? _b : []), 'style'], tagNames: [
|
|
34121
34132
|
...(index$2.defaultSchema.tagNames || []),
|
|
34133
|
+
// Table column definitions carry the column widths for tables
|
|
34134
|
+
// from sources like Google Sheets, where the table element
|
|
34135
|
+
// itself has no usable width.
|
|
34136
|
+
'colgroup',
|
|
34137
|
+
'col',
|
|
34122
34138
|
...allowedComponents.map((component) => component.tagName),
|
|
34123
34139
|
], attributes: Object.assign(Object.assign({}, index$2.defaultSchema.attributes), { p: [
|
|
34124
34140
|
...((_c = index$2.defaultSchema.attributes.p) !== null && _c !== void 0 ? _c : []),
|
|
@@ -50,6 +50,25 @@ 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
|
+
// NOTE: This has no effect. It is kept as a marker rather than
|
|
54
|
+
// removed, so that its absence isn't mistaken for an oversight.
|
|
55
|
+
//
|
|
56
|
+
// MDC indexes each row for typeahead by looking for
|
|
57
|
+
// `.mdc-deprecated-list-item__primary-text` inside it, while
|
|
58
|
+
// `limel-list-item` renders its label as `<span class="label">`. MDC
|
|
59
|
+
// therefore builds an empty index, and typing does nothing.
|
|
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.
|
|
53
72
|
this.mdcList.hasTypeahead = true;
|
|
54
73
|
};
|
|
55
74
|
/**
|
|
@@ -107,6 +107,11 @@ function getWhiteList(allowedComponents) {
|
|
|
107
107
|
// elements only.
|
|
108
108
|
clobberPrefix: '', strip: [...((_b = defaultSchema.strip) !== null && _b !== void 0 ? _b : []), 'style'], tagNames: [
|
|
109
109
|
...(defaultSchema.tagNames || []),
|
|
110
|
+
// Table column definitions carry the column widths for tables
|
|
111
|
+
// from sources like Google Sheets, where the table element
|
|
112
|
+
// itself has no usable width.
|
|
113
|
+
'colgroup',
|
|
114
|
+
'col',
|
|
110
115
|
...allowedComponents.map((component) => component.tagName),
|
|
111
116
|
], attributes: Object.assign(Object.assign({}, defaultSchema.attributes), { p: [
|
|
112
117
|
...((_c = defaultSchema.attributes.p) !== null && _c !== void 0 ? _c : []),
|
|
@@ -28,6 +28,7 @@ import { adaptColorContrast } from "../../util/adapt-color-contrast";
|
|
|
28
28
|
* @exampleComponent limel-example-markdown-code
|
|
29
29
|
* @exampleComponent limel-example-markdown-footnotes
|
|
30
30
|
* @exampleComponent limel-example-markdown-tables
|
|
31
|
+
* @exampleComponent limel-example-markdown-html-tables
|
|
31
32
|
* @exampleComponent limel-example-markdown-html
|
|
32
33
|
* @exampleComponent limel-example-markdown-keys
|
|
33
34
|
* @exampleComponent limel-example-markdown-blockquotes
|
|
@@ -143,7 +144,7 @@ export class Markdown {
|
|
|
143
144
|
this.cleanupImageIntersectionObserver();
|
|
144
145
|
}
|
|
145
146
|
render() {
|
|
146
|
-
return (h(Host, { key: '
|
|
147
|
+
return (h(Host, { key: '527dafa3c663a214860ebf7bae33f568f21d4b2d' }, h("div", { key: '79a0d02a4118e94723c6ddfbb94a91875dfe3d2b', id: "markdown", ref: (el) => (this.rootElement = el) })));
|
|
147
148
|
}
|
|
148
149
|
setupImageIntersectionObserver() {
|
|
149
150
|
if (this.lazyLoadImages) {
|
|
@@ -10,26 +10,31 @@ export function sanitizeStyle(node) {
|
|
|
10
10
|
var _a;
|
|
11
11
|
if (node.tagName && ((_a = node.properties) === null || _a === void 0 ? void 0 : _a.style)) {
|
|
12
12
|
// Sanitize the 'style' attribute of the node.
|
|
13
|
-
node.properties.style = sanitizeStyleValue(node.properties.style);
|
|
13
|
+
node.properties.style = sanitizeStyleValue(node.properties.style, node.tagName);
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Applies a whitelist to the CSS properties in the input string.
|
|
18
18
|
* Any CSS properties not in the whitelist will be removed.
|
|
19
19
|
*
|
|
20
|
+
* A zero width is also removed from `table` elements. Some HTML sources
|
|
21
|
+
* (e.g. Google Sheets) set `width: 0px` on the table element and define
|
|
22
|
+
* the real column widths in `colgroup`; a zero width forces the table
|
|
23
|
+
* down to its min-content size, crushing every column to its longest
|
|
24
|
+
* word. A zero width on a table can only ever harm rendering, since
|
|
25
|
+
* auto layout already floors the table at min-content.
|
|
26
|
+
*
|
|
20
27
|
* @param styleValue - a string with CSS properties and values
|
|
28
|
+
* @param tagName - tag name of the element the style belongs to
|
|
21
29
|
* @returns a sanitized version of the input string
|
|
22
30
|
*/
|
|
23
|
-
|
|
24
|
-
*
|
|
25
|
-
* @param styleValue
|
|
26
|
-
*/
|
|
27
|
-
export function sanitizeStyleValue(styleValue) {
|
|
31
|
+
export function sanitizeStyleValue(styleValue, tagName) {
|
|
28
32
|
try {
|
|
29
33
|
const css = parse(styleValue);
|
|
30
34
|
const normalizedCss = normalizeBackgroundColor(css);
|
|
31
35
|
return Object.entries(normalizedCss)
|
|
32
36
|
.filter(([key]) => allowedCssProperties.includes(key))
|
|
37
|
+
.filter(([key, value]) => !isCollapsedTableWidth(tagName, key, value))
|
|
33
38
|
.map(([key, value]) => `${key}: ${value}`)
|
|
34
39
|
.join('; ');
|
|
35
40
|
}
|
|
@@ -38,6 +43,12 @@ export function sanitizeStyleValue(styleValue) {
|
|
|
38
43
|
return '';
|
|
39
44
|
}
|
|
40
45
|
}
|
|
46
|
+
function isCollapsedTableWidth(tagName, property, value) {
|
|
47
|
+
return tagName === 'table' && property === 'width' && isZeroLength(value);
|
|
48
|
+
}
|
|
49
|
+
function isZeroLength(value) {
|
|
50
|
+
return Number.parseFloat(value) === 0;
|
|
51
|
+
}
|
|
41
52
|
/**
|
|
42
53
|
* Returns a copy of the input object with the `background` property removed.
|
|
43
54
|
* If the `background` property's value was a valid CSS color value, the
|
|
@@ -2,11 +2,23 @@ import { MDCFloatingLabel } from "@material/floating-label";
|
|
|
2
2
|
import { MDCSelectHelperText } from "@material/select/helper-text";
|
|
3
3
|
import { h, } from "@stencil/core";
|
|
4
4
|
import { isMobileDevice } from "../../util/device";
|
|
5
|
-
import { ENTER,
|
|
5
|
+
import { ENTER, SPACEBAR } from "../../util/keycodes";
|
|
6
6
|
import { isMultiple } from "../../util/multiple";
|
|
7
7
|
import { createRandomString } from "../../util/random-string";
|
|
8
|
-
import {
|
|
8
|
+
import { findTypeaheadMatch, isTypeaheadKey, NO_TYPEAHEAD_MATCH, TypeaheadBuffer, } from "../../util/typeahead";
|
|
9
|
+
import { createMenuItems, SelectTemplate, triggerIconColorWarning, } from "./select.template";
|
|
9
10
|
/**
|
|
11
|
+
* ## Keyboard
|
|
12
|
+
*
|
|
13
|
+
* Typing characters jumps to the option whose text starts with them, the way
|
|
14
|
+
* a native `<select>` does. Characters accumulate for a short while, so typing
|
|
15
|
+
* `n`, `e` finds "Netherlands" rather than the next option starting with `n`.
|
|
16
|
+
* Pressing the same character repeatedly cycles through all options starting
|
|
17
|
+
* with it. This works both while the dropdown is open, and while the closed
|
|
18
|
+
* component has focus — in which case the dropdown opens with the match
|
|
19
|
+
* highlighted. Typing only moves the highlight; the value is not changed until
|
|
20
|
+
* the option is picked with `Enter` or a click.
|
|
21
|
+
*
|
|
10
22
|
* @exampleComponent limel-example-select-basic
|
|
11
23
|
* @exampleComponent limel-example-select-with-icons
|
|
12
24
|
* @exampleComponent limel-example-select-with-separators
|
|
@@ -26,6 +38,15 @@ export class Select {
|
|
|
26
38
|
updateHasPrimaryComponent() {
|
|
27
39
|
this.hasPrimaryComponentMemo = this.computeHasPrimaryComponent();
|
|
28
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* `options` and `required` are the inputs that decide which rows the
|
|
43
|
+
* dropdown renders, and therefore which row an index refers to. When
|
|
44
|
+
* either changes, a buffered typeahead match no longer points at what the
|
|
45
|
+
* user was aiming for.
|
|
46
|
+
*/
|
|
47
|
+
resetTypeaheadOnItemsChange() {
|
|
48
|
+
this.resetTypeahead();
|
|
49
|
+
}
|
|
29
50
|
constructor() {
|
|
30
51
|
/**
|
|
31
52
|
* Set to `true` to make the field disabled.
|
|
@@ -57,10 +78,51 @@ export class Select {
|
|
|
57
78
|
this.hasChanged = false;
|
|
58
79
|
this.hasPrimaryComponentMemo = false;
|
|
59
80
|
this.checkValid = false;
|
|
81
|
+
this.typeaheadBuffer = new TypeaheadBuffer();
|
|
82
|
+
this.handleMenuTriggerKeyDown = (event) => {
|
|
83
|
+
// Typeahead runs first, so that a space extends an ongoing typeahead
|
|
84
|
+
// instead of just opening the dropdown. A *bare* space is declined by
|
|
85
|
+
// `handleTypeaheadKey`, and keeps its usual meaning below.
|
|
86
|
+
if (this.handleTypeaheadKey(event, NO_TYPEAHEAD_MATCH)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const isEnter = event.key === ENTER;
|
|
90
|
+
const isSpace = event.key === SPACEBAR;
|
|
91
|
+
if (!this.menuOpen && (isSpace || isEnter)) {
|
|
92
|
+
event.stopPropagation();
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
// `preventDefault` cancels the activation click that the trigger
|
|
95
|
+
// `button` would otherwise synthesize, so the menu has to be
|
|
96
|
+
// opened here rather than through the click handler.
|
|
97
|
+
this.openMenu();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
this.setListElement = (element) => {
|
|
101
|
+
var _a, _b;
|
|
102
|
+
if (this.list === element) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
(_a = this.list) === null || _a === void 0 ? void 0 : _a.removeEventListener('keydown', this.handleListKeyDownCapture, true);
|
|
106
|
+
this.list = element;
|
|
107
|
+
(_b = this.list) === null || _b === void 0 ? void 0 : _b.addEventListener('keydown', this.handleListKeyDownCapture, true);
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Key handler for the dropdown, in the capture phase.
|
|
111
|
+
*
|
|
112
|
+
* `MDCList` listens for `keydown` on the `ul` inside `limel-list`'s shadow
|
|
113
|
+
* root, so capturing on the `limel-list` element itself is the only place
|
|
114
|
+
* a typed character can be stopped before MDC sees it. It has to be
|
|
115
|
+
* stopped: MDC would add it to its own typeahead buffer, which suppresses
|
|
116
|
+
* selection with `Enter` for as long as that buffer lives, and it treats
|
|
117
|
+
* the space bar as a selection.
|
|
118
|
+
*
|
|
119
|
+
* @param event - the key that was pressed
|
|
120
|
+
*/
|
|
121
|
+
this.handleListKeyDownCapture = (event) => {
|
|
122
|
+
this.handleTypeaheadKey(event, this.getFocusedMenuItemIndex());
|
|
123
|
+
};
|
|
60
124
|
this.handleMenuChange = this.handleMenuChange.bind(this);
|
|
61
125
|
this.handleNativeChange = this.handleNativeChange.bind(this);
|
|
62
|
-
this.handleMenuTriggerKeyPress =
|
|
63
|
-
this.handleMenuTriggerKeyPress.bind(this);
|
|
64
126
|
this.openMenu = this.openMenu.bind(this);
|
|
65
127
|
this.closeMenu = this.closeMenu.bind(this);
|
|
66
128
|
this.portalId = createRandomString();
|
|
@@ -96,6 +158,7 @@ export class Select {
|
|
|
96
158
|
}
|
|
97
159
|
disconnectedCallback() {
|
|
98
160
|
this.cancelPendingFocus();
|
|
161
|
+
this.resetTypeahead();
|
|
99
162
|
if (this.mdcFloatingLabel) {
|
|
100
163
|
this.mdcFloatingLabel.destroy();
|
|
101
164
|
}
|
|
@@ -110,7 +173,7 @@ export class Select {
|
|
|
110
173
|
}
|
|
111
174
|
render() {
|
|
112
175
|
const dropdownZIndex = getComputedStyle(this.host).getPropertyValue('--dropdown-z-index');
|
|
113
|
-
return (h(SelectTemplate, { key: '
|
|
176
|
+
return (h(SelectTemplate, { key: '97e6b0b033a487e26ab7e4abe7165837ebe08911', id: this.portalId, disabled: this.disabled || this.readonly, readonly: this.readonly, required: this.required, invalid: this.invalid, label: this.label, helperText: this.helperText, value: this.value, options: this.options, onMenuChange: this.handleMenuChange, onNativeChange: this.handleNativeChange, onTriggerKeyDown: this.handleMenuTriggerKeyDown, listRef: this.setListElement, multiple: this.multiple, isOpen: this.menuOpen, open: this.openMenu, close: this.closeMenu, checkValid: this.checkValid, native: this.shouldRenderNative(), dropdownZIndex: dropdownZIndex, anchor: this.getAnchorElement() }));
|
|
114
177
|
}
|
|
115
178
|
watchOpen(newValue, oldValue) {
|
|
116
179
|
if (this.checkValid) {
|
|
@@ -145,6 +208,21 @@ export class Select {
|
|
|
145
208
|
if (!this.menuOpen) {
|
|
146
209
|
return;
|
|
147
210
|
}
|
|
211
|
+
// Consumed on read, so that a later, unrelated re-render
|
|
212
|
+
// cannot resurrect a stale index and yank focus.
|
|
213
|
+
const typeaheadIndex = this.pendingTypeaheadIndex;
|
|
214
|
+
this.pendingTypeaheadIndex = undefined;
|
|
215
|
+
if (typeaheadIndex !== undefined &&
|
|
216
|
+
this.focusMenuItemAtIndex(list, typeaheadIndex)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Picking an option while `multiple` re-renders the dropdown
|
|
220
|
+
// without closing it, which brings us back here with a row
|
|
221
|
+
// already focused. Moving to the first option then would lose
|
|
222
|
+
// the user's place in the list after every pick.
|
|
223
|
+
if (this.getFocusedMenuItemIndex() !== NO_TYPEAHEAD_MATCH) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
148
226
|
this.focusFirstMenuItem(list);
|
|
149
227
|
});
|
|
150
228
|
this.focusObserver.observe(list);
|
|
@@ -167,6 +245,30 @@ export class Select {
|
|
|
167
245
|
firstItem.focus({ preventScroll: true });
|
|
168
246
|
}
|
|
169
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Focus the row at a given index in the dropdown.
|
|
250
|
+
*
|
|
251
|
+
* The rows only become focusable once `limel-list` has set up `MDCList`,
|
|
252
|
+
* which is what gives them a `tabindex`. Callers use the return value to
|
|
253
|
+
* fall back to `pendingTypeaheadIndex` when that has not happened yet.
|
|
254
|
+
*
|
|
255
|
+
* @param list - the `limel-list` of the dropdown
|
|
256
|
+
* @param index - the index of the row, as rendered in its `data-index`
|
|
257
|
+
* @returns whether the row could be focused
|
|
258
|
+
*/
|
|
259
|
+
focusMenuItemAtIndex(list, index) {
|
|
260
|
+
var _a;
|
|
261
|
+
const item = (_a = list === null || list === void 0 ? void 0 : list.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(`[data-index="${index}"]`);
|
|
262
|
+
if (!item) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
// Scrolled explicitly, and only within the dropdown: letting `focus()`
|
|
266
|
+
// scroll would scroll the page, since the dropdown is rendered into a
|
|
267
|
+
// portal that is absolutely positioned on the `body`.
|
|
268
|
+
item.focus({ preventScroll: true });
|
|
269
|
+
item.scrollIntoView({ block: 'nearest' });
|
|
270
|
+
return list.shadowRoot.activeElement === item;
|
|
271
|
+
}
|
|
170
272
|
setTriggerFocus() {
|
|
171
273
|
const trigger = this.host.shadowRoot.querySelector('.limel-select-trigger');
|
|
172
274
|
trigger.focus();
|
|
@@ -218,6 +320,7 @@ export class Select {
|
|
|
218
320
|
this.change.emit(option);
|
|
219
321
|
this.menuOpen = false;
|
|
220
322
|
this.cancelPendingFocus();
|
|
323
|
+
this.resetTypeahead();
|
|
221
324
|
this.setTriggerFocus();
|
|
222
325
|
}
|
|
223
326
|
openMenu() {
|
|
@@ -251,17 +354,111 @@ export class Select {
|
|
|
251
354
|
closeMenu() {
|
|
252
355
|
this.menuOpen = false;
|
|
253
356
|
this.cancelPendingFocus();
|
|
357
|
+
this.resetTypeahead();
|
|
254
358
|
this.setTriggerFocus();
|
|
255
359
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
360
|
+
/**
|
|
361
|
+
* Move the highlight to the option matching the characters typed so far,
|
|
362
|
+
* the way a native `<select>` does. Never changes the value — the option
|
|
363
|
+
* still has to be picked with `Enter` or a click.
|
|
364
|
+
*
|
|
365
|
+
* @param event - the key that was pressed
|
|
366
|
+
* @param focusedIndex - the row that currently has focus, if any
|
|
367
|
+
* @returns whether the key was handled as typeahead
|
|
368
|
+
*/
|
|
369
|
+
handleTypeaheadKey(event, focusedIndex) {
|
|
370
|
+
// The native dropdown on mobile devices does its own typeahead. Note
|
|
371
|
+
// that `setMenuFocus` bails out on mobile too, so a pending index
|
|
372
|
+
// would never be consumed there.
|
|
373
|
+
if (this.isMobileDevice || !isTypeaheadKey(event)) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
// A space only continues an ongoing typeahead. On its own it keeps its
|
|
377
|
+
// usual meaning of opening the dropdown, or selecting the focused
|
|
378
|
+
// option.
|
|
379
|
+
if (event.key === SPACEBAR && this.typeaheadBuffer.isEmpty) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
event.preventDefault();
|
|
383
|
+
event.stopPropagation();
|
|
384
|
+
// Derived per key press rather than cached, so that the indices always
|
|
385
|
+
// refer to the rows the dropdown renders right now.
|
|
386
|
+
const items = createMenuItems(this.options, this.value, this.required);
|
|
387
|
+
const candidates = items.map((item) => ('separator' in item ? null : item));
|
|
388
|
+
const buffer = this.typeaheadBuffer.append(event.key);
|
|
389
|
+
const currentIndex = this.resolveTypeaheadIndex(items, focusedIndex);
|
|
390
|
+
const index = findTypeaheadMatch(candidates, buffer, currentIndex);
|
|
391
|
+
if (index !== NO_TYPEAHEAD_MATCH) {
|
|
392
|
+
this.focusTypeaheadMatch(index);
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* @param items - the items of the dropdown
|
|
398
|
+
* @param focusedIndex - the row that currently has focus, if any
|
|
399
|
+
* @returns the index a typeahead search should start from
|
|
400
|
+
*/
|
|
401
|
+
resolveTypeaheadIndex(items, focusedIndex) {
|
|
402
|
+
if (focusedIndex !== NO_TYPEAHEAD_MATCH) {
|
|
403
|
+
return focusedIndex;
|
|
404
|
+
}
|
|
405
|
+
// Typed again before focus had time to land in the dropdown.
|
|
406
|
+
if (this.pendingTypeaheadIndex !== undefined) {
|
|
407
|
+
return this.pendingTypeaheadIndex;
|
|
408
|
+
}
|
|
409
|
+
// Falls back to the selected option, so that typing continues from
|
|
410
|
+
// wherever the highlight already is. `findIndex` returning `-1` for a
|
|
411
|
+
// select without a value is exactly the "no current row" that
|
|
412
|
+
// `findTypeaheadMatch` expects.
|
|
413
|
+
return items.findIndex((item) => !('separator' in item) && item.selected);
|
|
414
|
+
}
|
|
415
|
+
focusTypeaheadMatch(index) {
|
|
416
|
+
// Overwrites any index still pending from an earlier character, even
|
|
417
|
+
// when this one can be focused right away. Opening the dropdown queues
|
|
418
|
+
// a `setMenuFocus` that waits for it to become visible, and someone
|
|
419
|
+
// typing quickly gets the next character in before that resolves;
|
|
420
|
+
// leaving the earlier index in place would let the queued focus apply
|
|
421
|
+
// it on top of this newer match.
|
|
422
|
+
this.pendingTypeaheadIndex = index;
|
|
423
|
+
if (this.menuOpen && this.focusMenuItemAtIndex(this.list, index)) {
|
|
424
|
+
// Focus landed synchronously, so this index has already done its
|
|
425
|
+
// job. Clearing it — rather than leaving it for `setMenuFocus` to
|
|
426
|
+
// consume later — stops it from being replayed by some unrelated
|
|
427
|
+
// future re-render, such as picking an option in a `multiple`
|
|
428
|
+
// select, after the user has since moved focus elsewhere with the
|
|
429
|
+
// arrow keys. A `setMenuFocus` still queued from opening the
|
|
430
|
+
// dropdown a moment ago is unaffected: it finds this row already
|
|
431
|
+
// focused and leaves it alone, below.
|
|
432
|
+
this.pendingTypeaheadIndex = undefined;
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (this.menuOpen) {
|
|
436
|
+
// Already open, so no re-render is coming that would trigger
|
|
437
|
+
// `componentDidUpdate`. Schedule the focus explicitly.
|
|
438
|
+
this.setMenuFocus();
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
this.openMenu();
|
|
263
442
|
}
|
|
264
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* @returns the index of the focused row of the dropdown, or
|
|
446
|
+
* `NO_TYPEAHEAD_MATCH` when no row has focus
|
|
447
|
+
*/
|
|
448
|
+
getFocusedMenuItemIndex() {
|
|
449
|
+
var _a, _b, _c;
|
|
450
|
+
// Deliberately not `event.target`: a listener on the `limel-list`
|
|
451
|
+
// element sees events from inside its shadow root retargeted to the
|
|
452
|
+
// element itself, never to the row that was actually focused.
|
|
453
|
+
const focused = (_b = (_a = this.list) === null || _a === void 0 ? void 0 : _a.shadowRoot) === null || _b === void 0 ? void 0 : _b.activeElement;
|
|
454
|
+
const row = focused === null || focused === void 0 ? void 0 : focused.closest('[data-index]');
|
|
455
|
+
const index = Number.parseInt((_c = row === null || row === void 0 ? void 0 : row.dataset.index) !== null && _c !== void 0 ? _c : '', 10);
|
|
456
|
+
return Number.isNaN(index) ? NO_TYPEAHEAD_MATCH : index;
|
|
457
|
+
}
|
|
458
|
+
resetTypeahead() {
|
|
459
|
+
this.typeaheadBuffer.clear();
|
|
460
|
+
this.pendingTypeaheadIndex = undefined;
|
|
461
|
+
}
|
|
265
462
|
handleNativeChange(event) {
|
|
266
463
|
event.stopPropagation();
|
|
267
464
|
const element = this.host.shadowRoot.querySelector('select.limel-select__native-control');
|
|
@@ -543,6 +740,12 @@ export class Select {
|
|
|
543
740
|
}, {
|
|
544
741
|
"propName": "options",
|
|
545
742
|
"methodName": "updateHasPrimaryComponent"
|
|
743
|
+
}, {
|
|
744
|
+
"propName": "options",
|
|
745
|
+
"methodName": "resetTypeaheadOnItemsChange"
|
|
746
|
+
}, {
|
|
747
|
+
"propName": "required",
|
|
748
|
+
"methodName": "resetTypeaheadOnItemsChange"
|
|
546
749
|
}, {
|
|
547
750
|
"propName": "menuOpen",
|
|
548
751
|
"methodName": "watchOpen"
|
|
@@ -52,7 +52,7 @@ const SelectValue = (props) => {
|
|
|
52
52
|
'limel-select-trigger': true,
|
|
53
53
|
'limel-select--focused': props.isOpen,
|
|
54
54
|
};
|
|
55
|
-
return (h("button", { slot: "content", class: anchorClassList, onClick: props.open,
|
|
55
|
+
return (h("button", { slot: "content", class: anchorClassList, onClick: props.open, onKeyDown: props.onTriggerKeyDown, "aria-haspopup": "listbox", "aria-expanded": props.isOpen, "aria-controls": props.id, "aria-labelledby": "s-label s-selected-text", "aria-required": props.required, disabled: props.disabled || props.readonly }, h("span", { class: "mdc-select__selected-text-container limel-select__selected-option" }, getSelectedIcon(props.value), getSelectedPrimaryComponent(props.value), h("span", { id: "s-selected-text", class: "mdc-select__selected-text limel-select__selected-option__text" }, getSelectedText(props.value))), h(ShowIcon, Object.assign({}, props, { isValid: props.isValid })), h("span", { class: "mdc-select__dropdown-icon" }, h("svg", { class: "mdc-select__dropdown-icon-graphic", viewBox: "7 10 10 5", focusable: "false" }, h("polygon", { stroke: "none", "fill-rule": "evenodd", points: "7 10 12 15 17 10" })))));
|
|
56
56
|
};
|
|
57
57
|
const ShowIcon = (props) => {
|
|
58
58
|
if (props.isValid) {
|
|
@@ -80,7 +80,7 @@ const MenuDropdown = (props) => {
|
|
|
80
80
|
display: 'flex',
|
|
81
81
|
'min-width': '100%',
|
|
82
82
|
width: 'fit-content',
|
|
83
|
-
} }, h("limel-list", { items: items, type: props.multiple ? 'checkbox' : 'selectable', onChange: props.onMenuChange }))));
|
|
83
|
+
} }, h("limel-list", { ref: props.listRef, items: items, type: props.multiple ? 'checkbox' : 'selectable', onChange: props.onMenuChange }))));
|
|
84
84
|
};
|
|
85
85
|
const NativeDropdown = (props) => {
|
|
86
86
|
const options = props.options
|
|
@@ -103,7 +103,21 @@ function isSelected(option, value) {
|
|
|
103
103
|
}
|
|
104
104
|
return option.value === value.value;
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Create the items for the dropdown.
|
|
108
|
+
*
|
|
109
|
+
* The returned array is index-aligned with the `data-index` attribute that
|
|
110
|
+
* `limel-list` renders on each row. Separators are included, and occupy an
|
|
111
|
+
* index. Anything that needs to address a row by index — such as the
|
|
112
|
+
* typeahead in `select.tsx` — must derive its indices from this same array.
|
|
113
|
+
*
|
|
114
|
+
* @param options - the options of the select
|
|
115
|
+
* @param value - the currently selected option or options
|
|
116
|
+
* @param selectIsRequired - whether the select requires a value, in which
|
|
117
|
+
* case the "empty" option is left out
|
|
118
|
+
* @returns the items to render in the dropdown
|
|
119
|
+
*/
|
|
120
|
+
export function createMenuItems(options, value, selectIsRequired = false) {
|
|
107
121
|
const menuOptionFilter = getMenuOptionFilter(selectIsRequired);
|
|
108
122
|
return options.filter(menuOptionFilter).map((option) => {
|
|
109
123
|
if ('separator' in option) {
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
export const TAB = 'Tab';
|
|
4
4
|
export const ENTER = 'Enter';
|
|
5
5
|
export const ESCAPE = 'Escape';
|
|
6
|
+
/** `KeyboardEvent.code` for the space bar. NOT a `KeyboardEvent.key`. */
|
|
6
7
|
export const SPACE = 'Space';
|
|
8
|
+
/** `KeyboardEvent.key` for the space bar. */
|
|
9
|
+
export const SPACEBAR = ' ';
|
|
7
10
|
export const BACKSPACE = 'Backspace';
|
|
8
11
|
export const DELETE = 'Delete';
|
|
9
12
|
export const ARROW_UP = 'ArrowUp';
|