@momentum-design/components 0.122.5 → 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.
Files changed (36) hide show
  1. package/dist/browser/index.js +428 -409
  2. package/dist/browser/index.js.map +4 -4
  3. package/dist/components/list/list.component.d.ts +12 -17
  4. package/dist/components/list/list.component.js +29 -39
  5. package/dist/components/listitem/listitem.component.d.ts +10 -0
  6. package/dist/components/listitem/listitem.component.js +7 -0
  7. package/dist/components/popover/popover.component.d.ts +7 -0
  8. package/dist/components/popover/popover.component.js +13 -0
  9. package/dist/components/popover/popover.constants.d.ts +1 -0
  10. package/dist/components/popover/popover.constants.js +1 -0
  11. package/dist/components/virtualizedlist/virtualizedlist.component.d.ts +244 -41
  12. package/dist/components/virtualizedlist/virtualizedlist.component.js +597 -78
  13. package/dist/components/virtualizedlist/virtualizedlist.constants.d.ts +7 -4
  14. package/dist/components/virtualizedlist/virtualizedlist.constants.js +7 -4
  15. package/dist/components/virtualizedlist/virtualizedlist.styles.js +17 -3
  16. package/dist/components/virtualizedlist/virtualizedlist.types.d.ts +12 -10
  17. package/dist/components/virtualizedlist/virtualizedlist.utils.d.ts +11 -0
  18. package/dist/components/virtualizedlist/virtualizedlist.utils.js +23 -0
  19. package/dist/custom-elements.json +2778 -1975
  20. package/dist/react/index.d.ts +7 -7
  21. package/dist/react/index.js +7 -7
  22. package/dist/react/virtualizedlist/index.d.ts +44 -6
  23. package/dist/react/virtualizedlist/index.js +44 -6
  24. package/dist/utils/mixins/AutoFocusOnMountMixin.js +2 -2
  25. package/dist/utils/mixins/ListNavigationMixin.d.ts +5 -2
  26. package/dist/utils/mixins/ListNavigationMixin.js +77 -68
  27. package/dist/utils/mixins/lifecycle/LifeCycleMixin.js +4 -0
  28. package/dist/utils/mixins/lifecycle/lifecycle.contants.d.ts +1 -0
  29. package/dist/utils/mixins/lifecycle/lifecycle.contants.js +1 -0
  30. package/dist/utils/range.d.ts +40 -0
  31. package/dist/utils/range.js +66 -0
  32. package/dist/utils/virtualIndexArray.d.ts +27 -0
  33. package/dist/utils/virtualIndexArray.js +42 -0
  34. package/package.json +2 -2
  35. package/dist/components/virtualizedlist/virtualizedlist.helper.test.d.ts +0 -22
  36. 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 { createRef, ref } from 'lit/directives/ref.js';
14
- import { Component } from '../../models';
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 for creating custom virtualized lists.
19
- * IMPORTANT: This component does not create it's own list/list items.
20
- * Use the setlistdata callback prop to update client state in order to
21
- * Pass list/listitems as a child of this component, which this will virtuailze
22
- * This implementation handles dynamic lists as well as fixed sized lists.
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 - Client side List with nested list items.
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 Component {
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 two required object props in order to get virtualization to work properly.
40
- * count - The length of your list that you are virtualizing.
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
- * estimateSize - A function that returns the estimated size of your items.
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
- this.scrollElementRef = createRef();
53
- this.virtualItems = [];
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.get('virtualizerProps')) {
67
- this.setVirtualizerOptions();
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
- * This is needed in order to ensure the initial render happens
362
+ * Sets the initial focus of the list based on the `initial-focus` prop and scrolls the item into view.
72
363
  */
73
- firstUpdated(changedProperties) {
74
- super.firstUpdated(changedProperties);
75
- this.setVirtualizerOptions();
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
- setVirtualizerOptions() {
82
- var _a;
83
- (_a = this.virtualizer) === null || _a === void 0 ? void 0 : _a.setOptions({ ...this.virtualizer.options, ...this.virtualizerProps });
84
- this.requestUpdate();
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
- connectedCallback() {
87
- var _a;
88
- this.virtualizerController = new VirtualizerController(this, {
89
- count: this.virtualizerProps.count,
90
- estimateSize: (_a = this.virtualizerProps) === null || _a === void 0 ? void 0 : _a.estimateSize,
91
- getScrollElement: () => this.scrollElementRef.value || null,
92
- ...this.virtualizerProps,
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
- super.connectedCallback();
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
- * Renders the list wrapper and invokes the callback which eventually will render in the slot.
99
- * Uses getTotalSize to update the height of the wrapper. This value is equal to the total size
100
- * OR the total estimated size (if you haven't physically scrolled the entire list)
101
- * Passes the virtualItems, measureElement, and listStyle to callback for client to pass in as child
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
- * @returns The template result containing the list wrapper.
504
+ * @internal
104
505
  */
105
- getVirtualizedListWrapper(virtualizerController) {
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 = virtualizerController.getVirtualizer();
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
- * Refires the scroll event from the internal scroll container to the host element
575
+ * Scrolls to the bottom of the list if `atBottom` is 'yes'.
576
+ * @internal
133
577
  */
134
- handleScroll(event) {
135
- const EventConstructor = event.constructor;
136
- this.dispatchEvent(new EventConstructor(event.type, event));
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
- return html `<div ${ref(this.scrollElementRef)} part="scroll" @scroll=${this.handleScroll}>
140
- ${this.virtualizerController ? this.getVirtualizedListWrapper(this.virtualizerController) : html ``}
141
- </div>`;
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 = [...Component.styles, ...styles];
629
+ VirtualizedList.styles = [...List.styles, ...styles];
145
630
  __decorate([
146
- property({ type: Object, attribute: 'virtualizerprops' }),
631
+ property({ type: Object }),
147
632
  __metadata("design:type", Object)
148
633
  ], VirtualizedList.prototype, "virtualizerProps", void 0);
149
634
  __decorate([
150
- property({ type: Function, attribute: 'setlistdata' }),
151
- __metadata("design:type", Object)
152
- ], VirtualizedList.prototype, "setlistdata", void 0);
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;