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')}
`;
}