diff --git a/packages/angular/src/components.ts b/packages/angular/src/components.ts index f5ac1a4bd14..642bbacad9a 100644 --- a/packages/angular/src/components.ts +++ b/packages/angular/src/components.ts @@ -1348,14 +1348,14 @@ export declare interface IxPill extends Components.IxPill {} @ProxyCmp({ - inputs: ['allowClear', 'disabled', 'editable', 'hideListHeader', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'mode', 'readonly', 'selectedIndices'] + inputs: ['allowClear', 'disabled', 'editable', 'hideListHeader', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'mode', 'readonly', 'selectedIndices'] }) @Component({ selector: 'ix-select', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['allowClear', 'disabled', 'editable', 'hideListHeader', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'mode', 'readonly', 'selectedIndices'], + inputs: ['allowClear', 'disabled', 'editable', 'hideListHeader', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'mode', 'readonly', 'selectedIndices'], }) export class IxSelect { protected el: HTMLElement; diff --git a/packages/core/component-doc.json b/packages/core/component-doc.json index 5ad450951f0..303acfa61eb 100644 --- a/packages/core/component-doc.json +++ b/packages/core/component-doc.json @@ -7059,6 +7059,28 @@ "optional": false, "required": false }, + { + "name": "i18nNoMatches", + "type": "string", + "mutable": false, + "attr": "i-1-8n-no-matches", + "reflectToAttr": false, + "docs": "Hint inside of dropdown if no items where found with current filter text", + "docsTags": [ + { + "name": "since", + "text": "1.5.0" + } + ], + "default": "'No matches'", + "values": [ + { + "type": "string" + } + ], + "optional": false, + "required": false + }, { "name": "i18nPlaceholder", "type": "string", @@ -7155,7 +7177,7 @@ "mutable": true, "attr": "selected-indices", "reflectToAttr": false, - "docs": "Indices of selected items", + "docs": "Indices of selected items\nThis corresponds to the value property of ix-select-items and therefor not neccessarily the indices of the items in the list.", "docsTags": [], "default": "[]", "values": [ diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index ad777f38d3e..68aa20d3792 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -1222,6 +1222,11 @@ export namespace Components { * @ */ "hideListHeader": boolean; + /** + * Hint inside of dropdown if no items where found with current filter text + * @since 1.5.0 + */ + "i18nNoMatches": string; /** * Input field placeholder */ @@ -1243,7 +1248,7 @@ export namespace Components { */ "readonly": boolean; /** - * Indices of selected items + * Indices of selected items This corresponds to the value property of ix-select-items and therefor not neccessarily the indices of the items in the list. */ "selectedIndices": string | string[]; } @@ -3661,6 +3666,11 @@ declare namespace LocalJSX { * @ */ "hideListHeader"?: boolean; + /** + * Hint inside of dropdown if no items where found with current filter text + * @since 1.5.0 + */ + "i18nNoMatches"?: string; /** * Input field placeholder */ @@ -3690,7 +3700,7 @@ declare namespace LocalJSX { */ "readonly"?: boolean; /** - * Indices of selected items + * Indices of selected items This corresponds to the value property of ix-select-items and therefor not neccessarily the indices of the items in the list. */ "selectedIndices"?: string | string[]; } diff --git a/packages/core/src/components/dropdown-item/dropdown-item.scss b/packages/core/src/components/dropdown-item/dropdown-item.scss index 088f72a3932..3c79cf8a25a 100644 --- a/packages/core/src/components/dropdown-item/dropdown-item.scss +++ b/packages/core/src/components/dropdown-item/dropdown-item.scss @@ -13,7 +13,6 @@ :host { display: block; - min-width: 10rem; &.icon-only { min-width: 0; diff --git a/packages/core/src/components/dropdown-item/dropdown-item.tsx b/packages/core/src/components/dropdown-item/dropdown-item.tsx index 0d72066f51b..50d80265b2f 100644 --- a/packages/core/src/components/dropdown-item/dropdown-item.tsx +++ b/packages/core/src/components/dropdown-item/dropdown-item.tsx @@ -82,6 +82,7 @@ export class DropdownItem { disabled: this.disabled, }} onClick={() => this.emitItemClick()} + tabindex={0} > {this.checked ? ( diff --git a/packages/core/src/components/dropdown/dropdown.tsx b/packages/core/src/components/dropdown/dropdown.tsx index 9e5e1da46a5..bc47e9b7050 100644 --- a/packages/core/src/components/dropdown/dropdown.tsx +++ b/packages/core/src/components/dropdown/dropdown.tsx @@ -357,7 +357,10 @@ export class Dropdown { } if (this.placement.includes('auto') || isSubmenu) { - positionConfig.middleware.push(flip()); + positionConfig.middleware.push( + flip({ fallbackStrategy: 'initialPlacement' }) + ); + positionConfig.placement = 'bottom-start'; } else { positionConfig.placement = this.placement as | BasePlacement diff --git a/packages/core/src/components/pagination/pagination.tsx b/packages/core/src/components/pagination/pagination.tsx index 2681657bf3d..5549ffbfc64 100644 --- a/packages/core/src/components/pagination/pagination.tsx +++ b/packages/core/src/components/pagination/pagination.tsx @@ -229,6 +229,7 @@ export class Pagination { {this.i18nItems} this.onItemClick(e)} > diff --git a/packages/core/src/components/select/select.tsx b/packages/core/src/components/select/select.tsx index 25d82edc6ba..87b5043b758 100644 --- a/packages/core/src/components/select/select.tsx +++ b/packages/core/src/components/select/select.tsx @@ -31,6 +31,7 @@ export class Select { /** * Indices of selected items + * This corresponds to the value property of ix-select-items and therefor not neccessarily the indices of the items in the list. */ @Prop({ mutable: true }) selectedIndices: string | string[] = []; @@ -74,6 +75,13 @@ export class Select { */ @Prop() i18nSelectListHeader = 'Please select an option'; + /** + * Hint inside of dropdown if no items where found with current filter text + * + * @since 1.5.0 + */ + @Prop() i18nNoMatches = 'No matches'; + /** * Hide list header * @@ -128,6 +136,10 @@ export class Select { return this.mode === 'multiple'; } + get isEveryDropdownItemHidden() { + return this.items.every((item) => item.classList.contains('d-none')); + } + @Watch('selectedIndices') watchSelectedIndices(newId: string | string[]) { if (!newId) { @@ -149,17 +161,6 @@ export class Select { this.emitItemClick(newId); } - @Watch('inputFilterText') - watchInputText(newValue: string) { - if (!this.editable) { - return; - } - - if (this.itemExists(newValue)) { - return; - } - } - private emitItemClick(newId: string) { if (this.isMultipleMode && Array.isArray(this.selectedIndices)) { if (this.selectedIndices.includes(newId)) { @@ -180,11 +181,11 @@ export class Select { return; } - const test = document.createElement('ix-select-item'); - test.value = value; - test.label = value; + const newItem = document.createElement('ix-select-item'); + newItem.value = value; + newItem.label = value; - this.addItemRef.appendChild(test); + this.addItemRef.appendChild(newItem); this.clearInput(); this.emitItemClick(value); @@ -239,16 +240,17 @@ export class Select { private dropdownVisibilityChanged(event: CustomEvent) { this.dropdownShow = event.detail; + this.hasFocus = event.detail; if (event.detail) { this.inputRef.focus(); this.inputRef.select(); - this.navigationItem = this.items[0]; - this.setHoverEffectForNavigatedSelectItem(); this.removeHiddenFromItems(); + this.isDropdownEmpty = this.isEveryDropdownItemHidden; + } else { + this.navigationItem = undefined; } - this.hasFocus = event.detail; } @Listen('keydown', { @@ -294,6 +296,11 @@ export class Select { event.stopPropagation(); event.preventDefault(); + const focusItem = this.items.find( + (item) => document.activeElement === item.querySelector('button') + ); + this.navigationItem = focusItem; + const selectItems = this.items.filter( (i) => !i.classList.contains('d-none') ); @@ -310,13 +317,12 @@ export class Select { } private setHoverEffectForNavigatedSelectItem() { - this.items.forEach((item: HTMLIxSelectItemElement) => { - item.hover = item === this.navigationItem; - }); + this.navigationItem?.querySelector('button').focus(); } private filterItemsWithTypeahead() { this.inputFilterText = this.inputRef.value; + if (this.inputFilterText) { this.items.forEach((item) => { item.classList.remove('d-none'); @@ -329,9 +335,8 @@ export class Select { } else { this.removeHiddenFromItems(); } - this.isDropdownEmpty = this.items.every((item) => - item.classList.contains('d-none') - ); + + this.isDropdownEmpty = this.isEveryDropdownItemHidden; } private removeHiddenFromItems() { @@ -353,6 +358,22 @@ export class Select { this.dropdownShow = false; } + private onInputBlur(e) { + if (this.editable) { + return; + } + + if (this.isSingleMode) { + if (this.dropdownShow && this.isDropdownEmpty) { + this.dropdownShow = false; + } + } + + if (!this.dropdownShow && this.mode !== 'multiple') { + e.target['value'] = this.value; + } + } + private placeholderValue() { if (this.editable) { return this.i18nPlaceholderEditable; @@ -411,6 +432,7 @@ export class Select { placeholder={this.placeholderValue()} value={this.inputValue} ref={(ref) => (this.inputRef = ref)} + onBlur={(e) => this.onInputBlur(e)} onInput={() => this.filterItemsWithTypeahead()} /> {this.allowClear && @@ -446,10 +468,7 @@ export class Select { ref={(ref) => (this.dropdownRef = ref)} show={this.dropdownShow} class={{ - 'd-none': - this.disabled || - this.readonly || - (this.isDropdownEmpty && !this.editable), + 'd-none': this.disabled || this.readonly, }} anchor={this.dropdownAnchor} trigger={this.dropdownWrapperRef} @@ -457,14 +476,14 @@ export class Select { placement="auto-start" overwriteDropdownStyle={async () => { return { - width: `${this.hostElement.clientWidth}px`, + minWidth: `${this.hostElement.clientWidth}px`, }; }} >
@@ -490,6 +509,11 @@ export class Select { }} > )} + {this.isDropdownEmpty && !this.editable ? ( +
{this.i18nNoMatches}
+ ) : ( + '' + )} ); diff --git a/packages/core/src/tests/dropdown-button/dropdown-button.e2e.ts b/packages/core/src/tests/dropdown-button/dropdown-button.e2e.ts index 69899bec1e2..3b1b5ce3c9e 100644 --- a/packages/core/src/tests/dropdown-button/dropdown-button.e2e.ts +++ b/packages/core/src/tests/dropdown-button/dropdown-button.e2e.ts @@ -29,6 +29,8 @@ regressionTest.describe('basic', () => { await page.waitForSelector('.dropdown.show'); - expect(await page.screenshot({ fullPage: true })).toMatchSnapshot(); + expect(await page.screenshot({ fullPage: true })).toMatchSnapshot({ + maxDiffPixelRatio: 0.01, + }); }); }); diff --git a/packages/vue/src/components.ts b/packages/vue/src/components.ts index 9005045e4a6..d9157237b2f 100644 --- a/packages/vue/src/components.ts +++ b/packages/vue/src/components.ts @@ -489,6 +489,7 @@ export const IxSelect = /*@__PURE__*/ defineContainer('ix-select', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', + 'i18nNoMatches', 'hideListHeader', 'itemSelectionChange', 'addItem'