Skip to content

Commit

Permalink
feat(ui5-combobox, ui5-multi-combobox): clear icon implementation (#8038
Browse files Browse the repository at this point in the history
)
  • Loading branch information
MapTo0 committed Jan 23, 2024
1 parent 6bb45a9 commit d3ad83b
Show file tree
Hide file tree
Showing 17 changed files with 440 additions and 118 deletions.
6 changes: 6 additions & 0 deletions packages/main/src/ComboBox.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
data-sap-focus-ref
/>

{{#if _effectiveShowClearIcon}}
<div @click={{_clear}} class="ui5-input-clear-icon-wrapper" input-icon tabindex="-1">
<ui5-icon tabindex="-1" class="ui5-input-clear-icon" name="decline" accessible-name="{{clearIconAccessibleName}}"></ui5-icon>
</div>
{{/if}}

{{#if icon}}
<slot name="icon"></slot>
{{/if}}
Expand Down
93 changes: 73 additions & 20 deletions packages/main/src/ComboBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import event from "@ui5/webcomponents-base/dist/decorators/event.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
import { isPhone, isAndroid, isSafari } from "@ui5/webcomponents-base/dist/Device.js";
import { isPhone, isAndroid } from "@ui5/webcomponents-base/dist/Device.js";
import Integer from "@ui5/webcomponents-base/dist/types/Integer.js";
import InvisibleMessageMode from "@ui5/webcomponents-base/dist/types/InvisibleMessageMode.js";
import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AriaLabelHelper.js";
Expand Down Expand Up @@ -52,6 +52,7 @@ import {
SELECT_OPTIONS,
LIST_ITEM_POSITION,
LIST_ITEM_GROUP_HEADER,
INPUT_CLEAR_ICON_ACC_NAME,
} from "./generated/i18n/i18n-defaults.js";

// Templates
Expand Down Expand Up @@ -79,6 +80,8 @@ import GroupHeaderListItem from "./GroupHeaderListItem.js";
import ComboBoxFilter from "./types/ComboBoxFilter.js";
import type FormSupportT from "./features/InputElementsFormSupport.js";
import PopoverHorizontalAlign from "./types/PopoverHorizontalAlign.js";
import Input, { InputEventDetail } from "./Input.js";
import SuggestionItem from "./SuggestionItem.js";

const SKIP_ITEMS_SIZE = 10;

Expand Down Expand Up @@ -170,6 +173,8 @@ type ComboBoxSelectionChangeEventDetail = {
GroupHeaderListItem,
Popover,
ComboBoxGroupItem,
Input,
SuggestionItem,
],
})
/**
Expand All @@ -180,7 +185,7 @@ type ComboBoxSelectionChangeEventDetail = {
@event("change")

/**
* Fired when typing in input.
* Fired when typing in input or clear icon is pressed.
* <br><br>
* <b>Note:</b> filterValue property is updated, input is changed.
* @public
Expand Down Expand Up @@ -306,6 +311,16 @@ class ComboBox extends UI5Element {
@property({ type: ComboBoxFilter, defaultValue: ComboBoxFilter.StartsWithPerTerm })
filter!: `${ComboBoxFilter}`;

/**
* Defines whether the clear icon of the combobox will be shown.
*
* @default false
* @public
* @since 1.20.1
*/
@property({ type: Boolean })
showClearIcon!: boolean;

/**
* Indicates whether the input is focssed
* @private
Expand Down Expand Up @@ -348,6 +363,9 @@ class ComboBox extends UI5Element {
@property({ validator: Integer, noAttribute: true })
_listWidth!: number;

@property({ type: Boolean, noAttribute: true })
_effectiveShowClearIcon!: boolean;

/**
* Defines the component items.
*
Expand Down Expand Up @@ -401,7 +419,8 @@ class ComboBox extends UI5Element {
this._itemFocused = false;
this._autocomplete = false;
this._isKeyNavigation = false;
this._lastValue = "";
// when an initial value is set it should be considered as a _lastValue
this._lastValue = this.getAttribute("value") || "";
this._selectionPerformed = false;
this._selectedItemText = "";
this._userTypedValue = "";
Expand All @@ -412,6 +431,8 @@ class ComboBox extends UI5Element {

this.FormSupport = getFeature<typeof FormSupportT>("FormSupport");

this._effectiveShowClearIcon = (this.showClearIcon && !!this.value && !this.readonly && !this.disabled);

if (this._initialRendering || this.filter === "None") {
this._filteredItems = this.items;
}
Expand All @@ -427,19 +448,20 @@ class ComboBox extends UI5Element {
this._selectMatchingItem();
this._initialRendering = false;

const slottedIconsCount = this.icon.length || 0;
this.style.setProperty(getScopedVarName("--_ui5-input-icons-count"), `${this.iconsCount}`);
}

get iconsCount() {
const slottedIconsCount = this.icon?.length || 0;
const clearIconCount = Number(this._effectiveShowClearIcon) ?? 0;
const arrowDownIconsCount = this.readonly ? 0 : 1;
this.style.setProperty(getScopedVarName("--_ui5-input-icons-count"), `${slottedIconsCount + arrowDownIconsCount}`);

return slottedIconsCount + clearIconCount + arrowDownIconsCount;
}

async onAfterRendering() {
const picker: ResponsivePopover = await this._getPicker();

if (isPhone() && picker.opened) {
// Set initial focus to the native input
this.inner.focus();
}

if ((await this.shouldClosePopover()) && !isPhone()) {
picker.close(false, false, true);
this._clearFocus();
Expand All @@ -449,13 +471,9 @@ class ComboBox extends UI5Element {
this.toggleValueStatePopover(this.shouldOpenValueStateMessagePopover);
this.storeResponsivePopoverWidth();

// Safari is quite slow and does not preserve text highlighting on control rerendering.
// That's why we need to restore it "manually".
if (isSafari() && this._autocomplete && this.filterValue !== this.value) {
this.inner.setSelectionRange(
(this._isKeyNavigation ? 0 : this.filterValue.length),
this.value.length,
);
if (isPhone()) {
this.value = this.inner.value;
this._selectMatchingItem();
}
}

Expand All @@ -467,7 +485,6 @@ class ComboBox extends UI5Element {

_focusin(e: FocusEvent) {
this.focused = true;
this._lastValue = this.value;
this._autocomplete = false;

!isPhone() && (e.target as HTMLInputElement).setSelectionRange(0, this.value.length);
Expand All @@ -476,6 +493,12 @@ class ComboBox extends UI5Element {
_focusout(e: FocusEvent) {
const toBeFocused = e.relatedTarget as HTMLElement;
const focusedOutToValueStateMessage = toBeFocused?.shadowRoot?.querySelector(".ui5-valuestatemessage-root");
const clearIconWrapper = this.shadowRoot!.querySelector(".ui5-input-clear-icon-wrapper");
const focusedOutToClearIcon = clearIconWrapper === toBeFocused || clearIconWrapper?.contains(toBeFocused);

if (this._effectiveShowClearIcon && focusedOutToClearIcon) {
return;
}

this._fireChangeEvent();

Expand All @@ -492,6 +515,7 @@ class ComboBox extends UI5Element {

_afterOpenPopover() {
this._iconPressed = true;
this.inner.focus();
}

_afterClosePopover() {
Expand Down Expand Up @@ -571,6 +595,13 @@ class ComboBox extends UI5Element {
this._toggleRespPopover();
}

_handleMobileInput(e: CustomEvent<InputEventDetail>) {
const { target } = e;
this.filterValue = (target as Input).value;
this.value = (target as Input).value;
this.fireEvent("input");
}

_input(e: InputEvent) {
const { value } = e.target as HTMLInputElement;
const shouldAutocomplete = this.shouldAutocomplete(e);
Expand Down Expand Up @@ -909,7 +940,7 @@ class ComboBox extends UI5Element {
}

async _openRespPopover() {
(await this._getPicker()).showAt(this);
(await this._getPicker()).showAt(this, true);
}

_filterItems(str: string) {
Expand Down Expand Up @@ -1045,6 +1076,24 @@ class ComboBox extends UI5Element {
}
}

_clear() {
const selectedItem = this.items.find(item => item.selected) as (ComboBoxItem | undefined);

if (selectedItem?.text === this.value) {
this.fireEvent("change");
}

this.value = "";
this.fireEvent("input");

if (this._isPhone) {
this._lastValue = "";
this.fireEvent("change");
} else {
this.focus();
}
}

async _scrollToItem(indexOfItem: number, forward: boolean) {
const picker = await this._getPicker();
const list = picker.querySelector(".ui5-combobox-items-list") as List;
Expand Down Expand Up @@ -1078,7 +1127,7 @@ class ComboBox extends UI5Element {
}

get inner(): HTMLInputElement {
return isPhone() ? this.responsivePopover!.querySelector(".ui5-input-inner-phone")! : this.shadowRoot!.querySelector("[inner-input]")!;
return isPhone() ? this.responsivePopover!.querySelector("[ui5-input]")!.shadowRoot!.querySelector("input")! : this.shadowRoot!.querySelector("[inner-input]")!;
}

async _getPicker() {
Expand Down Expand Up @@ -1184,6 +1233,10 @@ class ComboBox extends UI5Element {
return getEffectiveAriaLabelText(this);
}

get clearIconAccessibleName() {
return ComboBox.i18nBundle.getText(INPUT_CLEAR_ICON_ACC_NAME);
}

static async onDefine() {
ComboBox.i18nBundle = await getI18nBundle("@ui5/webcomponents");
}
Expand Down
26 changes: 11 additions & 15 deletions packages/main/src/ComboBoxPopover.hbs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<ui5-responsive-popover
class="{{classes.popover}}"
hide-arrow
_disable-initial-focus
placement-type="Bottom"
horizontal-align="Left"
style="{{styles.suggestionsPopover}}"
Expand Down Expand Up @@ -29,22 +28,19 @@
</div>

<div class="row">
<div
class="input-root-phone"
<ui5-input
.value="{{value}}"
@ui5-input="{{_handleMobileInput}}"
@ui5-change="{{_inputChange}}"
placeholder="{{placeholder}}"
value-state="{{valueState}}"
?show-clear-icon="{{showClearIcon}}"
?no-typeahead="{{noTypeahead}}"
>
<input
class="ui5-input-inner-phone"
.value="{{value}}"
inner-input
placeholder="{{placeholder}}"
value-state="{{valueState}}"
@input="{{_input}}"
@change="{{_inputChange}}"
@keydown="{{_keydown}}"
aria-autocomplete="both"
/>
</div>
{{#each _filteredItems}}
<ui5-suggestion-item text="{{this.text}}" additional-text="{{this.additionalText}}"></ui5-suggestion-item>
{{/each}}
</ui5-input>
</div>
</div>

Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/Input.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
max="{{nativeInputAttributes.max}}"
/>

{{#if effectiveShowClearIcon}}
{{#if _effectiveShowClearIcon}}
<div @click={{_clear}} @mousedown={{_iconMouseDown}} class="ui5-input-clear-icon-wrapper" input-icon tabindex="-1">
<ui5-icon tabindex="-1" class="ui5-input-clear-icon" name="decline" accessible-name="{{clearIconAccessibleName}}"></ui5-icon>
</div>
Expand Down
13 changes: 7 additions & 6 deletions packages/main/src/Input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ enum INPUT_ACTIONS {
}

type InputEventDetail = {
inputType?: string;
inputType: string;
}

type InputSuggestionItemSelectEventDetail = {
Expand Down Expand Up @@ -463,7 +463,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement {
* @since 1.2.0
*/
@property({ type: Boolean })
effectiveShowClearIcon!: boolean;
_effectiveShowClearIcon!: boolean;

/**
* @private
Expand Down Expand Up @@ -684,7 +684,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement {
this.suggestionObjects = this.Suggestions!.defaultSlotProperties(this.typedInValue);
}

this.effectiveShowClearIcon = (this.showClearIcon && !!this.value && !this.readonly && !this.disabled);
this._effectiveShowClearIcon = (this.showClearIcon && !!this.value && !this.readonly && !this.disabled);
this.style.setProperty(getScopedVarName("--_ui5-input-icons-count"), `${this.iconsCount}`);

this.FormSupport = getFeature<typeof FormSupportT>("FormSupport");
Expand Down Expand Up @@ -969,7 +969,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement {

this._keepInnerValue = false;

if (this.showClearIcon && !this.effectiveShowClearIcon) {
if (this.showClearIcon && !this._effectiveShowClearIcon) {
this._clearIconClicked = false;
this._handleChange();
}
Expand Down Expand Up @@ -1054,7 +1054,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement {
const inputDomRef = this.getInputDOMRefSync();
const emptyValueFiredOnNumberInput = this.value && this.isTypeNumber && !inputDomRef!.value;
const eventType: string = (e as InputEvent).inputType
|| (e.detail && (e as CustomEvent<InputEventDetail>).detail.inputType!)
|| (e.detail as InputEventDetail).inputType
|| "";
this._keepInnerValue = false;

Expand Down Expand Up @@ -1572,7 +1572,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement {

get iconsCount(): number {
const slottedIconsCount = this.icon ? this.icon.length : 0;
const clearIconCount = Number(this.effectiveShowClearIcon) ?? 0;
const clearIconCount = Number(this._effectiveShowClearIcon) ?? 0;
return slottedIconsCount + clearIconCount;
}

Expand Down Expand Up @@ -1778,4 +1778,5 @@ export type {
InputSuggestionScrollEventDetail,
InputSuggestionItemSelectEventDetail,
InputSuggestionItemPreviewEventDetail,
InputEventDetail,
};
6 changes: 6 additions & 0 deletions packages/main/src/MultiComboBox.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@
data-sap-focus-ref
/>

{{#if _effectiveShowClearIcon}}
<div @click={{_clear}} class="ui5-input-clear-icon-wrapper" input-icon tabindex="-1" @mousedown={{_iconMouseDown}}>
<ui5-icon tabindex="-1" class="ui5-input-clear-icon" name="decline" accessible-name="{{clearIconAccessibleName}}"></ui5-icon>
</div>
{{/if}}

{{#if icon}}
<slot name="icon"></slot>
{{/if}}
Expand Down
Loading

0 comments on commit d3ad83b

Please sign in to comment.