Skip to content

Commit

Permalink
fix(tree, tree-select, combo-box): apply filter when it's updated (#1217
Browse files Browse the repository at this point in the history
)

* fix(tree, tree-select, combo-box): apply filter when it's updated

* test(tree, tree-select, combo-box): add unit test for filter update

* refactor(tree): scope a radio group to radio button elements only

* refactor(combo-box): fix comment
  • Loading branch information
wattachai-lseg authored Sep 23, 2024
1 parent 30ed4b5 commit 3640760
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 38 deletions.
23 changes: 20 additions & 3 deletions packages/elements/src/combo-box/__demo__/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,15 @@
</demo-block>

<demo-block header="Custom Filter" layout="normal" tags="filters, custom">
<p>
Call the global <code>useLabelOnlyFilter()</code> & <code>useLabelValueFilter()</code> to switch
between these filters.<br />
<strong>Active Filter</strong>: <span id="active-filter">label only</span>.<br />
</p>
<ef-combo-box id="custom-filter"></ef-combo-box>
<script>
const customFilterCombo = document.getElementById('custom-filter');
const customFilter = (comboBox) => {
const customFilter = (comboBox, labelValue = false) => {
// reference query string for validating queryRegExp cache state
let query = '';
// cache RegExp
Expand All @@ -173,12 +178,24 @@
const value = item.value;
const label = item.label;
const regex = getRegularExpressionOfQuery();
const result = regex.test(value) || regex.test(label);
let result = regex.test(label);
if (labelValue) {
result = result || regex.test(value);
}
return result;
};
};

customFilterCombo.filter = customFilter(customFilterCombo);
const activeFilter = document.getElementById('active-filter');
globalThis.useLabelOnlyFilter = () => {
customFilterCombo.filter = customFilter(customFilterCombo);
activeFilter.textContent = 'label only';
};

globalThis.useLabelValueFilter = () => {
customFilterCombo.filter = customFilter(customFilterCombo, true);
activeFilter.textContent = 'label & value';
};
</script>
</demo-block>

Expand Down
39 changes: 39 additions & 0 deletions packages/elements/src/combo-box/__test__/combo-box.filter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '@refinitiv-ui/elements/combo-box';
import '@refinitiv-ui/elemental-theme/light/ef-combo-box';
import { elementUpdated, expect, fixture, oneEvent } from '@refinitiv-ui/test-helpers';

import { createDefaultFilter } from '../../../lib/combo-box/helpers/filter.js';
import { getData, openedUpdated, snapshotIgnore } from './utils.js';

const setInputEl = async (el, textInput) => {
Expand Down Expand Up @@ -74,5 +75,43 @@ describe('combo-box/Filter', function () {
expect(el.query).to.equal(textInput, `Query should be the same as input text: "${textInput}"`);
await expect(el).shadowDom.to.equalSnapshot(snapshotIgnore);
});

it('should update filter result when filter is updated', async function () {
const el = await fixture('<ef-combo-box opened></ef-combo-box>');
el.data = getData();
await elementUpdated(el);

const createCustomFilter = (comboBox) => {
let query = '';
let queryRegExp;
// Items could be filtered with case-insensitive partial match of both labels & values.
const getRegularExpressionOfQuery = () => {
if (comboBox.query !== query || !queryRegExp) {
query = comboBox.query || '';
queryRegExp = new RegExp(escapeStringRegexp(query), 'i');
}
return queryRegExp;
};
return (item) => {
const value = item.value;
const label = item.label;
const regex = getRegularExpressionOfQuery();
const result = regex.test(value) || regex.test(label);
return result;
};
};
el.filter = createCustomFilter(el);
await setInputEl(el, 'ax');
await elementUpdated(el);
const expectedLength = 2; // include header
expect(el.listEl.children.length).to.equal(
expectedLength,
`there should be only ${expectedLength} item in Combo Box`
);

el.filter = createDefaultFilter(el);
await elementUpdated(el);
expect(el.listEl).to.equal(null, 'there should be no List in Combo Box');
});
});
});
5 changes: 5 additions & 0 deletions packages/elements/src/combo-box/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,11 @@ export class ComboBox<T extends DataItem = ItemData> extends FormFieldElement {
triggerResize();
}

// If filter has been updated, filter items again with the updated filter.
if (changedProperties.has('filter')) {
this.filterItems();
}

super.update(changedProperties);
}

Expand Down
24 changes: 19 additions & 5 deletions packages/elements/src/tree-select/__demo__/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,16 @@

<demo-block header="Filter" tags="filter" layout="normal">
<p>
Items could be filtered with case-insensitive partial match of both labels & values.<br />
There is a debounce rate of 500ms applied.
Call the global <code>useLabelOnlyFilter()</code> & <code>useLabelValueFilter()</code> to switch
between these filters.<br />
<strong>Active Filter</strong>: <span id="active-filter">label only</span>.<br />
</p>
<ef-tree-select id="filter" query-debounce-rate="500" aria-label="Choose Country"></ef-tree-select>
<script type="module">
import escapeStringRegexp from 'escape-string-regexp';

const treeSelect = document.getElementById('filter');
const createCustomFilter = (treeSelect) => {
const createCustomFilter = (treeSelect, labelValue = false) => {
let query = '';
let queryRegExp;
const getRegularExpressionOfQuery = () => {
Expand All @@ -145,11 +146,24 @@
const treeNode = treeManager.getTreeNode(item);
const { label, value } = treeNode;
const regex = getRegularExpressionOfQuery();
const result = regex.test(value) || regex.test(label);
let result = regex.test(label);
if (labelValue) {
result = result || regex.test(value);
}
return result;
};
};
treeSelect.filter = createCustomFilter(treeSelect);

const activeFilter = document.getElementById('active-filter');
globalThis.useLabelOnlyFilter = () => {
treeSelect.filter = createCustomFilter(treeSelect);
activeFilter.textContent = 'label only';
};

globalThis.useLabelValueFilter = () => {
treeSelect.filter = createCustomFilter(treeSelect, true);
activeFilter.textContent = 'label & value';
};
</script>
</demo-block>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import '@refinitiv-ui/elements/tree-select';
import '@refinitiv-ui/elemental-theme/light/ef-tree-select';
import { aTimeout, elementUpdated, expect, fixture } from '@refinitiv-ui/test-helpers';

import { createDefaultFilter } from '../../../lib/combo-box/helpers/filter.js';
import { flatData, flatSelection } from './mock_data/flat.js';
import { multiLevelData } from './mock_data/multi-level.js';
import { nestedData, nestedSelection, selectableCount } from './mock_data/nested.js';
Expand Down Expand Up @@ -436,5 +437,51 @@ describe('tree-select/Filter', function () {
`there should be only ${expectedLength} children with a query of ${query}`
);
});

it('should update filter result when filter is updated', async function () {
const el = await fixture('<ef-tree-select opened></ef-tree-select>');
el.data = flatData;
await elementUpdated(el);

const createCustomFilter = (treeSelect) => {
let query = '';
let queryRegExp;
// Items could be filtered with case-insensitive partial match of both labels & values.
const getRegularExpressionOfQuery = () => {
if (treeSelect.query !== query || !queryRegExp) {
query = treeSelect.query || '';
queryRegExp = new RegExp(escapeStringRegexp(query), 'i');
}
return queryRegExp;
};
return (item, treeManager) => {
const treeNode = treeManager.getTreeNode(item);
const { label, value } = treeNode;
const regex = getRegularExpressionOfQuery();
const result = regex.test(value) || regex.test(label);
return result;
};
};
el.filter = createCustomFilter(el);
const query = 'DEU';
el.query = query;
await elementUpdated(el);

const expectedLength = 1;
const treeEl = getTreeElPart(el);
expect(treeEl.children.length).to.equal(
expectedLength,
`there should be only ${expectedLength} child(ren) with a query of ${query}`
);

el.filter = createDefaultFilter(el);
await elementUpdated(el);

const noChildren = 0;
expect(el.children.length).to.equal(
noChildren,
`there should be ${noChildren} child(ren) with the provided custom filter & query of ${query}`
);
});
});
});
78 changes: 49 additions & 29 deletions packages/elements/src/tree/__demo__/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
</head>
<body>
<script type="module">
import '@refinitiv-ui/elements/panel';
import '@refinitiv-ui/elements/radio-button';
import '@refinitiv-ui/elements/tree';

import '@refinitiv-ui/demo-block';
Expand All @@ -30,6 +32,8 @@

import(`../../../../../node_modules/@refinitiv-ui/${theme}-theme/${variant}/css/native-elements.css`);
import(`../../../lib/tree/themes/${theme}/${variant}/index.js`);
import(`../../../lib/radio-button/themes/${theme}/${variant}/index.js`);
import(`../../../lib/panel/themes/${theme}/${variant}/index.js`);
</script>
<script type="module">
import { createTreeRenderer } from '@refinitiv-ui/elements/tree';
Expand Down Expand Up @@ -240,17 +244,46 @@
</demo-block>

<demo-block header="Filter Query" tags="filter, query" layout="normal">
<p>
Items could be filtered with case-insensitive partial match of both labels & values through query
input
</p>
<input id="filter-query-input" type="text" placeholder="Input query" />
<ef-tree multiple class="custom-data" id="filter-query-tree"></ef-tree>
<p>Switching between custom filter</p>
<ef-panel spacing>
Filter item with:
<span id="radio-group">
<ef-radio-button name="filter" id="label-only" checked>Label Only</ef-radio-button>
<ef-radio-button name="filter" id="label-value">Label & Value</ef-radio-button>
</span>
<br />
<label for="filter-query-input">Query</label>
<input id="filter-query-input" type="text" placeholder="Input query" />
</ef-panel>
<ef-tree multiple class="custom-data" id="filter-switching-tree"></ef-tree>
<script type="module">
import escapeStringRegexp from 'escape-string-regexp';

const tree = document.getElementById('filter-query-tree');
const data = [
const tree = document.getElementById('filter-switching-tree');
const radioGroup = document.getElementById('radio-group');

const createSwitchingFilter = (tree, labelValue = false) => {
let query = '';
let queryRegExp;
const getRegularExpressionOfQuery = () => {
if (tree.query !== query || !queryRegExp) {
query = tree.query || '';
queryRegExp = new RegExp(escapeStringRegexp(query), 'i');
}
return queryRegExp;
};
return (item, manager) => {
const treeNode = manager.getTreeNode(item);
const { label, value } = treeNode;
const regex = getRegularExpressionOfQuery();
let result = regex.test(label);
if (labelValue) {
result = result || regex.test(value);
}
return result;
};
};
tree.data = [
{
label: 'Group One',
value: '1',
Expand Down Expand Up @@ -332,27 +365,14 @@
}
];

const createCustomFilter = (tree) => {
let query = '';
let queryRegExp;
const getRegularExpressionOfQuery = () => {
if (tree.query !== query || !queryRegExp) {
query = tree.query || '';
queryRegExp = new RegExp(escapeStringRegexp(query), 'i');
}
return queryRegExp;
};
return (item, manager) => {
const treeNode = manager.getTreeNode(item);
const { label, value } = treeNode;
const regex = getRegularExpressionOfQuery();
const result = regex.test(value) || regex.test(label);
return result;
};
};

tree.data = data;
tree.filter = createCustomFilter(tree);
radioGroup.addEventListener(
'checked-changed',
(e) => {
const labelValue = e.target.id === 'label-value';
tree.filter = createSwitchingFilter(tree, labelValue);
},
{ capture: true }
);

document.getElementById('filter-query-input').addEventListener(
'keyup',
Expand Down
44 changes: 44 additions & 0 deletions packages/elements/src/tree/__test__/tree.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import '@refinitiv-ui/elements/tree';
import '@refinitiv-ui/elemental-theme/light/ef-tree';
import { aTimeout, elementUpdated, expect, fixture, nextFrame, oneEvent } from '@refinitiv-ui/test-helpers';

import { createDefaultFilter } from '../../../lib/tree/helpers/filter.js';
import {
deepNestedData,
firstFilterData,
Expand Down Expand Up @@ -660,6 +661,49 @@ describe('tree/Tree', function () {
);
});

it('should update filter result when filter is updated', async function () {
const el = await fixture('<ef-tree></ef-tree>');
el.data = firstFilterData;
await elementUpdated(el);

const createCustomFilter = (tree) => {
let query = '';
let queryRegExp;
const getRegularExpressionOfQuery = () => {
if (tree.query !== query || !queryRegExp) {
query = tree.query || '';
queryRegExp = new RegExp(escapeStringRegexp(query), 'i');
}
return queryRegExp;
};
return (item, manager) => {
const treeNode = manager.getTreeNode(item);
const { label, value } = treeNode;
const regex = getRegularExpressionOfQuery();
const result = regex.test(value) || regex.test(label);
return result;
};
};
el.filter = createCustomFilter(el);
el.query = '3';
await elementUpdated(el);

const firstChildrenCount = 7;
expect(el.children.length).to.equal(
firstChildrenCount,
`there should be ${firstChildrenCount} child(ren) with the provided custom filter & query`
);

el.filter = createDefaultFilter(el);
await elementUpdated(el);

const noChildren = 0;
expect(el.children.length).to.equal(
noChildren,
`there should be ${noChildren} child(ren) with the provided custom filter & query`
);
});

it('should be able to filter items based on updated label value', async function () {
const el = await fixture('<ef-tree></ef-tree>');
el.data = flatData;
Expand Down
2 changes: 1 addition & 1 deletion packages/elements/src/tree/elements/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ export class Tree<T extends TreeDataItem = TreeDataItem> extends List<T> {
this._manager.setMode(this.mode);
}

if (changeProperties.has('query') || changeProperties.has('data')) {
if (changeProperties.has('query') || changeProperties.has('data') || changeProperties.has('filter')) {
this.filterItems();
}
}
Expand Down

0 comments on commit 3640760

Please sign in to comment.