Highlight options using a attribute instead of focus. This brings a few advantages.
This commit is contained in:
164
tp-dropdown.js
164
tp-dropdown.js
@@ -28,7 +28,7 @@ const BaseElement = mixins.reduce((baseClass, mixin) => {
|
||||
return mixin(baseClass);
|
||||
}, LitElement);
|
||||
|
||||
class TpDropdown extends BaseElement {
|
||||
export class TpDropdown extends BaseElement {
|
||||
static get styles() {
|
||||
return [
|
||||
css`
|
||||
@@ -152,10 +152,9 @@ class TpDropdown extends BaseElement {
|
||||
color: var(--tp-dropdown-hovered-item-color, inherit);
|
||||
}
|
||||
|
||||
div[role="option"]:focus {
|
||||
background: var(--tp-dropdown-focused-item-bg, #EEEEEE);
|
||||
color: var(--tp-dropdown-focused-item-color, inherit);
|
||||
outline: none;
|
||||
div[role="option"][highlighted] {
|
||||
background: var(--tp-dropdown-highlighted-item-bg, #EEEEEE);
|
||||
color: var(--tp-dropdown-highlighted-item-color, inherit);
|
||||
}
|
||||
|
||||
div[role="option"][selected] {
|
||||
@@ -251,15 +250,15 @@ class TpDropdown extends BaseElement {
|
||||
</div>
|
||||
<div id="popup" ?open="${isOpen}" part="popup">
|
||||
${filterable ? html`
|
||||
<tp-input part="filter" exportparts="wrap:filterWrap" id="filter" .value=${this._filterTerm || ''} @input=${e => this._filterTerm = e.target.value} .inert=${!isOpen} ?hidden=${!(filterable || extensible)}>
|
||||
<tp-input part="filter" exportparts="wrap:filterWrap" id="filter" .value=${this._filterTerm || ''} @input=${e => this._onFilterInput(e)} .inert=${!isOpen} ?hidden=${!(filterable || extensible)}>
|
||||
<input type="text" placeholder=${filterPlaceholder}>
|
||||
<tp-icon id="filterIcon" .icon=${extensible ? TpDropdown.addIcon : TpDropdown.filterIcon} slot="suffix"></tp-icon>
|
||||
</tp-input>
|
||||
` : null}
|
||||
<div id="itemList" part="list">
|
||||
${isOpen ? html`
|
||||
${items.map(item => this._filter(item) ? html`
|
||||
<div part="item" role="option" .value=${item.value} tabindex=${isOpen ? '0' : '-1'}>${item.label}</div>
|
||||
${items.map((item, index) => this._filter(item) ? html`
|
||||
<div part="item" role="option" .value=${item.value} ?highlighted=${this._getVisibleIndex(index) === this._highlightedIndex} tabindex="-1">${item.label}</div>
|
||||
` : null)}
|
||||
` : null}
|
||||
</div>
|
||||
@@ -298,7 +297,8 @@ class TpDropdown extends BaseElement {
|
||||
* back again.
|
||||
*/
|
||||
_memorizedValue: { type: String },
|
||||
_filterTerm: { type: String }
|
||||
_filterTerm: { type: String },
|
||||
_highlightedIndex: { type: Number }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -324,6 +324,7 @@ class TpDropdown extends BaseElement {
|
||||
super();
|
||||
this.items = [];
|
||||
this._onDocClickHandler = () => this.close();
|
||||
this._highlightedIndex = -1;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -440,22 +441,10 @@ class TpDropdown extends BaseElement {
|
||||
}
|
||||
|
||||
this.invalid = false;
|
||||
|
||||
setTimeout(() => {
|
||||
var ariaSel = this.shadowRoot.querySelector('div[aria-selected]');
|
||||
var item = this.shadowRoot.querySelector('div[selected]');
|
||||
if (ariaSel) {
|
||||
ariaSel.removeAttribute('aria-selected');
|
||||
}
|
||||
if (item) {
|
||||
item.setAttribute('aria-selected', true);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
_checkValue(newValue, oldValue) {
|
||||
if (newValue === null || newValue === undefined) {
|
||||
this.$.itemList.selection = null;
|
||||
this.label = '';
|
||||
}
|
||||
}
|
||||
@@ -498,7 +487,16 @@ class TpDropdown extends BaseElement {
|
||||
this._setListPosition();
|
||||
|
||||
this.isOpen = true;
|
||||
this.$.selItem.focus();
|
||||
|
||||
// Initialize highlighting and focus
|
||||
setTimeout(() => {
|
||||
this._setListHighlight();
|
||||
if (this.filterable) {
|
||||
this.$.filter.focus();
|
||||
} else {
|
||||
this.$.selItem.focus();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
_filterClicked(e) {
|
||||
@@ -535,6 +533,7 @@ class TpDropdown extends BaseElement {
|
||||
}
|
||||
|
||||
this.isOpen = false;
|
||||
this._highlightedIndex = -1;
|
||||
window.removeEventListener('resize', this._boundSetListPosition);
|
||||
document.removeEventListener('scroll', this._boundSetListPosition);
|
||||
document.removeEventListener('click', this._onDocClickHandler, true);
|
||||
@@ -659,11 +658,34 @@ class TpDropdown extends BaseElement {
|
||||
return new RegExp('.*' + this._filterTerm.toLowerCase() + '.*').test(item.label.toLowerCase());
|
||||
}
|
||||
|
||||
_getVisibleIndex(itemIndex) {
|
||||
// Calculate the visible index for filtered items
|
||||
let visibleIndex = 0;
|
||||
for (let i = 0; i < itemIndex; i++) {
|
||||
if (this._filter(this.items[i])) {
|
||||
visibleIndex++;
|
||||
}
|
||||
}
|
||||
return this._filter(this.items[itemIndex]) ? visibleIndex : -1;
|
||||
}
|
||||
|
||||
_getVisibleItems() {
|
||||
return this.items.filter(item => this._filter(item));
|
||||
}
|
||||
|
||||
_onFilterInput(e) {
|
||||
this._filterTerm = e.target.value;
|
||||
// Reset highlighting when filter changes
|
||||
this._highlightedIndex = 0;
|
||||
this._scrollHighlightedIntoView();
|
||||
}
|
||||
|
||||
_filterIconClicked() {
|
||||
if (this.extensible) {
|
||||
this._addItem();
|
||||
}
|
||||
this._filterTerm = '';
|
||||
this._highlightedIndex = 0;
|
||||
this.$.filter.focus();
|
||||
}
|
||||
|
||||
@@ -693,7 +715,7 @@ class TpDropdown extends BaseElement {
|
||||
// Toggle list only if it's not open or when the toggle icon is clicked or the click wasn't on the selector input.
|
||||
if (!this.isOpen || closest(rootTarget, '#toggleIcon', true) !== undefined || (!closest(rootTarget, '#filter', true))) {
|
||||
this.toggle();
|
||||
this._setListFocus();
|
||||
this._setListHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,7 +734,7 @@ class TpDropdown extends BaseElement {
|
||||
|
||||
if (this._isEnter(e)) {
|
||||
if (this.isOpen) {
|
||||
if (this.extensible && !this.$.itemList.querySelector('div[role="option"]:focus')) {
|
||||
if (this.extensible && this._highlightedIndex < 0) {
|
||||
this._addItem();
|
||||
}
|
||||
|
||||
@@ -728,30 +750,12 @@ class TpDropdown extends BaseElement {
|
||||
|
||||
if (this.isOpen) {
|
||||
if (this._isDownKey(e)) {
|
||||
this._focusNextItem();
|
||||
this._highlightNextItem();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (this._isUpKey(e)) {
|
||||
this._focusPrevItem();
|
||||
e.preventDefault();
|
||||
}
|
||||
} else {
|
||||
if (this._isDownKey(e)) {
|
||||
if (this.value === undefined && this.items && this.items.length > 0) {
|
||||
this.$.itemList.select(0);
|
||||
} else {
|
||||
this.$.itemList.selectNext();
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (this._isUpKey(e)) {
|
||||
if (this.value === undefined && this.items && this.items.length > 0) {
|
||||
this.$.itemList.select(this.items.length - 1);
|
||||
} else {
|
||||
this.$.itemList.selectPrevious();
|
||||
}
|
||||
this._highlightPrevItem();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
@@ -780,10 +784,16 @@ class TpDropdown extends BaseElement {
|
||||
}
|
||||
|
||||
this._filterTerm = '';
|
||||
this._highlightedIndex = 0;
|
||||
}
|
||||
|
||||
if (this._isUpKey(e) || this._isDownKey(e)) {
|
||||
this._setListFocus();
|
||||
// Navigate in the list while keeping focus in filter
|
||||
if (this._isDownKey(e)) {
|
||||
this._highlightNextItem();
|
||||
} else {
|
||||
this._highlightPrevItem();
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
@@ -814,54 +824,52 @@ class TpDropdown extends BaseElement {
|
||||
}
|
||||
|
||||
_selectItem() {
|
||||
var item = this.$.itemList.querySelector('div[role="option"]:focus');
|
||||
const visibleItems = this._getVisibleItems();
|
||||
const highlightedItem = visibleItems[this._highlightedIndex];
|
||||
|
||||
if (item) {
|
||||
this.value = item.value;
|
||||
if (highlightedItem) {
|
||||
this.value = highlightedItem.value;
|
||||
this.dispatchEvent(new CustomEvent('selection-changed', { detail: this.value, bubbles: true, composed: true }));
|
||||
}
|
||||
}
|
||||
|
||||
_setListFocus() {
|
||||
var item = this.$.itemList.querySelector('div[role="option"].iron-selected') || this.$.itemList.querySelector('div[role="option"]');
|
||||
|
||||
if (item) {
|
||||
item.focus();
|
||||
}
|
||||
}
|
||||
|
||||
_focusNextItem() {
|
||||
var item = this.$.itemList.querySelector('div[role="option"]:focus');
|
||||
if (!item) {
|
||||
_setListHighlight() {
|
||||
// Initialize highlighting to current value or first item
|
||||
const visibleItems = this._getVisibleItems();
|
||||
if (visibleItems.length === 0) {
|
||||
this._highlightedIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
var next = item.nextElementSibling;
|
||||
// Try to highlight the currently selected value
|
||||
const currentIndex = visibleItems.findIndex(item => item.value === this.value);
|
||||
this._highlightedIndex = currentIndex >= 0 ? currentIndex : 0;
|
||||
this._scrollHighlightedIntoView();
|
||||
}
|
||||
|
||||
if (!next || next.tagName !== 'DIV') {
|
||||
next = this.$.itemList.querySelector('div[role="option"]');
|
||||
}
|
||||
|
||||
if (next) {
|
||||
next.focus();
|
||||
_scrollHighlightedIntoView() {
|
||||
if (this._highlightedIndex < 0) return;
|
||||
|
||||
const highlightedElement = this.shadowRoot.querySelector('div[role="option"][highlighted]');
|
||||
if (highlightedElement) {
|
||||
highlightedElement.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
_focusPrevItem() {
|
||||
var item = this.$.itemList.querySelector('div[role="option"]:focus');
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
_highlightNextItem() {
|
||||
const visibleItems = this._getVisibleItems();
|
||||
if (visibleItems.length === 0) return;
|
||||
|
||||
var next = item.previousElementSibling;
|
||||
this._highlightedIndex = (this._highlightedIndex + 1) % visibleItems.length;
|
||||
this._scrollHighlightedIntoView();
|
||||
}
|
||||
|
||||
if (!next || next.tagName !== 'DIV') {
|
||||
next = this.$.itemList.querySelector('div[role="option"]:last-of-type');
|
||||
}
|
||||
_highlightPrevItem() {
|
||||
const visibleItems = this._getVisibleItems();
|
||||
if (visibleItems.length === 0) return;
|
||||
|
||||
if (next) {
|
||||
next.focus();
|
||||
}
|
||||
this._highlightedIndex = this._highlightedIndex <= 0 ? visibleItems.length - 1 : this._highlightedIndex - 1;
|
||||
this._scrollHighlightedIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user