diff --git a/documents/src/pages/elements/tree-select.md b/documents/src/pages/elements/tree-select.md index 321c47f766..92fcd5b5ca 100644 --- a/documents/src/pages/elements/tree-select.md +++ b/documents/src/pages/elements/tree-select.md @@ -210,6 +210,73 @@ By clicking the `Selected` button, Tree Select allows the items to be filtered b For custom filtering, Tree Select provides an identical interface as Combo Box. You provide a predicate function that tests an item. Please consult the [Combo Box docs](./elements/combo-box) for details on how to construct a compatible filter. +## Limiting Selected Items +Tree Select offers a convenient way to limit the number of selected items using `max` property. If users attempt to select more items than the specified limit, "Done" button will be automatically disabled. + +:: +```javascript +::tree-select:: +const el = document.querySelector("ef-tree-select"); +el.data = [{ + 'value': 'AFR', + 'label': 'Africa', + 'expanded': true, + 'items': [{ + 'value': 'DZA', + 'label': 'Algeria', + 'expanded': true, + 'items': [{ + 'value': 'ADR', + 'label': 'Adrar', + 'selected': true, + 'items': [] + }, { + 'value': 'TAM', + 'label': 'Tamanghasset', + 'selected': true, + 'items': [] + }, { + 'value': 'GUE', + 'label': 'Guelma', + 'selected': false, + 'items': [] + }] + }, { + 'value': 'AGO', + 'label': 'Angola', + 'selected': false, + 'items': [] + }, { + 'value': 'BEN', + 'label': 'Benin', + 'selected': false, + 'items': [] + }, { + 'value': 'BWA', + 'label': 'Botswana', + 'selected': false, + 'items': [] + }] +}]; +setTimeout(() => { el.opened = true; }, 1000); +``` +```css +.wrapper { + padding: 5px; + height: 450px; +} +``` +```html +
+ +
+``` +:: + +```html + +``` + ## UI Controls Tree Select has several controls. diff --git a/packages/elements/src/tree-select/__demo__/index.html b/packages/elements/src/tree-select/__demo__/index.html index c733aa842a..7ce3b38a84 100644 --- a/packages/elements/src/tree-select/__demo__/index.html +++ b/packages/elements/src/tree-select/__demo__/index.html @@ -64,6 +64,21 @@ + +

+ max = 0 + +

+

+ max = 2 + +

+

+ max = 10 + +

+
+ diff --git a/packages/elements/src/tree-select/__test__/tree-select.interaction.test.js b/packages/elements/src/tree-select/__test__/tree-select.interaction.test.js index 4c76aeced7..b89b796338 100644 --- a/packages/elements/src/tree-select/__test__/tree-select.interaction.test.js +++ b/packages/elements/src/tree-select/__test__/tree-select.interaction.test.js @@ -303,5 +303,26 @@ describe('tree-select/Interaction', function () { ); expect(el.shadowRoot.querySelector('[part="pills"]')).to.equal(null, 'pills should hide'); }); + + it('has correct disabled state on confirm button when select an item', async function () { + const el = await fixture(''); + el.data = flatData; + el.opened = true; + await elementUpdated(el); + const treeItems = el.treeEl.querySelectorAll('[role=treeitem]'); + const confirmButton = el.popupEl.querySelector('#done'); + treeItems[0].click(); + treeItems[1].click(); + treeItems[2].click(); + treeItems[3].click(); + await elementUpdated(el); + expect(confirmButton.disabled).to.equal(false); + treeItems[4].click(); + await elementUpdated(el); + expect(confirmButton.disabled).to.equal(true); + treeItems[4].click(); // uncheck item + await elementUpdated(el); + expect(confirmButton.disabled).to.equal(false); + }); }); }); diff --git a/packages/elements/src/tree-select/__test__/tree-select.value.test.js b/packages/elements/src/tree-select/__test__/tree-select.value.test.js index ebcc793f1a..6f85d3b0a9 100644 --- a/packages/elements/src/tree-select/__test__/tree-select.value.test.js +++ b/packages/elements/src/tree-select/__test__/tree-select.value.test.js @@ -10,12 +10,12 @@ import '@refinitiv-ui/elements/tree-select'; import '@refinitiv-ui/elemental-theme/light/ef-tree-select'; import { elementUpdated, expect, fixture } from '@refinitiv-ui/test-helpers'; -const data1 = [{ items: [{ selected: true, value: '1' }] }]; +const data1 = [{ items: [{ selected: true, value: '1', label: '1' }] }]; const data2 = [ { items: [ - { selected: true, value: '1' }, - { selected: true, value: '2' } + { selected: true, value: '1', label: '1' }, + { selected: true, value: '2', label: '2' } ] } ]; @@ -53,4 +53,35 @@ describe('tree-select/Value', function () { expect(el.values).to.deep.equal([]); }); }); + describe('max', function () { + it('has correct disabled state on confirm button when values changed', async function () { + const el = await fixture(''); + el.data = data2; + el.opened = true; + await elementUpdated(el); + const confirmButton = el.popupEl.querySelector('#done'); + expect(confirmButton.disabled).to.equal(true); + el.values = []; + await elementUpdated(el); + expect(confirmButton.disabled).to.equal(false); + }); + it('has correct disabled state on confirm button when max value changed', async function () { + const el = await fixture(''); + el.data = data2; + el.opened = true; + await elementUpdated(el); + const confirmButton = el.popupEl.querySelector('#done'); + expect(confirmButton.disabled).to.equal(true); + el.max = '2'; + await elementUpdated(el); + expect(confirmButton.disabled).to.equal(false); + }); + it('Should reset max to null when define negative max value', async function () { + const el = await fixture(''); + await elementUpdated(el); + expect(el.max).to.equal(null); + el.max = '2'; + expect(el.max).to.equal('2'); + }); + }); }); diff --git a/packages/elements/src/tree-select/index.ts b/packages/elements/src/tree-select/index.ts index 5c2d15f0e2..1f4aef4d60 100644 --- a/packages/elements/src/tree-select/index.ts +++ b/packages/elements/src/tree-select/index.ts @@ -23,6 +23,7 @@ import { VERSION } from '../version.js'; import type { CheckChangedEvent } from '../events'; import type { Overlay } from '../overlay'; import type { Pill } from '../pill'; +import type { Tree } from '../tree/index.js'; import type { TreeSelectData, TreeSelectDataItem } from './helpers/types'; export { TreeSelectRenderer }; @@ -202,12 +203,41 @@ export class TreeSelect extends ComboBox { @property({ type: Function, attribute: false }) public override renderer = new TreeSelectRenderer(this); + private _max: string | null = null; + /** + * Set maximum number of selected items + * @param value max value + * @default - + */ + @property({ type: String }) + public set max(value: string | null) { + value = Number(value) >= 0 ? value : null; + const oldValue = this._max; + if (oldValue !== value) { + this._max = value; + this.requestUpdate('max', oldValue); + } + } + /** + * Set maximum number of selected items + * @returns max value + */ + public get max(): string | null { + return this._max; + } + /** * Internal reference to popup element */ @query('[part=list]') protected popupEl?: Overlay; + /** + * Internal reference to tree element + */ + @query('[part=tree]') + protected treeEl?: Tree; + /** * Set resolved data * @param value resolved data @@ -364,6 +394,15 @@ export class TreeSelect extends ComboBox { return checkedGroupItems; } + /** + * Determines whether the "Done" button element should be disabled, + * based on the current state and certain conditions. + * @returns {boolean} True if the "Done" button should be disabled, false otherwise. + */ + protected get isConfirmDisabled(): boolean { + return Boolean(this.treeEl && this.max && this.treeEl.values.length > Number(this.max)); + } + /** * Persist the current selection * Takes the current selection and uses it for {@link TreeSelect.values} @@ -926,7 +965,9 @@ export class TreeSelect extends ComboBox { */ protected get commitControlsTemplate(): TemplateResult { return html` - ${this.t('DONE')} + ${this.t('DONE')} ${this.t('CANCEL')} `; }