${this.searchable && this.searchEl && this.searchText !== ''
? html`
`
: null}
${this.isInvalid
? html`
`
: null}
${this.multiple && !this.hideTags && this.value.length
? html`
${this.value.map((value: string) => {
const option = this.options.find(
(option) => option.value === value
);
const nodes = option.shadowRoot
.querySelector('slot')
.assignedNodes({
flatten: true,
});
let text = '';
for (let i = 0; i < nodes.length; i++) {
text += nodes[i].textContent.trim();
}
return html`
`;
})}
`
: null}
${this.caption !== ''
? html`
${this.caption}
`
: null}
${this.isInvalid
? html`
${this.invalidText || this.internalValidationMsg}
`
: null}
${this.assistiveText}
`;
}
override firstUpdated() {
// set a default placeholder if none provided
if (this.placeholder === '') {
if (this.searchable) {
this.placeholder = 'Search';
} else {
if (this.multiple) {
this.placeholder = 'Select items';
} else {
this.placeholder = 'Select an option';
}
}
}
}
private handleSlotChange() {
this.resetSelection();
}
/**
* Retrieves the selected values from the list of child options and sets value property.
* @function
*/
public resetSelection() {
this._updateChildren();
this.emitValue();
}
private handleClick() {
if (!this.disabled) {
this.open = !this.open;
// focus search input if searchable
if (this.searchable) {
this.searchEl.focus();
}
}
}
private handleButtonKeydown(e: any) {
this.handleKeyboard(e, e.keyCode, 'button');
}
private handleListKeydown(e: any) {
const TAB_KEY_CODE = 9;
if (e.keyCode !== TAB_KEY_CODE) {
e.preventDefault();
}
this.handleKeyboard(e, e.keyCode, 'list');
}
private handleListBlur(e: any) {
this.options.forEach((option) => (option.highlighted = false));
// don't blur if clicking an option inside
if (
!e.relatedTarget ||
(e.relatedTarget && e.relatedTarget.localName !== 'kyn-dropdown-option')
) {
this.open = false;
}
this.assistiveText = 'Dropdown menu options.';
}
private handleKeyboard(e: any, keyCode: number, target: string) {
const SPACEBAR_KEY_CODE = [0, 32];
const ENTER_KEY_CODE = 13;
const DOWN_ARROW_KEY_CODE = 40;
const UP_ARROW_KEY_CODE = 38;
const ESCAPE_KEY_CODE = 27;
// get highlighted element + index and selected element
const highlightedEl = this.options.find(
(option: any) => option.highlighted
);
const selectedEl = this.options.find((option: any) => option.selected);
const highlightedIndex = highlightedEl
? this.options.indexOf(highlightedEl)
: this.options.find((option: any) => option.selected)
? this.options.indexOf(selectedEl)
: 0;
// prevent page scroll on spacebar press
if (SPACEBAR_KEY_CODE.includes(keyCode)) {
e.preventDefault();
}
// open the listbox
if (target === 'button') {
const openDropdown =
SPACEBAR_KEY_CODE.includes(keyCode) ||
keyCode === ENTER_KEY_CODE ||
keyCode == DOWN_ARROW_KEY_CODE ||
keyCode == UP_ARROW_KEY_CODE;
if (openDropdown) {
this.open = true;
this.options[highlightedIndex].highlighted = true;
// scroll to highlighted option
if (!this.multiple && this.value !== '') {
this.options[highlightedIndex].scrollIntoView({ block: 'nearest' });
}
}
}
switch (keyCode) {
case ENTER_KEY_CODE: {
// select highlighted option
if (target === 'list') {
this.updateValue(
this.options[highlightedIndex].value,
!this.options[highlightedIndex].selected
);
this.assistiveText = 'Selected an item.';
}
return;
}
case DOWN_ARROW_KEY_CODE: {
// go to next option
let nextIndex =
!highlightedEl && !selectedEl
? 0
: highlightedIndex === this.options.length - 1
? 0
: highlightedIndex + 1;
// skip disabled options
if (this.options[nextIndex].disabled) {
nextIndex = nextIndex === this.options.length - 1 ? 0 : nextIndex + 1;
}
this.options[highlightedIndex].highlighted = false;
this.options[nextIndex].highlighted = true;
// scroll to option
this.options[nextIndex].scrollIntoView({ block: 'nearest' });
this.assistiveText = this.options[nextIndex].text;
return;
}
case UP_ARROW_KEY_CODE: {
// go to previous option
let nextIndex =
highlightedIndex === 0
? this.options.length - 1
: highlightedIndex - 1;
// skip disabled options
if (this.options[nextIndex].disabled) {
nextIndex = nextIndex === 0 ? this.options.length - 1 : nextIndex - 1;
}
this.options[highlightedIndex].highlighted = false;
this.options[nextIndex].highlighted = true;
// scroll to option
this.options[nextIndex].scrollIntoView({ block: 'nearest' });
this.assistiveText = this.options[nextIndex].text;
return;
}
case ESCAPE_KEY_CODE: {
// close listbox
this.open = false;
// restore focus
if (this.searchable) {
this.searchEl.focus();
} else {
this.buttonEl.focus();
}
this.assistiveText = 'Dropdown menu options.';
return;
}
default: {
return;
}
}
}
private handleClearMultiple(e: any) {
e.stopPropagation();
// clear values
if (this.multiple) {
this.value = [];
} else {
this.value = '';
}
this._validate(true, false);
this._updateSelectedOptions();
this.emitValue();
}
private handleTagClear(value: string) {
// remove value
this.updateValue(value, false);
this._updateSelectedOptions();
this.emitValue();
}
private handleClear(e: any) {
e.stopPropagation();
// reset search input text
this.text = '';
this.searchText = '';
this.searchEl.value = '';
// clear selection for single select
if (!this.multiple) {
this.value = '';
this._updateSelectedOptions();
this.emitValue();
}
}
private handleSearchClick(e: any) {
e.stopPropagation();
this.open = true;
}
private handleButtonBlur(e: any) {
// don't blur if entering listbox or search input
if (
!e.relatedTarget?.classList.contains('options') &&
!e.relatedTarget?.classList.contains('search')
) {
this.open = false;
}
}
private handleSearchBlur(e: any) {
// don't blur if entering listbox of button
if (
!e.relatedTarget ||
(e.relatedTarget.localName !== 'kyn-dropdown-option' &&
!e.relatedTarget?.classList.contains('options') &&
!e.relatedTarget?.classList.contains('select'))
) {
this.open = false;
}
}
private handleSearchKeydown(e: any) {
e.stopPropagation();
const ENTER_KEY_CODE = 13;
const ESCAPE_KEY_CODE = 27;
const option = this.options.find((option) => option.highlighted);
// select option
if (e.keyCode === ENTER_KEY_CODE && option) {
this.updateValue(option.value, option.selected);
this.assistiveText = 'Selected an item.';
}
// close listbox
if (e.keyCode === ESCAPE_KEY_CODE) {
this.open = false;
this.buttonEl.focus();
}
}
private handleSearchInput(e: any) {
const value = e.target.value;
this.searchText = value;
this.open = true;
// find matches
const options = this.options.filter((option: any) => {
const text = option.text;
return text.toLowerCase().startsWith(value.toLowerCase());
});
// reset options highlighted state
this.options.forEach((option) => (option.highlighted = false));
// option highlight and scroll
if (value !== '' && options.length) {
options[0].highlighted = true;
options[0].scrollIntoView({ block: 'nearest' });
}
}
private _updateSelectedOptions() {
// set selected state for each option
this.options.forEach((option: any) => {
if (this.multiple) {
option.selected = this.value.includes(option.value);
} else {
option.selected = this.value === option.value;
}
});
}
private _handleClick(e: any) {
if (e.detail.value === 'selectAll') {
if (e.detail.selected) {
this.value = this.options
.filter((option) => !option.disabled)
.map((option) => {
return option.value;
});
this.assistiveText = 'Selected all items.';
} else {
this.value = [];
this.assistiveText = 'Deselected all items.';
}
this._validate(true, false);
} else {
this.updateValue(e.detail.value, e.detail.selected);
this.assistiveText = 'Selected an item.';
}
this._updateSelectedOptions();
// emit selected value
this.emitValue();
}
private _handleBlur(e: any) {
const relatedTarget = e.detail.origEvent.relatedTarget;
if (
!relatedTarget ||
(relatedTarget.localName !== 'kyn-dropdown-option' &&
relatedTarget.localName !== 'kyn-dropdown')
) {
this.open = false;
}
}
private _handleFormdata(e: any) {
if (this.multiple) {
this.value.forEach((value: string) => {
e.formData.append(this.name, value);
});
} else {
e.formData.append(this.name, this.value);
}
}
private _handleInvalid() {
this._validate(true, false);
}
override connectedCallback() {
super.connectedCallback();
// capture child options click event
this.addEventListener('on-click', (e: any) => this._handleClick(e));
// capture child options blur event
this.addEventListener('on-blur', (e: any) => this._handleBlur(e));
if (this.internals.form) {
this.internals.form.addEventListener('formdata', (e) =>
this._handleFormdata(e)
);
this.addEventListener('invalid', () => {
this._handleInvalid();
});
}
}
override disconnectedCallback() {
this.addEventListener('on-click', (e: any) => this._handleClick(e));
this.addEventListener('on-blur', (e: any) => this._handleBlur(e));
if (this.internals.form) {
this.internals.form.removeEventListener('formdata', (e) =>
this._handleFormdata(e)
);
this.removeEventListener('invalid', () => {
this._handleInvalid();
});
}
super.disconnectedCallback();
}
private updateValue(value: string, selected = false) {
const values = JSON.parse(JSON.stringify(this.value));
// set value
if (this.multiple) {
// update array
if (selected) {
values.push(value);
} else {
const index = values.indexOf(value);
values.splice(index, 1);
}
this.value = values;
} else {
this.value = value;
}
this._validate(true, false);
// reset focus
if (!this.multiple) {
if (this.searchable) {
this.searchEl.focus();
} else {
this.buttonEl.focus();
}
}
}
private _validate(interacted: Boolean, report: Boolean) {
// set validity flags
const Validity = {
customError: this.invalidText !== '',
valueMissing:
this.required &&
(!this.value ||
(this.multiple && !this.value.length) ||
(!this.multiple && this.value === '')),
};
// set validationMessage
const InternalMsg =
this.required && !this.value.length ? 'Please fill out this field.' : '';
const ValidationMessage =
this.invalidText !== '' ? this.invalidText : InternalMsg;
// set validity on custom element, anchor to buttonEl
this.internals.setValidity(Validity, ValidationMessage, this.buttonEl);
// set internal validation message if value was changed by user input
if (interacted) {
this.internalValidationMsg = InternalMsg;
}
// focus the buttonEl to show validity
if (report) {
this.internals.reportValidity();
}
}
private emitValue() {
const event = new CustomEvent('on-change', {
detail: {
value: this.value,
},
});
this.dispatchEvent(event);
}
override willUpdate(changedProps: any) {
if (changedProps.has('open')) {
if (this.open) {
// open dropdown upwards if closer to bottom fo viewport
if (
this.buttonEl.getBoundingClientRect().top >
window.innerHeight * 0.6
) {
this._openUpwards = true;
} else {
this._openUpwards = false;
}
}
}
}
override updated(changedProps: any) {
if (
changedProps.has('invalidText') ||
changedProps.has('internalValidationMsg')
) {
//check if any (internal / external )error msg. present then isInvalid is true
this.isInvalid =
this.invalidText !== '' || this.internalValidationMsg !== ''
? true
: false;
}
if (
changedProps.has('invalidText') &&
changedProps.get('invalidText') !== undefined
) {
this._validate(false, false);
}
if (changedProps.has('value')) {
this._validate(false, false);
const Slot: any = this.shadowRoot?.querySelector('slot#children');
const Options: Array