Skip to content

Commit

Permalink
fix(ui5-multi-combobox): select all implementation (#8066)
Browse files Browse the repository at this point in the history
  • Loading branch information
MapTo0 committed Jan 23, 2024
1 parent d3ad83b commit 7e8a355
Show file tree
Hide file tree
Showing 8 changed files with 319 additions and 18 deletions.
108 changes: 93 additions & 15 deletions packages/main/src/MultiComboBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
} from "@ui5/webcomponents-base/dist/Keys.js";
import Integer from "@ui5/webcomponents-base/dist/types/Integer.js";
import "@ui5/webcomponents-icons/dist/slim-arrow-down.js";
import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
import {
isPhone,
isAndroid,
Expand Down Expand Up @@ -86,6 +87,7 @@ import {
SHOW_SELECTED_BUTTON,
MULTICOMBOBOX_DIALOG_OK_BUTTON,
VALUE_STATE_ERROR_ALREADY_SELECTED,
MCB_SELECTED_ITEMS,
INPUT_CLEAR_ICON_ACC_NAME,
} from "./generated/i18n/i18n-defaults.js";

Expand All @@ -102,6 +104,7 @@ import MultiComboBoxPopover from "./generated/themes/MultiComboBoxPopover.css.js
import ComboBoxFilter from "./types/ComboBoxFilter.js";
import type FormSupportT from "./features/InputElementsFormSupport.js";
import type ListItemBase from "./ListItemBase.js";
import CheckBox from "./CheckBox.js";
import Input, { InputEventDetail } from "./Input.js";

type ValueStateAnnouncement = Record<Exclude<ValueState, ValueState.None>, string>;
Expand Down Expand Up @@ -183,6 +186,7 @@ type MultiComboboxItemWithSelection = {
GroupHeaderListItem,
ToggleButton,
Button,
CheckBox,
],
})
/**
Expand Down Expand Up @@ -350,6 +354,15 @@ class MultiComboBox extends UI5Element {
@property()
accessibleNameRef!: string;

/**
* Determines if the select all checkbox is visible on top of suggestions.
*
* @default false
* @public
*/
@property({ type: Boolean })
showSelectAll!: boolean;

@property({ type: ValueState, defaultValue: ValueState.None })
_effectiveValueState!: `${ValueState}`;
/**
Expand Down Expand Up @@ -391,6 +404,9 @@ class MultiComboBox extends UI5Element {
@property({ type: Boolean, noAttribute: true })
_performingSelectionTwice!: boolean;

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

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

Expand Down Expand Up @@ -912,9 +928,10 @@ class MultiComboBox extends UI5Element {
}
}

_onValueStateKeydown(e: KeyboardEvent) {
async _onListHeaderKeydown(e: KeyboardEvent) {
const isArrowDown = isDown(e);
const isArrowUp = isUp(e);
const isSelectAllFocused = (e.target as HTMLElement).classList.contains("ui5-mcb-select-all-checkbox");

if (isTabNext(e) || isTabPrevious(e)) {
this._onItemTab();
Expand All @@ -924,12 +941,44 @@ class MultiComboBox extends UI5Element {
e.preventDefault();

if (isArrowDown || isDownCtrl(e)) {
if (this.showSelectAll && !isSelectAllFocused) {
return ((await this._getResponsivePopover())!.querySelector(".ui5-mcb-select-all-checkbox") as CheckBox).focus();
}

this._handleArrowDown();
}

if (isArrowUp || isUpCtrl(e)) {
this._shouldAutocomplete = true;
this._inputDom.focus();
if (e.target === this.valueStateHeader || !this.valueStateHeader) {
this._shouldAutocomplete = true;
return this._inputDom.focus();
}

if (this.showSelectAll && isSelectAllFocused) {
this.valueStateHeader?.focus();
}
}
}

_handleSelectAllCheckboxClick(e: CustomEvent) {
if (!this.filterSelected) {
this._handleSelectAll();
this.filterSelected = false;
} else {
this._previouslySelectedItems = this._getSelectedItems();
this.selectedItems?.forEach(item => {
item.selected = (e.target as CheckBox).checked;
});

if (!(e.target as CheckBox).checked) {
this.filterSelected = false;
}

const changePrevented = this.fireSelectionChange();

if (changePrevented) {
this._revertSelection();
}
}
}

Expand Down Expand Up @@ -982,13 +1031,15 @@ class MultiComboBox extends UI5Element {
return;
}

if (((isArrowUp && isFirstItem) || isHome(e)) && this.valueStateHeader) {
this.valueStateHeader.focus();
}

if (!this.valueStateHeader && isFirstItem && isArrowUp) {
this._inputDom.focus();
this._shouldAutocomplete = true;
if (isFirstItem && isArrowUp) {
if (this.showSelectAll) {
((await this._getResponsivePopover())!.querySelector(".ui5-mcb-select-all-checkbox") as CheckBox).focus();
} else if (this.valueStateHeader) {
this.valueStateHeader.focus();
} else {
this._inputDom.focus();
this._shouldAutocomplete = true;
}
}
}

Expand Down Expand Up @@ -1021,10 +1072,17 @@ class MultiComboBox extends UI5Element {
await this._setValueStateHeader();
}

if (isArrowDown && isOpen && this.valueStateHeader) {
this.value = this.valueBeforeAutoComplete || this.value;
this.valueStateHeader.focus();
return;
if (isArrowDown && isOpen) {
if (this.valueStateHeader) {
this.value = this.valueBeforeAutoComplete || this.value;
this.valueStateHeader.focus();
return;
}

if (this.showSelectAll) {
((await this._getResponsivePopover())!.querySelector(".ui5-mcb-select-all-checkbox") as CheckBox).focus();
return;
}
}

if (isArrowDown && hasSuggestions) {
Expand All @@ -1036,13 +1094,16 @@ class MultiComboBox extends UI5Element {
}
}

_handleArrowDown() {
async _handleArrowDown() {
const isOpen = this.allItemsPopover?.opened;
const firstListItem = this.list?.items[0];

if (isOpen) {
firstListItem && this.list?._itemNavigation.setCurrentItem(firstListItem);
this.value = this.valueBeforeAutoComplete || this.value;

// wait item navigation to apply correct tabindex
await renderFinished();
firstListItem?.focus();
} else if (!this.readonly) {
this._navigateToNextItem();
Expand Down Expand Up @@ -1315,6 +1376,10 @@ class MultiComboBox extends UI5Element {
// casted to KeyboardEvent since isSpace and isSpaceCtrl accepts KeyboardEvent only
const castedEvent = { key: e.detail.key } as KeyboardEvent;

if (!e.detail.selectedItems.length && this.filterSelected) {
this.filterSelected = false;
}

if (!e.detail.selectionComponentPressed && !isSpace(castedEvent) && !isSpaceCtrl(castedEvent)) {
this.allItemsPopover?.close();
this.value = "";
Expand Down Expand Up @@ -1462,6 +1527,13 @@ class MultiComboBox extends UI5Element {
const autoCompletedChars = input && (input.selectionEnd || 0) - (input.selectionStart || 0);
const value = input && input.value;

if (this.open) {
this._getList().then(list => {
const selectedItemsCount = list?.querySelectorAll("[ui5-li][selected]")?.length;
const allItemsCount = list?.querySelectorAll("[ui5-li]")?.length;
this._allSelected = selectedItemsCount === allItemsCount;
});
}
this._effectiveShowClearIcon = (this.showClearIcon && !!this.value && !this.readonly && !this.disabled);

this.FormSupport = getFeature("FormSupport");
Expand Down Expand Up @@ -1844,6 +1916,12 @@ class MultiComboBox extends UI5Element {
return MultiComboBox.i18nBundle.getText(INPUT_CLEAR_ICON_ACC_NAME);
}

get selectAllCheckboxLabel() {
const items = this.items.filter(item => !item.isGroupItem);
const selected = items.filter(item => item.selected);
return MultiComboBox.i18nBundle.getText(MCB_SELECTED_ITEMS, selected.length, items.length);
}

get classes(): ClassMap {
return {
popover: {
Expand Down
14 changes: 13 additions & 1 deletion packages/main/src/MultiComboBoxPopover.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,19 @@
{{> valueStateMessage}}
</div>
{{/if}}

{{> selectAllWrapper}}
{{/if}}

{{#unless _isPhone}}
{{#if hasValueStateMessage}}
<div slot="header" @keydown="{{_onValueStateKeydown}}" tabindex="0" class="ui5-responsive-popover-header {{classes.popoverValueState}}" style={{styles.popoverValueStateMessage}}>
<div slot="header" @keydown="{{_onListHeaderKeydown}}" tabindex="0" class="ui5-responsive-popover-header {{classes.popoverValueState}}" style={{styles.popoverValueStateMessage}}>
<ui5-icon class="ui5-input-value-state-message-icon" name="{{_valueStateMessageIcon}}"></ui5-icon>
{{> valueStateMessage}}
</div>
{{/if}}

{{> selectAllWrapper}}
{{/unless}}

{{#if filterSelected}}
Expand Down Expand Up @@ -135,3 +139,11 @@
{{this.text}}
</ui5-li>
{{/inline}}

{{#*inline "selectAllWrapper"}}
{{#if showSelectAll}}
<div class="ui5-mcb-select-all-header" @keydown="{{_onListHeaderKeydown}}" tabindex="0">
<ui5-checkbox ?checked={{_allSelected}} class="ui5-mcb-select-all-checkbox" text="{{selectAllCheckboxLabel}}" @ui5-change="{{_handleSelectAllCheckboxClick}}"></ui5-checkbox>
</div>
{{/if}}
{{/inline}}
3 changes: 3 additions & 0 deletions packages/main/src/i18n/messagebundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ SHOW_SELECTED_BUTTON=Show Selected Items Only
#XBUT: A link that can be clicked to display more/all items
INPUT_SUGGESTIONS=Suggestions available

#XCKL: Select all checkbox label
MCB_SELECTED_ITEMS=Select All ({0} of {1})

#XBUT: Default title text for mobile
INPUT_SUGGESTIONS_TITLE=Select

Expand Down
31 changes: 31 additions & 0 deletions packages/main/src/themes/MultiComboBoxPopover.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
.ui5-suggestions-popover .ui5-multi-combobox-all-items-list {
--_ui5_checkbox_width_height: var(--_ui5_list_item_dropdown_base_height);
}

.ui5-mcb-select-all-header {
width: 100%;
height: 44px;
border-bottom: 0.0625rem solid var(--sapGroup_TitleBorderColor);
display: flex;
align-items: center;
font-family: "72override", var(--sapFontFamily);
position: sticky;
top: 0;
z-index: 2;
background: var(--sapToolbar_Background);
}

.ui5-mcb-select-all-checkbox {
width: 100%;
font-family: var(--sapFontBoldFamily);
}

.ui5-mcb-select-all-checkbox::part(root):focus::before {
border: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor);
border-radius: 0;
right: 2px;
left: 2px;
bottom: 0;
top: 0;
}

.ui5-mcb-select-all-checkbox::part(label) {
font-family: var(--sapFontBoldFamily);
}
2 changes: 1 addition & 1 deletion packages/main/src/themes/ValueStateMessage.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
flex-direction: column;
}

.ui5-popover-with-value-state-header-phone .ui5-valuestatemessage-root + [ui5-list] {
.ui5-popover-with-value-state-header-phone [ui5-list] {
overflow: auto;
}

Expand Down
78 changes: 78 additions & 0 deletions packages/main/test/pages/MultiComboBox.html
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,84 @@
</ui5-multi-combobox>
</div>

<div class="demo-section">
<span>Select all</span>

<br>
<ui5-multi-combobox show-select-all id="mcb-select-all-vs" value-state="Error">
<ui5-mcb-item text="Qui minim"></ui5-mcb-item>
<ui5-mcb-item text="minim amet"></ui5-mcb-item>
<ui5-mcb-item text="amet mollit"></ui5-mcb-item>
<ui5-mcb-item text="mollit pariatur"></ui5-mcb-item>
<ui5-mcb-item text="pariatur"></ui5-mcb-item>
<ui5-mcb-item text="In"></ui5-mcb-item>
<ui5-mcb-item text="irure"></ui5-mcb-item>
<ui5-mcb-item text="minim"></ui5-mcb-item>
<ui5-mcb-item text="tempor"></ui5-mcb-item>
<ui5-mcb-item text="ipsum"></ui5-mcb-item>
<ui5-mcb-item text="consequat"></ui5-mcb-item>
<ui5-mcb-item text="est"></ui5-mcb-item>
<ui5-mcb-item text="nostrud"></ui5-mcb-item>
<ui5-mcb-item text="dolor"></ui5-mcb-item>
<ui5-mcb-item text="Ad"></ui5-mcb-item>
<ui5-mcb-item text="minim"></ui5-mcb-item>
<ui5-mcb-item text="esse"></ui5-mcb-item>
<ui5-mcb-item text="anim"></ui5-mcb-item>
<ui5-mcb-item text="veniam"></ui5-mcb-item>
<ui5-mcb-item text="veniam"></ui5-mcb-item>
<ui5-mcb-item text="non"></ui5-mcb-item>
<ui5-mcb-item text="id"></ui5-mcb-item>
<ui5-mcb-item text="esse"></ui5-mcb-item>
<ui5-mcb-item text="do"></ui5-mcb-item>
<ui5-mcb-item text="irure"></ui5-mcb-item>
<ui5-mcb-item text="sint"></ui5-mcb-item>
<ui5-mcb-item text="est"></ui5-mcb-item>
</ui5-multi-combobox>

<br>

<ui5-multi-combobox show-select-all id="mcb-select-all">
<ui5-mcb-item text="Qui minim"></ui5-mcb-item>
<ui5-mcb-item text="minim amet"></ui5-mcb-item>
<ui5-mcb-item text="amet mollit"></ui5-mcb-item>
<ui5-mcb-item text="mollit pariatur"></ui5-mcb-item>
<ui5-mcb-item text="pariatur"></ui5-mcb-item>
<ui5-mcb-item text="In"></ui5-mcb-item>
<ui5-mcb-item text="irure"></ui5-mcb-item>
<ui5-mcb-item text="minim"></ui5-mcb-item>
<ui5-mcb-item text="tempor"></ui5-mcb-item>
<ui5-mcb-item text="ipsum"></ui5-mcb-item>
<ui5-mcb-item text="consequat"></ui5-mcb-item>
<ui5-mcb-item text="est"></ui5-mcb-item>
<ui5-mcb-item text="nostrud"></ui5-mcb-item>
<ui5-mcb-item text="dolor"></ui5-mcb-item>
<ui5-mcb-item text="Ad"></ui5-mcb-item>
<ui5-mcb-item text="minim"></ui5-mcb-item>
<ui5-mcb-item text="esse"></ui5-mcb-item>
<ui5-mcb-item text="anim"></ui5-mcb-item>
<ui5-mcb-item text="veniam"></ui5-mcb-item>
<ui5-mcb-item text="veniam"></ui5-mcb-item>
<ui5-mcb-item text="non"></ui5-mcb-item>
<ui5-mcb-item text="id"></ui5-mcb-item>
<ui5-mcb-item text="esse"></ui5-mcb-item>
<ui5-mcb-item text="do"></ui5-mcb-item>
<ui5-mcb-item text="irure"></ui5-mcb-item>
<ui5-mcb-item text="sint"></ui5-mcb-item>
<ui5-mcb-item text="est"></ui5-mcb-item>
</ui5-multi-combobox>

<br>

<span id="select-all-event">Selected items count: <span>0</span></span>

<script>
document.getElementById("mcb-select-all-vs").addEventListener("ui5-selectionChange", e => {
console.log("selection change")
document.querySelector("#select-all-event span").textContent = e.detail.items.length;
});
</script>
</div>

<div class="demo-section">
<span>MultiComboBox with items</span>

Expand Down
Loading

0 comments on commit 7e8a355

Please sign in to comment.