@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,10 +9,10 @@ export { default as Avatar } from './avatar';
9
9
  export { default as AvatarButton } from './avatarbutton';
10
10
  export { default as Badge } from './badge';
11
11
  export { default as Banner } from './banner';
12
- export { default as Brandvisual } from './brandvisual';
13
12
  export { default as Bullet } from './bullet';
14
13
  export { default as Button } from './button';
15
14
  export { default as ButtonGroup } from './buttongroup';
15
+ export { default as Brandvisual } from './brandvisual';
16
16
  export { default as ButtonLink } from './buttonlink';
17
17
  export { default as Buttonsimple } from './buttonsimple';
18
18
  export { default as Card } from './card';
@@ -57,8 +57,8 @@ export { default as Popover } from './popover';
57
57
  export { default as Presence } from './presence';
58
58
  export { default as Progressbar } from './progressbar';
59
59
  export { default as Progressspinner } from './progressspinner';
60
- export { default as RadioGroup } from './radiogroup';
61
60
  export { default as Radio } from './radio';
61
+ export { default as RadioGroup } from './radiogroup';
62
62
  export { default as ScreenreaderAnnouncer } from './screenreaderannouncer';
63
63
  export { default as Searchfield } from './searchfield';
64
64
  export { default as Searchpopover } from './searchpopover';
@@ -68,21 +68,21 @@ export { default as SideNavigation } from './sidenavigation';
68
68
  export { default as Skeleton } from './skeleton';
69
69
  export { default as Slider } from './slider';
70
70
  export { default as Spinner } from './spinner';
71
- export { default as StaticChip } from './staticchip';
72
71
  export { default as StaticCheckbox } from './staticcheckbox';
73
- export { default as StaticToggle } from './statictoggle';
72
+ export { default as StaticChip } from './staticchip';
74
73
  export { default as StaticRadio } from './staticradio';
74
+ export { default as StaticToggle } from './statictoggle';
75
75
  export { default as Stepper } from './stepper';
76
76
  export { default as StepperConnector } from './stepperconnector';
77
+ export { default as StepperItem } from './stepperitem';
77
78
  export { default as Tab } from './tab';
78
79
  export { default as TabList } from './tablist';
79
- export { default as StepperItem } from './stepperitem';
80
- export { default as Text } from './text';
81
80
  export { default as Textarea } from './textarea';
82
81
  export { default as ThemeProvider } from './themeprovider';
82
+ export { default as Toast } from './toast';
83
+ export { default as Text } from './text';
83
84
  export { default as Toggle } from './toggle';
84
85
  export { default as ToggleTip } from './toggletip';
85
- export { default as Toast } from './toast';
86
86
  export { default as Tooltip } from './tooltip';
87
87
  export { default as Typewriter } from './typewriter';
88
88
  export { default as VirtualizedList } from './virtualizedlist';
@@ -9,10 +9,10 @@ export { default as Avatar } from './avatar';
9
9
  export { default as AvatarButton } from './avatarbutton';
10
10
  export { default as Badge } from './badge';
11
11
  export { default as Banner } from './banner';
12
- export { default as Brandvisual } from './brandvisual';
13
12
  export { default as Bullet } from './bullet';
14
13
  export { default as Button } from './button';
15
14
  export { default as ButtonGroup } from './buttongroup';
15
+ export { default as Brandvisual } from './brandvisual';
16
16
  export { default as ButtonLink } from './buttonlink';
17
17
  export { default as Buttonsimple } from './buttonsimple';
18
18
  export { default as Card } from './card';
@@ -57,8 +57,8 @@ export { default as Popover } from './popover';
57
57
  export { default as Presence } from './presence';
58
58
  export { default as Progressbar } from './progressbar';
59
59
  export { default as Progressspinner } from './progressspinner';
60
- export { default as RadioGroup } from './radiogroup';
61
60
  export { default as Radio } from './radio';
61
+ export { default as RadioGroup } from './radiogroup';
62
62
  export { default as ScreenreaderAnnouncer } from './screenreaderannouncer';
63
63
  export { default as Searchfield } from './searchfield';
64
64
  export { default as Searchpopover } from './searchpopover';
@@ -68,21 +68,21 @@ export { default as SideNavigation } from './sidenavigation';
68
68
  export { default as Skeleton } from './skeleton';
69
69
  export { default as Slider } from './slider';
70
70
  export { default as Spinner } from './spinner';
71
- export { default as StaticChip } from './staticchip';
72
71
  export { default as StaticCheckbox } from './staticcheckbox';
73
- export { default as StaticToggle } from './statictoggle';
72
+ export { default as StaticChip } from './staticchip';
74
73
  export { default as StaticRadio } from './staticradio';
74
+ export { default as StaticToggle } from './statictoggle';
75
75
  export { default as Stepper } from './stepper';
76
76
  export { default as StepperConnector } from './stepperconnector';
77
+ export { default as StepperItem } from './stepperitem';
77
78
  export { default as Tab } from './tab';
78
79
  export { default as TabList } from './tablist';
79
- export { default as StepperItem } from './stepperitem';
80
- export { default as Text } from './text';
81
80
  export { default as Textarea } from './textarea';
82
81
  export { default as ThemeProvider } from './themeprovider';
82
+ export { default as Toast } from './toast';
83
+ export { default as Text } from './text';
83
84
  export { default as Toggle } from './toggle';
84
85
  export { default as ToggleTip } from './toggletip';
85
- export { default as Toast } from './toast';
86
86
  export { default as Tooltip } from './tooltip';
87
87
  export { default as Typewriter } from './typewriter';
88
88
  export { default as VirtualizedList } from './virtualizedlist';
@@ -2,23 +2,61 @@ import { type EventName } from '@lit/react';
2
2
  import Component from '../../components/virtualizedlist';
3
3
  import type { Events } from '../../components/virtualizedlist/virtualizedlist.types';
4
4
  /**
5
- * `mdc-virtualizedlist` component for creating custom virtualized lists.
6
- * IMPORTANT: This component does not create it's own list/list items.
7
- * Use the setlistdata callback prop to update client state in order to
8
- * Pass list/listitems as a child of this component, which this will virtuailze
9
- * This implementation handles dynamic lists as well as fixed sized lists.
5
+ * `mdc-virtualizedlist` is an extension of the `mdc-list` component that adds virtualization capabilities using
6
+ * the Tanstack Virtual library.
7
+ *
8
+ * This component is thin wrapper around the Tanstack libray to provide additional funtionalities such as
9
+ * keyboard navigation, focus management, scroll anchoring and accessibility features.
10
+ *
10
11
  * Please refer to [Tanstack Virtual Docs](https://tanstack.com/virtual/latest) for more in depth documentation.
11
12
  *
13
+ * ## Setup
14
+ *
15
+ * `virtualizerProps` is a required prop that requires at least two properties to be set: `count` and `estimateSize`.
16
+ * `count` is the total number of items in the list, and `estimateSize` is a function that returns the estimated
17
+ * size (in pixels) of each item in the list. `getItemKey` is also strongly recommended to be set to provide unique
18
+ * keys for each item in the list.
19
+ *
20
+ * ### Render list items
21
+ *
22
+ * To keep the component framework-agnostic, the rendering of the list items is left to the consumer.
23
+ *
24
+ * We need to render only the visible items. The list of visible items are provided by the `virtualitemschange` event.
25
+ * The event detail contains the `virtualItems` array, which contains the information for the rendering.
26
+ * List items must have an `data-index` attribute, the indexes are in the `virtualItems` list.
27
+ *
28
+ * ## Best practices
29
+ *
30
+ * ### List updates
31
+ *
32
+ * Tanstack needs only the count of the items in the list and the size of each item to perform virtualization.
33
+ * List updates happens when
34
+ * - when `virtualizerProps` property of the component instance changes
35
+ * - when `observe-size-changes` is set and the item's size changes (it uses ResizeObserver internally)
36
+ * - when `component.visualiser.measure` called manually.
37
+ *
38
+ * ### Header
39
+ *
40
+ * To add a header to the list, use the `mdc-listheader` component and place it in the `list-header` slot.
41
+ *
42
+ * ### Lists with dynamic content
43
+ *
44
+ * Unique keys for the list items are critical for dynamically changing list items or item's content.
45
+ * If the key change with the content it will cause scrollbar and content shuttering.
46
+ *
12
47
  * @tagname mdc-virtualizedlist
13
48
  *
14
49
  * @event scroll - (React: onScroll) Event that gets called when user scrolls inside of list.
50
+ * @event virtualitemschange - (React: onVirtualItemsChange) Event that gets called when the virtual items change.
15
51
  *
16
- * @slot - Client side List with nested list items.
52
+ * @slot default - This is a default/unnamed slot, where listitems can be placed.
53
+ * @slot list-header - This slot is used to pass a header for the list, which can be a `mdc-listheader` component.
17
54
  *
18
55
  * @csspart container - The container of the virtualized list.
19
56
  * @csspart scroll - The scrollable area of the virtualized list.
20
57
  */
21
58
  declare const reactWrapper: import("@lit/react").ReactWebComponent<Component, {
59
+ onVirtualItemsChange: EventName<Events["onVirtualItemsChangeEvent"]>;
22
60
  onScroll: EventName<Events["onScrollEvent"]>;
23
61
  }>;
24
62
  export default reactWrapper;
@@ -3,18 +3,55 @@ import { createComponent } from '@lit/react';
3
3
  import Component from '../../components/virtualizedlist';
4
4
  import { TAG_NAME } from '../../components/virtualizedlist/virtualizedlist.constants';
5
5
  /**
6
- * `mdc-virtualizedlist` component for creating custom virtualized lists.
7
- * IMPORTANT: This component does not create it's own list/list items.
8
- * Use the setlistdata callback prop to update client state in order to
9
- * Pass list/listitems as a child of this component, which this will virtuailze
10
- * This implementation handles dynamic lists as well as fixed sized lists.
6
+ * `mdc-virtualizedlist` is an extension of the `mdc-list` component that adds virtualization capabilities using
7
+ * the Tanstack Virtual library.
8
+ *
9
+ * This component is thin wrapper around the Tanstack libray to provide additional funtionalities such as
10
+ * keyboard navigation, focus management, scroll anchoring and accessibility features.
11
+ *
11
12
  * Please refer to [Tanstack Virtual Docs](https://tanstack.com/virtual/latest) for more in depth documentation.
12
13
  *
14
+ * ## Setup
15
+ *
16
+ * `virtualizerProps` is a required prop that requires at least two properties to be set: `count` and `estimateSize`.
17
+ * `count` is the total number of items in the list, and `estimateSize` is a function that returns the estimated
18
+ * size (in pixels) of each item in the list. `getItemKey` is also strongly recommended to be set to provide unique
19
+ * keys for each item in the list.
20
+ *
21
+ * ### Render list items
22
+ *
23
+ * To keep the component framework-agnostic, the rendering of the list items is left to the consumer.
24
+ *
25
+ * We need to render only the visible items. The list of visible items are provided by the `virtualitemschange` event.
26
+ * The event detail contains the `virtualItems` array, which contains the information for the rendering.
27
+ * List items must have an `data-index` attribute, the indexes are in the `virtualItems` list.
28
+ *
29
+ * ## Best practices
30
+ *
31
+ * ### List updates
32
+ *
33
+ * Tanstack needs only the count of the items in the list and the size of each item to perform virtualization.
34
+ * List updates happens when
35
+ * - when `virtualizerProps` property of the component instance changes
36
+ * - when `observe-size-changes` is set and the item's size changes (it uses ResizeObserver internally)
37
+ * - when `component.visualiser.measure` called manually.
38
+ *
39
+ * ### Header
40
+ *
41
+ * To add a header to the list, use the `mdc-listheader` component and place it in the `list-header` slot.
42
+ *
43
+ * ### Lists with dynamic content
44
+ *
45
+ * Unique keys for the list items are critical for dynamically changing list items or item's content.
46
+ * If the key change with the content it will cause scrollbar and content shuttering.
47
+ *
13
48
  * @tagname mdc-virtualizedlist
14
49
  *
15
50
  * @event scroll - (React: onScroll) Event that gets called when user scrolls inside of list.
51
+ * @event virtualitemschange - (React: onVirtualItemsChange) Event that gets called when the virtual items change.
16
52
  *
17
- * @slot - Client side List with nested list items.
53
+ * @slot default - This is a default/unnamed slot, where listitems can be placed.
54
+ * @slot list-header - This slot is used to pass a header for the list, which can be a `mdc-listheader` component.
18
55
  *
19
56
  * @csspart container - The container of the virtualized list.
20
57
  * @csspart scroll - The scrollable area of the virtualized list.
@@ -24,6 +61,7 @@ const reactWrapper = createComponent({
24
61
  elementClass: Component,
25
62
  react: React,
26
63
  events: {
64
+ onVirtualItemsChange: 'virtualitemschange',
27
65
  onScroll: 'scroll',
28
66
  },
29
67
  displayName: 'VirtualizedList',
@@ -15,7 +15,7 @@ export const AutoFocusOnMountMixin = (superClass) => {
15
15
  /**
16
16
  * @internal
17
17
  */
18
- this.elementToAutoFocus = this;
18
+ this.elementToAutoFocus = null;
19
19
  /**
20
20
  * This property indicates whether the element should receive focus automatically when it is mounted.
21
21
  *
@@ -30,7 +30,7 @@ export const AutoFocusOnMountMixin = (superClass) => {
30
30
  if (this.autoFocusOnMount) {
31
31
  // wait for the element to be fully updated before focusing
32
32
  await this.updateComplete;
33
- this.elementToAutoFocus.focus();
33
+ (this.elementToAutoFocus || this).focus();
34
34
  }
35
35
  }
36
36
  }
@@ -1,12 +1,15 @@
1
1
  import type { Component } from '../../models';
2
+ import type { BaseArray } from '../virtualIndexArray';
2
3
  import type { Constructor } from './index.types';
3
4
  export declare abstract class ListNavigationMixinInterface {
4
5
  protected loop: 'true' | 'false';
5
6
  protected propagateAllKeyEvents: boolean;
6
7
  protected initialFocus: number;
7
- protected abstract get navItems(): HTMLElement[];
8
- protected resetTabIndexes(index: number): void;
8
+ protected abstract get navItems(): BaseArray<HTMLElement>;
9
+ protected resetTabIndexes(index: number, focusElement?: boolean): void;
9
10
  protected resetTabIndexAndSetFocus(newIndex: number, oldIndex?: number, focusNewItem?: boolean): void;
11
+ protected setInitialFocus(): void;
12
+ protected handleNavigationKeyDown(event: KeyboardEvent): void;
10
13
  }
11
14
  /**
12
15
  * This mixin extends the passed class with list like navigation capabilities.
@@ -47,64 +47,6 @@ export const ListNavigationMixin = (superClass) => {
47
47
  * @internal
48
48
  */
49
49
  this.initialFocus = 0;
50
- /**
51
- * Handles keydown events for navigation within the list.
52
- * It allows users to navigate through the list items using arrow keys, home, and end keys.
53
- * The navigation is based on the current focused item and the direction of the layout (RTL or LTR).
54
- *
55
- * By default, it will stop propagation for key events which are handled by the component.
56
- * Check the mixin options to change this behavior.
57
- *
58
- * @param event - The keyboard event triggered by user interaction.
59
- * @internal
60
- */
61
- this.handleNavigationKeyDown = (event) => {
62
- const keysToHandle = new Set([KEYS.ARROW_DOWN, KEYS.ARROW_UP, KEYS.HOME, KEYS.END]);
63
- const isRtl = window.getComputedStyle(this).direction === 'rtl';
64
- const targetKey = this.resolveDirectionKey(event.key, isRtl);
65
- if (!keysToHandle.has(targetKey)) {
66
- return;
67
- }
68
- const target = event.target;
69
- const currentIndex = this.getCurrentIndex(target);
70
- if (currentIndex === -1)
71
- return;
72
- this.resetTabIndexes(currentIndex);
73
- switch (targetKey) {
74
- case KEYS.HOME: {
75
- // Move focus to the first item
76
- this.resetTabIndexAndSetFocus(0, currentIndex);
77
- break;
78
- }
79
- case KEYS.END: {
80
- // Move focus to the last item
81
- this.resetTabIndexAndSetFocus(this.navItems.length - 1, currentIndex);
82
- break;
83
- }
84
- case KEYS.ARROW_DOWN: {
85
- // Move focus to the next item
86
- const eolIndex = this.shouldLoop() ? 0 : currentIndex;
87
- const newIndex = currentIndex + 1 === this.navItems.length ? eolIndex : currentIndex + 1;
88
- this.resetTabIndexAndSetFocus(newIndex, currentIndex);
89
- break;
90
- }
91
- case KEYS.ARROW_UP: {
92
- // Move focus to the prev item
93
- const eolIndex = this.shouldLoop() ? this.navItems.length - 1 : currentIndex;
94
- const newIndex = currentIndex - 1 === -1 ? eolIndex : currentIndex - 1;
95
- this.resetTabIndexAndSetFocus(newIndex, currentIndex);
96
- break;
97
- }
98
- default:
99
- break;
100
- }
101
- // When the component consume any of the pressed key, we need to stop propagation
102
- // to prevent the event from bubbling up and being handled by parent components which might use the same key.
103
- if (!this.propagateAllKeyEvents) {
104
- event.stopPropagation();
105
- event.preventDefault();
106
- }
107
- };
108
50
  /**
109
51
  * Handles click events on the navigation items.
110
52
  * It retrieves the index of the clicked item and resets the tabindex accordingly.
@@ -118,11 +60,11 @@ export const ListNavigationMixin = (superClass) => {
118
60
  if (newIndex !== -1) {
119
61
  // When user clicked on a focusable element inside the item, we update the navigation index, but
120
62
  // keep the focus on the clicked element.
121
- const focusNewItem = !(this.navItems[newIndex] !== target && document.activeElement === event.target);
63
+ const focusNewItem = !(this.navItems.at(newIndex) !== target && document.activeElement === event.target);
122
64
  this.resetTabIndexAndSetFocus(newIndex, undefined, focusNewItem);
123
65
  }
124
66
  };
125
- this.addEventListener('keydown', this.handleNavigationKeyDown);
67
+ this.addEventListener('keydown', this.handleNavigationKeyDown.bind(this));
126
68
  this.addEventListener('click', this.handleNavigationClick);
127
69
  }
128
70
  /**
@@ -132,9 +74,70 @@ export const ListNavigationMixin = (superClass) => {
132
74
  */
133
75
  async firstUpdated(changedProperties) {
134
76
  super.firstUpdated(changedProperties);
77
+ this.setInitialFocus();
78
+ }
79
+ setInitialFocus() {
135
80
  const indexToFocus = Math.max(Math.min(this.initialFocus, this.navItems.length - 1), 0);
136
81
  this.resetTabIndexAndSetFocus(indexToFocus, undefined, false);
137
82
  }
83
+ /**
84
+ * Handles keydown events for navigation within the list.
85
+ * It allows users to navigate through the list items using arrow keys, home, and end keys.
86
+ * The navigation is based on the current focused item and the direction of the layout (RTL or LTR).
87
+ *
88
+ * By default, it will stop propagation for key events which are handled by the component.
89
+ * Check the mixin options to change this behavior.
90
+ *
91
+ * @param event - The keyboard event triggered by user interaction.
92
+ * @internal
93
+ */
94
+ handleNavigationKeyDown(event) {
95
+ const keysToHandle = new Set([KEYS.ARROW_DOWN, KEYS.ARROW_UP, KEYS.HOME, KEYS.END]);
96
+ const isRtl = window.getComputedStyle(this).direction === 'rtl';
97
+ const targetKey = this.resolveDirectionKey(event.key, isRtl);
98
+ if (!keysToHandle.has(targetKey)) {
99
+ return;
100
+ }
101
+ const target = event.target;
102
+ const currentIndex = this.getCurrentIndex(target);
103
+ if (currentIndex === -1)
104
+ return;
105
+ this.resetTabIndexes(currentIndex);
106
+ switch (targetKey) {
107
+ case KEYS.HOME: {
108
+ // Move focus to the first item
109
+ this.resetTabIndexAndSetFocus(0, currentIndex);
110
+ break;
111
+ }
112
+ case KEYS.END: {
113
+ // Move focus to the last item
114
+ this.resetTabIndexAndSetFocus(this.navItems.length - 1, currentIndex);
115
+ break;
116
+ }
117
+ case KEYS.ARROW_DOWN: {
118
+ // Move focus to the next item
119
+ const eolIndex = this.shouldLoop() ? 0 : currentIndex;
120
+ const newIndex = currentIndex + 1 === this.navItems.length ? eolIndex : currentIndex + 1;
121
+ this.resetTabIndexAndSetFocus(newIndex, currentIndex);
122
+ break;
123
+ }
124
+ case KEYS.ARROW_UP: {
125
+ // Move focus to the prev item
126
+ const eolIndex = this.shouldLoop() ? this.navItems.length - 1 : currentIndex;
127
+ const newIndex = currentIndex - 1 === -1 ? eolIndex : currentIndex - 1;
128
+ this.resetTabIndexAndSetFocus(newIndex, currentIndex);
129
+ break;
130
+ }
131
+ default:
132
+ break;
133
+ }
134
+ // When the component consume any of the pressed key, we need to stop propagation
135
+ // to prevent the event from bubbling up and being handled by parent components which might use the same key.
136
+ if (!this.propagateAllKeyEvents) {
137
+ event.stopPropagation();
138
+ event.preventDefault();
139
+ }
140
+ }
138
141
  /**
139
142
  * Retrieves the current index of the item that triggered the event.
140
143
  *
@@ -152,14 +155,20 @@ export const ListNavigationMixin = (superClass) => {
152
155
  * Reset all tabindex to -1 and set the tabindex of the current item to 0
153
156
  *
154
157
  * @param index - The index of the currently focused item.
158
+ * @param focusElement - Call focus() on the current item or not.
155
159
  */
156
- resetTabIndexes(index) {
160
+ resetTabIndexes(index, focusElement = true) {
157
161
  var _a;
158
162
  if (this.navItems.length > 0) {
159
163
  this.navItems.forEach(item => item.setAttribute('tabindex', '-1'));
160
- const currentIndex = this.navItems[index] ? index : 0;
161
- this.navItems[currentIndex].setAttribute('tabindex', '0');
162
- (_a = this.navItems[currentIndex]) === null || _a === void 0 ? void 0 : _a.focus();
164
+ const currentIndex = this.navItems.at(index) ? index : 0;
165
+ const currentItem = (_a = this.navItems.at(currentIndex)) !== null && _a !== void 0 ? _a : this.navItems.find(Boolean);
166
+ if (currentItem) {
167
+ currentItem.setAttribute('tabindex', '0');
168
+ if (focusElement) {
169
+ currentItem.focus();
170
+ }
171
+ }
163
172
  }
164
173
  }
165
174
  /**
@@ -171,21 +180,21 @@ export const ListNavigationMixin = (superClass) => {
171
180
  * @returns - This method does not return anything.
172
181
  */
173
182
  resetTabIndexAndSetFocus(newIndex, oldIndex, focusNewItem = true) {
183
+ var _a;
174
184
  const { navItems } = this;
175
185
  if (navItems.length === 0)
176
186
  return;
177
187
  // Ensure newIndex is valid
178
- const newIdx = navItems[newIndex] ? newIndex : 0;
179
- const newItem = navItems[newIdx];
188
+ const newItem = (_a = navItems.at(newIndex)) !== null && _a !== void 0 ? _a : navItems.find(Boolean);
180
189
  if (newIndex === oldIndex && newItem && newItem.getAttribute('tabindex') === '0') {
181
190
  return;
182
191
  }
183
192
  if (oldIndex === undefined) {
184
193
  navItems.forEach(item => item.setAttribute('tabindex', '-1'));
185
194
  }
186
- else if (navItems[oldIndex]) {
195
+ else if (navItems.at(oldIndex)) {
187
196
  // Reset tabindex of the old item
188
- navItems[oldIndex].setAttribute('tabindex', '-1');
197
+ navItems.at(oldIndex).setAttribute('tabindex', '-1');
189
198
  }
190
199
  newItem.setAttribute('tabindex', '0');
191
200
  if (focusNewItem) {
@@ -26,6 +26,10 @@ export const LifeCycleMixin = (superClass) => {
26
26
  connectedCallback() {
27
27
  super.connectedCallback();
28
28
  this.dispatchEvent(new Event(LIFE_CYCLE_EVENTS.CREATED, { bubbles: true, composed: true }));
29
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
30
+ this.updateComplete.then(() => {
31
+ this.dispatchEvent(new Event(LIFE_CYCLE_EVENTS.FIRST_UPDATE_COMPLETED, { bubbles: true, composed: true }));
32
+ });
29
33
  }
30
34
  disconnectedCallback() {
31
35
  super.disconnectedCallback();
@@ -1,5 +1,6 @@
1
1
  export declare const LIFE_CYCLE_EVENTS: {
2
2
  readonly CREATED: "created";
3
+ readonly FIRST_UPDATE_COMPLETED: "first-update-completed";
3
4
  readonly DESTROYED: "destroyed";
4
5
  readonly MODIFIED: "modified";
5
6
  };
@@ -1,5 +1,6 @@
1
1
  export const LIFE_CYCLE_EVENTS = {
2
2
  CREATED: 'created',
3
+ FIRST_UPDATE_COMPLETED: 'first-update-completed',
3
4
  DESTROYED: 'destroyed',
4
5
  MODIFIED: 'modified',
5
6
  };
@@ -0,0 +1,40 @@
1
+ interface RangeOptions {
2
+ includeEnd?: boolean;
3
+ }
4
+ /**
5
+ * Represents a numeric interval between two numbers.
6
+ *
7
+ * Start is inclusive, end is exclusive by default.
8
+ */
9
+ export declare class Interval implements Iterable<number> {
10
+ /** The start of the Interval (inclusive). */
11
+ readonly start: number;
12
+ /** The end of the Interval (exclusive) by default. */
13
+ readonly end: number;
14
+ /** Whether the end is inclusive. Default is false (exclusive). */
15
+ readonly includeEnd: boolean;
16
+ constructor(start: number, end: number, options?: RangeOptions);
17
+ /**
18
+ * Checks if a number is within the Interval.
19
+ * @param value - The number to check.
20
+ */
21
+ includes(value: number): boolean;
22
+ /**
23
+ * Returns an iterator for the Interval.
24
+ *
25
+ * If the start is greater than the end, it decrements by step.
26
+ * The iteration stops when the current number reaches or exceeds the end (or equals if includeEnd is true).
27
+ *
28
+ * @param step - The step between each number in the Interval. Default is the step defined in the constructor.
29
+ * It must be a positive non-zero number.
30
+ * @returns An iterator that yields numbers from start to end, incremented by step.
31
+ */
32
+ iter(step?: number): Iterator<number>;
33
+ /**
34
+ * Returns the default iterator for the Interval.
35
+ *
36
+ * it steps by 1.
37
+ */
38
+ [Symbol.iterator](): Iterator<number, any, any>;
39
+ }
40
+ export {};
@@ -0,0 +1,66 @@
1
+ const DEFAULT_OPTIONS = {
2
+ includeEnd: false,
3
+ };
4
+ /**
5
+ * Represents a numeric interval between two numbers.
6
+ *
7
+ * Start is inclusive, end is exclusive by default.
8
+ */
9
+ export class Interval {
10
+ constructor(start, end, options = {}) {
11
+ const opt = { ...DEFAULT_OPTIONS, ...options };
12
+ this.start = start;
13
+ this.end = end;
14
+ this.includeEnd = opt.includeEnd;
15
+ }
16
+ /**
17
+ * Checks if a number is within the Interval.
18
+ * @param value - The number to check.
19
+ */
20
+ includes(value) {
21
+ const { start, end, includeEnd } = this;
22
+ const min = Math.min(start, end);
23
+ const max = Math.max(start, end);
24
+ return includeEnd ? value >= min && value <= max : value >= min && value < max;
25
+ }
26
+ /**
27
+ * Returns an iterator for the Interval.
28
+ *
29
+ * If the start is greater than the end, it decrements by step.
30
+ * The iteration stops when the current number reaches or exceeds the end (or equals if includeEnd is true).
31
+ *
32
+ * @param step - The step between each number in the Interval. Default is the step defined in the constructor.
33
+ * It must be a positive non-zero number.
34
+ * @returns An iterator that yields numbers from start to end, incremented by step.
35
+ */
36
+ iter(step = 1) {
37
+ if (step <= 0) {
38
+ throw new Error('Step must be a positive non-zero number');
39
+ }
40
+ const stepper = step * (this.start <= this.end ? 1 : -1);
41
+ const { includeEnd, end } = this;
42
+ let current = this.start;
43
+ let done = false;
44
+ return {
45
+ next: () => {
46
+ if (done)
47
+ return { value: undefined, done: true };
48
+ const value = current;
49
+ current += stepper;
50
+ if (includeEnd ? value > end : value >= end) {
51
+ done = true;
52
+ return { value: undefined, done: true };
53
+ }
54
+ return { value, done: false };
55
+ },
56
+ };
57
+ }
58
+ /**
59
+ * Returns the default iterator for the Interval.
60
+ *
61
+ * it steps by 1.
62
+ */
63
+ [Symbol.iterator]() {
64
+ return this.iter();
65
+ }
66
+ }
@@ -0,0 +1,27 @@
1
+ export interface BaseArray<TItem> extends Pick<Array<TItem>, 'length' | 'forEach' | 'findIndex' | 'map' | 'at' | 'find'> {
2
+ }
3
+ /**
4
+ * A wrapper around an array that applies a "virtual" index to the index when accessing items.
5
+ * This is useful when the array is just a view of the real data and the real data has gaps in the indexes.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const originalArray = [{realIndex: 2, value: 10}, {realIndex: 3, value: 20}];
10
+ * const offsetArray = new OffsetArray(originalArray, (item) => item.realIndex);
11
+ *
12
+ * console.log(offsetArray.at(0)); // Output: undefined
13
+ * console.log(offsetArray.at(2).value); // Output: 10
14
+ * ```
15
+ */
16
+ export declare class VirtualIndexArray<TItem> implements BaseArray<TItem> {
17
+ readonly items: BaseArray<TItem>;
18
+ private readonly getIndex;
19
+ private lengthFn;
20
+ constructor(items: BaseArray<TItem>, getIndex: (item: TItem) => number, length: () => number);
21
+ get length(): number;
22
+ at(index: number): TItem | undefined;
23
+ map<U>(cb: (value: TItem, index: number, array: TItem[]) => U, thisArg?: any): U[];
24
+ forEach(cb: (value: TItem, index: number, array: TItem[]) => void, thisArg?: any): void;
25
+ findIndex(predicate: (value: TItem, index: number, obj: TItem[]) => boolean, thisArg?: any): number;
26
+ find(predicate: (value: TItem, index: number, obj: TItem[]) => boolean, thisArg?: any): TItem | undefined;
27
+ }