diff --git a/documents/src/pages/elements/combo-box.md b/documents/src/pages/elements/combo-box.md
index 3d7681ebb5..b70027682a 100644
--- a/documents/src/pages/elements/combo-box.md
+++ b/documents/src/pages/elements/combo-box.md
@@ -218,25 +218,29 @@ comboBox.data = [
{ label: 'Brazil', value: 'br' },
{ label: 'Argentina', value: 'ar' }
];
-const customFilter = (comboBox) => {
+const createCustomFilter = (comboBox) => {
let query = '';
let queryRegExp;
const getRegularExpressionOfQuery = () => {
if (comboBox.query !== query || !queryRegExp) {
query = comboBox.query || '';
+ // Non-word characters are escaped to prevent ReDoS attack.
+ // This serves as a demo only.
+ // For production, use a proven implementation instead.
queryRegExp = new RegExp(query.replace(/(\W)/g, '\\$1'), 'i');
}
return queryRegExp;
};
return (item) => {
+ const label = item.label;
+ const value = item.value;
const regex = getRegularExpressionOfQuery();
- const result = query === item.value || regex.test(item.label);
- regex.lastIndex = 0; // do not forget to reset last index
+ const result = regex.test(value) || regex.test(label);
return result;
};
};
-comboBox.filter = customFilter(comboBox);
+comboBox.filter = createCustomFilter(comboBox);
```
```css
.wrapper {
@@ -255,7 +259,7 @@ comboBox.filter = customFilter(comboBox);
const comboBox = document.querySelector('ef-combo-box');
// Make a scoped re-usable filter for performance
-const customFilter = (comboBox) => {
+const createCustomFilter = (comboBox) => {
let query = ''; // reference query string for validating queryRegExp cache state
let queryRegExp; // cache RegExp
@@ -265,6 +269,9 @@ const customFilter = (comboBox) => {
const getRegularExpressionOfQuery = () => {
if (comboBox.query !== query || !queryRegExp) {
query = comboBox.query || '';
+ // Non-word characters are escaped to prevent ReDoS attack.
+ // This serves as a demo only.
+ // For production, use a proven implementation instead.
queryRegExp = new RegExp(query.replace(/(\W)/g, '\\$1'), 'i');
}
return queryRegExp;
@@ -272,18 +279,18 @@ const customFilter = (comboBox) => {
// return scoped custom filter
return (item) => {
+ const label = item.label;
+ const value = item.value;
const regex = getRegularExpressionOfQuery();
- const result = query === item.value || regex.test(item.label);
- regex.lastIndex = 0; // do not forget to reset last index
+ const result = regex.test(value) || regex.test(label);
return result;
};
};
-comboBox.filter = customFilter(comboBox);
+comboBox.filter = createCustomFilter(comboBox);
```
-
-@> Regardless of filter configuration Combo Box always treats `type: 'header'` items as group headers, which persist as long as at least one item within the group is visible.
+@> Regardless of filter configuration, Combo Box always treats `type: 'header'` items as group headers, which persist as long as at least one item within the group is visible.
## Asynchronous filtering
@@ -345,6 +352,9 @@ comboBox.filter = null;
// A function to make request. In real life scenario it may wrap fetch
const request = (query, value) => {
+ // Non-word characters are escaped to prevent ReDoS attack.
+ // This serves as a demo only.
+ // For production, use a proven implementation instead.
const regex = new RegExp(query.replace(/(\W)/g, '\\$1'), 'i');
// Always keep a promise to let Combo Box know that the data is loading
@@ -360,13 +370,11 @@ const request = (query, value) => {
selected: true,
hidden: query ? !regex.test(item.label) : false
}));
- regex.lastIndex = 0;
continue;
}
if (query && regex.test(item.label)) {
filterData.push(item);
- regex.lastIndex = 0;
}
}
}
diff --git a/documents/src/pages/elements/tree-select.md b/documents/src/pages/elements/tree-select.md
index bb4070b906..f019c05632 100644
--- a/documents/src/pages/elements/tree-select.md
+++ b/documents/src/pages/elements/tree-select.md
@@ -300,12 +300,10 @@ setTimeout(() => { el.opened = true; }, 1000);
*> If the number of selected items is likely to be large, pills may not be a good choice for display or performance.
## Filtering
-Tree select has built in text filtering and selection editing.
-By clicking the `Selected` button, Tree Select allows the items to be filtered by selected state, and that subset to be operated on in isolation from the main item list.
-
-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.
+Tree Select has built in text filtering and selection editing. By clicking the `Selected` button, Tree Select allows the items to be filtered by selected state, and that subset to be operated on in isolation from the main item list.
+For custom filtering, Tree Select provides an identical interface as Combo Box. You provide a predicate function testing each item. Please consult the [Combo Box docs](./elements/combo-box#filtering) 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.
diff --git a/documents/src/pages/elements/tree.md b/documents/src/pages/elements/tree.md
index ec565d41f4..b407f2eb06 100644
--- a/documents/src/pages/elements/tree.md
+++ b/documents/src/pages/elements/tree.md
@@ -361,7 +361,122 @@ tree.addEventListener('value-changed', (event) => {
});
```
+## Filtering
+
+Filtering happens when `query` property or attribute is not empty. By Default, the filter is applied on the data `label` property. Developers may wish to do their own filtering by implementing the `filter` property. A typical example is to apply filter on multiple data properties e.g. `label` and `value`.
+
+::
+```javascript
+::import-elements::
+const tree = document.querySelector('ef-tree');
+tree.data = [
+ { label: 'EMEA', value: 'emea', expanded: true, items: [
+ { label: 'France', value: 'fr' },
+ { label: 'Russian Federation', value: 'ru' },
+ { label: 'Spain', value: 'es' },
+ { label: 'United Kingdom', value: 'gb' }
+ ]},
+ { label: 'APAC', value: 'apac', expanded: true, items: [
+ { label: 'China', value: 'ch' },
+ { label: 'Australia', value: 'au' },
+ { label: 'India', value: 'in' },
+ { label: 'Thailand', value: 'th' }
+ ]},
+ { label: 'AMERS', value: 'amers', expanded: true, items: [
+ { label: 'Canada', value: 'ca' },
+ { label: 'United States', value: 'us' },
+ { label: 'Brazil', value: 'br' },
+ { label: 'Argentina', value: 'ar' }
+ ]}
+];
+const createCustomFilter = (tree) => {
+ let query = '';
+ let queryRegExp;
+ const getRegularExpressionOfQuery = () => {
+ if (tree.query !== query || !queryRegExp) {
+ query = tree.query || '';
+ // Non-word characters are escaped to prevent ReDoS attack.
+ // This serves as a demo only.
+ // For production, use a proven implementation instead.
+ queryRegExp = new RegExp(query.replace(/(\W)/g, '\\$1'), 'i');
+ }
+ return queryRegExp;
+ };
+ return (item) => {
+ const label = item.label;
+ const value = item.value;
+ const regex = getRegularExpressionOfQuery();
+ const result = regex.test(value) || regex.test(label);
+ return result;
+ };
+};
+tree.filter = createCustomFilter(tree);
+
+const input = document.getElementById('query');
+input.addEventListener('value-changed', e => {
+ tree.query = e.detail.value;
+});
+```
+```css
+.wrapper {
+ padding: 5px;
+ width: 300px;
+ height: 430px;
+}
+
+#query {
+ width: 200px;
+}
+```
+```html
+
+
+
+
+
+
+```
+::
+
+```javascript
+const tree = document.querySelector('ef-tree');
+
+// Make a scoped re-usable filter for performance
+const createCustomFilter = (tree) => {
+ let query = ''; // reference query string for validating queryRegExp cache state
+ let queryRegExp; // cache RegExp
+
+ // Get current RegExp, or renew if out of date
+ // this is fetched on demand by filter/renderer
+ // only created once per query
+ const getRegularExpressionOfQuery = () => {
+ if (tree.query !== query || !queryRegExp) {
+ query = tree.query || '';
+ // Non-word characters are escaped to prevent ReDoS attack.
+ // This serves as a demo only.
+ // For production, use a proven implementation instead.
+ queryRegExp = new RegExp(query.replace(/(\W)/g, '\\$1'), 'i');
+ }
+ return queryRegExp;
+ };
+
+ // return scoped custom filter
+ return (item) => {
+ const label = item.label;
+ const value = item.value;
+ const regex = getRegularExpressionOfQuery();
+ const result = regex.test(value) || regex.test(label);
+ return result;
+ };
+};
+
+tree.filter = createCustomFilter(tree);
+```
+
+@> Regardless of filter configuration, Tree always shows parent items as long as at least one of their child is visible.
+
## Accessibility
+
::a11y-intro::
`ef-tree` is assigned `role="tree"` and can include properties such as `aria-multiselectable`, `aria-label`, or `aria-labelledby`. It receives focus once at host and it is navigable through items using `Up` and `Down` arrow keys and expandable or collapsable using `Left` and `Right`. Each item is assigned `role="treeitem"` and can include properties such as `aria-selected` or `aria-checked` in `multiple` mode.
diff --git a/packages/elements/src/combo-box/__demo__/index.html b/packages/elements/src/combo-box/__demo__/index.html
index be8c51abd0..7e25b695f1 100644
--- a/packages/elements/src/combo-box/__demo__/index.html
+++ b/packages/elements/src/combo-box/__demo__/index.html
@@ -156,7 +156,6 @@
const regex = getRegularExpressionOfQuery();
// test on label or value
const result = query === item.value || regex.test(item.label);
- regex.lastIndex = 0; // do not forget to reset last index
return result;
};
};
@@ -297,13 +296,11 @@
hidden: query ? !regex.test(item.label) : false
})
);
- regex.lastIndex = 0;
continue;
}
if (query && regex.test(item.label)) {
filterData.push(item);
- regex.lastIndex = 0;
}
}
}
diff --git a/packages/elements/src/combo-box/__snapshots__/Filter.md b/packages/elements/src/combo-box/__snapshots__/Filter.md
index bcc36131df..ccc5941d5e 100644
--- a/packages/elements/src/combo-box/__snapshots__/Filter.md
+++ b/packages/elements/src/combo-box/__snapshots__/Filter.md
@@ -67,9 +67,69 @@
+```
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
```
+#### `Should be able to use custom filter function`
+
```html
');
+ 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);
+ const textInput = 'ax';
+ await setInputEl(el, textInput);
+ await elementUpdated(el);
+ expect(el.query).to.equal(textInput, `Query should be the same as input text: "${textInput}"`);
+ await expect(el).shadowDom.to.equalSnapshot(snapshotIgnore);
+ });
});
});
diff --git a/packages/elements/src/combo-box/helpers/filter.ts b/packages/elements/src/combo-box/helpers/filter.ts
index 32a1fa4352..ce0061dd93 100644
--- a/packages/elements/src/combo-box/helpers/filter.ts
+++ b/packages/elements/src/combo-box/helpers/filter.ts
@@ -11,7 +11,7 @@ import type { ComboBoxFilter } from './types';
* @param el ComboBox instance to filter
* @returns Filter accepting an item
*/
-export const defaultFilter = (el: ComboBox): ComboBoxFilter => {
+export const createDefaultFilter = (el: ComboBox): ComboBoxFilter => {
// reference query string for validating queryRegExp cache state
let query = '';
// cache RegExp
@@ -32,8 +32,6 @@ export const defaultFilter = (el: ComboBox): C
return (item): boolean => {
const regex = getRegularExpressionOfQuery();
const result = regex.test((item as unknown as ItemText).label);
- // this example uses global scope, so the index needs resetting
- regex.lastIndex = 0;
return result;
};
};
diff --git a/packages/elements/src/combo-box/index.ts b/packages/elements/src/combo-box/index.ts
index 8b85a12d39..820a138a65 100644
--- a/packages/elements/src/combo-box/index.ts
+++ b/packages/elements/src/combo-box/index.ts
@@ -33,7 +33,7 @@ import '../list/index.js';
import '../overlay/index.js';
import { registerOverflowTooltip } from '../tooltip/index.js';
import { VERSION } from '../version.js';
-import { defaultFilter } from './helpers/filter.js';
+import { createDefaultFilter } from './helpers/filter.js';
import { CustomKeyboardEvent } from './helpers/keyboard-event.js';
import { ComboBoxRenderer, createComboBoxRenderer } from './helpers/renderer.js';
import type { ComboBoxData, ComboBoxFilter } from './helpers/types';
@@ -114,13 +114,13 @@ export class ComboBox extends FormFieldElement {
* Set this to null when data is filtered externally, eg XHR
* @type {ComboBoxFilter | null}
*/
- @property({ type: Function, attribute: false })
- public filter: ComboBoxFilter | null = defaultFilter(this);
+ @property({ attribute: false })
+ public filter: ComboBoxFilter | null = createDefaultFilter(this);
/**
* Renderer used to render list item elements
*/
- @property({ type: Function, attribute: false })
+ @property({ attribute: false })
public renderer = createComboBoxRenderer(this);
private _multiple = false;
diff --git a/packages/elements/src/list/elements/list.ts b/packages/elements/src/list/elements/list.ts
index b5155f7110..c355224ed7 100644
--- a/packages/elements/src/list/elements/list.ts
+++ b/packages/elements/src/list/elements/list.ts
@@ -86,7 +86,7 @@ export class List extends ControlElement {
/**
* Renderer used to render list item elements
*/
- @property({ type: Function, attribute: false })
+ @property({ attribute: false })
public renderer = createListRenderer(this);
/**
diff --git a/packages/elements/src/swing-gauge/index.ts b/packages/elements/src/swing-gauge/index.ts
index 08670a89e0..661fdded65 100644
--- a/packages/elements/src/swing-gauge/index.ts
+++ b/packages/elements/src/swing-gauge/index.ts
@@ -196,7 +196,7 @@ export class SwingGauge extends ResponsiveElement {
* Custom value formatter
* @type {SwingGaugeValueFormatter}
*/
- @property({ type: Function, attribute: false })
+ @property({ attribute: false })
public valueFormatter: SwingGaugeValueFormatter = this.defaultValueFormatter;
/**
diff --git a/packages/elements/src/tooltip/index.ts b/packages/elements/src/tooltip/index.ts
index fb07d1337a..7a08bfebb1 100644
--- a/packages/elements/src/tooltip/index.ts
+++ b/packages/elements/src/tooltip/index.ts
@@ -123,7 +123,7 @@ class Tooltip extends BasicElement {
* Return `true` if the target matches
* @type {TooltipCondition}
*/
- @property({ type: Function, attribute: false })
+ @property({ attribute: false })
public condition: TooltipCondition | undefined;
/**
@@ -132,7 +132,7 @@ class Tooltip extends BasicElement {
* If the content is not present, tooltip will not be displayed
* @type {TooltipRenderer}
*/
- @property({ type: Function, attribute: false })
+ @property({ attribute: false })
public renderer: TooltipRenderer | undefined;
/**
diff --git a/packages/elements/src/tree-select/__demo__/countries.js b/packages/elements/src/tree-select/__demo__/countries.js
index 1a5841bb27..bf1995dca1 100644
--- a/packages/elements/src/tree-select/__demo__/countries.js
+++ b/packages/elements/src/tree-select/__demo__/countries.js
@@ -1,5 +1,5 @@
/* eslint-disable */
-const treeCollection = [
+export const countries = [
{
value: 'AFR',
label: 'Africa',
diff --git a/packages/elements/src/tree-select/__demo__/index.html b/packages/elements/src/tree-select/__demo__/index.html
index 7ce3b38a84..ad1378c5bc 100644
--- a/packages/elements/src/tree-select/__demo__/index.html
+++ b/packages/elements/src/tree-select/__demo__/index.html
@@ -4,8 +4,6 @@
Tree Select
-
-
@@ -20,6 +18,8 @@
import '@refinitiv-ui/phrasebook/locale/zh/tree-select.js';
+
+
+
+ Items could be filtered with case-insensitive partial match of both labels & values.
+ There is a debounce rate of 500ms applied.
+