feat(combobox): subclasser features and fixes
This commit is contained in:
parent
8cd22107ea
commit
143cdb5ac6
4 changed files with 427 additions and 102 deletions
|
|
@ -67,7 +67,7 @@ to the configurable values `none`, `list`, `inline` and `both`.
|
||||||
| | list | filter | focus | check | complete |
|
| | list | filter | focus | check | complete |
|
||||||
| -----: | :--: | :----: | :---: | :---: | :------: |
|
| -----: | :--: | :----: | :---: | :---: | :------: |
|
||||||
| none | ✓ | | | | |
|
| none | ✓ | | | | |
|
||||||
| list | ✓ | ✓ | ✓ | ✓ | |
|
| list | ✓ | ✓ | | | |
|
||||||
| inline | ✓ | | ✓ | ✓ | ✓ |
|
| inline | ✓ | | ✓ | ✓ | ✓ |
|
||||||
| both | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| both | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@
|
||||||
"docs/md-combobox/md-combobox.js",
|
"docs/md-combobox/md-combobox.js",
|
||||||
"docs/md-combobox/md-input.js",
|
"docs/md-combobox/md-input.js",
|
||||||
"docs/md-combobox/style/md-ripple.js",
|
"docs/md-combobox/style/md-ripple.js",
|
||||||
"docs/md-combobox/style/load-roboto.js"
|
"docs/md-combobox/style/load-roboto.js",
|
||||||
|
"docs/google-combobox/google-combobox.js"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lion/core": "0.13.2",
|
"@lion/core": "0.13.2",
|
||||||
|
|
|
||||||
|
|
@ -70,15 +70,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @enhance FormControlMixin - add form-control to [slot=input] instead of _inputNode
|
|
||||||
*/
|
|
||||||
_enhanceLightDomClasses() {
|
|
||||||
if (this.querySelector('[slot=input]')) {
|
|
||||||
this.querySelector('[slot=input]').classList.add('form-control');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enhance FormControlMixin - add slot[name=selection-display]
|
* @enhance FormControlMixin - add slot[name=selection-display]
|
||||||
*/
|
*/
|
||||||
|
|
@ -104,7 +95,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enhance FormControlMixin
|
* @enhance FormControlMixin - add overlay
|
||||||
*/
|
*/
|
||||||
_groupTwoTemplate() {
|
_groupTwoTemplate() {
|
||||||
return html` ${super._groupTwoTemplate()} ${this._overlayListboxTemplate()}`;
|
return html` ${super._groupTwoTemplate()} ${this._overlayListboxTemplate()}`;
|
||||||
|
|
@ -274,7 +265,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
this.__prevCboxValue = '';
|
this.__prevCboxValue = '';
|
||||||
|
|
||||||
/** @type {EventListener} */
|
/** @type {EventListener} */
|
||||||
this.__showOverlay = this.__showOverlay.bind(this);
|
this.__requestShowOverlay = this.__requestShowOverlay.bind(this);
|
||||||
/** @type {EventListener} */
|
/** @type {EventListener} */
|
||||||
this._textboxOnInput = this._textboxOnInput.bind(this);
|
this._textboxOnInput = this._textboxOnInput.bind(this);
|
||||||
/** @type {EventListener} */
|
/** @type {EventListener} */
|
||||||
|
|
@ -297,9 +288,13 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
if (name === 'disabled' || name === 'readOnly') {
|
if (name === 'disabled' || name === 'readOnly') {
|
||||||
this.__setComboboxDisabledAndReadOnly();
|
this.__setComboboxDisabledAndReadOnly();
|
||||||
}
|
}
|
||||||
if (name === 'modelValue' && this.modelValue !== oldValue) {
|
if (name === 'modelValue' && this.modelValue && this.modelValue !== oldValue) {
|
||||||
if (this.modelValue) {
|
if (this._syncToTextboxCondition(this.modelValue, this.__oldModelValue)) {
|
||||||
this._setTextboxValue(this.modelValue);
|
if (!this.multipleChoice) {
|
||||||
|
this._setTextboxValue(this.modelValue);
|
||||||
|
} else {
|
||||||
|
this._syncToTextboxMultiple(this.modelValue, this.__oldModelValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -309,6 +304,11 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
*/
|
*/
|
||||||
updated(changedProperties) {
|
updated(changedProperties) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
if (changedProperties.has('focused')) {
|
||||||
|
if (this.focused) {
|
||||||
|
this.__requestShowOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (changedProperties.has('opened')) {
|
if (changedProperties.has('opened')) {
|
||||||
if (this.opened) {
|
if (this.opened) {
|
||||||
|
|
@ -318,7 +318,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.opened && changedProperties.get('opened') !== undefined) {
|
if (!this.opened && changedProperties.get('opened') !== undefined) {
|
||||||
this._syncCheckedWithTextboxOnInteraction();
|
this.__onOverlayClose();
|
||||||
this.activeIndex = -1;
|
this.activeIndex = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -362,6 +362,38 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
return idx === 0; // matches beginning of value
|
return idx === 0; // matches beginning of value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* Allows Sub Classer to control when the overlay should become visible
|
||||||
|
* Note that this condition is separate from whether the option listbox is
|
||||||
|
* shown (use 'showAllOnEmpty, matchMode and autocomplete configurations for this')
|
||||||
|
*
|
||||||
|
* Separating these conditions allows the user to show different content in the dialog/overlay
|
||||||
|
* that wraps the listbox with options
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* _showOverlayCondition(options) {
|
||||||
|
* return this.focused || super.showOverlayCondition(options);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* _showOverlayCondition({ lastKey }) {
|
||||||
|
* return lastKey === 'ArrowDown';
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* _showOverlayCondition(options) {
|
||||||
|
* return options.currentValue.length > 4 && super.showOverlayCondition(options);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param {{ currentValue: string, lastKey:string }} options
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_showOverlayCondition({ lastKey }) {
|
||||||
|
const doNotOpenOn = ['Tab', 'Esc', 'Enter'];
|
||||||
|
return lastKey && !doNotOpenOn.includes(lastKey);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Event} ev
|
* @param {Event} ev
|
||||||
*/
|
*/
|
||||||
|
|
@ -378,7 +410,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
if (ev.key === 'Tab') {
|
if (ev.key === 'Tab') {
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
}
|
}
|
||||||
this.__hasSelection = this._inputNode.value.length !== this._inputNode.selectionStart;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -397,33 +428,26 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
* @param {string} v
|
* @param {string} v
|
||||||
*/
|
*/
|
||||||
_setTextboxValue(v) {
|
_setTextboxValue(v) {
|
||||||
|
// Make sure that we don't loose inputNode.selectionStart and inputNode.selectionEnd
|
||||||
if (this._inputNode.value !== v) {
|
if (this._inputNode.value !== v) {
|
||||||
this._inputNode.value = v;
|
this._inputNode.value = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
__onOverlayClose() {
|
||||||
* For multiple choice, a subclasser could do something like:
|
if (!this.multipleChoice) {
|
||||||
* @example
|
if (this.checkedIndex !== -1) {
|
||||||
* _syncCheckedWithTextboxOnInteraction() {
|
this._inputNode.value = this.formElements[
|
||||||
* super._syncCheckedWithTextboxOnInteraction();
|
/** @type {number} */ (this.checkedIndex)
|
||||||
* if (this.multipleChoice) {
|
].choiceValue;
|
||||||
* this._inputNode.value = this.checkedElements.map(o => o.value).join(', ');
|
}
|
||||||
* }
|
} else {
|
||||||
* }
|
this._syncToTextboxMultiple(this.modelValue, this.__oldModelValue);
|
||||||
* @overridable
|
|
||||||
*/
|
|
||||||
_syncCheckedWithTextboxOnInteraction() {
|
|
||||||
if (!this.multipleChoice && this._inputNode.value === '') {
|
|
||||||
this._uncheckChildren();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.multipleChoice && this.checkedIndex !== -1) {
|
|
||||||
this._inputNode.value = this.formElements[/** @type {number} */ (this.checkedIndex)].value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @enhance FormControlMixin
|
||||||
* We need to extend the repropagation prevention conditions here.
|
* We need to extend the repropagation prevention conditions here.
|
||||||
* Usually form groups with single choice will not repropagate model-value-changed of an option upwards
|
* Usually form groups with single choice will not repropagate model-value-changed of an option upwards
|
||||||
* if this option itself is not the checked one. We want to prevent duplicates. However, for combobox
|
* if this option itself is not the checked one. We want to prevent duplicates. However, for combobox
|
||||||
|
|
@ -475,24 +499,30 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes whether a user intends to autofill (inline autocomplete textbox)
|
* Computes whether a user intends to autofill (inline autocomplete textbox)
|
||||||
* @overridable
|
|
||||||
* @param {{ prevValue:string, curValue:string }} config
|
* @param {{ prevValue:string, curValue:string }} config
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_computeUserIntendsAutoFill({ prevValue, curValue }) {
|
__computeUserIntendsAutoFill({ prevValue, curValue }) {
|
||||||
const userIsAddingChars = prevValue.length < curValue.length;
|
const userIsAddingChars = prevValue.length < curValue.length;
|
||||||
const userStartsNewWord =
|
const userStartsNewWord =
|
||||||
prevValue.length &&
|
prevValue.length &&
|
||||||
curValue.length &&
|
curValue.length &&
|
||||||
prevValue[0].toLowerCase() !== curValue[0].toLowerCase();
|
prevValue[0].toLowerCase() !== curValue[0].toLowerCase();
|
||||||
|
|
||||||
return userIsAddingChars || userStartsNewWord;
|
return userIsAddingChars || userStartsNewWord;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-enable no-param-reassign, class-methods-use-this */
|
/* eslint-enable no-param-reassign, class-methods-use-this */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matches visibility of listbox options against current ._inputNode contents
|
* Handles autocompletion. This entails:
|
||||||
|
* - list: shows a list on keydown character press
|
||||||
|
* - filter: filters list of potential matches according to matchmode or provided matchCondition
|
||||||
|
* - focus: automatically focuses closest match (makes it the activedescendant)
|
||||||
|
* - check: automatically checks/selects closest match when selection-follows-focus is enabled
|
||||||
|
* (this is the default configuration)
|
||||||
|
* - complete: completes the textbox value inline (the 'missing characters' will be added as
|
||||||
|
* selected text)
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
_handleAutocompletion() {
|
_handleAutocompletion() {
|
||||||
// TODO: this is captured by 'noFilter'
|
// TODO: this is captured by 'noFilter'
|
||||||
|
|
@ -502,8 +532,13 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasSelection = this._inputNode.value.length !== this._inputNode.selectionStart;
|
||||||
|
|
||||||
const curValue = this._inputNode.value;
|
const curValue = this._inputNode.value;
|
||||||
const prevValue = this.__hasSelection ? this.__prevCboxValueNonSelected : this.__prevCboxValue;
|
const prevValue =
|
||||||
|
hasSelection || this.__hadSelectionLastAutofill
|
||||||
|
? this.__prevCboxValueNonSelected
|
||||||
|
: this.__prevCboxValue;
|
||||||
const isEmpty = !curValue;
|
const isEmpty = !curValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -511,38 +546,55 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
* @type {LionOption[]}
|
* @type {LionOption[]}
|
||||||
*/
|
*/
|
||||||
const visibleOptions = [];
|
const visibleOptions = [];
|
||||||
|
/** Whether autofill (activeIndex/checkedIndex and ) has taken place in this 'cycle' */
|
||||||
let hasAutoFilled = false;
|
let hasAutoFilled = false;
|
||||||
const userIntendsAutoFill = this._computeUserIntendsAutoFill({ prevValue, curValue });
|
const userIntendsInlineAutoFill = this.__computeUserIntendsAutoFill({ prevValue, curValue });
|
||||||
const isCandidate = this.autocomplete === 'both' || this.autocomplete === 'inline';
|
const isInlineAutoFillCandidate =
|
||||||
|
this.autocomplete === 'both' || this.autocomplete === 'inline';
|
||||||
|
const autoselect = this._autoSelectCondition();
|
||||||
// @ts-ignore this.autocomplete === 'none' needs to be there if statement above is removed
|
// @ts-ignore this.autocomplete === 'none' needs to be there if statement above is removed
|
||||||
const noFilter = this.autocomplete === 'inline' || this.autocomplete === 'none';
|
const noFilter = this.autocomplete === 'inline' || this.autocomplete === 'none';
|
||||||
|
|
||||||
/** @typedef {LionOption & { onFilterUnmatch?:function, onFilterMatch?:function }} OptionWithFilterFn */
|
/** @typedef {LionOption & { onFilterUnmatch?:function, onFilterMatch?:function }} OptionWithFilterFn */
|
||||||
this.formElements.forEach((/** @type {OptionWithFilterFn} */ option, i) => {
|
this.formElements.forEach((/** @type {OptionWithFilterFn} */ option, i) => {
|
||||||
// [1]. Decide whether otion should be shown
|
// [1]. Decide whether option should be shown
|
||||||
|
const matches = this.matchCondition(option, curValue);
|
||||||
let show = false;
|
let show = false;
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
show = this.showAllOnEmpty;
|
show = this.showAllOnEmpty;
|
||||||
} else {
|
} else {
|
||||||
show = noFilter ? true : this.matchCondition(option, curValue);
|
show = noFilter || matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [2]. Synchronize ._inputNode value and active descendant with closest match
|
// [2]. Synchronize ._inputNode value and active descendant with closest match
|
||||||
if (isCandidate && !hasAutoFilled && show && userIntendsAutoFill && !option.disabled) {
|
if (autoselect && !hasAutoFilled && matches && !option.disabled) {
|
||||||
const stringValues = typeof option.choiceValue === 'string' && typeof curValue === 'string';
|
const doAutoSelect = () => {
|
||||||
const beginsWith =
|
|
||||||
stringValues && option.choiceValue.toLowerCase().indexOf(curValue.toLowerCase()) === 0;
|
|
||||||
|
|
||||||
if (beginsWith) {
|
|
||||||
const prevLen = this._inputNode.value.length;
|
|
||||||
this._inputNode.value = option.choiceValue;
|
|
||||||
this._inputNode.selectionStart = prevLen;
|
|
||||||
this._inputNode.selectionEnd = this._inputNode.value.length;
|
|
||||||
this.activeIndex = i;
|
this.activeIndex = i;
|
||||||
if (this.selectionFollowsFocus && !this.multipleChoice) {
|
if (this.selectionFollowsFocus && !this.multipleChoice) {
|
||||||
this.setCheckedIndex(this.activeIndex);
|
this.setCheckedIndex(this.activeIndex);
|
||||||
}
|
}
|
||||||
hasAutoFilled = true;
|
hasAutoFilled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (userIntendsInlineAutoFill) {
|
||||||
|
// We should never directly select when removing chars or starting a new word
|
||||||
|
// This leads to bad UX and unwanted syncing of modelValue (based on checkedIndex)
|
||||||
|
// and _inputNode.value
|
||||||
|
|
||||||
|
if (isInlineAutoFillCandidate) {
|
||||||
|
const stringValues =
|
||||||
|
typeof option.choiceValue === 'string' && typeof curValue === 'string';
|
||||||
|
const beginsWith =
|
||||||
|
stringValues &&
|
||||||
|
option.choiceValue.toLowerCase().indexOf(curValue.toLowerCase()) === 0;
|
||||||
|
// We only can do proper inline autofilling when the beginning of the word matches
|
||||||
|
if (beginsWith) {
|
||||||
|
this.__textboxInlineComplete(option);
|
||||||
|
doAutoSelect();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
doAutoSelect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -578,7 +630,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// [7]. If no autofill took place, we are left with the previously matched option; correct this
|
// [7]. If no autofill took place, we are left with the previously matched option; correct this
|
||||||
if (!hasAutoFilled && isCandidate && !this.multipleChoice) {
|
if (!hasAutoFilled && autoselect && !this.multipleChoice) {
|
||||||
// This means there is no match for checkedIndex
|
// This means there is no match for checkedIndex
|
||||||
this.checkedIndex = -1;
|
this.checkedIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
@ -587,7 +639,8 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
this.__prevCboxValueNonSelected = curValue;
|
this.__prevCboxValueNonSelected = curValue;
|
||||||
// See test 'computation of "user intends autofill" works correctly afer autofill'
|
// See test 'computation of "user intends autofill" works correctly afer autofill'
|
||||||
this.__prevCboxValue = this._inputNode.value;
|
this.__prevCboxValue = this._inputNode.value;
|
||||||
this.__hasSelection = hasAutoFilled;
|
this.__hadSelectionLastAutofill =
|
||||||
|
this._inputNode.value.length !== this._inputNode.selectionStart;
|
||||||
|
|
||||||
// [9]. Reposition overlay
|
// [9]. Reposition overlay
|
||||||
if (this._overlayCtrl && this._overlayCtrl._popper) {
|
if (this._overlayCtrl && this._overlayCtrl._popper) {
|
||||||
|
|
@ -595,6 +648,23 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__textboxInlineComplete(option = this.formElements[this.activeIndex]) {
|
||||||
|
const prevLen = this._inputNode.value.length;
|
||||||
|
this._inputNode.value = option.choiceValue;
|
||||||
|
this._inputNode.selectionStart = prevLen;
|
||||||
|
this._inputNode.selectionEnd = this._inputNode.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When this condition is false, an end user will have to manually select a suggested
|
||||||
|
* option from the list (by default when autocomplete is 'none' or 'list').
|
||||||
|
* For autocomplete 'both' or 'inline', it will automatically select on a match.
|
||||||
|
* @overridable
|
||||||
|
*/
|
||||||
|
_autoSelectCondition() {
|
||||||
|
return this.autocomplete === 'both' || this.autocomplete === 'inline';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enhance ListboxMixin
|
* @enhance ListboxMixin
|
||||||
*/
|
*/
|
||||||
|
|
@ -629,7 +699,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
*/
|
*/
|
||||||
_setupOpenCloseListeners() {
|
_setupOpenCloseListeners() {
|
||||||
super._setupOpenCloseListeners();
|
super._setupOpenCloseListeners();
|
||||||
this._overlayInvokerNode.addEventListener('keydown', this.__showOverlay);
|
this._inputNode.addEventListener('keydown', this.__requestShowOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -637,7 +707,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
*/
|
*/
|
||||||
_teardownOpenCloseListeners() {
|
_teardownOpenCloseListeners() {
|
||||||
super._teardownOpenCloseListeners();
|
super._teardownOpenCloseListeners();
|
||||||
this._overlayInvokerNode.removeEventListener('keydown', this.__showOverlay);
|
this._inputNode.removeEventListener('keydown', this.__requestShowOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -650,7 +720,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
this.__shouldAutocompleteNextUpdate = true;
|
|
||||||
this._setTextboxValue('');
|
this._setTextboxValue('');
|
||||||
break;
|
break;
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
|
|
@ -665,6 +734,35 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string|string[]} modelValue
|
||||||
|
* @param {string|string[]} oldModelValue
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
_syncToTextboxCondition(modelValue, oldModelValue) {
|
||||||
|
return this.autocomplete === 'inline' || this.autocomplete === 'both';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* Allows to control what happens when checkedIndexes change
|
||||||
|
* @param {string[]} modelValue
|
||||||
|
* @param {string[]} oldModelValue
|
||||||
|
*/
|
||||||
|
_syncToTextboxMultiple(modelValue, oldModelValue = []) {
|
||||||
|
const diff = modelValue.filter(x => !oldModelValue.includes(x));
|
||||||
|
this._setTextboxValue(diff); // or last selected value?
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override FormControlMixin - add form-control to [slot=input] instead of _inputNode
|
||||||
|
*/
|
||||||
|
_enhanceLightDomClasses() {
|
||||||
|
if (this.querySelector('[slot=input]')) {
|
||||||
|
this.querySelector('[slot=input]').classList.add('form-control');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
__initFilterListbox() {
|
__initFilterListbox() {
|
||||||
this._handleAutocompletion();
|
this._handleAutocompletion();
|
||||||
}
|
}
|
||||||
|
|
@ -705,12 +803,16 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {KeyboardEvent} ev
|
* @param {KeyboardEvent} [ev]
|
||||||
*/
|
*/
|
||||||
__showOverlay(ev) {
|
__requestShowOverlay(ev) {
|
||||||
if (ev.key === 'Tab' || ev.key === 'Esc' || ev.key === 'Enter') {
|
if (
|
||||||
return;
|
this._showOverlayCondition({
|
||||||
|
lastKey: ev && ev.key,
|
||||||
|
currentValue: this._inputNode.value,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
this.opened = true;
|
||||||
}
|
}
|
||||||
this.opened = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import '../lion-combobox.js';
|
||||||
import { LionOptions } from '@lion/listbox/src/LionOptions.js';
|
import { LionOptions } from '@lion/listbox/src/LionOptions.js';
|
||||||
import { browserDetection, LitElement } from '@lion/core';
|
import { browserDetection, LitElement } from '@lion/core';
|
||||||
import { Required } from '@lion/form-core';
|
import { Required } from '@lion/form-core';
|
||||||
|
import { LionCombobox } from '../src/LionCombobox.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../src/LionCombobox.js').LionCombobox} LionCombobox
|
* @typedef {import('../src/LionCombobox.js').LionCombobox} LionCombobox
|
||||||
|
|
@ -20,7 +21,7 @@ function mimicUserTyping(el, value) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
el._inputNode.value = value;
|
el._inputNode.value = value;
|
||||||
el._inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
el._inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
||||||
el._overlayInvokerNode.dispatchEvent(new Event('keydown'));
|
el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -31,31 +32,32 @@ async function mimicUserTypingAdvanced(el, values) {
|
||||||
const inputNode = /** @type {HTMLInputElement & {selectionStart:number, selectionEnd:number}} */ (el._inputNode);
|
const inputNode = /** @type {HTMLInputElement & {selectionStart:number, selectionEnd:number}} */ (el._inputNode);
|
||||||
inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
|
inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
|
||||||
|
|
||||||
let hasSelection = inputNode.selectionStart !== inputNode.selectionEnd;
|
|
||||||
|
|
||||||
for (const key of values) {
|
for (const key of values) {
|
||||||
// eslint-disable-next-line no-await-in-loop, no-loop-func
|
// eslint-disable-next-line no-await-in-loop, no-loop-func
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
setTimeout(() => {
|
const hasSelection = inputNode.selectionStart !== inputNode.selectionEnd;
|
||||||
if (key === 'Backspace') {
|
|
||||||
if (hasSelection) {
|
if (key === 'Backspace') {
|
||||||
inputNode.value =
|
if (hasSelection) {
|
||||||
inputNode.value.slice(0, inputNode.selectionStart) +
|
|
||||||
inputNode.value.slice(inputNode.selectionEnd, inputNode.value.length);
|
|
||||||
} else {
|
|
||||||
inputNode.value = inputNode.value.slice(0, -1);
|
|
||||||
}
|
|
||||||
} else if (hasSelection) {
|
|
||||||
inputNode.value =
|
inputNode.value =
|
||||||
inputNode.value.slice(0, inputNode.selectionStart) +
|
inputNode.value.slice(0, inputNode.selectionStart) +
|
||||||
key +
|
|
||||||
inputNode.value.slice(inputNode.selectionEnd, inputNode.value.length);
|
inputNode.value.slice(inputNode.selectionEnd, inputNode.value.length);
|
||||||
} else {
|
} else {
|
||||||
inputNode.value += key;
|
inputNode.value = inputNode.value.slice(0, -1);
|
||||||
}
|
}
|
||||||
hasSelection = false;
|
} else if (hasSelection) {
|
||||||
inputNode.dispatchEvent(new KeyboardEvent('keydown', { key }));
|
inputNode.value =
|
||||||
el._inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
inputNode.value.slice(0, inputNode.selectionStart) +
|
||||||
|
key +
|
||||||
|
inputNode.value.slice(inputNode.selectionEnd, inputNode.value.length);
|
||||||
|
} else {
|
||||||
|
inputNode.value += key;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputNode.dispatchEvent(new KeyboardEvent('keydown', { key }));
|
||||||
|
el._inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
||||||
|
|
||||||
|
el.updateComplete.then(() => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -246,8 +248,8 @@ describe('lion-combobox', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Listbox visibility', () => {
|
describe('Overlay visibility', () => {
|
||||||
it('does not show listbox on focusin', async () => {
|
it('does not show overlay on focusin', async () => {
|
||||||
const el = /** @type {LionCombobox} */ (await fixture(html`
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
<lion-combobox name="foo" multiple-choice>
|
<lion-combobox name="foo" multiple-choice>
|
||||||
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
|
@ -263,14 +265,14 @@ describe('lion-combobox', () => {
|
||||||
expect(el.opened).to.equal(false);
|
expect(el.opened).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows listbox again after select and char keydown', async () => {
|
it('shows overlay again after select and char keydown', async () => {
|
||||||
/**
|
/**
|
||||||
* Scenario:
|
* Scenario:
|
||||||
* [1] user focuses textbox: listbox hidden
|
* [1] user focuses textbox: overlay hidden
|
||||||
* [2] user types char: listbox shows
|
* [2] user types char: overlay shows
|
||||||
* [3] user selects "Artichoke": listbox closes, textbox gets value "Artichoke" and textbox
|
* [3] user selects "Artichoke": overlay closes, textbox gets value "Artichoke" and textbox
|
||||||
* still has focus
|
* still has focus
|
||||||
* [4] user changes textbox value to "Artichoke": the listbox should show again
|
* [4] user changes textbox value to "Artichoke": the overlay should show again
|
||||||
*/
|
*/
|
||||||
const el = /** @type {LionCombobox} */ (await fixture(html`
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
<lion-combobox name="foo">
|
<lion-combobox name="foo">
|
||||||
|
|
@ -306,7 +308,7 @@ describe('lion-combobox', () => {
|
||||||
expect(el.opened).to.equal(true);
|
expect(el.opened).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides (and clears) listbox on [Escape]', async () => {
|
it('hides (and clears) overlay on [Escape]', async () => {
|
||||||
const el = /** @type {LionCombobox} */ (await fixture(html`
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
<lion-combobox name="foo">
|
<lion-combobox name="foo">
|
||||||
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
|
@ -329,7 +331,7 @@ describe('lion-combobox', () => {
|
||||||
expect(el._inputNode.value).to.equal('');
|
expect(el._inputNode.value).to.equal('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides listbox on [Tab]', async () => {
|
it('hides overlay on [Tab]', async () => {
|
||||||
const el = /** @type {LionCombobox} */ (await fixture(html`
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
<lion-combobox name="foo">
|
<lion-combobox name="foo">
|
||||||
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
|
@ -371,13 +373,40 @@ describe('lion-combobox', () => {
|
||||||
expect(el._inputNode.value).to.equal('Artichoke');
|
expect(el._inputNode.value).to.equal('Artichoke');
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
|
||||||
el._inputNode.value = '';
|
|
||||||
mimicUserTyping(el, '');
|
mimicUserTyping(el, '');
|
||||||
|
await el.updateComplete;
|
||||||
el.opened = false;
|
el.opened = false;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.checkedIndex).to.equal(-1);
|
expect(el.checkedIndex).to.equal(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NB: If this becomes a suite, move to separate file
|
||||||
|
describe('Subclassers', () => {
|
||||||
|
it('allows to control overlay visibility via "_showOverlayCondition"', async () => {
|
||||||
|
class ShowOverlayConditionCombobox extends LionCombobox {
|
||||||
|
_showOverlayCondition(options) {
|
||||||
|
return this.focused || super.showOverlayCondition(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const tagName = defineCE(ShowOverlayConditionCombobox);
|
||||||
|
const tag = unsafeStatic(tagName);
|
||||||
|
|
||||||
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
|
<${tag} name="foo" multiple-choice>
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
|
</${tag}>
|
||||||
|
`));
|
||||||
|
|
||||||
|
expect(el.opened).to.equal(false);
|
||||||
|
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.opened).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('sets "aria-posinset" and "aria-setsize" on visible entries', async () => {
|
it('sets "aria-posinset" and "aria-setsize" on visible entries', async () => {
|
||||||
const el = /** @type {LionCombobox} */ (await fixture(html`
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
|
|
@ -783,7 +812,42 @@ describe('lion-combobox', () => {
|
||||||
expect(el._inputNode.selectionEnd).to.equal('ch'.length);
|
expect(el._inputNode.selectionEnd).to.equal('ch'.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does autocompletion when adding chars', async () => {
|
it('synchronizes textbox on overlay close', async () => {
|
||||||
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
|
<lion-combobox name="foo" autocomplete="none">
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
|
</lion-combobox>
|
||||||
|
`));
|
||||||
|
expect(el._inputNode.value).to.equal('');
|
||||||
|
|
||||||
|
async function performChecks(autocomplete, index, valueOnClose) {
|
||||||
|
await el.updateComplete;
|
||||||
|
el.opened = true;
|
||||||
|
el.setCheckedIndex(-1);
|
||||||
|
await el.updateComplete;
|
||||||
|
el.autocomplete = autocomplete;
|
||||||
|
el.setCheckedIndex(index);
|
||||||
|
el.opened = false;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el._inputNode.value).to.equal(valueOnClose);
|
||||||
|
}
|
||||||
|
|
||||||
|
await performChecks('none', 0, 'Artichoke');
|
||||||
|
await performChecks('list', 0, 'Artichoke');
|
||||||
|
await performChecks('inline', 0, 'Artichoke');
|
||||||
|
await performChecks('both', 0, 'Artichoke');
|
||||||
|
|
||||||
|
el.multipleChoice = true;
|
||||||
|
// await performChecks('none', [0, 1], 'Chard');
|
||||||
|
// await performChecks('list', [0, 1], 'Chard');
|
||||||
|
// await performChecks('inline', [0, 1], 'Chard');
|
||||||
|
// await performChecks('both', [0, 1], 'Chard');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does inline autocompletion when adding chars', async () => {
|
||||||
const el = /** @type {LionCombobox} */ (await fixture(html`
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
<lion-combobox name="foo" autocomplete="inline">
|
<lion-combobox name="foo" autocomplete="inline">
|
||||||
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
|
@ -809,7 +873,7 @@ describe('lion-combobox', () => {
|
||||||
expect(el.checkedIndex).to.equal(1);
|
expect(el.checkedIndex).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does autocompletion when changing the word', async () => {
|
it('does inline autocompletion when changing the word', async () => {
|
||||||
const el = /** @type {LionCombobox} */ (await fixture(html`
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
<lion-combobox name="foo" autocomplete="inline">
|
<lion-combobox name="foo" autocomplete="inline">
|
||||||
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
|
@ -824,7 +888,7 @@ describe('lion-combobox', () => {
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
expect(el.checkedIndex).to.equal(1);
|
expect(el.checkedIndex).to.equal(1);
|
||||||
|
|
||||||
await mimicUserTypingAdvanced(el, 'ic'.split(''));
|
await mimicUserTypingAdvanced(el, ['i']);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.activeIndex).to.equal(2);
|
expect(el.activeIndex).to.equal(2);
|
||||||
expect(el.checkedIndex).to.equal(2);
|
expect(el.checkedIndex).to.equal(2);
|
||||||
|
|
@ -854,11 +918,37 @@ describe('lion-combobox', () => {
|
||||||
|
|
||||||
// Autocompletion happened. When we go backwards ('Char'), we should not
|
// Autocompletion happened. When we go backwards ('Char'), we should not
|
||||||
// autocomplete to 'Chard' anymore.
|
// autocomplete to 'Chard' anymore.
|
||||||
mimicUserTyping(el, 'Char');
|
await mimicUserTypingAdvanced(el, ['Backspace']);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el._inputNode.value).to.equal('Char'); // so not 'Chard'
|
expect(el._inputNode.value).to.equal('Ch'); // so not 'Chard'
|
||||||
expect(el._inputNode.selectionStart).to.equal('Char'.length);
|
expect(el._inputNode.selectionStart).to.equal('Ch'.length);
|
||||||
expect(el._inputNode.selectionEnd).to.equal('Char'.length);
|
expect(el._inputNode.selectionEnd).to.equal('Ch'.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Subclassers', () => {
|
||||||
|
it('allows to configure autoselect', async () => {
|
||||||
|
class X extends LionCombobox {
|
||||||
|
_autoSelectCondition() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const tagName = defineCE(X);
|
||||||
|
const tag = unsafeStatic(tagName);
|
||||||
|
|
||||||
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
|
<${tag} name="foo" autocomplete="list" opened>
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
|
</${tag}>
|
||||||
|
`));
|
||||||
|
// This ensures autocomplete would be off originally
|
||||||
|
el.autocomplete = 'list';
|
||||||
|
await mimicUserTypingAdvanced(el, 'vi'); // so we have options ['Victoria Plum']
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.checkedIndex).to.equal(3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('highlights matching options', async () => {
|
it('highlights matching options', async () => {
|
||||||
|
|
@ -889,6 +979,125 @@ describe('lion-combobox', () => {
|
||||||
expect(options[3]).lightDom.to.equal(`Victoria Plum`);
|
expect(options[3]).lightDom.to.equal(`Victoria Plum`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('synchronizes textbox when autocomplete is "inline" or "both"', async () => {
|
||||||
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
|
<lion-combobox name="foo" autocomplete="none">
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
|
</lion-combobox>
|
||||||
|
`));
|
||||||
|
expect(el._inputNode.value).to.equal('');
|
||||||
|
|
||||||
|
el.setCheckedIndex(-1);
|
||||||
|
el.autocomplete = 'none';
|
||||||
|
el.setCheckedIndex(0);
|
||||||
|
expect(el._inputNode.value).to.equal('');
|
||||||
|
|
||||||
|
el.setCheckedIndex(-1);
|
||||||
|
el.autocomplete = 'list';
|
||||||
|
el.setCheckedIndex(0);
|
||||||
|
expect(el._inputNode.value).to.equal('');
|
||||||
|
|
||||||
|
el.setCheckedIndex(-1);
|
||||||
|
el.autocomplete = 'inline';
|
||||||
|
el.setCheckedIndex(0);
|
||||||
|
expect(el._inputNode.value).to.equal('Artichoke');
|
||||||
|
|
||||||
|
el.setCheckedIndex(-1);
|
||||||
|
el.autocomplete = 'both';
|
||||||
|
el.setCheckedIndex(0);
|
||||||
|
expect(el._inputNode.value).to.equal('Artichoke');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('synchronizes last index to textbox when autocomplete is "inline" or "both" when multipleChoice', async () => {
|
||||||
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
|
<lion-combobox name="foo" autocomplete="none" multiple-choice>
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
|
</lion-combobox>
|
||||||
|
`));
|
||||||
|
expect(el._inputNode.value).to.eql('');
|
||||||
|
|
||||||
|
el.setCheckedIndex(-1);
|
||||||
|
el.autocomplete = 'none';
|
||||||
|
el.setCheckedIndex([0]);
|
||||||
|
el.setCheckedIndex([1]);
|
||||||
|
expect(el._inputNode.value).to.equal('');
|
||||||
|
|
||||||
|
el.setCheckedIndex(-1);
|
||||||
|
el.autocomplete = 'list';
|
||||||
|
el.setCheckedIndex([0]);
|
||||||
|
el.setCheckedIndex([1]);
|
||||||
|
expect(el._inputNode.value).to.equal('');
|
||||||
|
|
||||||
|
el.setCheckedIndex(-1);
|
||||||
|
el.autocomplete = 'inline';
|
||||||
|
el.setCheckedIndex([0]);
|
||||||
|
expect(el._inputNode.value).to.equal('Artichoke');
|
||||||
|
el.setCheckedIndex([1]);
|
||||||
|
expect(el._inputNode.value).to.equal('Chard');
|
||||||
|
|
||||||
|
el.setCheckedIndex(-1);
|
||||||
|
el.autocomplete = 'both';
|
||||||
|
el.setCheckedIndex([0]);
|
||||||
|
expect(el._inputNode.value).to.equal('Artichoke');
|
||||||
|
el.setCheckedIndex([1]);
|
||||||
|
expect(el._inputNode.value).to.equal('Chard');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Subclassers', () => {
|
||||||
|
it('allows to override "_syncCheckedWithTextboxMultiple"', async () => {
|
||||||
|
class X extends LionCombobox {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
_syncToTextboxCondition() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
_syncToTextboxMultiple(modelValue, oldModelValue) {
|
||||||
|
// In a real scenario (depending on how selection display works),
|
||||||
|
// you could override the default (last selected option) with '' for instance
|
||||||
|
this._setTextboxValue(`${modelValue}-${oldModelValue}-multi`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const tagName = defineCE(X);
|
||||||
|
const tag = unsafeStatic(tagName);
|
||||||
|
|
||||||
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
|
<${tag} name="foo" autocomplete="none" multiple-choice>
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
|
</${tag}>
|
||||||
|
`));
|
||||||
|
|
||||||
|
el.setCheckedIndex(-1);
|
||||||
|
el.autocomplete = 'none';
|
||||||
|
el.setCheckedIndex([0]);
|
||||||
|
expect(el._inputNode.value).to.equal('Artichoke--multi');
|
||||||
|
|
||||||
|
el.setCheckedIndex(-1);
|
||||||
|
el.autocomplete = 'list';
|
||||||
|
el.setCheckedIndex([0]);
|
||||||
|
expect(el._inputNode.value).to.equal('Artichoke--multi');
|
||||||
|
|
||||||
|
el.setCheckedIndex(-1);
|
||||||
|
el.autocomplete = 'inline';
|
||||||
|
el.setCheckedIndex([0]);
|
||||||
|
expect(el._inputNode.value).to.equal('Artichoke--multi');
|
||||||
|
|
||||||
|
el.setCheckedIndex(-1);
|
||||||
|
el.autocomplete = 'both';
|
||||||
|
el.setCheckedIndex([0]);
|
||||||
|
expect(el._inputNode.value).to.equal('Artichoke--multi');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Active index behavior', () => {
|
describe('Active index behavior', () => {
|
||||||
it('sets the active index to the closest match on open by default', async () => {
|
it('sets the active index to the closest match on open by default', async () => {
|
||||||
const el = /** @type {LionCombobox} */ (await fixture(html`
|
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
|
|
@ -919,9 +1128,19 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
|
/** @param {LionCombobox} elm */
|
||||||
|
function reset(elm) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
elm.activeIndex = -1;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
elm.checkedIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
// https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
|
// https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
|
||||||
// Example 1. List Autocomplete with Manual Selection:
|
// Example 1. List Autocomplete with Manual Selection:
|
||||||
// does not set active at all until user selects
|
// does not set active at all until user selects
|
||||||
|
reset(el);
|
||||||
el.autocomplete = 'none';
|
el.autocomplete = 'none';
|
||||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -932,6 +1151,7 @@ describe('lion-combobox', () => {
|
||||||
// https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
|
// https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
|
||||||
// Example 2. List Autocomplete with Automatic Selection:
|
// Example 2. List Autocomplete with Automatic Selection:
|
||||||
// does not set active at all until user selects
|
// does not set active at all until user selects
|
||||||
|
reset(el);
|
||||||
el.autocomplete = 'list';
|
el.autocomplete = 'list';
|
||||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -941,6 +1161,7 @@ describe('lion-combobox', () => {
|
||||||
|
|
||||||
// https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
|
// https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
|
||||||
// Example 3. List with Inline Autocomplete (mostly, but with aria-autocomplete="inline")
|
// Example 3. List with Inline Autocomplete (mostly, but with aria-autocomplete="inline")
|
||||||
|
reset(el);
|
||||||
el.autocomplete = 'inline';
|
el.autocomplete = 'inline';
|
||||||
mimicUserTyping(/** @type {LionCombobox} */ (el), '');
|
mimicUserTyping(/** @type {LionCombobox} */ (el), '');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -948,18 +1169,18 @@ describe('lion-combobox', () => {
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
// TODO: enable this, so it does not open listbox and is different from [autocomplete=both]?
|
|
||||||
// expect(el.opened).to.be.false;
|
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
|
|
||||||
el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(el.activeIndex).to.equal(-1);
|
expect(el.activeIndex).to.equal(-1);
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
|
|
||||||
// https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
|
// https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
|
||||||
// Example 3. List with Inline Autocomplete
|
// Example 3. List with Inline Autocomplete
|
||||||
|
reset(el);
|
||||||
el.autocomplete = 'both';
|
el.autocomplete = 'both';
|
||||||
mimicUserTyping(/** @type {LionCombobox} */ (el), '');
|
mimicUserTyping(/** @type {LionCombobox} */ (el), '');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -1022,6 +1243,7 @@ describe('lion-combobox', () => {
|
||||||
el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el._inputNode.textContent).to.equal('');
|
expect(el._inputNode.textContent).to.equal('');
|
||||||
|
|
||||||
el.formElements.forEach(option => expect(option.active).to.be.false);
|
el.formElements.forEach(option => expect(option.active).to.be.false);
|
||||||
|
|
||||||
// change selection, active index should update to closest match
|
// change selection, active index should update to closest match
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue