@momentum-design/components 0.122.6 → 0.122.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/index.js +428 -409
- package/dist/browser/index.js.map +4 -4
- package/dist/components/list/list.component.d.ts +12 -17
- package/dist/components/list/list.component.js +29 -39
- package/dist/components/listitem/listitem.component.d.ts +10 -0
- package/dist/components/listitem/listitem.component.js +7 -0
- package/dist/components/virtualizedlist/virtualizedlist.component.d.ts +244 -41
- package/dist/components/virtualizedlist/virtualizedlist.component.js +597 -78
- package/dist/components/virtualizedlist/virtualizedlist.constants.d.ts +7 -4
- package/dist/components/virtualizedlist/virtualizedlist.constants.js +7 -4
- package/dist/components/virtualizedlist/virtualizedlist.styles.js +17 -3
- package/dist/components/virtualizedlist/virtualizedlist.types.d.ts +12 -10
- package/dist/components/virtualizedlist/virtualizedlist.utils.d.ts +11 -0
- package/dist/components/virtualizedlist/virtualizedlist.utils.js +23 -0
- package/dist/custom-elements.json +976 -305
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +2 -2
- package/dist/react/virtualizedlist/index.d.ts +44 -6
- package/dist/react/virtualizedlist/index.js +44 -6
- package/dist/utils/mixins/AutoFocusOnMountMixin.js +2 -2
- package/dist/utils/mixins/ListNavigationMixin.d.ts +5 -2
- package/dist/utils/mixins/ListNavigationMixin.js +77 -68
- package/dist/utils/mixins/lifecycle/LifeCycleMixin.js +4 -0
- package/dist/utils/mixins/lifecycle/lifecycle.contants.d.ts +1 -0
- package/dist/utils/mixins/lifecycle/lifecycle.contants.js +1 -0
- package/dist/utils/range.d.ts +40 -0
- package/dist/utils/range.js +66 -0
- package/dist/utils/virtualIndexArray.d.ts +27 -0
- package/dist/utils/virtualIndexArray.js +42 -0
- package/package.json +2 -2
- package/dist/components/virtualizedlist/virtualizedlist.helper.test.d.ts +0 -22
- package/dist/components/virtualizedlist/virtualizedlist.helper.test.js +0 -82
|
@@ -9,145 +9,664 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
};
|
|
10
10
|
import { html } from 'lit';
|
|
11
11
|
import { VirtualizerController } from '@tanstack/lit-virtual';
|
|
12
|
-
import { property } from 'lit/decorators.js';
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
12
|
+
import { property, eventOptions, query } from 'lit/decorators.js';
|
|
13
|
+
import { defaultRangeExtractor } from '@tanstack/virtual-core';
|
|
14
|
+
import List from '../list/list.component';
|
|
15
|
+
import { DataAriaLabelMixin } from '../../utils/mixins/DataAriaLabelMixin';
|
|
16
|
+
import { Interval } from '../../utils/range';
|
|
17
|
+
import { VirtualIndexArray } from '../../utils/virtualIndexArray';
|
|
18
|
+
import { KEYS } from '../../utils/keys';
|
|
19
|
+
import { LIFE_CYCLE_EVENTS } from '../../utils/mixins/lifecycle/lifecycle.contants';
|
|
15
20
|
import styles from './virtualizedlist.styles';
|
|
16
21
|
import { DEFAULTS } from './virtualizedlist.constants';
|
|
17
22
|
/**
|
|
18
|
-
* `mdc-virtualizedlist` component
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* `mdc-virtualizedlist` is an extension of the `mdc-list` component that adds virtualization capabilities using
|
|
24
|
+
* the Tanstack Virtual library.
|
|
25
|
+
*
|
|
26
|
+
* This component is thin wrapper around the Tanstack libray to provide additional funtionalities such as
|
|
27
|
+
* keyboard navigation, focus management, scroll anchoring and accessibility features.
|
|
28
|
+
*
|
|
23
29
|
* Please refer to [Tanstack Virtual Docs](https://tanstack.com/virtual/latest) for more in depth documentation.
|
|
24
30
|
*
|
|
31
|
+
* ## Setup
|
|
32
|
+
*
|
|
33
|
+
* `virtualizerProps` is a required prop that requires at least two properties to be set: `count` and `estimateSize`.
|
|
34
|
+
* `count` is the total number of items in the list, and `estimateSize` is a function that returns the estimated
|
|
35
|
+
* size (in pixels) of each item in the list. `getItemKey` is also strongly recommended to be set to provide unique
|
|
36
|
+
* keys for each item in the list.
|
|
37
|
+
*
|
|
38
|
+
* ### Render list items
|
|
39
|
+
*
|
|
40
|
+
* To keep the component framework-agnostic, the rendering of the list items is left to the consumer.
|
|
41
|
+
*
|
|
42
|
+
* We need to render only the visible items. The list of visible items are provided by the `virtualitemschange` event.
|
|
43
|
+
* The event detail contains the `virtualItems` array, which contains the information for the rendering.
|
|
44
|
+
* List items must have an `data-index` attribute, the indexes are in the `virtualItems` list.
|
|
45
|
+
*
|
|
46
|
+
* ## Best practices
|
|
47
|
+
*
|
|
48
|
+
* ### List updates
|
|
49
|
+
*
|
|
50
|
+
* Tanstack needs only the count of the items in the list and the size of each item to perform virtualization.
|
|
51
|
+
* List updates happens when
|
|
52
|
+
* - when `virtualizerProps` property of the component instance changes
|
|
53
|
+
* - when `observe-size-changes` is set and the item's size changes (it uses ResizeObserver internally)
|
|
54
|
+
* - when `component.visualiser.measure` called manually.
|
|
55
|
+
*
|
|
56
|
+
* ### Header
|
|
57
|
+
*
|
|
58
|
+
* To add a header to the list, use the `mdc-listheader` component and place it in the `list-header` slot.
|
|
59
|
+
*
|
|
60
|
+
* ### Lists with dynamic content
|
|
61
|
+
*
|
|
62
|
+
* Unique keys for the list items are critical for dynamically changing list items or item's content.
|
|
63
|
+
* If the key change with the content it will cause scrollbar and content shuttering.
|
|
64
|
+
*
|
|
25
65
|
* @tagname mdc-virtualizedlist
|
|
26
66
|
*
|
|
27
67
|
* @event scroll - (React: onScroll) Event that gets called when user scrolls inside of list.
|
|
68
|
+
* @event virtualitemschange - (React: onVirtualItemsChange) Event that gets called when the virtual items change.
|
|
28
69
|
*
|
|
29
|
-
* @slot -
|
|
70
|
+
* @slot default - This is a default/unnamed slot, where listitems can be placed.
|
|
71
|
+
* @slot list-header - This slot is used to pass a header for the list, which can be a `mdc-listheader` component.
|
|
30
72
|
*
|
|
31
73
|
* @csspart container - The container of the virtualized list.
|
|
32
74
|
* @csspart scroll - The scrollable area of the virtualized list.
|
|
33
75
|
*/
|
|
34
|
-
class VirtualizedList extends
|
|
76
|
+
class VirtualizedList extends DataAriaLabelMixin(List) {
|
|
77
|
+
/**
|
|
78
|
+
* The current virtual items being rendered.
|
|
79
|
+
*/
|
|
80
|
+
get virtualItems() {
|
|
81
|
+
var _a, _b;
|
|
82
|
+
return (_b = (_a = this.virtualizer) === null || _a === void 0 ? void 0 : _a.getVirtualItems()) !== null && _b !== void 0 ? _b : [];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
get navItems() {
|
|
88
|
+
var _a, _b, _c, _d;
|
|
89
|
+
if (((_a = this.virtualizedNavItems) === null || _a === void 0 ? void 0 : _a.items) !== super.navItems) {
|
|
90
|
+
const attrName = (_d = (_c = (_b = this.virtualizer) === null || _b === void 0 ? void 0 : _b.options) === null || _c === void 0 ? void 0 : _c.indexAttribute) !== null && _d !== void 0 ? _d : 'data-index';
|
|
91
|
+
this.virtualizedNavItems = new VirtualIndexArray(super.navItems, e => Number(e.getAttribute(attrName)), () => this.virtualizerProps.count);
|
|
92
|
+
}
|
|
93
|
+
return this.virtualizedNavItems;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Getter for atBottom
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
get atBottom() {
|
|
100
|
+
return this.atBottomValue;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Setter for atBottom to handle side effects when the value changes.
|
|
104
|
+
* @param value - new value for atBottom
|
|
105
|
+
*
|
|
106
|
+
* @internal
|
|
107
|
+
*/
|
|
108
|
+
set atBottom(value) {
|
|
109
|
+
if (this.atBottomValue !== value || (value === 'yes' && this.atBottomTimer === -1)) {
|
|
110
|
+
this.atBottomValue = value;
|
|
111
|
+
if (value === 'yes') {
|
|
112
|
+
this.scrollToBottom();
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.clearScrollToBottomTimer();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The total height of the list based on the virtualizer's calculations.
|
|
121
|
+
* @internal
|
|
122
|
+
*/
|
|
123
|
+
get totalListHeight() {
|
|
124
|
+
var _a, _b;
|
|
125
|
+
return (_b = (_a = this.virtualizer) === null || _a === void 0 ? void 0 : _a.getTotalSize()) !== null && _b !== void 0 ? _b : 0;
|
|
126
|
+
}
|
|
35
127
|
constructor() {
|
|
36
128
|
super();
|
|
37
129
|
/**
|
|
38
130
|
* Object that sets and updates the virtualizer with any relevant props.
|
|
39
|
-
* There are
|
|
40
|
-
*
|
|
131
|
+
* There are three required object props in order to get virtualization to work properly.
|
|
132
|
+
*
|
|
133
|
+
* **count** - The length of your list that you are virtualizing.
|
|
41
134
|
* As your list grows/shrinks, this component must be updated with the appropriate value
|
|
42
135
|
* (Same with any other updated prop).
|
|
43
|
-
*
|
|
136
|
+
*
|
|
137
|
+
* **estimateSize** - A function that returns the estimated size of your items.
|
|
44
138
|
* If your list is fixed, this will just be the size of your items.
|
|
45
139
|
* If your list is dynamic, try to return approximate the size of each item.
|
|
46
140
|
*
|
|
141
|
+
* **getItemKey** - A function that returns a unique key for each item in your list based on its index.
|
|
142
|
+
*
|
|
47
143
|
* A full list of possible props can be in
|
|
48
144
|
* [Tanstack Virtualizer API Docs](https://tanstack.com/virtual/latest/docs/api/virtualizer)
|
|
49
145
|
*
|
|
50
146
|
*/
|
|
51
147
|
this.virtualizerProps = DEFAULTS.VIRTUALIZER_PROPS;
|
|
52
|
-
|
|
53
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Whether to loop navigation when reaching the end of the list.
|
|
150
|
+
* If 'true', pressing the down arrow on the last item will focus the first item,
|
|
151
|
+
* and pressing the up arrow on the first item will focus the last item.
|
|
152
|
+
* If 'false', navigation will stop at the first or last item.
|
|
153
|
+
*
|
|
154
|
+
* @default 'false'
|
|
155
|
+
*/
|
|
156
|
+
this.loop = DEFAULTS.LOOP;
|
|
157
|
+
/**
|
|
158
|
+
* Enable automatic scroll anchoring when the list size changes.
|
|
159
|
+
* By default, list does not scroll to the very end it keeps the scroll position otherwise
|
|
160
|
+
* it try hold the scroll position on the last selected when list updates.
|
|
161
|
+
*
|
|
162
|
+
* It is handy when list size or list item sizes change dynamically (e.g., incoming messages in a chat app).
|
|
163
|
+
*
|
|
164
|
+
* @default false
|
|
165
|
+
*/
|
|
166
|
+
this.scrollAnchoring = DEFAULTS.SCROLL_ANCHORING;
|
|
167
|
+
/**
|
|
168
|
+
* When true, the list will observe size changes of its items and re-measure them as needed.
|
|
169
|
+
* This is useful if your list items can change size dynamically (e.g., due to content changes or window resizing).
|
|
170
|
+
*/
|
|
171
|
+
this.observeSizeChanges = false;
|
|
172
|
+
/**
|
|
173
|
+
* When true, the list items will be aligned to the bottom of the list, and it anchors scroll to the bottom
|
|
174
|
+
* until the first user scroll interaction.
|
|
175
|
+
*
|
|
176
|
+
* Note: It does not affect on the rendering order, the first item is still at the top of the list.
|
|
177
|
+
*/
|
|
178
|
+
this.revertList = false;
|
|
179
|
+
/**
|
|
180
|
+
* The maximum gap (in pixels) between the very bottom of the list end the current scroll positioning
|
|
181
|
+
* It is used to calculate scroll anchoring.
|
|
182
|
+
*/
|
|
183
|
+
this.atBottomThreshold = DEFAULTS.IS_AT_BOTTOM_THRESHOLD;
|
|
184
|
+
/**
|
|
185
|
+
* The virtualizer instance created by the VirtualizerController.
|
|
186
|
+
*/
|
|
187
|
+
this.virtualizer = null;
|
|
188
|
+
/**
|
|
189
|
+
* @internal
|
|
190
|
+
*/
|
|
191
|
+
this.virtualizedNavItems = null;
|
|
192
|
+
/**
|
|
193
|
+
* @internal
|
|
194
|
+
*/
|
|
195
|
+
this.virtualizerController = null;
|
|
196
|
+
/**
|
|
197
|
+
* The currently selected index in the list.
|
|
198
|
+
* If keyboard navigation is being used, this will be the focused item.
|
|
199
|
+
* If the list is clicked, this will be the last clicked item.
|
|
200
|
+
* @internal
|
|
201
|
+
*/
|
|
202
|
+
this.selectedIndex = this.initialFocus;
|
|
203
|
+
/**
|
|
204
|
+
* The key of the currently selected item.
|
|
205
|
+
* This is used to keep track where the selected item goes when the list changes size.
|
|
206
|
+
* @internal
|
|
207
|
+
*/
|
|
208
|
+
this.selectedKey = null;
|
|
209
|
+
/**
|
|
210
|
+
* The index of the first item in the current virtual items.
|
|
211
|
+
* @internal
|
|
212
|
+
*/
|
|
213
|
+
this.firstIndex = 0;
|
|
214
|
+
/**
|
|
215
|
+
* The key of the first item in the current virtual items.
|
|
216
|
+
* @internal
|
|
217
|
+
*/
|
|
218
|
+
this.firstKey = null;
|
|
219
|
+
/**
|
|
220
|
+
* The indexes of items that are not in the current virtual items, but need to be rendered for focus purposes.
|
|
221
|
+
* @internal
|
|
222
|
+
*/
|
|
223
|
+
this.hiddenIndexes = [];
|
|
224
|
+
/**
|
|
225
|
+
* Is the scroll position at the bottom of the list?
|
|
226
|
+
*
|
|
227
|
+
* - 'no' - The scroll position is not at the bottom of the list.
|
|
228
|
+
* - 'yes' - The scroll position is at the bottom of the list.
|
|
229
|
+
* - 're-evaluate' - The scroll position needs to be re-evaluated on the next scroll event.
|
|
230
|
+
* @internal
|
|
231
|
+
*/
|
|
232
|
+
this.atBottomValue = 'no';
|
|
233
|
+
/**
|
|
234
|
+
* rAF ID for the scroll to bottom action when atBottom is 'yes'.
|
|
235
|
+
* @internal
|
|
236
|
+
*/
|
|
237
|
+
this.atBottomTimer = -1;
|
|
238
|
+
/**
|
|
239
|
+
* Last recorded scroll position to help determine scroll direction.
|
|
240
|
+
* @internal
|
|
241
|
+
*/
|
|
242
|
+
this.lastScrollPosition = 0;
|
|
243
|
+
/**
|
|
244
|
+
* List of functions executed aster the virtualizer finishes scrolling.
|
|
245
|
+
* @internal
|
|
246
|
+
*/
|
|
247
|
+
this.endOfScrollQueue = [];
|
|
248
|
+
this.handleElementFirstUpdateCompleted = (event) => {
|
|
249
|
+
var _a, _b;
|
|
250
|
+
if (this.observeSizeChanges && this.navItems.find(el => el === event.target) !== undefined) {
|
|
251
|
+
(_b = (_a = this.virtualizer) === null || _a === void 0 ? void 0 : _a.measureElement) === null || _b === void 0 ? void 0 : _b.call(_a, event.target);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
this.addEventListener('wheel', this.handleWheelEvent.bind(this));
|
|
255
|
+
this.addEventListener(LIFE_CYCLE_EVENTS.FIRST_UPDATE_COMPLETED, this.handleElementFirstUpdateCompleted.bind(this));
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Create the virtualizer controller and the virtualizer instance when the component is first connected to the DOM.
|
|
259
|
+
*/
|
|
260
|
+
connectedCallback() {
|
|
261
|
+
this.virtualizerController = new VirtualizerController(this, {
|
|
262
|
+
...this.virtualizerProps,
|
|
263
|
+
horizontal: false,
|
|
264
|
+
getScrollElement: () => this.scrollRef,
|
|
265
|
+
onChange: this.onVListStateChangeHandler.bind(this),
|
|
266
|
+
rangeExtractor: this.virtualizerRangeExtractor.bind(this),
|
|
267
|
+
});
|
|
268
|
+
this.virtualizer = this.virtualizerController.getVirtualizer();
|
|
269
|
+
super.connectedCallback();
|
|
270
|
+
// Set the role attribute for accessibility.
|
|
271
|
+
this.role = null;
|
|
272
|
+
this.atBottom = this.revertList && this.scrollAnchoring ? 'yes' : 'no';
|
|
273
|
+
}
|
|
274
|
+
disconnectedCallback() {
|
|
275
|
+
super.disconnectedCallback();
|
|
276
|
+
this.clearScrollToBottomTimer();
|
|
54
277
|
this.virtualizerController = null;
|
|
55
278
|
this.virtualizer = null;
|
|
56
|
-
this.setlistdata = null;
|
|
57
|
-
this.onscroll = null;
|
|
58
279
|
}
|
|
59
280
|
/**
|
|
60
281
|
* This override is necessary to update the virtualizer with relevant props
|
|
61
282
|
* if the client updates any props (most commonly, count). Updating the options
|
|
62
283
|
* this way ensures we don't initialize a new virtualizer upon very prop change.
|
|
63
284
|
*/
|
|
64
|
-
update(changedProperties) {
|
|
285
|
+
async update(changedProperties) {
|
|
65
286
|
super.update(changedProperties);
|
|
66
|
-
if (changedProperties.
|
|
67
|
-
this.
|
|
287
|
+
if (changedProperties.has('virtualizerProps')) {
|
|
288
|
+
await this.handleVirtualizerPropsUpdate(changedProperties.get('virtualizerProps'));
|
|
289
|
+
}
|
|
290
|
+
if (changedProperties.has('observeSizeChanges')) {
|
|
291
|
+
this.navItems.forEach(item => {
|
|
292
|
+
var _a, _b;
|
|
293
|
+
if (this.observeSizeChanges) {
|
|
294
|
+
(_b = (_a = this.virtualizer) === null || _a === void 0 ? void 0 : _a.measureElement) === null || _b === void 0 ? void 0 : _b.call(_a, item);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if (changedProperties.has('scrollAnchoring')) {
|
|
299
|
+
if (this.scrollAnchoring) {
|
|
300
|
+
this.checkAtBottom();
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
this.atBottom = 'no';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (changedProperties.has('revertList') && this.revertList && this.scrollAnchoring) {
|
|
307
|
+
this.atBottom = 'yes';
|
|
308
|
+
}
|
|
309
|
+
if (changedProperties.has('atBottomThreshold') && this.scrollAnchoring) {
|
|
310
|
+
this.checkAtBottom();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Handles updates to the virtualizerProps property.
|
|
315
|
+
* @param prevProps - The previous virtualizerProps before the update.
|
|
316
|
+
* @internal
|
|
317
|
+
*/
|
|
318
|
+
async handleVirtualizerPropsUpdate(prevProps) {
|
|
319
|
+
var _a, _b;
|
|
320
|
+
const { virtualizer } = this;
|
|
321
|
+
if (!virtualizer)
|
|
322
|
+
return;
|
|
323
|
+
const prevMeasurements = virtualizer.measurementsCache.slice();
|
|
324
|
+
virtualizer.setOptions({
|
|
325
|
+
...virtualizer.options,
|
|
326
|
+
...this.virtualizerProps,
|
|
327
|
+
});
|
|
328
|
+
// Change in the length of the dataset is, does not count as TanStack Virtual's internal state change
|
|
329
|
+
// so we need to manually call the onChange handler to ensure the list updates correctly.
|
|
330
|
+
if (this.virtualizerProps.count !== (prevProps === null || prevProps === void 0 ? void 0 : prevProps.count)) {
|
|
331
|
+
this.emitChangeEvent();
|
|
332
|
+
this.syncUI();
|
|
333
|
+
}
|
|
334
|
+
if (this.scrollAnchoring && prevMeasurements.length > 0) {
|
|
335
|
+
const countDiff = Math.abs(this.virtualizerProps.count - ((_a = prevProps === null || prevProps === void 0 ? void 0 : prevProps.count) !== null && _a !== void 0 ? _a : 0));
|
|
336
|
+
const prevSelectedIndex = this.selectedIndex;
|
|
337
|
+
const prevFirstKey = this.firstKey;
|
|
338
|
+
const searchRange = new Interval(prevSelectedIndex - countDiff, prevSelectedIndex + countDiff, {
|
|
339
|
+
includeEnd: true,
|
|
340
|
+
});
|
|
341
|
+
const newSelectedIndex = (_b = Array.from(searchRange).find(i => virtualizer.options.getItemKey(i) === this.selectedKey)) !== null && _b !== void 0 ? _b : this.selectedIndex;
|
|
342
|
+
this.setSelectedIndex(newSelectedIndex);
|
|
343
|
+
// Wait for the virtualizer to finish updating before we read the new scrollHeight
|
|
344
|
+
this.requestUpdate();
|
|
345
|
+
await this.updateComplete;
|
|
346
|
+
if (this.atBottom === 'yes') {
|
|
347
|
+
this.scrollToBottom();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
// If the user has focus within the list, use the selected index as the anchor point for scroll adjustment.
|
|
351
|
+
// If the user does not have focus within the list, use the first visible item as the anchor point for scroll adjustment.
|
|
352
|
+
const shouldAdjustScroll = (this.focusWithin && prevSelectedIndex !== this.selectedIndex) ||
|
|
353
|
+
(!this.focusWithin && this.firstKey !== prevFirstKey);
|
|
354
|
+
// Update the scroll position to keep the same items in view (and in roughly the same position)
|
|
355
|
+
if (shouldAdjustScroll) {
|
|
356
|
+
const scrollDiff = virtualizer.measurementsCache[this.selectedIndex].end - prevMeasurements[prevSelectedIndex].end;
|
|
357
|
+
this.scrollRef.scrollTop += scrollDiff;
|
|
358
|
+
}
|
|
68
359
|
}
|
|
69
360
|
}
|
|
70
361
|
/**
|
|
71
|
-
*
|
|
362
|
+
* Sets the initial focus of the list based on the `initial-focus` prop and scrolls the item into view.
|
|
72
363
|
*/
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
364
|
+
setInitialFocus() {
|
|
365
|
+
setTimeout(async () => {
|
|
366
|
+
if (!this.virtualizer)
|
|
367
|
+
return;
|
|
368
|
+
const { scrollToIndex } = this.virtualizer;
|
|
369
|
+
this.setSelectedIndex(this.initialFocus);
|
|
370
|
+
if (this.selectedIndex >= this.navItems.length) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
scrollToIndex(this.selectedIndex, { align: 'center' });
|
|
374
|
+
// We try to set the tabIndex immediately,
|
|
375
|
+
// but if the items haven't been rendered, yet we leave the work to onValidElementAdded
|
|
376
|
+
const selectedElement = this.navItems.find(this.isElementSelected, this);
|
|
377
|
+
if (selectedElement) {
|
|
378
|
+
this.resetTabIndexes(this.selectedIndex, false);
|
|
379
|
+
}
|
|
380
|
+
}, 0);
|
|
381
|
+
}
|
|
382
|
+
emitChangeEvent() {
|
|
383
|
+
var _a, _b, _c, _d, _e;
|
|
384
|
+
(_b = (_a = this.virtualizerProps).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, this.virtualizer, this.virtualizer.isScrolling);
|
|
385
|
+
const eventDetails = {
|
|
386
|
+
virtualizer: this.virtualizer,
|
|
387
|
+
virtualItems: (_e = (_d = (_c = this.virtualizer) === null || _c === void 0 ? void 0 : _c.getVirtualItems) === null || _d === void 0 ? void 0 : _d.call(_c)) !== null && _e !== void 0 ? _e : [],
|
|
388
|
+
};
|
|
389
|
+
this.dispatchEvent(new CustomEvent('virtualitemschange', { detail: eventDetails, bubbles: false, cancelable: false }));
|
|
76
390
|
}
|
|
77
391
|
/**
|
|
392
|
+
* Calculates the array of indexes to render. We add the selected index (and +1/-1) if it's
|
|
393
|
+
* outside the current range so the focus can be kept correctly.
|
|
394
|
+
*
|
|
395
|
+
* @param range - The current range of items being rendered
|
|
396
|
+
* @returns An array of indexes to render, including the selected index if it's outside the current range.
|
|
78
397
|
* @internal
|
|
79
|
-
* Update virtuailzer with the union of the two virtualizer options (current, passed in).
|
|
80
398
|
*/
|
|
81
|
-
|
|
82
|
-
var _a;
|
|
83
|
-
|
|
84
|
-
|
|
399
|
+
virtualizerRangeExtractor(range) {
|
|
400
|
+
var _a, _b, _c, _d;
|
|
401
|
+
const { navItems, virtualizerProps, virtualizer } = this;
|
|
402
|
+
const defaultIndexes = (_b = (_a = virtualizerProps.rangeExtractor) === null || _a === void 0 ? void 0 : _a.call(virtualizerProps, range)) !== null && _b !== void 0 ? _b : defaultRangeExtractor(range);
|
|
403
|
+
this.hiddenIndexes.forEach(index => {
|
|
404
|
+
const el = navItems.at(index);
|
|
405
|
+
if (el) {
|
|
406
|
+
el.removeAttribute('data-virtualized-hidden');
|
|
407
|
+
el.style.removeProperty('--mdc-virtualizedlist-hidden-top');
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
this.firstIndex = range.startIndex;
|
|
411
|
+
this.firstKey = (_d = (_c = this.virtualizer) === null || _c === void 0 ? void 0 : _c.options.getItemKey(this.firstIndex)) !== null && _d !== void 0 ? _d : null;
|
|
412
|
+
this.hiddenIndexes.length = 0;
|
|
413
|
+
const stickyIndexes = [this.selectedIndex - 1, this.selectedIndex, this.selectedIndex + 1];
|
|
414
|
+
stickyIndexes.forEach(index => {
|
|
415
|
+
var _a;
|
|
416
|
+
if (!defaultIndexes.includes(index)) {
|
|
417
|
+
if (index < range.startIndex && index >= 0) {
|
|
418
|
+
defaultIndexes.unshift(index);
|
|
419
|
+
this.hiddenIndexes.push(index);
|
|
420
|
+
}
|
|
421
|
+
if (index > range.endIndex && index < ((_a = virtualizer === null || virtualizer === void 0 ? void 0 : virtualizer.options.count) !== null && _a !== void 0 ? _a : 0)) {
|
|
422
|
+
defaultIndexes.push(index);
|
|
423
|
+
this.hiddenIndexes.push(index);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
this.updateHiddenItemsPosition();
|
|
428
|
+
defaultIndexes.sort((a, b) => a - b);
|
|
429
|
+
return defaultIndexes;
|
|
85
430
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
431
|
+
/** @internal */
|
|
432
|
+
updateHiddenItemsPosition() {
|
|
433
|
+
const { navItems, virtualizerProps, virtualizer } = this;
|
|
434
|
+
if (!virtualizer || virtualizerProps.count === 0)
|
|
435
|
+
return;
|
|
436
|
+
const { measurementsCache, range } = virtualizer;
|
|
437
|
+
this.hiddenIndexes.forEach(index => {
|
|
438
|
+
var _a;
|
|
439
|
+
const el = navItems.at(index);
|
|
440
|
+
if (el) {
|
|
441
|
+
const first = measurementsCache[(_a = range === null || range === void 0 ? void 0 : range.startIndex) !== null && _a !== void 0 ? _a : 0];
|
|
442
|
+
const current = measurementsCache[index];
|
|
443
|
+
el.setAttribute('data-virtualized-hidden', 'true');
|
|
444
|
+
el.style.setProperty('--mdc-virtualizedlist-hidden-top', `${current.start - first.start}px`);
|
|
445
|
+
}
|
|
93
446
|
});
|
|
94
|
-
|
|
447
|
+
}
|
|
448
|
+
/** @internal */
|
|
449
|
+
isElementSelected(item) {
|
|
450
|
+
var _a;
|
|
451
|
+
return ((_a = this.virtualizer) === null || _a === void 0 ? void 0 : _a.indexFromElement(item)) === this.selectedIndex;
|
|
452
|
+
}
|
|
453
|
+
/** @internal */
|
|
454
|
+
setSelectedIndex(newIndex) {
|
|
455
|
+
if (this.virtualizer) {
|
|
456
|
+
const { count, getItemKey } = this.virtualizer.options;
|
|
457
|
+
this.selectedIndex = Math.max(0, Math.min(count - 1, newIndex));
|
|
458
|
+
this.selectedKey = getItemKey(newIndex);
|
|
459
|
+
if (this.scrollAnchoring && this.selectedIndex + 1 === count) {
|
|
460
|
+
this.atBottom = 'yes';
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
onElementStoreUpdate(item, changeType) {
|
|
465
|
+
if (changeType === 'added') {
|
|
466
|
+
const tabable = this.isElementSelected(item);
|
|
467
|
+
// eslint-disable-next-line no-param-reassign
|
|
468
|
+
item.tabIndex = tabable ? 0 : -1;
|
|
469
|
+
this.setAriaSetSize(item);
|
|
470
|
+
}
|
|
471
|
+
else if (changeType === 'removed') {
|
|
472
|
+
if (item.tabIndex === 0) {
|
|
473
|
+
// We must queue the reset because onElementStoreUpdate called before the `navItems` updated,
|
|
474
|
+
// but `resetTabIndexes` expecting the updated virtual list.
|
|
475
|
+
queueMicrotask(() => {
|
|
476
|
+
this.resetTabIndexes(this.selectedIndex, this.focusWithin);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
95
480
|
}
|
|
96
481
|
/**
|
|
482
|
+
* Handle the virtualizer's onChange event to emit the virtualitemschange event
|
|
483
|
+
* This is called when the internal state of the virtualizer changes.
|
|
484
|
+
*
|
|
97
485
|
* @internal
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
486
|
+
*/
|
|
487
|
+
async onVListStateChangeHandler(_, isScrolling) {
|
|
488
|
+
this.syncUI();
|
|
489
|
+
// Request an update, this is in Tanstack's VirtualizerController but gets overridden when updating the
|
|
490
|
+
// virtualizer's options therefore we need to call it here ourselves.
|
|
491
|
+
await this.updateComplete;
|
|
492
|
+
this.requestUpdate();
|
|
493
|
+
if (!isScrolling && this.endOfScrollQueue.length > 0) {
|
|
494
|
+
this.endOfScrollQueue.forEach(fn => fn());
|
|
495
|
+
this.endOfScrollQueue.length = 0;
|
|
496
|
+
}
|
|
497
|
+
this.checkAtBottom();
|
|
498
|
+
this.emitChangeEvent();
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Refires the scroll event from the internal scroll container to the host element.
|
|
502
|
+
* Also updates whether the scroll is at the bottom of the list for scroll anchoring purposes.
|
|
102
503
|
*
|
|
103
|
-
* @
|
|
504
|
+
* @internal
|
|
104
505
|
*/
|
|
105
|
-
|
|
506
|
+
onScrollHandler(event) {
|
|
507
|
+
const scrollEl = event.target;
|
|
508
|
+
// Skip the current Scroll event to prevent shattering
|
|
509
|
+
if (this.atBottom === 're-evaluate' || scrollEl.scrollTop < this.lastScrollPosition) {
|
|
510
|
+
this.atBottom = 'no';
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
// Check if we are at the bottom of the list
|
|
514
|
+
this.checkAtBottom();
|
|
515
|
+
}
|
|
516
|
+
this.lastScrollPosition = scrollEl.scrollTop;
|
|
517
|
+
}
|
|
518
|
+
checkAtBottom() {
|
|
519
|
+
const { clientHeight, scrollHeight, scrollTop } = this.scrollRef;
|
|
520
|
+
if (this.scrollAnchoring &&
|
|
521
|
+
this.virtualizer &&
|
|
522
|
+
this.atBottom === 'no' &&
|
|
523
|
+
scrollHeight > clientHeight - this.atBottomThreshold &&
|
|
524
|
+
!this.virtualizer.isScrolling) {
|
|
525
|
+
this.atBottom = scrollHeight - scrollTop <= clientHeight + this.atBottomThreshold ? 'yes' : 'no';
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
handleNavigationKeyDown(event) {
|
|
529
|
+
var _a, _b, _c, _d;
|
|
530
|
+
switch (event.key) {
|
|
531
|
+
case KEYS.HOME: {
|
|
532
|
+
// Move focus to the first item
|
|
533
|
+
(_b = (_a = this.virtualizer) === null || _a === void 0 ? void 0 : _a.scrollToIndex) === null || _b === void 0 ? void 0 : _b.call(_a, 0, { align: 'start' });
|
|
534
|
+
this.endOfScrollQueue.push(() => this.resetTabIndexes(0));
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
case KEYS.END: {
|
|
538
|
+
// Move focus to the last item
|
|
539
|
+
const selectedItem = this.virtualizerProps.count - 1;
|
|
540
|
+
(_d = (_c = this.virtualizer) === null || _c === void 0 ? void 0 : _c.scrollToIndex) === null || _d === void 0 ? void 0 : _d.call(_c, selectedItem, { align: 'end' });
|
|
541
|
+
this.endOfScrollQueue.push(() => this.resetTabIndexes(selectedItem));
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
case KEYS.ARROW_UP: {
|
|
545
|
+
this.atBottom = 're-evaluate';
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
default:
|
|
549
|
+
}
|
|
550
|
+
super.handleNavigationKeyDown(event);
|
|
551
|
+
}
|
|
552
|
+
resetTabIndexes(index, focusElement = true) {
|
|
553
|
+
super.resetTabIndexes(index, focusElement);
|
|
554
|
+
this.setSelectedIndex(index);
|
|
555
|
+
}
|
|
556
|
+
resetTabIndexAndSetFocus(newIndex, oldIndex, focusNewItem) {
|
|
557
|
+
const elementToFocus = this.navItems.find(element => { var _a; return ((_a = this.virtualizer) === null || _a === void 0 ? void 0 : _a.indexFromElement(element)) === newIndex; });
|
|
558
|
+
if (elementToFocus === undefined) {
|
|
559
|
+
this.scrollToIndex(newIndex, {});
|
|
560
|
+
this.endOfScrollQueue.push(() => {
|
|
561
|
+
super.resetTabIndexAndSetFocus(newIndex, oldIndex, focusNewItem);
|
|
562
|
+
this.setSelectedIndex(newIndex);
|
|
563
|
+
});
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
super.resetTabIndexAndSetFocus(newIndex, oldIndex, focusNewItem);
|
|
567
|
+
this.setSelectedIndex(newIndex);
|
|
568
|
+
}
|
|
569
|
+
/** @internal */
|
|
570
|
+
setAriaSetSize(element) {
|
|
106
571
|
var _a, _b;
|
|
107
|
-
this.virtualizer
|
|
108
|
-
const { getTotalSize, getVirtualItems, measureElement } = this.virtualizer;
|
|
109
|
-
const newVirtualItems = getVirtualItems();
|
|
110
|
-
// Only update client if there's a difference in virtual items
|
|
111
|
-
if (newVirtualItems !== this.virtualItems) {
|
|
112
|
-
this.virtualItems = newVirtualItems;
|
|
113
|
-
const virtualItems = getVirtualItems();
|
|
114
|
-
// this style is required to be rendered by the client side list in order to handle scrolling properly
|
|
115
|
-
const listStyle = {
|
|
116
|
-
position: 'absolute',
|
|
117
|
-
top: 0,
|
|
118
|
-
left: 0,
|
|
119
|
-
width: '100%',
|
|
120
|
-
transform: `translateY(${(_b = (_a = virtualItems[0]) === null || _a === void 0 ? void 0 : _a.start) !== null && _b !== void 0 ? _b : 0}px)`,
|
|
121
|
-
};
|
|
122
|
-
// pass back data to client for rendering
|
|
123
|
-
if (this.setlistdata) {
|
|
124
|
-
this.setlistdata({ virtualItems, measureElement, listStyle });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return html `<div part="container" style="height: ${getTotalSize()}px;">
|
|
128
|
-
<slot></slot>
|
|
129
|
-
</div>`;
|
|
572
|
+
element.setAttribute('aria-setsize', `${(_b = (_a = this.virtualizer) === null || _a === void 0 ? void 0 : _a.options.count) !== null && _b !== void 0 ? _b : -1}`);
|
|
130
573
|
}
|
|
131
574
|
/**
|
|
132
|
-
*
|
|
575
|
+
* Scrolls to the bottom of the list if `atBottom` is 'yes'.
|
|
576
|
+
* @internal
|
|
133
577
|
*/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.
|
|
578
|
+
scrollToBottom() {
|
|
579
|
+
this.clearScrollToBottomTimer();
|
|
580
|
+
if (this.atBottom === 'yes' && this.scrollRef) {
|
|
581
|
+
const { clientHeight, scrollHeight, scrollTop } = this.scrollRef;
|
|
582
|
+
if (this.totalListHeight > clientHeight) {
|
|
583
|
+
this.scrollRef.scrollTop += scrollHeight - clientHeight - scrollTop;
|
|
584
|
+
}
|
|
585
|
+
this.atBottomTimer = requestAnimationFrame(this.scrollToBottom.bind(this));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
clearScrollToBottomTimer() {
|
|
589
|
+
cancelAnimationFrame(this.atBottomTimer);
|
|
590
|
+
this.atBottomTimer = -1;
|
|
591
|
+
}
|
|
592
|
+
scrollToIndex(index, options) {
|
|
593
|
+
var _a, _b;
|
|
594
|
+
(_b = (_a = this.virtualizer).scrollToIndex) === null || _b === void 0 ? void 0 : _b.call(_a, index, options);
|
|
595
|
+
this.atBottom = this.scrollAnchoring && index + 1 === this.virtualizerProps.count ? 'yes' : 'no';
|
|
596
|
+
}
|
|
597
|
+
syncUI() {
|
|
598
|
+
var _a;
|
|
599
|
+
const visibleItems = this.virtualItems.find(({ index }) => !this.hiddenIndexes.includes(index));
|
|
600
|
+
const firstItemOffset = (_a = visibleItems === null || visibleItems === void 0 ? void 0 : visibleItems.start) !== null && _a !== void 0 ? _a : 0;
|
|
601
|
+
window.getComputedStyle(this);
|
|
602
|
+
let initialOffset = 0;
|
|
603
|
+
if (this.revertList) {
|
|
604
|
+
if (this.scrollRef.clientHeight >= this.totalListHeight) {
|
|
605
|
+
initialOffset = this.scrollRef.clientHeight - this.totalListHeight;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
this.wrapperRef.style.height = `${this.totalListHeight}px`;
|
|
609
|
+
this.containerRef.style.transform = `translateY(${initialOffset + firstItemOffset}px)`;
|
|
610
|
+
}
|
|
611
|
+
handleWheelEvent(e) {
|
|
612
|
+
if (e.deltaY < 0)
|
|
613
|
+
this.atBottom = 're-evaluate';
|
|
137
614
|
}
|
|
138
615
|
render() {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
616
|
+
var _a;
|
|
617
|
+
return html `
|
|
618
|
+
<slot name="list-header"></slot>
|
|
619
|
+
<div part="scroll" tabindex="-1" @scroll="${this.onScrollHandler}">
|
|
620
|
+
<div part="wrapper">
|
|
621
|
+
<div part="container" role="list" aria-label="${(_a = this.dataAriaLabel) !== null && _a !== void 0 ? _a : ''}">
|
|
622
|
+
<slot role="presentation"></slot>
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
626
|
+
`;
|
|
142
627
|
}
|
|
143
628
|
}
|
|
144
|
-
VirtualizedList.styles = [...
|
|
629
|
+
VirtualizedList.styles = [...List.styles, ...styles];
|
|
145
630
|
__decorate([
|
|
146
|
-
property({ type: Object
|
|
631
|
+
property({ type: Object }),
|
|
147
632
|
__metadata("design:type", Object)
|
|
148
633
|
], VirtualizedList.prototype, "virtualizerProps", void 0);
|
|
149
634
|
__decorate([
|
|
150
|
-
property({ type:
|
|
151
|
-
__metadata("design:type",
|
|
152
|
-
], VirtualizedList.prototype, "
|
|
635
|
+
property({ type: String, reflect: true }),
|
|
636
|
+
__metadata("design:type", String)
|
|
637
|
+
], VirtualizedList.prototype, "loop", void 0);
|
|
638
|
+
__decorate([
|
|
639
|
+
property({ type: Boolean, attribute: 'scroll-anchoring', reflect: true }),
|
|
640
|
+
__metadata("design:type", Boolean)
|
|
641
|
+
], VirtualizedList.prototype, "scrollAnchoring", void 0);
|
|
642
|
+
__decorate([
|
|
643
|
+
property({ type: Boolean, reflect: true, attribute: 'observe-size-changes' }),
|
|
644
|
+
__metadata("design:type", Boolean)
|
|
645
|
+
], VirtualizedList.prototype, "observeSizeChanges", void 0);
|
|
646
|
+
__decorate([
|
|
647
|
+
property({ type: Boolean, reflect: true, attribute: 'revert-list' }),
|
|
648
|
+
__metadata("design:type", Boolean)
|
|
649
|
+
], VirtualizedList.prototype, "revertList", void 0);
|
|
650
|
+
__decorate([
|
|
651
|
+
property({ type: Number, reflect: true, attribute: 'at-bottom-threshold' }),
|
|
652
|
+
__metadata("design:type", Number)
|
|
653
|
+
], VirtualizedList.prototype, "atBottomThreshold", void 0);
|
|
654
|
+
__decorate([
|
|
655
|
+
query('[part="scroll"]', true),
|
|
656
|
+
__metadata("design:type", HTMLElement)
|
|
657
|
+
], VirtualizedList.prototype, "scrollRef", void 0);
|
|
658
|
+
__decorate([
|
|
659
|
+
query('[part="wrapper"]', true),
|
|
660
|
+
__metadata("design:type", HTMLElement)
|
|
661
|
+
], VirtualizedList.prototype, "wrapperRef", void 0);
|
|
662
|
+
__decorate([
|
|
663
|
+
query('[part="container"]', true),
|
|
664
|
+
__metadata("design:type", HTMLElement)
|
|
665
|
+
], VirtualizedList.prototype, "containerRef", void 0);
|
|
666
|
+
__decorate([
|
|
667
|
+
eventOptions({ passive: true }),
|
|
668
|
+
__metadata("design:type", Function),
|
|
669
|
+
__metadata("design:paramtypes", [Event]),
|
|
670
|
+
__metadata("design:returntype", void 0)
|
|
671
|
+
], VirtualizedList.prototype, "onScrollHandler", null);
|
|
153
672
|
export default VirtualizedList;
|