Skip to content

Commit

Permalink
experiment: add LitElement based version of combo-box (#7042)
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan authored Mar 22, 2024
1 parent 5b41105 commit 846454a
Show file tree
Hide file tree
Showing 41 changed files with 589 additions and 34 deletions.
4 changes: 3 additions & 1 deletion packages/combo-box/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"vaadin-*.d.ts",
"vaadin-*.js",
"web-types.json",
"web-types.lit.json"
"web-types.lit.json",
"!vaadin-lit-*.d.ts",
"!vaadin-lit-*.js"
],
"keywords": [
"Vaadin",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const ComboBoxDataProviderMixin = (superClass) =>
type: Number,
value: 50,
observer: '_pageSizeChanged',
sync: true,
},

/**
Expand All @@ -30,6 +31,7 @@ export const ComboBoxDataProviderMixin = (superClass) =>
size: {
type: Number,
observer: '_sizeChanged',
sync: true,
},

/**
Expand All @@ -49,6 +51,7 @@ export const ComboBoxDataProviderMixin = (superClass) =>
dataProvider: {
type: Object,
observer: '_dataProviderChanged',
sync: true,
},

/** @private */
Expand Down
2 changes: 1 addition & 1 deletion packages/combo-box/src/vaadin-combo-box-item-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const ComboBoxItemMixin = (superClass) =>
}

static get observers() {
return ['__rendererOrItemChanged(renderer, index, item.*, selected, focused)', '__updateLabel(label, renderer)'];
return ['__rendererOrItemChanged(renderer, index, item, selected, focused)', '__updateLabel(label, renderer)'];
}

static get observedAttributes() {
Expand Down
68 changes: 55 additions & 13 deletions packages/combo-box/src/vaadin-combo-box-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const ComboBoxMixin = (subclass) =>
notify: true,
value: false,
reflectToAttribute: true,
sync: true,
observer: '_openedChanged',
},

Expand All @@ -80,6 +81,7 @@ export const ComboBoxMixin = (subclass) =>
*/
autoOpenDisabled: {
type: Boolean,
sync: true,
},

/**
Expand All @@ -104,7 +106,10 @@ export const ComboBoxMixin = (subclass) =>
* - `model.item` The item.
* @type {ComboBoxRenderer | undefined}
*/
renderer: Function,
renderer: {
type: Object,
sync: true,
},

/**
* A full set of items to filter the visible options from.
Expand All @@ -113,6 +118,7 @@ export const ComboBoxMixin = (subclass) =>
*/
items: {
type: Array,
sync: true,
observer: '_itemsChanged',
},

Expand All @@ -138,6 +144,7 @@ export const ComboBoxMixin = (subclass) =>
filteredItems: {
type: Array,
observer: '_filteredItemsChanged',
sync: true,
},

/**
Expand All @@ -154,6 +161,7 @@ export const ComboBoxMixin = (subclass) =>
type: Boolean,
value: false,
reflectToAttribute: true,
sync: true,
},

/**
Expand All @@ -164,6 +172,7 @@ export const ComboBoxMixin = (subclass) =>
type: Number,
observer: '_focusedIndexChanged',
value: -1,
sync: true,
},

/**
Expand All @@ -174,6 +183,7 @@ export const ComboBoxMixin = (subclass) =>
type: String,
value: '',
notify: true,
sync: true,
},

/**
Expand All @@ -183,6 +193,7 @@ export const ComboBoxMixin = (subclass) =>
selectedItem: {
type: Object,
notify: true,
sync: true,
},

/**
Expand All @@ -199,6 +210,7 @@ export const ComboBoxMixin = (subclass) =>
type: String,
value: 'label',
observer: '_itemLabelPathChanged',
sync: true,
},

/**
Expand All @@ -214,6 +226,7 @@ export const ComboBoxMixin = (subclass) =>
itemValuePath: {
type: String,
value: 'value',
sync: true,
},

/**
Expand All @@ -223,7 +236,10 @@ export const ComboBoxMixin = (subclass) =>
* `dataProvider` callback).
* @attr {string} item-id-path
*/
itemIdPath: String,
itemIdPath: {
type: String,
sync: true,
},

/**
* @type {!HTMLElement | undefined}
Expand All @@ -240,17 +256,22 @@ export const ComboBoxMixin = (subclass) =>
*/
_dropdownItems: {
type: Array,
sync: true,
},

/** @private */
_closeOnBlurIsPrevented: Boolean,

/** @private */
_scroller: Object,
_scroller: {
type: Object,
sync: true,
},

/** @private */
_overlayOpened: {
type: Boolean,
sync: true,
observer: '_overlayOpenedChanged',
},
};
Expand All @@ -260,7 +281,7 @@ export const ComboBoxMixin = (subclass) =>
return [
'_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
'_openedOrItemsChanged(opened, _dropdownItems, loading)',
'_updateScroller(_scroller, _dropdownItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, theme)',
'_updateScroller(_scroller, _dropdownItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, _theme)',
];
}

Expand Down Expand Up @@ -413,6 +434,18 @@ export const ComboBoxMixin = (subclass) =>
}
}

/**
* Override LitElement lifecycle callback to handle filter property change.
* @param {Object} props
*/
updated(props) {
super.updated(props);

if (props.has('filter')) {
this._filterChanged(this.filter);
}
}

/** @private */
_initOverlay() {
const overlay = this.$.overlay;
Expand Down Expand Up @@ -441,25 +474,23 @@ export const ComboBoxMixin = (subclass) =>
* @protected
*/
_initScroller(host) {
const scrollerTag = `${this._tagNamePrefix}-scroller`;
const scroller = document.createElement(`${this._tagNamePrefix}-scroller`);

scroller.owner = host || this;
scroller.getItemLabel = this._getItemLabel.bind(this);
scroller.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);

const overlay = this._overlayElement;

overlay.renderer = (root) => {
if (!root.firstChild) {
root.appendChild(document.createElement(scrollerTag));
if (!root.innerHTML) {
root.appendChild(scroller);
}
};

// Ensure the scroller is rendered
overlay.requestContentUpdate();

const scroller = overlay.querySelector(scrollerTag);

scroller.owner = host || this;
scroller.getItemLabel = this._getItemLabel.bind(this);
scroller.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);

// Trigger the observer to set properties
this._scroller = scroller;
}
Expand All @@ -483,6 +514,17 @@ export const ComboBoxMixin = (subclass) =>
renderer,
theme,
});

// NOTE: in PolylitMixin, setProperties() waits for `hasUpdated` to be set.
// This means for the first opening, properties won't be set synchronously.
// Call `performUpdate()` in this case to mimic the Polymer version logic.
if (scroller.performUpdate && !scroller.hasUpdated) {
try {
scroller.performUpdate();
} catch (_) {
// Suppress errors in synchronous tests for pre-opened combo-box.
}
}
}
}

Expand Down
14 changes: 13 additions & 1 deletion packages/combo-box/src/vaadin-combo-box-scroller-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const ComboBoxScrollerMixin = (superClass) =>
*/
items: {
type: Array,
sync: true,
observer: '__itemsChanged',
},

Expand All @@ -30,6 +31,7 @@ export const ComboBoxScrollerMixin = (superClass) =>
*/
focusedIndex: {
type: Number,
sync: true,
observer: '__focusedIndexChanged',
},

Expand All @@ -38,6 +40,7 @@ export const ComboBoxScrollerMixin = (superClass) =>
*/
loading: {
type: Boolean,
sync: true,
observer: '__loadingChanged',
},

Expand All @@ -47,6 +50,7 @@ export const ComboBoxScrollerMixin = (superClass) =>
*/
opened: {
type: Boolean,
sync: true,
observer: '__openedChanged',
},

Expand All @@ -55,6 +59,7 @@ export const ComboBoxScrollerMixin = (superClass) =>
*/
selectedItem: {
type: Object,
sync: true,
observer: '__selectedItemChanged',
},

Expand Down Expand Up @@ -84,6 +89,7 @@ export const ComboBoxScrollerMixin = (superClass) =>
*/
renderer: {
type: Object,
sync: true,
observer: '__rendererChanged',
},

Expand Down Expand Up @@ -158,7 +164,7 @@ export const ComboBoxScrollerMixin = (superClass) =>
* @param {number} index
*/
scrollIntoView(index) {
if (!(this.opened && index >= 0)) {
if (!this.__virtualizer || !(this.opened && index >= 0)) {
return;
}

Expand Down Expand Up @@ -286,6 +292,12 @@ export const ComboBoxScrollerMixin = (superClass) =>
focused: !this.loading && focusedIndex === index,
});

// NOTE: in PolylitMixin, setProperties() waits for `hasUpdated` to be set.
// However, this causes issues with virtualizer. So we enforce sync update.
if (el.performUpdate && !el.hasUpdated) {
el.performUpdate();
}

el.id = `${this.__hostTagName}-item-${index}`;
el.setAttribute('role', index !== undefined ? 'option' : false);
el.setAttribute('aria-selected', selected.toString());
Expand Down
50 changes: 50 additions & 0 deletions packages/combo-box/src/vaadin-lit-combo-box-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* @license
* Copyright (c) 2015 - 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { css, html, LitElement } from 'lit';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { ComboBoxItemMixin } from './vaadin-combo-box-item-mixin.js';

/**
* LitElement based version of `<vaadin-combo-box-item>` web component.
*
* ## Disclaimer
*
* This component is an experiment not intended for publishing to npm.
* There is no ETA regarding specific Vaadin version where it'll land.
* Feel free to try this code in your apps as per Apache 2.0 license.
*/
export class ComboBoxItem extends ComboBoxItemMixin(ThemableMixin(DirMixin(PolylitMixin(LitElement)))) {
static get is() {
return 'vaadin-combo-box-item';
}

static get styles() {
return css`
:host {
display: block;
}
:host([hidden]) {
display: none;
}
`;
}

/** @protected */
render() {
return html`
<span part="checkmark" aria-hidden="true"></span>
<div part="content">
<slot></slot>
</div>
`;
}
}

defineCustomElement(ComboBoxItem);
Loading

0 comments on commit 846454a

Please sign in to comment.