Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: virtual list item selection #8318

Open
wants to merge 50 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
627689b
feat: virtual list item selection
tomivirkki Nov 22, 2024
02e00f0
test: add an integration test
tomivirkki Nov 25, 2024
1287b6c
test: more tests and fixes
tomivirkki Nov 25, 2024
5273a0a
fix: do not try to select undefined items
tomivirkki Nov 27, 2024
9892d7a
cleanup
tomivirkki Nov 29, 2024
374e690
fix: scroll the focused index to view on space
tomivirkki Dec 2, 2024
57f6dfd
refactor: scroll to focused index
tomivirkki Dec 2, 2024
8033ace
feat: add selected in renderer model
tomivirkki Dec 2, 2024
2516e39
feat: aria semantics
tomivirkki Dec 2, 2024
e33511e
feat: add itemAccessibleNameGenerator
tomivirkki Dec 3, 2024
a549207
fix: remove navigating state on selection mode unset
tomivirkki Dec 3, 2024
734d6e1
refactor: cleanup and test coverage
tomivirkki Dec 3, 2024
3d78d3f
fix: bugfixes
tomivirkki Dec 3, 2024
a9fd9b0
refactor: cleanup
tomivirkki Dec 4, 2024
8e7e837
refactor: cleanup
tomivirkki Dec 5, 2024
2acdbf6
fix an issue with single-selection
tomivirkki Dec 5, 2024
26af05c
refactor: change selection mode type
tomivirkki Dec 5, 2024
e408af5
refactor: cleanup
tomivirkki Dec 5, 2024
a407c4a
refactor: cleanup
tomivirkki Dec 6, 2024
1854b36
fix: do not mark element focused if it does not match focus index
tomivirkki Dec 9, 2024
c626bcb
refactor: avoid excess re-renders on click select
tomivirkki Dec 9, 2024
7e2eb5b
fix: do not rerender when tabbing focusable children of same parent
tomivirkki Dec 9, 2024
d16de3d
docs: clarify the need for focus exit
tomivirkki Dec 9, 2024
5a9e6f9
refactor: remove theme changes for now
tomivirkki Dec 10, 2024
6c8734a
fix: browser-specific fixes
tomivirkki Dec 10, 2024
33833c7
fix types
tomivirkki Dec 10, 2024
7c5e2ec
refactor: cleanup
tomivirkki Dec 10, 2024
272c046
fix: make sure focused index element gets focused on space select
tomivirkki Dec 10, 2024
2c4dae6
refactor: avoid jumpiness on arrow navigation
tomivirkki Dec 10, 2024
24001d4
chore: clean up dev page
tomivirkki Dec 10, 2024
3fbfd1f
refactor: cleanup and fixes
tomivirkki Dec 10, 2024
19ebae1
docs: document new state attributes
tomivirkki Dec 10, 2024
b61a645
fix: mark root element focused in interaction mode
tomivirkki Dec 10, 2024
28ded0e
test: fix a timing issue in an integration test
tomivirkki Dec 10, 2024
275aac1
refactor: cleanup
tomivirkki Dec 11, 2024
993af45
fix: use position absolute for focus-exit to avoid scroll position ch…
tomivirkki Dec 11, 2024
a0bc08a
fix: always update focus index on click
tomivirkki Dec 11, 2024
c4802cf
refactor: use getter for __isSelectable
tomivirkki Dec 11, 2024
a499cbd
fix: do not throw on enter when focus item not rendered
tomivirkki Dec 11, 2024
20854b0
refactor: split out virtual list base mixin
tomivirkki Dec 11, 2024
9b37d4d
refactor __getItemModel
tomivirkki Dec 11, 2024
3a5f2d4
chore: cleanup
tomivirkki Dec 11, 2024
bb503ee
refactor: relocate a11y
tomivirkki Dec 11, 2024
d67dd5b
Merge remote-tracking branch 'origin/main' into feat/virtual-list-sel…
tomivirkki Dec 12, 2024
3b5bda3
Update packages/virtual-list/src/vaadin-virtual-list-selection-mixin.js
tomivirkki Dec 12, 2024
86b695f
test: add explicit typing tests for undefined
tomivirkki Dec 12, 2024
7901b01
fix: set aria-selected asynchronously
tomivirkki Dec 12, 2024
70dbd70
fix: flush virtualizer on arrow navigation
tomivirkki Dec 12, 2024
46369a8
fix: limit the ariaSelected workaround to single-select only
tomivirkki Dec 12, 2024
16ed88c
only have navigating attribute on keyboard navigation
tomivirkki Dec 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 54 additions & 7 deletions dev/virtual-list.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
Expand All @@ -9,23 +9,70 @@

<script type="module">
import '@vaadin/virtual-list';
// import '@vaadin/virtual-list/vaadin-lit-virtual-list.js';

const items = Array.from({ length: 100000 }).map((_, i) => {
return { label: `Item ${i}` };
});
import { render, html } from 'lit';

function generateItems(count) {
return Array.from({ length: count }).map((_, i) => {
return { label: `Item ${i}` };
});
}

const renderer = (root, _, { item }) => {
root.innerHTML = `<div>${item.label}</div>`;
render(html`<div>${item.label}</div>`, root);
};

const list = document.querySelector('vaadin-virtual-list');
list.items = items;
list.items = generateItems(10000);
list.renderer = renderer;
list.selectionMode = 'multi';
list.itemIdPath = 'label';

const selectionMode = document.querySelector('#selection-mode');
selectionMode.value = list.selectionMode;
selectionMode.addEventListener('change', (e) => {
list.selectionMode = e.target.value;
});

document.querySelector('#empty').addEventListener('click', () => {
list.items = [];
});

document.querySelector('#set-1000-items').addEventListener('click', () => {
list.items = generateItems(1000);
});

document.querySelector('#set-5-items').addEventListener('click', () => {
list.items = generateItems(5);
});
</script>

<style>
vaadin-virtual-list[navigating] > [focused] {
box-shadow: inset 0 0 0 2px var(--lumo-primary-color-50pct);
}

vaadin-virtual-list > [selected] {
background-color: var(--lumo-primary-color-10pct);
}

vaadin-virtual-list > * {
outline: none;
}
</style>
</head>

<body>
<vaadin-virtual-list style="height: 400px"></vaadin-virtual-list>

<select id="selection-mode">
<option>none</option>
<option>single</option>
<option>multi</option>
</select>

<button id="empty">Empty items</button>
<button id="set-1000-items">Set 1000 items</button>
<button id="set-5-items">Set 5 items</button>
</body>
</html>
1 change: 1 addition & 0 deletions packages/virtual-list/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"dependencies": {
"@open-wc/dedupe-mixin": "^1.3.0",
"@polymer/polymer": "^3.0.0",
"@vaadin/a11y-base": "24.7.0-alpha1",
"@vaadin/component-base": "24.7.0-alpha1",
"@vaadin/lit-renderer": "24.7.0-alpha1",
"@vaadin/vaadin-lumo-styles": "24.7.0-alpha1",
Expand Down
2 changes: 2 additions & 0 deletions packages/virtual-list/src/vaadin-lit-virtual-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class VirtualList extends VirtualListMixin(ThemableMixin(ElementMixin(PolylitMix
<div id="items">
<slot></slot>
</div>

<div id="focusexit" tabindex="0" hidden></div>
`;
}
}
Expand Down
23 changes: 18 additions & 5 deletions packages/virtual-list/src/vaadin-virtual-list-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
* Copyright (c) 2021 - 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import type { Constructor } from '@open-wc/dedupe-mixin';
import type { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js';
import type { VirtualList } from './vaadin-virtual-list.js';
import type { VirtualListSelectionMixinClass } from './vaadin-virtual-list-selection-mixin.js';

export type VirtualListDefaultItem = any;

export interface VirtualListItemModel<TItem> {
index: number;
item: TItem;
selected?: boolean;
}

export type VirtualListRenderer<TItem> = (
Expand All @@ -20,11 +21,18 @@ export type VirtualListRenderer<TItem> = (
model: VirtualListItemModel<TItem>,
) => void;

export declare function VirtualListMixin<TItem, T extends Constructor<HTMLElement>>(
base: T,
): Constructor<ControllerMixinClass> & Constructor<VirtualListMixinClass<TItem>> & T;
/**
* Fired when the `selectedItems` property changes.
*/
export type VirtualListSelectedItemsChangedEvent<TItem> = CustomEvent<{ value: TItem[] }>;

export interface VirtualListCustomEventMap<TItem> {
'selected-items-changed': VirtualListSelectedItemsChangedEvent<TItem>;
}

export interface VirtualListEventMap<TItem> extends HTMLElementEventMap, VirtualListCustomEventMap<TItem> {}

export declare class VirtualListMixinClass<TItem = VirtualListDefaultItem> {
export declare class VirtualListBaseMixinClass<TItem = VirtualListDefaultItem> {
/**
* Gets the index of the first visible item in the viewport.
*/
Expand Down Expand Up @@ -71,3 +79,8 @@ export declare class VirtualListMixinClass<TItem = VirtualListDefaultItem> {
*/
requestContentUpdate(): void;
}

export interface VirtualListMixinClass<TItem>
extends VirtualListBaseMixinClass,
ControllerMixinClass,
VirtualListSelectionMixinClass<TItem> {}
21 changes: 18 additions & 3 deletions packages/virtual-list/src/vaadin-virtual-list-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js'
import { OverflowController } from '@vaadin/component-base/src/overflow-controller.js';
import { processTemplates } from '@vaadin/component-base/src/templates.js';
import { Virtualizer } from '@vaadin/component-base/src/virtualizer.js';
import { SelectionMixin } from './vaadin-virtual-list-selection-mixin.js';

/**
* @polymerMixin
* @mixes ControllerMixin
*/
export const VirtualListMixin = (superClass) =>
class VirtualListMixinClass extends ControllerMixin(superClass) {
export const VirtualListBaseMixin = (superClass) =>
class extends ControllerMixin(superClass) {
static get properties() {
return {
/**
Expand Down Expand Up @@ -144,7 +145,7 @@ export const VirtualListMixin = (superClass) =>
}

if (this.renderer) {
this.renderer(el, this, { item, index });
this.renderer(el, this, this.__getItemModel(index));
}
}

Expand All @@ -153,6 +154,13 @@ export const VirtualListMixin = (superClass) =>
el.role = 'listitem';
}

/**
* @private
*/
__getItemModel(index) {
return { index, item: this.items[index] };
}

/**
* Clears the content of a render target.
* @private
Expand Down Expand Up @@ -213,3 +221,10 @@ export const VirtualListMixin = (superClass) =>
}
}
};

/**
* @polymerMixin
* @mixes SelectionMixin
* @mixes VirtualListBaseMixin
*/
export const VirtualListMixin = (superClass) => class extends SelectionMixin(VirtualListBaseMixin(superClass)) {};
29 changes: 29 additions & 0 deletions packages/virtual-list/src/vaadin-virtual-list-selection-mixin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @license
* Copyright (c) 2021 - 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import type { Constructor } from '@open-wc/dedupe-mixin';
import type { VirtualListDefaultItem } from './vaadin-virtual-list.js';

export declare function VirtualListSelectionMixin<TItem, T extends Constructor<HTMLElement>>(
base: T,
): Constructor<TItem> & T;

export declare class VirtualListSelectionMixinClass<TItem = VirtualListDefaultItem> {
/**
* Selection mode for the virtual list. Available modes are: `none`, `single` and `multi`.
*/
selectionMode: 'none' | 'single' | 'multi';

/**
* Path to an item sub-property that identifies the item.
* @attr {string} item-id-path
*/
itemIdPath: string | null | undefined;

/**
* An array that contains the selected items.
*/
selectedItems: TItem[];
}
Loading
Loading