From 7376c115f441c290909b9151fba1561984166639 Mon Sep 17 00:00:00 2001 From: rihansiddhi Date: Tue, 14 Nov 2023 11:23:24 +0530 Subject: [PATCH 1/7] feat(fw-list-options): virtualise list options --- packages/crayons-core/package.json | 1 + packages/crayons-core/src/components.d.ts | 28 +++ .../components/options-list/list-options.tsx | 207 +++++++++++++++--- .../src/components/options-list/readme.md | 63 +++--- .../src/components/select/readme.md | 83 +++---- .../src/components/select/select.tsx | 7 + .../src/utils/stencil-virtual-scroll.ts | 129 +++++++++++ 7 files changed, 422 insertions(+), 96 deletions(-) create mode 100644 packages/crayons-core/src/utils/stencil-virtual-scroll.ts diff --git a/packages/crayons-core/package.json b/packages/crayons-core/package.json index f16170676..9070711b2 100644 --- a/packages/crayons-core/package.json +++ b/packages/crayons-core/package.json @@ -107,6 +107,7 @@ "@freshworks/crayons-i18n": "^4.2.0", "@popperjs/core": "^2.10.2", "@stencil/core": "2.17.4", + "@tanstack/virtual-core": "^3.0.0-beta.68", "date-fns": "^2.28.0", "libphonenumber-js": "^1.10.8", "multi-nprogress": "0.3.5" diff --git a/packages/crayons-core/src/components.d.ts b/packages/crayons-core/src/components.d.ts index ea791b4f7..d3815243c 100644 --- a/packages/crayons-core/src/components.d.ts +++ b/packages/crayons-core/src/components.d.ts @@ -1230,6 +1230,10 @@ export namespace Components { * Disables the component on the interface. If the attribute’s value is undefined, the value is set to false. */ "disabled": boolean; + /** + * Virtualize long list of elements in list options *Experimental* + */ + "enableVirtualScroll": boolean; /** * The text to filter the options. */ @@ -1247,6 +1251,10 @@ export namespace Components { * Allows user to create the option if the provided input doesn't match with any of the options. */ "isCreatable": boolean; + /** + * Is the popover in open state + */ + "isPopoverOpen": boolean; /** * Works with `multiple` enabled. Configures the maximum number of options that can be selected with a multi-select component. */ @@ -1310,6 +1318,10 @@ export namespace Components { * Standard is the default option without any graphics other options are icon and avatar which places either the icon or avatar at the beginning of the row. The props for the icon or avatar are passed as an object via the graphicsProps. */ "variant": DropdownVariant; + /** + * WorkAround for wait until next render in stenciljs https://github.com/ionic-team/stencil/issues/2744 + */ + "waitForNextRender": () => Promise; } interface FwMenu { } @@ -1692,6 +1704,10 @@ export namespace Components { * Disables the component on the interface. If the attribute’s value is undefined, the value is set to false. */ "disabled": boolean; + /** + * Virtualize long list of elements in list options *Experimental* + */ + "enableVirtualScroll": boolean; /** * Error text displayed below the text box. */ @@ -4178,6 +4194,10 @@ declare namespace LocalJSX { * Disables the component on the interface. If the attribute’s value is undefined, the value is set to false. */ "disabled"?: boolean; + /** + * Virtualize long list of elements in list options *Experimental* + */ + "enableVirtualScroll"?: boolean; /** * The text to filter the options. */ @@ -4194,6 +4214,10 @@ declare namespace LocalJSX { * Allows user to create the option if the provided input doesn't match with any of the options. */ "isCreatable"?: boolean; + /** + * Is the popover in open state + */ + "isPopoverOpen"?: boolean; /** * Works with `multiple` enabled. Configures the maximum number of options that can be selected with a multi-select component. */ @@ -4644,6 +4668,10 @@ declare namespace LocalJSX { * Disables the component on the interface. If the attribute’s value is undefined, the value is set to false. */ "disabled"?: boolean; + /** + * Virtualize long list of elements in list options *Experimental* + */ + "enableVirtualScroll"?: boolean; /** * Error text displayed below the text box. */ diff --git a/packages/crayons-core/src/components/options-list/list-options.tsx b/packages/crayons-core/src/components/options-list/list-options.tsx index f2c21bcbd..a67d727d4 100644 --- a/packages/crayons-core/src/components/options-list/list-options.tsx +++ b/packages/crayons-core/src/components/options-list/list-options.tsx @@ -48,11 +48,24 @@ export class ListOptions { resolve(filteredValue); }); }; + // private filteredFilesRef = null; + private scrollVirtualizer = null; + private scrollVirtualizerCleanup = null; + private renderPromiseResolve = null; @State() filteredOptions = []; @State() selectOptions = []; @State() selectedOptionsState = []; @State() isLoading = false; + /** + * State to trigger rerender. + * + * Necessary for virtual-scroll to initiate + * As virtual scroll is created from inside the lifecycle event (we need reference container) + * but stencil expects store to be imported before render, thus necessary to trigger rerender + * to subscribe to changes in scroller + */ + @State() tick = {}; /** * Value corresponding to the option, that is saved when the form data is saved. @@ -158,6 +171,17 @@ export class ListOptions { * Key for determining the value for a given option */ @Prop() optionValuePath = 'value'; + + /** + * Is the popover in open state + */ + @Prop() isPopoverOpen = false; + + /** + * Virtualize long list of elements in list options *Experimental* + */ + @Prop() enableVirtualScroll = false; + /** * Triggered when a value is selected or deselected from the list box options. */ @@ -167,6 +191,32 @@ export class ListOptions { */ @Event({ cancelable: true }) fwLoading: EventEmitter; + /** + * componentDidRender lifecycle event + */ + componentDidRender() { + if (this.renderPromiseResolve) { + this.renderPromiseResolve(); + this.renderPromiseResolve = null; + } + } + + /** + * WorkAround for wait until next render in stenciljs + * https://github.com/ionic-team/stencil/issues/2744 + */ + waitForNextRender() { + return new Promise((resolve) => (this.renderPromiseResolve = resolve)); + } + + connectedCallback() { + this.waitForNextRender().then(() => this.initScroller()); + } + + disconnectedCallback() { + this.scrollVirtualizerCleanup?.(); + } + @Listen('fwSelected') fwSelectedHandler(selectedItem) { const { value, selected } = selectedItem.detail; @@ -338,6 +388,49 @@ export class ListOptions { this.setSelectedOptions(newValue); } + @Watch('isPopoverOpen') + isPopoverOpenWatcher(): void { + if (this.isPopoverOpen) { + this.initScroller(); + } else { + this.scrollVirtualizerCleanup(); + } + } + + async initScroller() { + const scrollElement = this.getScrollElement(); + if (this.filteredOptions?.length && scrollElement) { + const rect = scrollElement.getBoundingClientRect(); + if (rect.height) { + const options: any = { + count: this.filteredOptions.length, + getScrollElement: () => { + return scrollElement; + }, + estimateSize: () => 35, + }; + if (this.scrollVirtualizer) { + this.scrollVirtualizerCleanup(); + } + const createVirtualizer = await ( + await import('../../utils/stencil-virtual-scroll') + ).createVirtualizer; + const virtualScroll = createVirtualizer(options); + this.scrollVirtualizer = virtualScroll.virtualizer; + this.scrollVirtualizerCleanup = () => { + virtualScroll.cleanup(); + this.scrollVirtualizer = null; + this.scrollVirtualizerCleanup = null; + }; + this.tick = {}; + } else { + this.scrollVirtualizer = null; + } + } else { + this.scrollVirtualizer = null; + } + } + valueExists() { return this.multiple ? this.value.length > 0 : !!this.value; } @@ -505,36 +598,78 @@ export class ListOptions { this.filteredOptions = this.selectOptions; } + renderVirtualList(options: Array): JSX.Element { + const scrollElement = this.getScrollElement(); + const virtualItems = this.scrollVirtualizer?.getVirtualItems(); + return ( + scrollElement && + this.scrollVirtualizer && ( +
+
+ {virtualItems.map((virtualItem) => { + const option = options[virtualItem.index]; + return ( +
+ {this.renderSelectOption(option)} +
+ ); + })} +
+
+ ) + ); + } + + renderSelectOption(option) { + const isDisabled = + this.selectedOptionsState?.find( + (selected) => + selected[this.optionValuePath] === option[this.optionValuePath] + )?.disabled || + option.disabled || + (!this.allowDeselect && option.selected) || + (this.multiple && !option.selected && this.value?.length >= this.max); + const isDefaultOption = [ + this.noDataText, + TranslationController.t('search.noDataAvailable'), + this.notFoundText, + TranslationController.t('search.noItemsFound'), + ].includes(option[this.optionLabelPath]); + const checkbox = !isDefaultOption && (this.checkbox || option.checkbox); + return ( + + ); + } + renderSelectOptions(options: Array) { - return options.map((option) => { - const isDisabled = - this.selectedOptionsState?.find( - (selected) => - selected[this.optionValuePath] === option[this.optionValuePath] - )?.disabled || - option.disabled || - (!this.allowDeselect && option.selected) || - (this.multiple && !option.selected && this.value?.length >= this.max); - const isDefaultOption = [ - this.noDataText, - TranslationController.t('search.noDataAvailable'), - this.notFoundText, - TranslationController.t('search.noItemsFound'), - ].includes(option[this.optionLabelPath]); - const checkbox = !isDefaultOption && (this.checkbox || option.checkbox); - return ( - - ); - }); + return options.map((option) => this.renderSelectOption(option)); } renderSearchInput() { @@ -570,6 +705,16 @@ export class ListOptions { this.setDataSource(this.options); } + getScrollElement() { + const parent = this.host?.parentElement; + if (parent && parent.tagName === 'FW-POPOVER') { + const content = parent.shadowRoot?.querySelector('.popper-content'); + return content; + } else { + return parent; + } + } + render() { return (
{this.searchable && this.renderSearchInput()} - {this.renderSelectOptions(this.filteredOptions)} + {this.enableVirtualScroll + ? this.renderVirtualList(this.filteredOptions) + : this.renderSelectOptions(this.filteredOptions)}
); } diff --git a/packages/crayons-core/src/components/options-list/readme.md b/packages/crayons-core/src/components/options-list/readme.md index 6bb6115f5..65fd92dc6 100644 --- a/packages/crayons-core/src/components/options-list/readme.md +++ b/packages/crayons-core/src/components/options-list/readme.md @@ -326,31 +326,33 @@ The data-source and the visual variant for the list options can be altered via t ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------- | -| `allowDeselect` | `allow-deselect` | Whether clicking on the already selected option disables it. | `boolean` | `true` | -| `allowSelect` | `allow-select` | Whether clicking on option selects it. | `boolean` | `true` | -| `checkbox` | `checkbox` | Place a checkbox. | `boolean` | `false` | -| `debounceTimer` | `debounce-timer` | Debounce timer for the search promise function. | `number` | `300` | -| `disabled` | `disabled` | Disables the component on the interface. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | -| `filterText` | `filter-text` | The text to filter the options. | `any` | `undefined` | -| `formatCreateLabel` | -- | Works only when 'isCreatable' is selected. Function to format the create label displayed as an option. | `(value: string) => string` | `undefined` | -| `hideTick` | `hide-tick` | hide tick mark icon on select option | `boolean` | `false` | -| `isCreatable` | `is-creatable` | Allows user to create the option if the provided input doesn't match with any of the options. | `boolean` | `false` | -| `max` | `max` | Works with `multiple` enabled. Configures the maximum number of options that can be selected with a multi-select component. | `number` | `Number.MAX_VALUE` | -| `multiple` | `multiple` | Enables selection of multiple options. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | -| `noDataText` | `no-data-text` | Text to be displayed when there is no data available in the select. | `string` | `''` | -| `notFoundText` | `not-found-text` | Default option to be shown if the option doesn't match the filterText. | `string` | `''` | -| `optionLabelPath` | `option-label-path` | Key for determining the label for a given option | `string` | `'text'` | -| `optionValuePath` | `option-value-path` | Key for determining the value for a given option | `string` | `'value'` | -| `options` | -- | Value corresponding to the option, that is saved when the form data is saved. | `any[]` | `[]` | -| `search` | -- | Filter function which takes in filterText and dataSource and return a Promise. Where filter text is the text to filter the value in dataSource array. The returned promise should contain the array of options to be displayed. | `(text: string, dataSource: any[]) => Promise` | `this.defaultSearchFunction` | -| `searchText` | `search-text` | Placeholder to placed on the search text box. | `string` | `''` | -| `searchable` | `searchable` | Enables the input with in the popup for filtering the options. | `boolean` | `false` | -| `selectedOptions` | -- | The option that is displayed as the default selection, in the list box. Must be a valid value corresponding to the fw-select-option components used in Select. | `any[]` | `[]` | -| `validateNewOption` | -- | Works only when 'isCreatable' is selected. Function to validate the newly created value. Should return true if new option is valid or false if invalid. | `(value: string) => boolean` | `undefined` | -| `value` | `value` | Value of the option that is displayed as the default selection, in the list box. Must be a valid value corresponding to the fw-select-option components used in Select. | `any` | `''` | -| `variant` | `variant` | Standard is the default option without any graphics other options are icon and avatar which places either the icon or avatar at the beginning of the row. The props for the icon or avatar are passed as an object via the graphicsProps. | `"avatar" \| "icon" \| "standard"` | `'standard'` | +| Property | Attribute | Description | Type | Default | +| --------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------- | +| `allowDeselect` | `allow-deselect` | Whether clicking on the already selected option disables it. | `boolean` | `true` | +| `allowSelect` | `allow-select` | Whether clicking on option selects it. | `boolean` | `true` | +| `checkbox` | `checkbox` | Place a checkbox. | `boolean` | `false` | +| `debounceTimer` | `debounce-timer` | Debounce timer for the search promise function. | `number` | `300` | +| `disabled` | `disabled` | Disables the component on the interface. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | +| `enableVirtualScroll` | `enable-virtual-scroll` | Virtualize long list of elements in list options *Experimental* | `boolean` | `true` | +| `filterText` | `filter-text` | The text to filter the options. | `any` | `undefined` | +| `formatCreateLabel` | -- | Works only when 'isCreatable' is selected. Function to format the create label displayed as an option. | `(value: string) => string` | `undefined` | +| `hideTick` | `hide-tick` | hide tick mark icon on select option | `boolean` | `false` | +| `isCreatable` | `is-creatable` | Allows user to create the option if the provided input doesn't match with any of the options. | `boolean` | `false` | +| `isPopoverOpen` | `is-popover-open` | Is the popover in open state | `boolean` | `false` | +| `max` | `max` | Works with `multiple` enabled. Configures the maximum number of options that can be selected with a multi-select component. | `number` | `Number.MAX_VALUE` | +| `multiple` | `multiple` | Enables selection of multiple options. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | +| `noDataText` | `no-data-text` | Text to be displayed when there is no data available in the select. | `string` | `''` | +| `notFoundText` | `not-found-text` | Default option to be shown if the option doesn't match the filterText. | `string` | `''` | +| `optionLabelPath` | `option-label-path` | Key for determining the label for a given option | `string` | `'text'` | +| `optionValuePath` | `option-value-path` | Key for determining the value for a given option | `string` | `'value'` | +| `options` | -- | Value corresponding to the option, that is saved when the form data is saved. | `any[]` | `[]` | +| `search` | -- | Filter function which takes in filterText and dataSource and return a Promise. Where filter text is the text to filter the value in dataSource array. The returned promise should contain the array of options to be displayed. | `(text: string, dataSource: any[]) => Promise` | `this.defaultSearchFunction` | +| `searchText` | `search-text` | Placeholder to placed on the search text box. | `string` | `''` | +| `searchable` | `searchable` | Enables the input with in the popup for filtering the options. | `boolean` | `false` | +| `selectedOptions` | -- | The option that is displayed as the default selection, in the list box. Must be a valid value corresponding to the fw-select-option components used in Select. | `any[]` | `[]` | +| `validateNewOption` | -- | Works only when 'isCreatable' is selected. Function to validate the newly created value. Should return true if new option is valid or false if invalid. | `(value: string) => boolean` | `undefined` | +| `value` | `value` | Value of the option that is displayed as the default selection, in the list box. Must be a valid value corresponding to the fw-select-option components used in Select. | `any` | `''` | +| `variant` | `variant` | Standard is the default option without any graphics other options are icon and avatar which places either the icon or avatar at the beginning of the row. The props for the icon or avatar are passed as an object via the graphicsProps. | `"avatar" \| "icon" \| "standard"` | `'standard'` | ## Events @@ -423,6 +425,17 @@ Type: `Promise` +### `waitForNextRender() => Promise` + +WorkAround for wait until next render in stenciljs +https://github.com/ionic-team/stencil/issues/2744 + +#### Returns + +Type: `Promise` + + + ## Dependencies diff --git a/packages/crayons-core/src/components/select/readme.md b/packages/crayons-core/src/components/select/readme.md index a363dc2ab..60a4a046c 100644 --- a/packages/crayons-core/src/components/select/readme.md +++ b/packages/crayons-core/src/components/select/readme.md @@ -1867,47 +1867,48 @@ Refer the [css variables](#css-custom-properties) for modifying the appearance o ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| `allowDeselect` | `allow-deselect` | Whether clicking on the already selected option disables it. | `boolean` | `true` | -| `boundary` | -- | Describes the select's boundary HTMLElement | `HTMLElement` | `undefined` | -| `caret` | `caret` | Whether the arrow/caret should be shown in the select. | `boolean` | `true` | -| `checkbox` | `checkbox` | Place a checkbox. | `boolean` | `false` | -| `creatableProps` | -- | Props to be passed for creatable select isCreatable: boolean - If true, select accepts user input that are not present as options and add them as options validateNewOption: (value) => boolean - If passed, this function will determine the error state for every new option entered. If return value is true, error state of the newly created option will be false and if return value is false, then the error state of the newly created option will be true. formatCreateLabel: (label) => string - Gets the label for the "create new ..." option in the menu. Current input value is provided as argument. | `{ isCreatable: boolean; validateNewOption: (_value: any) => boolean; formatCreateLabel: (label: any) => string; }` | `{ isCreatable: false, validateNewOption: (_value): boolean => true, formatCreateLabel: (label): string => label, }` | -| `debounceTimer` | `debounce-timer` | Debounce timer for the search promise function. | `number` | `300` | -| `disabled` | `disabled` | Disables the component on the interface. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | -| `errorText` | `error-text` | Error text displayed below the text box. | `string` | `''` | -| `fallbackPlacements` | -- | Alternative placement for popover if the default placement is not possible. | `[PopoverPlacementType]` | `['top']` | -| `forceSelect` | `force-select` | If true, the user must select a value. The default value is not displayed. | `boolean` | `true` | -| `hintText` | `hint-text` | Hint text displayed below the text box. | `string` | `''` | -| `hoist` | `hoist` | Option to prevent the select options from being clipped when the component is placed inside a container with `overflow: auto\|hidden\|scroll`. | `boolean` | `false` | -| `label` | `label` | Label displayed on the interface, for the component. | `string` | `''` | -| `labelledBy` | `labelled-by` | If the default label prop is not used, then use this prop to pass the id of the label. | `string` | `''` | -| `max` | `max` | Works with `multiple` enabled. Configures the maximum number of options that can be selected with a multi-select component. | `number` | `Number.MAX_VALUE` | -| `maxHeight` | `max-height` | Sets the max height of select with multiple options selected and displays a scroll when maxHeight value is exceeded | `string` | `'none'` | -| `multiple` | `multiple` | Enables selection of multiple options. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | -| `name` | `name` | Name of the component, saved as part of form data. | `string` | `''` | -| `noDataText` | `no-data-text` | Text to be displayed when there is no data available in the select. | `string` | `''` | -| `notFoundText` | `not-found-text` | Default option to be shown if the option doesn't match the filterText. | `string` | `''` | -| `optionLabelPath` | `option-label-path` | Key for determining the label for a given option | `string` | `'text'` | -| `optionValuePath` | `option-value-path` | Key for determining the value for a given option | `string` | `'value'` | -| `options` | `options` | The data for the select component, the options will be of type array of fw-select-options. | `any` | `undefined` | -| `optionsPlacement` | `options-placement` | Placement of the options list with respect to select. | `"bottom" \| "bottom-end" \| "bottom-start" \| "left" \| "left-end" \| "left-start" \| "right" \| "right-end" \| "right-start" \| "top" \| "top-end" \| "top-start"` | `'bottom'` | -| `optionsVariant` | `options-variant` | Standard is the default option without any graphics other options are icon and avatar which places either the icon or avatar at the beginning of the row. The props for the icon or avatar are passed as an object via the graphicsProps. | `"avatar" \| "icon" \| "standard"` | `'standard'` | -| `placeholder` | `placeholder` | Text displayed in the list box before an option is selected. | `string` | `undefined` | -| `readonly` | `readonly` | If true, the user cannot modify the default value selected. If the attribute's value is undefined, the value is set to true. | `boolean` | `false` | -| `required` | `required` | Specifies the select field as a mandatory field and displays an asterisk next to the label. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | -| `sameWidth` | `same-width` | Whether the select width to be same as that of the options. | `boolean` | `true` | -| `search` | `search` | Filter function which takes in filterText and dataSource and return a Promise. Where filter text is the text to filter the value in dataSource array. The returned promise should contain the array of options to be displayed. | `any` | `undefined` | -| `searchable` | `searchable` | Allow to search for value. Default is true. | `boolean` | `true` | -| `selectedOptions` | -- | Array of the options that is displayed as the default selection, in the list box. Must be a valid option corresponding to the fw-select-option components used in Select. | `any[]` | `[]` | -| `state` | `state` | Theme based on which the list box is styled. | `"error" \| "normal" \| "warning"` | `'normal'` | -| `tagProps` | -- | Props to be passed for fw-tag components displayed in multi-select. | `{}` | `{}` | -| `tagVariant` | `tag-variant` | The variant of tag to be used. | `"avatar" \| "standard"` | `'standard'` | -| `type` | `type` | Type of option accepted as the input value. If a user tries to enter an option other than the specified type, the list is not populated. | `"number" \| "text"` | `'text'` | -| `value` | `value` | Value of the option that is displayed as the default selection, in the list box. Must be a valid value corresponding to the fw-select-option components used in Select. | `any` | `undefined` | -| `variant` | `variant` | The UI variant of the select to be used. | `"button" \| "mail" \| "search" \| "standard"` | `'standard'` | -| `warningText` | `warning-text` | Warning text displayed below the text box. | `string` | `''` | +| Property | Attribute | Description | Type | Default | +| --------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `allowDeselect` | `allow-deselect` | Whether clicking on the already selected option disables it. | `boolean` | `true` | +| `boundary` | -- | Describes the select's boundary HTMLElement | `HTMLElement` | `undefined` | +| `caret` | `caret` | Whether the arrow/caret should be shown in the select. | `boolean` | `true` | +| `checkbox` | `checkbox` | Place a checkbox. | `boolean` | `false` | +| `creatableProps` | -- | Props to be passed for creatable select isCreatable: boolean - If true, select accepts user input that are not present as options and add them as options validateNewOption: (value) => boolean - If passed, this function will determine the error state for every new option entered. If return value is true, error state of the newly created option will be false and if return value is false, then the error state of the newly created option will be true. formatCreateLabel: (label) => string - Gets the label for the "create new ..." option in the menu. Current input value is provided as argument. | `{ isCreatable: boolean; validateNewOption: (_value: any) => boolean; formatCreateLabel: (label: any) => string; }` | `{ isCreatable: false, validateNewOption: (_value): boolean => true, formatCreateLabel: (label): string => label, }` | +| `debounceTimer` | `debounce-timer` | Debounce timer for the search promise function. | `number` | `300` | +| `disabled` | `disabled` | Disables the component on the interface. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | +| `enableVirtualScroll` | `enable-virtual-scroll` | Virtualize long list of elements in list options *Experimental* | `boolean` | `true` | +| `errorText` | `error-text` | Error text displayed below the text box. | `string` | `''` | +| `fallbackPlacements` | -- | Alternative placement for popover if the default placement is not possible. | `[PopoverPlacementType]` | `['top']` | +| `forceSelect` | `force-select` | If true, the user must select a value. The default value is not displayed. | `boolean` | `true` | +| `hintText` | `hint-text` | Hint text displayed below the text box. | `string` | `''` | +| `hoist` | `hoist` | Option to prevent the select options from being clipped when the component is placed inside a container with `overflow: auto\|hidden\|scroll`. | `boolean` | `false` | +| `label` | `label` | Label displayed on the interface, for the component. | `string` | `''` | +| `labelledBy` | `labelled-by` | If the default label prop is not used, then use this prop to pass the id of the label. | `string` | `''` | +| `max` | `max` | Works with `multiple` enabled. Configures the maximum number of options that can be selected with a multi-select component. | `number` | `Number.MAX_VALUE` | +| `maxHeight` | `max-height` | Sets the max height of select with multiple options selected and displays a scroll when maxHeight value is exceeded | `string` | `'none'` | +| `multiple` | `multiple` | Enables selection of multiple options. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | +| `name` | `name` | Name of the component, saved as part of form data. | `string` | `''` | +| `noDataText` | `no-data-text` | Text to be displayed when there is no data available in the select. | `string` | `''` | +| `notFoundText` | `not-found-text` | Default option to be shown if the option doesn't match the filterText. | `string` | `''` | +| `optionLabelPath` | `option-label-path` | Key for determining the label for a given option | `string` | `'text'` | +| `optionValuePath` | `option-value-path` | Key for determining the value for a given option | `string` | `'value'` | +| `options` | `options` | The data for the select component, the options will be of type array of fw-select-options. | `any` | `undefined` | +| `optionsPlacement` | `options-placement` | Placement of the options list with respect to select. | `"bottom" \| "bottom-end" \| "bottom-start" \| "left" \| "left-end" \| "left-start" \| "right" \| "right-end" \| "right-start" \| "top" \| "top-end" \| "top-start"` | `'bottom'` | +| `optionsVariant` | `options-variant` | Standard is the default option without any graphics other options are icon and avatar which places either the icon or avatar at the beginning of the row. The props for the icon or avatar are passed as an object via the graphicsProps. | `"avatar" \| "icon" \| "standard"` | `'standard'` | +| `placeholder` | `placeholder` | Text displayed in the list box before an option is selected. | `string` | `undefined` | +| `readonly` | `readonly` | If true, the user cannot modify the default value selected. If the attribute's value is undefined, the value is set to true. | `boolean` | `false` | +| `required` | `required` | Specifies the select field as a mandatory field and displays an asterisk next to the label. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | +| `sameWidth` | `same-width` | Whether the select width to be same as that of the options. | `boolean` | `true` | +| `search` | `search` | Filter function which takes in filterText and dataSource and return a Promise. Where filter text is the text to filter the value in dataSource array. The returned promise should contain the array of options to be displayed. | `any` | `undefined` | +| `searchable` | `searchable` | Allow to search for value. Default is true. | `boolean` | `true` | +| `selectedOptions` | -- | Array of the options that is displayed as the default selection, in the list box. Must be a valid option corresponding to the fw-select-option components used in Select. | `any[]` | `[]` | +| `state` | `state` | Theme based on which the list box is styled. | `"error" \| "normal" \| "warning"` | `'normal'` | +| `tagProps` | -- | Props to be passed for fw-tag components displayed in multi-select. | `{}` | `{}` | +| `tagVariant` | `tag-variant` | The variant of tag to be used. | `"avatar" \| "standard"` | `'standard'` | +| `type` | `type` | Type of option accepted as the input value. If a user tries to enter an option other than the specified type, the list is not populated. | `"number" \| "text"` | `'text'` | +| `value` | `value` | Value of the option that is displayed as the default selection, in the list box. Must be a valid value corresponding to the fw-select-option components used in Select. | `any` | `undefined` | +| `variant` | `variant` | The UI variant of the select to be used. | `"button" \| "mail" \| "search" \| "standard"` | `'standard'` | +| `warningText` | `warning-text` | Warning text displayed below the text box. | `string` | `''` | ## Events diff --git a/packages/crayons-core/src/components/select/select.tsx b/packages/crayons-core/src/components/select/select.tsx index cb754ff62..832445735 100644 --- a/packages/crayons-core/src/components/select/select.tsx +++ b/packages/crayons-core/src/components/select/select.tsx @@ -279,6 +279,11 @@ export class Select { */ @Prop() tagProps = {}; + /** + * Virtualize long list of elements in list options *Experimental* + */ + @Prop() enableVirtualScroll = false; + // Events /** * Triggered when a value is selected or deselected from the list box options. @@ -1120,6 +1125,8 @@ export class Select { slot='popover-content' optionLabelPath={this.optionLabelPath} optionValuePath={this.optionValuePath} + isPopoverOpen={this.isExpanded} + enableVirtualScroll={this.enableVirtualScroll} {...listAttributes} > diff --git a/packages/crayons-core/src/utils/stencil-virtual-scroll.ts b/packages/crayons-core/src/utils/stencil-virtual-scroll.ts new file mode 100644 index 000000000..7fa40a508 --- /dev/null +++ b/packages/crayons-core/src/utils/stencil-virtual-scroll.ts @@ -0,0 +1,129 @@ +/** + * Usage: + * + * Tanstack virtual does not have an adapter for stencil framework. + * This is an attempt to bridge the gap. We have made use of stencils store to identify changes to + * virtual scroll instance states and update UI accordingly. + * + * Only one method is exposed from the adapter, createVirtualizer. + * This method takes virtualizerOptions as a parameter. The required options are similar to virtualizer core as in below docs: + * https://tanstack.com/virtual/v3/docs/api/virtualizer + * + * + * EX: + * const { virtualizerInstance, cleanup } = createVirtualizer({ + * count: 1000, // Total items to virtualize + * getScrollElement: () => parentRef.current, // function to return scroll container + * estimateSize: () => 35 // the actual size of items (or estimated size if you will be dynamically measuring items) + * }); + * + * In components lifecycle methods, + * connectedCallback() { + * // waiting for render as options require ref. This will be available only after render. + * this.waitForNextRender().then(() => { + * // this.virtualizerInstance and this.cleanup are private properties to a component. + * ({virtualizer: this.virtualizerInstance, cleanup: this.cleanup} = createVirtualizer(options)); + * // force rerender once for stencil to pick up changes from store. We can use 'this.tick = {};' where tick is a state to component, just for purpose of rerendering. + * // Stencil seems to pick up store updates only after rerender if store is dynamically created after a render. + * }); + * } + * disconnectedCallback() { + * this.cleanup(); + * } + * + * For usage in render method, refer to below link: + * https://tanstack.com/virtual/v3/docs/guide/introduction + */ + +import { + elementScroll, + observeElementOffset, + observeElementRect, + PartialKeys, + Virtualizer, + VirtualizerOptions, +} from '@tanstack/virtual-core'; +export * from '@tanstack/virtual-core'; + +import { createStore } from '@stencil/store'; + +function createVirtualizerBase< + TScrollElement extends Element | Window, + TItemElement extends Element +>( + options: VirtualizerOptions +): { + virtualizer: Virtualizer; + cleanup: () => void; +} { + const scrollVirtualizer = new Virtualizer( + options as unknown as VirtualizerOptions + ); + const cleanup = scrollVirtualizer._didMount(); + const virtualizerStore = createStore({ + totalSize: scrollVirtualizer.getTotalSize(), + virtualItems: scrollVirtualizer.getVirtualItems(), + }); + scrollVirtualizer._willUpdate(); + + const handler = { + get( + target: Virtualizer, + prop: keyof Virtualizer + ) { + switch (prop) { + case 'getVirtualItems': + return () => virtualizerStore.state.virtualItems; + case 'getTotalSize': + return () => virtualizerStore.state.totalSize; + default: + return Reflect.get(target, prop); + } + }, + }; + + const scrollVirtualizerProxy = new Proxy(scrollVirtualizer, handler); + + scrollVirtualizerProxy.setOptions({ + ...options, + onChange( + newVirtualizer: Virtualizer, + sync: boolean + ) { + virtualizerStore.set('virtualItems', scrollVirtualizer.getVirtualItems()); + virtualizerStore.set('totalSize', scrollVirtualizer.getTotalSize()); + options.onChange?.(newVirtualizer, sync); + }, + }); + + const storeCleanup = () => { + cleanup(); + virtualizerStore.dispose(); + }; + scrollVirtualizerProxy.measure(); + + return { + virtualizer: scrollVirtualizerProxy, + cleanup: storeCleanup, + }; +} + +export function createVirtualizer< + TScrollElement extends Element, + TItemElement extends Element +>( + options: PartialKeys< + VirtualizerOptions, + 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' + > +): { + virtualizer: Virtualizer; + cleanup: () => void; +} { + return createVirtualizerBase({ + observeElementRect: observeElementRect, + observeElementOffset: observeElementOffset, + scrollToFn: elementScroll, + ...options, + }); +} From b6962bf4ae8b27fc4406945ff1c9a9bfc35bc3df Mon Sep 17 00:00:00 2001 From: Arvindan Date: Tue, 14 Nov 2023 11:43:12 +0530 Subject: [PATCH 2/7] chore(package-lock.json): updated package lock for tanstack/virtual-core --- package-lock.json | 16 ++++++++++++++++ packages/crayons-core/src/components.d.ts | 4 ---- .../src/components/options-list/readme.md | 13 +------------ .../crayons-core/src/components/select/readme.md | 2 +- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 78575aa61..cdeb2a19c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8750,6 +8750,15 @@ "node": ">=6" } }, + "node_modules/@tanstack/virtual-core": { + "version": "3.0.0-beta.68", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.68.tgz", + "integrity": "sha512-CnvsEJWK7cugigckt13AeY80FMzH+OMdEP0j0bS3/zjs44NiRe49x8FZC6R9suRXGMVMXtUHet0zbTp/Ec9Wfg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", @@ -48162,6 +48171,7 @@ "@freshworks/crayons-i18n": "^4.2.0", "@popperjs/core": "^2.10.2", "@stencil/core": "2.17.4", + "@tanstack/virtual-core": "^3.0.0-beta.68", "date-fns": "^2.28.0", "libphonenumber-js": "^1.10.8", "multi-nprogress": "0.3.5" @@ -51009,6 +51019,7 @@ "@stencil/core": "2.17.4", "@stencil/postcss": "^2.1.0", "@stencil/sass": "^1.4.1", + "@tanstack/virtual-core": "3.0.0-beta.68", "@types/jest": "^26.0.22", "@types/node": "^14.0.10", "@types/puppeteer": "^5.4.3", @@ -56520,6 +56531,11 @@ "defer-to-connect": "^1.0.1" } }, + "@tanstack/virtual-core": { + "version": "3.0.0-beta.68", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.68.tgz", + "integrity": "sha512-CnvsEJWK7cugigckt13AeY80FMzH+OMdEP0j0bS3/zjs44NiRe49x8FZC6R9suRXGMVMXtUHet0zbTp/Ec9Wfg==" + }, "@tootallnate/once": { "version": "1.1.2", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" diff --git a/packages/crayons-core/src/components.d.ts b/packages/crayons-core/src/components.d.ts index d3815243c..8719e5727 100644 --- a/packages/crayons-core/src/components.d.ts +++ b/packages/crayons-core/src/components.d.ts @@ -1318,10 +1318,6 @@ export namespace Components { * Standard is the default option without any graphics other options are icon and avatar which places either the icon or avatar at the beginning of the row. The props for the icon or avatar are passed as an object via the graphicsProps. */ "variant": DropdownVariant; - /** - * WorkAround for wait until next render in stenciljs https://github.com/ionic-team/stencil/issues/2744 - */ - "waitForNextRender": () => Promise; } interface FwMenu { } diff --git a/packages/crayons-core/src/components/options-list/readme.md b/packages/crayons-core/src/components/options-list/readme.md index 65fd92dc6..371c66ea8 100644 --- a/packages/crayons-core/src/components/options-list/readme.md +++ b/packages/crayons-core/src/components/options-list/readme.md @@ -333,7 +333,7 @@ The data-source and the visual variant for the list options can be altered via t | `checkbox` | `checkbox` | Place a checkbox. | `boolean` | `false` | | `debounceTimer` | `debounce-timer` | Debounce timer for the search promise function. | `number` | `300` | | `disabled` | `disabled` | Disables the component on the interface. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | -| `enableVirtualScroll` | `enable-virtual-scroll` | Virtualize long list of elements in list options *Experimental* | `boolean` | `true` | +| `enableVirtualScroll` | `enable-virtual-scroll` | Virtualize long list of elements in list options *Experimental* | `boolean` | `false` | | `filterText` | `filter-text` | The text to filter the options. | `any` | `undefined` | | `formatCreateLabel` | -- | Works only when 'isCreatable' is selected. Function to format the create label displayed as an option. | `(value: string) => string` | `undefined` | | `hideTick` | `hide-tick` | hide tick mark icon on select option | `boolean` | `false` | @@ -425,17 +425,6 @@ Type: `Promise` -### `waitForNextRender() => Promise` - -WorkAround for wait until next render in stenciljs -https://github.com/ionic-team/stencil/issues/2744 - -#### Returns - -Type: `Promise` - - - ## Dependencies diff --git a/packages/crayons-core/src/components/select/readme.md b/packages/crayons-core/src/components/select/readme.md index 60a4a046c..5a97354ab 100644 --- a/packages/crayons-core/src/components/select/readme.md +++ b/packages/crayons-core/src/components/select/readme.md @@ -1876,7 +1876,7 @@ Refer the [css variables](#css-custom-properties) for modifying the appearance o | `creatableProps` | -- | Props to be passed for creatable select isCreatable: boolean - If true, select accepts user input that are not present as options and add them as options validateNewOption: (value) => boolean - If passed, this function will determine the error state for every new option entered. If return value is true, error state of the newly created option will be false and if return value is false, then the error state of the newly created option will be true. formatCreateLabel: (label) => string - Gets the label for the "create new ..." option in the menu. Current input value is provided as argument. | `{ isCreatable: boolean; validateNewOption: (_value: any) => boolean; formatCreateLabel: (label: any) => string; }` | `{ isCreatable: false, validateNewOption: (_value): boolean => true, formatCreateLabel: (label): string => label, }` | | `debounceTimer` | `debounce-timer` | Debounce timer for the search promise function. | `number` | `300` | | `disabled` | `disabled` | Disables the component on the interface. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | -| `enableVirtualScroll` | `enable-virtual-scroll` | Virtualize long list of elements in list options *Experimental* | `boolean` | `true` | +| `enableVirtualScroll` | `enable-virtual-scroll` | Virtualize long list of elements in list options *Experimental* | `boolean` | `false` | | `errorText` | `error-text` | Error text displayed below the text box. | `string` | `''` | | `fallbackPlacements` | -- | Alternative placement for popover if the default placement is not possible. | `[PopoverPlacementType]` | `['top']` | | `forceSelect` | `force-select` | If true, the user must select a value. The default value is not displayed. | `boolean` | `true` | From fa72cad6ce98b98a9ef28fbfd3ecd26a8bef77fc Mon Sep 17 00:00:00 2001 From: rihansiddhi Date: Thu, 16 Nov 2023 01:42:35 +0530 Subject: [PATCH 3/7] feat(fw-list-options): modify virtual scroll adapter --- .../components/options-list/list-options.tsx | 142 ++++++++++++------ .../src/components/options-list/readme.md | 93 +++++++++++- .../src/components/popover/popover.scss | 1 + .../src/components/select/select.tsx | 7 + .../src/utils/stencil-virtual-scroll.ts | 49 +++++- 5 files changed, 236 insertions(+), 56 deletions(-) diff --git a/packages/crayons-core/src/components/options-list/list-options.tsx b/packages/crayons-core/src/components/options-list/list-options.tsx index a67d727d4..94ab09752 100644 --- a/packages/crayons-core/src/components/options-list/list-options.tsx +++ b/packages/crayons-core/src/components/options-list/list-options.tsx @@ -173,7 +173,7 @@ export class ListOptions { @Prop() optionValuePath = 'value'; /** - * Is the popover in open state + * Is the popover in open state. Used when 'enableVirtualScroll' is true */ @Prop() isPopoverOpen = false; @@ -182,6 +182,11 @@ export class ListOptions { */ @Prop() enableVirtualScroll = false; + /** + * Works only when 'enableVirtualScroll' is true. Estimated size of each item in the list box to ensure smooth-scrolling. + */ + @Prop() estimatedSize = 35; + /** * Triggered when a value is selected or deselected from the list box options. */ @@ -199,6 +204,13 @@ export class ListOptions { this.renderPromiseResolve(); this.renderPromiseResolve = null; } + if (this.enableVirtualScroll) { + const parent = this.host?.parentElement; + if (parent && parent.tagName === 'FW-POPOVER') { + parent.addEventListener('fwShow', this.debouncedFwShowHandler); + parent.addEventListener('fwHide', this.debouncedFwHideHandler); + } + } } /** @@ -210,13 +222,36 @@ export class ListOptions { } connectedCallback() { - this.waitForNextRender().then(() => this.initScroller()); + if (this.enableVirtualScroll) { + this.waitForNextRender().then(() => this.initScroller()); + } } disconnectedCallback() { + parent.removeEventListener('fwShow', this.debouncedFwShowHandler); + parent.removeEventListener('fwHide', this.debouncedFwHideHandler); this.scrollVirtualizerCleanup?.(); } + debouncedFwShowHandler = debounce( + () => { + console.log('this is fw show event'); + this.initScroller(); + }, + this, + 100 + ); + + debouncedFwHideHandler = debounce( + () => { + console.log('this is fw hide event'); + this.scrollVirtualizer?.scrollToOffset(0); + this.scrollVirtualizer?.measure(); + }, + this, + 100 + ); + @Listen('fwSelected') fwSelectedHandler(selectedItem) { const { value, selected } = selectedItem.detail; @@ -388,44 +423,50 @@ export class ListOptions { this.setSelectedOptions(newValue); } - @Watch('isPopoverOpen') - isPopoverOpenWatcher(): void { - if (this.isPopoverOpen) { + // @Watch('isPopoverOpen') + // isPopoverOpenWatcher(): void { + // if (this.enableVirtualScroll) { + // if (this.isPopoverOpen) { + // console.log('this is initializer popover', this.scrollVirtualizer); + // this.initScroller(); + // } else { + // this.scrollVirtualizerCleanup?.(); + // } + // } + // } + + @Watch('filteredOptions') + onFilteredOptionsChange(): void { + if (this.enableVirtualScroll) { this.initScroller(); - } else { - this.scrollVirtualizerCleanup(); } } async initScroller() { const scrollElement = this.getScrollElement(); if (this.filteredOptions?.length && scrollElement) { - const rect = scrollElement.getBoundingClientRect(); - if (rect.height) { - const options: any = { - count: this.filteredOptions.length, - getScrollElement: () => { - return scrollElement; - }, - estimateSize: () => 35, - }; - if (this.scrollVirtualizer) { - this.scrollVirtualizerCleanup(); - } - const createVirtualizer = await ( - await import('../../utils/stencil-virtual-scroll') - ).createVirtualizer; - const virtualScroll = createVirtualizer(options); - this.scrollVirtualizer = virtualScroll.virtualizer; - this.scrollVirtualizerCleanup = () => { - virtualScroll.cleanup(); - this.scrollVirtualizer = null; - this.scrollVirtualizerCleanup = null; - }; - this.tick = {}; - } else { - this.scrollVirtualizer = null; + const options: any = { + count: this.filteredOptions.length, + getScrollElement: () => { + return scrollElement; + }, + estimateSize: () => this.estimatedSize, + }; + if (this.scrollVirtualizer) { + this.scrollVirtualizerCleanup?.(); } + const createVirtualizer = await ( + await import('../../utils/stencil-virtual-scroll') + ).createVirtualizer; + const virtualScroll = createVirtualizer(options); + this.scrollVirtualizer = virtualScroll.virtualizer; + this.scrollVirtualizer?.scrollToOffset(0); + this.scrollVirtualizerCleanup = () => { + virtualScroll.cleanup(); + this.scrollVirtualizer = null; + this.scrollVirtualizerCleanup = null; + }; + this.tick = {}; } else { this.scrollVirtualizer = null; } @@ -611,7 +652,7 @@ export class ListOptions { position: 'relative', }} > -
- {virtualItems.map((virtualItem) => { - const option = options[virtualItem.index]; - return ( -
- {this.renderSelectOption(option)} -
- ); - })} -
+ > */} + {virtualItems.map((virtualItem) => { + const option = options[virtualItem.index]; + return ( +
this.scrollVirtualizer?.measureElement(item)} + style={{ + position: 'absolute', + top: '0px', + left: '0px', + width: '100%', + transform: `translateY(${virtualItem.start ?? 0}px)`, + }} + > + {this.renderSelectOption(option)} +
+ ); + })} + {/* */} ) ); diff --git a/packages/crayons-core/src/components/options-list/readme.md b/packages/crayons-core/src/components/options-list/readme.md index 371c66ea8..5afd38ee9 100644 --- a/packages/crayons-core/src/components/options-list/readme.md +++ b/packages/crayons-core/src/components/options-list/readme.md @@ -321,6 +321,96 @@ The data-source and the visual variant for the list options can be altered via t +### Demo with virtual scroll + +**This feature is experimental, it needs to be explicitly activated using the `enableVirtualScroll` feature flag.** + +`enableVirtualScroll` property can be used to enable virtualisation of long list of options. +`isPopoverOpen` property is used to determine if the popover displaying the list options is in open state to initialise virtualisation. + +```html live + + + Open Long List + + + + +``` + +### Usage of Virtual scroll + + + + +```html + + + Open Long List + + + + +``` + + + + +```jsx +function ListOptions() { + const longListOptions = Array.from(Array(50000), (_,i) => ({ + text: `Item No: ${i + 1}`, + value: i + })); + return ( + + Open Long List + + + ); +} +export default ListOptions; +``` + + + + @@ -334,11 +424,12 @@ The data-source and the visual variant for the list options can be altered via t | `debounceTimer` | `debounce-timer` | Debounce timer for the search promise function. | `number` | `300` | | `disabled` | `disabled` | Disables the component on the interface. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | | `enableVirtualScroll` | `enable-virtual-scroll` | Virtualize long list of elements in list options *Experimental* | `boolean` | `false` | +| `estimatedSize` | `estimated-size` | Works only when 'enableVirtualScroll' is true. Estimated size of each item in the list box to ensure smooth-scrolling. | `number` | `35` | | `filterText` | `filter-text` | The text to filter the options. | `any` | `undefined` | | `formatCreateLabel` | -- | Works only when 'isCreatable' is selected. Function to format the create label displayed as an option. | `(value: string) => string` | `undefined` | | `hideTick` | `hide-tick` | hide tick mark icon on select option | `boolean` | `false` | | `isCreatable` | `is-creatable` | Allows user to create the option if the provided input doesn't match with any of the options. | `boolean` | `false` | -| `isPopoverOpen` | `is-popover-open` | Is the popover in open state | `boolean` | `false` | +| `isPopoverOpen` | `is-popover-open` | Is the popover in open state. Used when 'enableVirtualScroll' is true | `boolean` | `false` | | `max` | `max` | Works with `multiple` enabled. Configures the maximum number of options that can be selected with a multi-select component. | `number` | `Number.MAX_VALUE` | | `multiple` | `multiple` | Enables selection of multiple options. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | | `noDataText` | `no-data-text` | Text to be displayed when there is no data available in the select. | `string` | `''` | diff --git a/packages/crayons-core/src/components/popover/popover.scss b/packages/crayons-core/src/components/popover/popover.scss index 25311d92e..c020551d1 100644 --- a/packages/crayons-core/src/components/popover/popover.scss +++ b/packages/crayons-core/src/components/popover/popover.scss @@ -29,6 +29,7 @@ transform: scale(0.01); transition: 150ms color, 150ms border, 150ms box-shadow; will-change: auto; + overflow-anchor: none; } .popper-content.no-border { diff --git a/packages/crayons-core/src/components/select/select.tsx b/packages/crayons-core/src/components/select/select.tsx index 832445735..ca7c80129 100644 --- a/packages/crayons-core/src/components/select/select.tsx +++ b/packages/crayons-core/src/components/select/select.tsx @@ -284,6 +284,11 @@ export class Select { */ @Prop() enableVirtualScroll = false; + /** + * Works only when 'enableVirtualScroll' is true. Estimated size of each item in the list box to ensure smooth-scrolling. + */ + @Prop() estimatedSize = 35; + // Events /** * Triggered when a value is selected or deselected from the list box options. @@ -310,6 +315,7 @@ export class Select { @Listen('fwShow') onDropdownOpen(e) { if (e.composedPath()[0].id === 'select-popover') { + console.log('number of logs fwshow'); this.isExpanded = true; } } @@ -1127,6 +1133,7 @@ export class Select { optionValuePath={this.optionValuePath} isPopoverOpen={this.isExpanded} enableVirtualScroll={this.enableVirtualScroll} + estimatedSize={this.estimatedSize} {...listAttributes} > diff --git a/packages/crayons-core/src/utils/stencil-virtual-scroll.ts b/packages/crayons-core/src/utils/stencil-virtual-scroll.ts index 7fa40a508..159272671 100644 --- a/packages/crayons-core/src/utils/stencil-virtual-scroll.ts +++ b/packages/crayons-core/src/utils/stencil-virtual-scroll.ts @@ -82,6 +82,11 @@ function createVirtualizerBase< }, }; + const storeCleanup = () => { + cleanup(); + virtualizerStore.dispose(); + }; + const scrollVirtualizerProxy = new Proxy(scrollVirtualizer, handler); scrollVirtualizerProxy.setOptions({ @@ -90,16 +95,44 @@ function createVirtualizerBase< newVirtualizer: Virtualizer, sync: boolean ) { - virtualizerStore.set('virtualItems', scrollVirtualizer.getVirtualItems()); - virtualizerStore.set('totalSize', scrollVirtualizer.getTotalSize()); - options.onChange?.(newVirtualizer, sync); + const virtualItems = newVirtualizer.getVirtualItems(); + console.log( + 'virtualizer', + newVirtualizer, + newVirtualizer.scrollRect?.height, + virtualItems.length, + virtualizerStore.state.virtualItems.length + ); + virtualizerStore.set('virtualItems', virtualItems); + virtualizerStore.set('totalSize', newVirtualizer.getTotalSize()); + if (newVirtualizer.scrollRect?.height) { + // console.log( + // 'this is on change', + // newVirtualizer.getVirtualItems(), + // newVirtualizer.getTotalSize() + // ); + scrollVirtualizerProxy._willUpdate(); + + // console.log( + // 'measure', + // virtualItems.length, + // virtualizerStore.state.virtualItems.length + // ); + // if ( + // !newVirtualizer.scrollRect?.height && + // virtualItems.length === 0 && + // virtualizerStore.state.virtualItems.length !== 0 + // ) { + // console.log('this is next change'); + // newVirtualizer.scrollToIndex(0); + // scrollVirtualizerProxy._didMount(); + // storeCleanup(); + // scrollVirtualizerProxy.measure(); + // } + options.onChange?.(newVirtualizer, sync); + } }, }); - - const storeCleanup = () => { - cleanup(); - virtualizerStore.dispose(); - }; scrollVirtualizerProxy.measure(); return { From 65c48df1d40cc4e5956aa022cc3d986eec45af57 Mon Sep 17 00:00:00 2001 From: rihansiddhi Date: Thu, 16 Nov 2023 02:01:50 +0530 Subject: [PATCH 4/7] feat(fw-list-options): modify virtual scroll properties --- .../components/options-list/list-options.tsx | 9 ++- .../src/components/select/readme.md | 79 +++++++++++++++++++ .../src/utils/stencil-virtual-scroll.ts | 35 +------- 3 files changed, 86 insertions(+), 37 deletions(-) diff --git a/packages/crayons-core/src/components/options-list/list-options.tsx b/packages/crayons-core/src/components/options-list/list-options.tsx index 94ab09752..796e802a8 100644 --- a/packages/crayons-core/src/components/options-list/list-options.tsx +++ b/packages/crayons-core/src/components/options-list/list-options.tsx @@ -235,7 +235,6 @@ export class ListOptions { debouncedFwShowHandler = debounce( () => { - console.log('this is fw show event'); this.initScroller(); }, this, @@ -244,7 +243,6 @@ export class ListOptions { debouncedFwHideHandler = debounce( () => { - console.log('this is fw hide event'); this.scrollVirtualizer?.scrollToOffset(0); this.scrollVirtualizer?.measure(); }, @@ -320,7 +318,11 @@ export class ListOptions { @Method() async scrollToLastSelected() { - if (this.filteredOptions.length > 0 && this.valueExists()) { + if ( + !this.enableVirtualScroll && + this.filteredOptions.length > 0 && + this.valueExists() + ) { this.container .querySelector( `fw-select-option[id='${ @@ -460,7 +462,6 @@ export class ListOptions { ).createVirtualizer; const virtualScroll = createVirtualizer(options); this.scrollVirtualizer = virtualScroll.virtualizer; - this.scrollVirtualizer?.scrollToOffset(0); this.scrollVirtualizerCleanup = () => { virtualScroll.cleanup(); this.scrollVirtualizer = null; diff --git a/packages/crayons-core/src/components/select/readme.md b/packages/crayons-core/src/components/select/readme.md index 5a97354ab..2f69d14ac 100644 --- a/packages/crayons-core/src/components/select/readme.md +++ b/packages/crayons-core/src/components/select/readme.md @@ -1857,6 +1857,84 @@ export default Select; +### Demo with virtual scroll + +**This feature is experimental, it needs to be explicitly activated using the `enableVirtualScroll` feature flag.** + +`enableVirtualScroll` property can be used to enable virtualisation of long list of options. +`estimatedSize` property can be used to set estimated size of items in the list box to ensure smooth-scrolling. + +```html live +

+ + + +``` + +### Usage of Virtual scroll + + + + +```html +

+ + + +``` +
+ + + +```jsx +function Select() { + const longListOptions = Array.from(Array(50000), (_,i) => ({ + text: `Item No: ${i + 1}`, + value: i + })); + return ( + + ); +} +export default Select; +``` + + +
+ ## Styling Refer the css variables in fw-popover to control the height and width of the select popup. @@ -1878,6 +1956,7 @@ Refer the [css variables](#css-custom-properties) for modifying the appearance o | `disabled` | `disabled` | Disables the component on the interface. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | | `enableVirtualScroll` | `enable-virtual-scroll` | Virtualize long list of elements in list options *Experimental* | `boolean` | `false` | | `errorText` | `error-text` | Error text displayed below the text box. | `string` | `''` | +| `estimatedSize` | `estimated-size` | Works only when 'enableVirtualScroll' is true. Estimated size of each item in the list box to ensure smooth-scrolling. | `number` | `35` | | `fallbackPlacements` | -- | Alternative placement for popover if the default placement is not possible. | `[PopoverPlacementType]` | `['top']` | | `forceSelect` | `force-select` | If true, the user must select a value. The default value is not displayed. | `boolean` | `true` | | `hintText` | `hint-text` | Hint text displayed below the text box. | `string` | `''` | diff --git a/packages/crayons-core/src/utils/stencil-virtual-scroll.ts b/packages/crayons-core/src/utils/stencil-virtual-scroll.ts index 159272671..7484a97dd 100644 --- a/packages/crayons-core/src/utils/stencil-virtual-scroll.ts +++ b/packages/crayons-core/src/utils/stencil-virtual-scroll.ts @@ -96,41 +96,10 @@ function createVirtualizerBase< sync: boolean ) { const virtualItems = newVirtualizer.getVirtualItems(); - console.log( - 'virtualizer', - newVirtualizer, - newVirtualizer.scrollRect?.height, - virtualItems.length, - virtualizerStore.state.virtualItems.length - ); virtualizerStore.set('virtualItems', virtualItems); virtualizerStore.set('totalSize', newVirtualizer.getTotalSize()); - if (newVirtualizer.scrollRect?.height) { - // console.log( - // 'this is on change', - // newVirtualizer.getVirtualItems(), - // newVirtualizer.getTotalSize() - // ); - scrollVirtualizerProxy._willUpdate(); - - // console.log( - // 'measure', - // virtualItems.length, - // virtualizerStore.state.virtualItems.length - // ); - // if ( - // !newVirtualizer.scrollRect?.height && - // virtualItems.length === 0 && - // virtualizerStore.state.virtualItems.length !== 0 - // ) { - // console.log('this is next change'); - // newVirtualizer.scrollToIndex(0); - // scrollVirtualizerProxy._didMount(); - // storeCleanup(); - // scrollVirtualizerProxy.measure(); - // } - options.onChange?.(newVirtualizer, sync); - } + scrollVirtualizerProxy._willUpdate(); + options.onChange?.(newVirtualizer, sync); }, }); scrollVirtualizerProxy.measure(); From 9de981a0822da0739da19c28cb36f686aa7f2124 Mon Sep 17 00:00:00 2001 From: rihansiddhi Date: Thu, 16 Nov 2023 09:27:00 +0530 Subject: [PATCH 5/7] feat(fw-list-options): virtualize long list of options --- packages/crayons-core/src/components.d.ts | 24 ++++++++++++------- .../components/options-list/list-options.tsx | 17 ------------- .../src/components/options-list/readme.md | 2 -- .../src/components/select/select.tsx | 1 - 4 files changed, 16 insertions(+), 28 deletions(-) diff --git a/packages/crayons-core/src/components.d.ts b/packages/crayons-core/src/components.d.ts index 8719e5727..060f08bb2 100644 --- a/packages/crayons-core/src/components.d.ts +++ b/packages/crayons-core/src/components.d.ts @@ -1234,6 +1234,10 @@ export namespace Components { * Virtualize long list of elements in list options *Experimental* */ "enableVirtualScroll": boolean; + /** + * Works only when 'enableVirtualScroll' is true. Estimated size of each item in the list box to ensure smooth-scrolling. + */ + "estimatedSize": number; /** * The text to filter the options. */ @@ -1251,10 +1255,6 @@ export namespace Components { * Allows user to create the option if the provided input doesn't match with any of the options. */ "isCreatable": boolean; - /** - * Is the popover in open state - */ - "isPopoverOpen": boolean; /** * Works with `multiple` enabled. Configures the maximum number of options that can be selected with a multi-select component. */ @@ -1708,6 +1708,10 @@ export namespace Components { * Error text displayed below the text box. */ "errorText": string; + /** + * Works only when 'enableVirtualScroll' is true. Estimated size of each item in the list box to ensure smooth-scrolling. + */ + "estimatedSize": number; /** * Alternative placement for popover if the default placement is not possible. */ @@ -4194,6 +4198,10 @@ declare namespace LocalJSX { * Virtualize long list of elements in list options *Experimental* */ "enableVirtualScroll"?: boolean; + /** + * Works only when 'enableVirtualScroll' is true. Estimated size of each item in the list box to ensure smooth-scrolling. + */ + "estimatedSize"?: number; /** * The text to filter the options. */ @@ -4210,10 +4218,6 @@ declare namespace LocalJSX { * Allows user to create the option if the provided input doesn't match with any of the options. */ "isCreatable"?: boolean; - /** - * Is the popover in open state - */ - "isPopoverOpen"?: boolean; /** * Works with `multiple` enabled. Configures the maximum number of options that can be selected with a multi-select component. */ @@ -4672,6 +4676,10 @@ declare namespace LocalJSX { * Error text displayed below the text box. */ "errorText"?: string; + /** + * Works only when 'enableVirtualScroll' is true. Estimated size of each item in the list box to ensure smooth-scrolling. + */ + "estimatedSize"?: number; /** * Alternative placement for popover if the default placement is not possible. */ diff --git a/packages/crayons-core/src/components/options-list/list-options.tsx b/packages/crayons-core/src/components/options-list/list-options.tsx index 796e802a8..49817500e 100644 --- a/packages/crayons-core/src/components/options-list/list-options.tsx +++ b/packages/crayons-core/src/components/options-list/list-options.tsx @@ -172,11 +172,6 @@ export class ListOptions { */ @Prop() optionValuePath = 'value'; - /** - * Is the popover in open state. Used when 'enableVirtualScroll' is true - */ - @Prop() isPopoverOpen = false; - /** * Virtualize long list of elements in list options *Experimental* */ @@ -425,18 +420,6 @@ export class ListOptions { this.setSelectedOptions(newValue); } - // @Watch('isPopoverOpen') - // isPopoverOpenWatcher(): void { - // if (this.enableVirtualScroll) { - // if (this.isPopoverOpen) { - // console.log('this is initializer popover', this.scrollVirtualizer); - // this.initScroller(); - // } else { - // this.scrollVirtualizerCleanup?.(); - // } - // } - // } - @Watch('filteredOptions') onFilteredOptionsChange(): void { if (this.enableVirtualScroll) { diff --git a/packages/crayons-core/src/components/options-list/readme.md b/packages/crayons-core/src/components/options-list/readme.md index 5afd38ee9..3791b4c94 100644 --- a/packages/crayons-core/src/components/options-list/readme.md +++ b/packages/crayons-core/src/components/options-list/readme.md @@ -326,7 +326,6 @@ The data-source and the visual variant for the list options can be altered via t **This feature is experimental, it needs to be explicitly activated using the `enableVirtualScroll` feature flag.** `enableVirtualScroll` property can be used to enable virtualisation of long list of options. -`isPopoverOpen` property is used to determine if the popover displaying the list options is in open state to initialise virtualisation. ```html live @@ -429,7 +428,6 @@ export default ListOptions; | `formatCreateLabel` | -- | Works only when 'isCreatable' is selected. Function to format the create label displayed as an option. | `(value: string) => string` | `undefined` | | `hideTick` | `hide-tick` | hide tick mark icon on select option | `boolean` | `false` | | `isCreatable` | `is-creatable` | Allows user to create the option if the provided input doesn't match with any of the options. | `boolean` | `false` | -| `isPopoverOpen` | `is-popover-open` | Is the popover in open state. Used when 'enableVirtualScroll' is true | `boolean` | `false` | | `max` | `max` | Works with `multiple` enabled. Configures the maximum number of options that can be selected with a multi-select component. | `number` | `Number.MAX_VALUE` | | `multiple` | `multiple` | Enables selection of multiple options. If the attribute’s value is undefined, the value is set to false. | `boolean` | `false` | | `noDataText` | `no-data-text` | Text to be displayed when there is no data available in the select. | `string` | `''` | diff --git a/packages/crayons-core/src/components/select/select.tsx b/packages/crayons-core/src/components/select/select.tsx index ca7c80129..d6c477efa 100644 --- a/packages/crayons-core/src/components/select/select.tsx +++ b/packages/crayons-core/src/components/select/select.tsx @@ -1131,7 +1131,6 @@ export class Select { slot='popover-content' optionLabelPath={this.optionLabelPath} optionValuePath={this.optionValuePath} - isPopoverOpen={this.isExpanded} enableVirtualScroll={this.enableVirtualScroll} estimatedSize={this.estimatedSize} {...listAttributes} From 677ceb261c00be787942bc0809f5b1bdb98791fa Mon Sep 17 00:00:00 2001 From: rihansiddhi Date: Thu, 23 Nov 2023 09:39:19 +0530 Subject: [PATCH 6/7] feat(fw-list-options): virtualise list options --- .../options-list/list-options.e2e.ts | 23 +++++++ .../components/options-list/list-options.tsx | 60 +++++++++---------- .../src/components/options-list/readme.md | 7 ++- .../src/components/select/readme.md | 2 +- .../src/components/select/select.e2e.ts | 24 ++++++++ .../src/components/select/select.tsx | 1 - .../src/utils/stencil-virtual-scroll.ts | 21 +++++-- 7 files changed, 96 insertions(+), 42 deletions(-) diff --git a/packages/crayons-core/src/components/options-list/list-options.e2e.ts b/packages/crayons-core/src/components/options-list/list-options.e2e.ts index a70c968ba..1187cb86d 100644 --- a/packages/crayons-core/src/components/options-list/list-options.e2e.ts +++ b/packages/crayons-core/src/components/options-list/list-options.e2e.ts @@ -406,4 +406,27 @@ describe('fw-list-options', () => { expect(await options[0].getProperty('checkbox')).toBeFalsy(); expect(await options[0].getProperty('text')).toBe('Cannot find item'); }); + + it('should have virtual scroll (display only limited nodes based on space available in parent) when popup opens up', async () => { + const page = await newE2EPage(); + + await page.setContent(` + Click Me! + + { + elm.options = Array.from(Array(7000), (_, i) => ({ + text: `Item No: ${i + 1}`, + value: i, + })); + }); + await page.waitForChanges(); + const trigger = await page.find('fw-button'); + await trigger.click(); + await page.waitForChanges(); + await new Promise((resolve) => setTimeout(() => resolve(null), 2000)); + const options = await page.findAll('fw-list-options >>> fw-select-option'); + expect(options.length).toBeGreaterThan(5); + expect(options.length).toBeLessThan(20); + }); }); diff --git a/packages/crayons-core/src/components/options-list/list-options.tsx b/packages/crayons-core/src/components/options-list/list-options.tsx index 49817500e..76cdafc12 100644 --- a/packages/crayons-core/src/components/options-list/list-options.tsx +++ b/packages/crayons-core/src/components/options-list/list-options.tsx @@ -48,7 +48,6 @@ export class ListOptions { resolve(filteredValue); }); }; - // private filteredFilesRef = null; private scrollVirtualizer = null; private scrollVirtualizerCleanup = null; private renderPromiseResolve = null; @@ -199,13 +198,6 @@ export class ListOptions { this.renderPromiseResolve(); this.renderPromiseResolve = null; } - if (this.enableVirtualScroll) { - const parent = this.host?.parentElement; - if (parent && parent.tagName === 'FW-POPOVER') { - parent.addEventListener('fwShow', this.debouncedFwShowHandler); - parent.addEventListener('fwHide', this.debouncedFwHideHandler); - } - } } /** @@ -218,7 +210,13 @@ export class ListOptions { connectedCallback() { if (this.enableVirtualScroll) { - this.waitForNextRender().then(() => this.initScroller()); + const parent = this.host?.parentElement; + if (parent && parent.tagName === 'FW-POPOVER') { + parent.addEventListener('fwShow', this.debouncedFwShowHandler); + parent.addEventListener('fwHide', this.debouncedFwHideHandler); + } else { + this.waitForNextRender().then(() => this.initScroller()); + } } } @@ -238,8 +236,9 @@ export class ListOptions { debouncedFwHideHandler = debounce( () => { + this.scrollVirtualizerCleanup?.(); this.scrollVirtualizer?.scrollToOffset(0); - this.scrollVirtualizer?.measure(); + this.scrollVirtualizer = null; }, this, 100 @@ -423,6 +422,7 @@ export class ListOptions { @Watch('filteredOptions') onFilteredOptionsChange(): void { if (this.enableVirtualScroll) { + this.scrollVirtualizerCleanup?.(); this.initScroller(); } } @@ -436,6 +436,7 @@ export class ListOptions { return scrollElement; }, estimateSize: () => this.estimatedSize, + paddingEnd: 10, }; if (this.scrollVirtualizer) { this.scrollVirtualizerCleanup?.(); @@ -636,7 +637,7 @@ export class ListOptions { position: 'relative', }} > - {/*
*/} - {virtualItems.map((virtualItem) => { - const option = options[virtualItem.index]; - return ( -
this.scrollVirtualizer?.measureElement(item)} - style={{ - position: 'absolute', - top: '0px', - left: '0px', - width: '100%', - transform: `translateY(${virtualItem.start ?? 0}px)`, - }} - > - {this.renderSelectOption(option)} -
- ); - })} - {/*
*/} + > + {virtualItems + .filter((virtualItem) => !!options[virtualItem.index]) + .map((virtualItem) => { + const option = options[virtualItem.index]; + return ( +
this.scrollVirtualizer?.measureElement(item)} + > + {this.renderSelectOption(option)} +
+ ); + })} + ) ); diff --git a/packages/crayons-core/src/components/options-list/readme.md b/packages/crayons-core/src/components/options-list/readme.md index 3791b4c94..925d6c7fa 100644 --- a/packages/crayons-core/src/components/options-list/readme.md +++ b/packages/crayons-core/src/components/options-list/readme.md @@ -326,6 +326,7 @@ The data-source and the visual variant for the list options can be altered via t **This feature is experimental, it needs to be explicitly activated using the `enableVirtualScroll` feature flag.** `enableVirtualScroll` property can be used to enable virtualisation of long list of options. +`estimatedSize` property is used to set estimated size of items in the list box to ensure smooth-scrolling. ```html live @@ -334,7 +335,7 @@ The data-source and the visual variant for the list options can be altered via t
@@ -364,7 +365,7 @@ The data-source and the visual variant for the list options can be altered via t @@ -399,7 +400,7 @@ function ListOptions() { slot='popover-content' options={longListOptions} enableVirtualScroll - estimatedSize={75} + estimatedSize={52} > ); diff --git a/packages/crayons-core/src/components/select/readme.md b/packages/crayons-core/src/components/select/readme.md index 2f69d14ac..6b625b8f3 100644 --- a/packages/crayons-core/src/components/select/readme.md +++ b/packages/crayons-core/src/components/select/readme.md @@ -1862,7 +1862,7 @@ export default Select; **This feature is experimental, it needs to be explicitly activated using the `enableVirtualScroll` feature flag.** `enableVirtualScroll` property can be used to enable virtualisation of long list of options. -`estimatedSize` property can be used to set estimated size of items in the list box to ensure smooth-scrolling. +`estimatedSize` property is used to set estimated size of items in the list box to ensure smooth-scrolling. ```html live

diff --git a/packages/crayons-core/src/components/select/select.e2e.ts b/packages/crayons-core/src/components/select/select.e2e.ts index 1d7f6ae89..45a9b4e5c 100644 --- a/packages/crayons-core/src/components/select/select.e2e.ts +++ b/packages/crayons-core/src/components/select/select.e2e.ts @@ -864,4 +864,28 @@ describe('fw-select', () => { ); expect(popoverContent).not.toHaveAttribute('data-show'); }); + + it('should have virtual scroll (display only limited nodes based on space available in parent) when popup opens up', async () => { + const page = await newE2EPage(); + + await page.setContent(` + `); + await page.$eval('fw-select', (elm: any) => { + elm.options = Array.from(Array(7000), (_, i) => ({ + text: `Item No: ${i + 1}`, + value: i, + })); + }); + await page.waitForChanges(); + const element = await page.find('fw-select'); + await element.click(); + await page.waitForChanges(); + await new Promise((resolve) => setTimeout(() => resolve(null), 2000)); + const popover = await page.find('fw-select >>> fw-popover'); + const options = await popover.findAll( + 'fw-list-options >>> fw-select-option' + ); + expect(options.length).toBeGreaterThan(5); + expect(options.length).toBeLessThan(20); + }); }); diff --git a/packages/crayons-core/src/components/select/select.tsx b/packages/crayons-core/src/components/select/select.tsx index d6c477efa..50b97dbb0 100644 --- a/packages/crayons-core/src/components/select/select.tsx +++ b/packages/crayons-core/src/components/select/select.tsx @@ -315,7 +315,6 @@ export class Select { @Listen('fwShow') onDropdownOpen(e) { if (e.composedPath()[0].id === 'select-popover') { - console.log('number of logs fwshow'); this.isExpanded = true; } } diff --git a/packages/crayons-core/src/utils/stencil-virtual-scroll.ts b/packages/crayons-core/src/utils/stencil-virtual-scroll.ts index 7484a97dd..70ac594bb 100644 --- a/packages/crayons-core/src/utils/stencil-virtual-scroll.ts +++ b/packages/crayons-core/src/utils/stencil-virtual-scroll.ts @@ -46,6 +46,7 @@ import { export * from '@tanstack/virtual-core'; import { createStore } from '@stencil/store'; +import { debounce } from '.'; function createVirtualizerBase< TScrollElement extends Element | Window, @@ -89,18 +90,28 @@ function createVirtualizerBase< const scrollVirtualizerProxy = new Proxy(scrollVirtualizer, handler); - scrollVirtualizerProxy.setOptions({ - ...options, - onChange( + const debouncedOnChange = debounce( + ( newVirtualizer: Virtualizer, - sync: boolean - ) { + sync: boolean, + options: any + ) => { const virtualItems = newVirtualizer.getVirtualItems(); virtualizerStore.set('virtualItems', virtualItems); virtualizerStore.set('totalSize', newVirtualizer.getTotalSize()); scrollVirtualizerProxy._willUpdate(); options.onChange?.(newVirtualizer, sync); }, + this, + 50 + ); + + scrollVirtualizerProxy.setOptions({ + ...options, + onChange: ( + newVirtualizer: Virtualizer, + sync: boolean + ) => debouncedOnChange(newVirtualizer, sync, options), }); scrollVirtualizerProxy.measure(); From 2de81fbb7456216e974d3f7d1f406dcfd0dd6155 Mon Sep 17 00:00:00 2001 From: rihansiddhi Date: Fri, 24 Nov 2023 11:39:12 +0530 Subject: [PATCH 7/7] feat(fw-list-options): modify conditions to virtualise list options --- .../src/components/options-list/list-options.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/crayons-core/src/components/options-list/list-options.tsx b/packages/crayons-core/src/components/options-list/list-options.tsx index 76cdafc12..9afd99f0e 100644 --- a/packages/crayons-core/src/components/options-list/list-options.tsx +++ b/packages/crayons-core/src/components/options-list/list-options.tsx @@ -194,7 +194,7 @@ export class ListOptions { * componentDidRender lifecycle event */ componentDidRender() { - if (this.renderPromiseResolve) { + if (this.enableVirtualScroll && this.renderPromiseResolve) { this.renderPromiseResolve(); this.renderPromiseResolve = null; } @@ -221,9 +221,14 @@ export class ListOptions { } disconnectedCallback() { - parent.removeEventListener('fwShow', this.debouncedFwShowHandler); - parent.removeEventListener('fwHide', this.debouncedFwHideHandler); - this.scrollVirtualizerCleanup?.(); + if (this.enableVirtualScroll) { + const parent = this.host?.parentElement; + if (parent && parent.tagName === 'FW-POPOVER') { + parent.removeEventListener('fwShow', this.debouncedFwShowHandler); + parent.removeEventListener('fwHide', this.debouncedFwHideHandler); + } + this.scrollVirtualizerCleanup?.(); + } } debouncedFwShowHandler = debounce( @@ -629,7 +634,8 @@ export class ListOptions { const virtualItems = this.scrollVirtualizer?.getVirtualItems(); return ( scrollElement && - this.scrollVirtualizer && ( + this.scrollVirtualizer && + virtualItems && (