Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ui5-multi-combobox): select all implementation #8066

Merged
merged 6 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 94 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,
} from "./generated/i18n/i18n-defaults.js";

// Templates
Expand All @@ -101,6 +103,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";

type ValueStateAnnouncement = Record<Exclude<ValueState, ValueState.None>, string>;
type ValueStateTypeAnnouncement = Record<Exclude<ValueState, ValueState.None>, string>;
Expand Down Expand Up @@ -181,6 +184,7 @@ type MultiComboboxItemWithSelection = {
GroupHeaderListItem,
ToggleButton,
Button,
CheckBox,
],
})
/**
Expand Down Expand Up @@ -339,6 +343,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 @@ -377,6 +390,9 @@ class MultiComboBox extends UI5Element {
@property({ type: Boolean, noAttribute: true })
_performingSelectionTwice!: boolean;

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

/**
* Indicates whether the tokenizer has tokens
* @private
Expand Down Expand Up @@ -854,9 +870,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 @@ -866,12 +883,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();
MapTo0 marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -924,13 +973,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 @@ -963,10 +1014,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 @@ -978,13 +1036,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 @@ -1257,6 +1318,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 @@ -1399,6 +1464,14 @@ 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.FormSupport = getFeature("FormSupport");
this._inputLastValue = value;

Expand Down Expand Up @@ -1747,6 +1820,12 @@ class MultiComboBox extends UI5Element {
return slottedIconsCount + arrowDownIconsCount;
}

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 @@ -56,15 +56,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 @@ -139,3 +143,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 @@ -163,6 +163,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