diff --git a/.changeset/metal-apes-bathe.md b/.changeset/metal-apes-bathe.md new file mode 100644 index 000000000..93ca7bfc3 --- /dev/null +++ b/.changeset/metal-apes-bathe.md @@ -0,0 +1,29 @@ +--- +"@lion/combobox": patch +"@lion/listbox": patch +--- + +Combobox: demos, Subclasser features and fixes + + +### Features +- Subclassers can configure `_syncToTextboxCondition()`. By default only for `autocomplete="inline|both"` +- Subclassers can configure `_showOverlayCondition(options)`. For instance, already show once textbox gets focus or add your own custom +- Subclassers can configure `_syncToTextboxMultiple(modelValue, oldModelValue)`. See https://github.com/ing-bank/lion/issues/1038 +- Subclassers can configure `_autoSelectCondition`, for instance to have autcomplete="list" with auto select instead of manual selection. Both are possible according to w3c specs + +### Fixes +- listbox multiselect can deselect again on 'Enter' and 'Space'. Closes https://github.com/ing-bank/lion/issues/1059 +- combobox multiselect display only shows last selected option in textbox (instead of all). See https://github.com/ing-bank/lion/issues/1038 +- default sync to textbox behavior for `autocomplete="none|list"` is no sync with textbox + +### Demos +- created a google combobox demo (with anchors as options) + - advanced styling example + - uses autocomplete 'list' as a fundament and enhances `_showOverlayCondition` and `_syncToTextboxCondition` +- enhanced whatsapp combobox demo + - how to match/highlight text on multiple rows of the option (not just choiceValue) + +### Potentially breaking for subclassers: +- `_computeUserIntendsAutoFill` -> `__computeUserIntendsAutoFill` (not overridable) +- `_syncCheckedWithTextboxOnInteraction ` is removed. Use `_syncToTextboxCondition` and/or `_syncToTextboxMultiple` diff --git a/packages/combobox/README.md b/packages/combobox/README.md index 583989a55..1d41e8446 100644 --- a/packages/combobox/README.md +++ b/packages/combobox/README.md @@ -67,7 +67,7 @@ to the configurable values `none`, `list`, `inline` and `both`. | | list | filter | focus | check | complete | | -----: | :--: | :----: | :---: | :---: | :------: | | none | ✓ | | | | | -| list | ✓ | ✓ | ✓ | ✓ | | +| list | ✓ | ✓ | | | | | inline | ✓ | | ✓ | ✓ | ✓ | | both | ✓ | ✓ | ✓ | ✓ | ✓ | diff --git a/packages/combobox/docs/Subclassers.md b/packages/combobox/docs/Subclassers.md index 7ae142276..823ef2206 100644 --- a/packages/combobox/docs/Subclassers.md +++ b/packages/combobox/docs/Subclassers.md @@ -5,13 +5,22 @@ import { html } from 'lit-html'; import './md-combobox/md-combobox.js'; import './gh-combobox/gh-combobox.js'; import './wa-combobox/wa-combobox.js'; -import './lm-option/lm-option.js'; +import './google-combobox/google-combobox.js'; export default { title: 'Forms/Combobox/Extensions', }; ``` +Below several extensions can be found. They illustrate that complex UI components can be created +easily from an extended Lion component, just by: + +- **configuring**: setting properties or providing conditions via methods +- **enhancing**: adding extra html/styles/logic without changing behavior of the extended element +- **overriding**: replace html/styles/logic of the extended element with your own + +## Material Design + ```js preview-story export const MaterialDesign = () => html` @@ -24,6 +33,8 @@ export const MaterialDesign = () => html` `; ``` +## Github + ```js preview-story export const Github = () => html` @@ -36,6 +47,8 @@ export const Github = () => html` `; ``` +## Whatsapp + ```js preview-story export const Whatsapp = () => html` @@ -81,44 +94,66 @@ export const Whatsapp = () => html` `; ``` +**Whatsapp example shows:** + +- advanced styling +- how to match/highlight text on multiple rows of the option (not just choiceValue) +- how to animate options + +## Google Search + ```js preview-story -export const LinkMixinBox = () => html` - - Apple - Artichoke - Asparagus - Banana - Beets - -`; +export const GoogleSearch = () => { + const appleLogoUrl = new URL('./google-combobox/assets/appleLogo.png', import.meta.url).href; + return html` + + Apple + Artichoke + Asparagus + Banana + Beets + +
+ `; +}; ``` + +**Google Search example shows:** + +- advanced styling +- how to use options that are links +- create exact user experience of Google Search, by: + - using autocomplete 'list' as a fundament (we don't want inline completion in textbox) + - enhancing `_showOverlayCondition`: open on focus + - enhancing `_syncToTextboxCondition`: always sync to textbox when navigating by keyboard (this needs to be enabled, since it's not provided in the "autocomplete=list" preset) diff --git a/packages/combobox/docs/google-combobox/assets/appleLogo.png b/packages/combobox/docs/google-combobox/assets/appleLogo.png new file mode 100644 index 000000000..ec15637c3 Binary files /dev/null and b/packages/combobox/docs/google-combobox/assets/appleLogo.png differ diff --git a/packages/combobox/docs/google-combobox/assets/google-clear-icon.js b/packages/combobox/docs/google-combobox/assets/google-clear-icon.js new file mode 100644 index 000000000..d31ec2637 --- /dev/null +++ b/packages/combobox/docs/google-combobox/assets/google-clear-icon.js @@ -0,0 +1,9 @@ +import { html } from '@lion/core'; + +export default html` + + + +`; diff --git a/packages/combobox/docs/google-combobox/assets/google-search-icon.js b/packages/combobox/docs/google-combobox/assets/google-search-icon.js new file mode 100644 index 000000000..8ceaa571e --- /dev/null +++ b/packages/combobox/docs/google-combobox/assets/google-search-icon.js @@ -0,0 +1,9 @@ +import { html } from '@lion/core'; + +export default html` + + + +`; diff --git a/packages/combobox/docs/google-combobox/assets/google-voice-search-icon.js b/packages/combobox/docs/google-combobox/assets/google-voice-search-icon.js new file mode 100644 index 000000000..c439b89c7 --- /dev/null +++ b/packages/combobox/docs/google-combobox/assets/google-voice-search-icon.js @@ -0,0 +1,19 @@ +import { html } from '@lion/core'; + +export default html` + + + + + + +`; diff --git a/packages/combobox/docs/google-combobox/assets/googlelogo_color_272x92dp.png b/packages/combobox/docs/google-combobox/assets/googlelogo_color_272x92dp.png new file mode 100644 index 000000000..333bda937 Binary files /dev/null and b/packages/combobox/docs/google-combobox/assets/googlelogo_color_272x92dp.png differ diff --git a/packages/combobox/docs/google-combobox/google-combobox.js b/packages/combobox/docs/google-combobox/google-combobox.js new file mode 100644 index 000000000..f4aa653a7 --- /dev/null +++ b/packages/combobox/docs/google-combobox/google-combobox.js @@ -0,0 +1,460 @@ +import { css, html } from '@lion/core'; +import { LionOption } from '@lion/listbox'; +import { renderLitAsNode } from '@lion/helpers'; +import { LionCombobox } from '../../src/LionCombobox.js'; +import { LinkMixin } from '../LinkMixin.js'; +import googleSearchIcon from './assets/google-search-icon.js'; +import googleVoiceSearchIcon from './assets/google-voice-search-icon.js'; +import googleClearIcon from './assets/google-clear-icon.js'; + +const googleSearchLogoUrl = new URL('./assets/googlelogo_color_272x92dp.png', import.meta.url).href; + +export class GoogleOption extends LinkMixin(LionOption) { + static get properties() { + return { + imageUrl: { + type: String, + }, + }; + } + + static get styles() { + return [ + super.styles, + css` + :host { + position: relative; + padding: 8px 16px; + display: flex; + align-items: center; + background: none; + } + + :host:hover, + :host([active]) { + background: #eee !important; + } + + :host([checked]) { + background: none; + } + + /* :host([active]) { + color: #1867c0 !important; + caret-color: #1867c0 !important; + } */ + + :host { + font-weight: bold; + } + + :host ::slotted(.google-option__highlight) { + font-weight: normal; + } + + .google-option__icon { + height: 20px; + width: 20px; + margin-right: 12px; + fill: var(--icon-color); + } + `, + ]; + } + + /** + * @configure + * @param {string} currentValue + */ + onFilterMatch(currentValue) { + const { innerHTML } = this; + // eslint-disable-next-line no-param-reassign + this.__originalInnerHTML = innerHTML; + const newInnerHTML = innerHTML.replace( + new RegExp(`(${currentValue})`, 'i'), + `$1`, + ); + // For Safari, we need to add a label to the element + this.setAttribute('aria-label', this.textContent); + this.innerHTML = newInnerHTML; + // Alternatively, an extension can add an animation here + this.style.display = ''; + } + + /** + * @configure LionCombobox + */ + onFilterUnmatch() { + this.removeAttribute('aria-label'); + if (this.__originalInnerHTML) { + this.innerHTML = this.__originalInnerHTML; + } + this.style.display = 'none'; + } + + render() { + return html` + ${!this.imageUrl + ? html`
${googleSearchIcon}
` + : html` `} + ${super.render()} + `; + } +} +customElements.define('google-option', GoogleOption); + +export class GoogleCombobox extends LionCombobox { + static get styles() { + return [ + super.styles, + css` + /** @configure FormControlMixin */ + + /* ======================= + block | .form-field + ======================= */ + + :host { + font-family: arial, sans-serif; + } + + .form-field__label { + margin-top: 36px; + margin-bottom: 24px; + display: flex; + justify-content: center; + } + + /* ============================== + element | .input-group + ============================== */ + + .input-group { + margin-bottom: 16px; + max-width: 582px; + margin: auto; + } + + .input-group__container { + position: relative; + background: #fff; + display: flex; + border: 1px solid #dfe1e5; + box-shadow: none; + border-radius: 24px; + height: 44px; + } + + .input-group__container:hover, + :host([opened]) .input-group__container { + border-color: rgba(223, 225, 229, 0); + box-shadow: 0 1px 6px rgba(32, 33, 36, 0.28); + } + + :host([opened]) .input-group__container { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + + :host([opened]) .input-group__container::before { + content: ''; + position: absolute; + background: white; + left: 0; + right: 0; + height: 10px; + bottom: -10px; + } + + :host([opened]) .input-group__container::after { + content: ''; + position: absolute; + background: #eee; + left: 16px; + right: 16px; + height: 1px; + bottom: 0; + z-index: 3; + } + + .input-group__prefix, + .input-group__suffix { + display: block; + fill: var(--icon-color); + display: flex; + place-items: center; + } + + .input-group__input { + flex: 1; + } + + .input-group__input ::slotted([slot='input']) { + border: transparent; + width: 100%; + } + + /** @configure LionCombobox */ + + /* ======================= + block | .form-field + ======================= */ + + #overlay-content-node-wrapper { + box-shadow: 0 4px 6px rgba(32, 33, 36, 0.28); + border-radius: 0 0 24px 24px; + margin-top: -2px; + padding-top: 6px; + background: white; + } + + * > ::slotted([slot='listbox']) { + margin-bottom: 8px; + background: none; + } + + :host { + --icon-color: #9aa0a6; + } + + /** @enhance LionCombobox */ + + /* =================================== + block | .google-search-clear-btn + =================================== */ + + .google-search-clear-btn { + position: relative; + height: 100%; + align-items: center; + display: none; + } + + .google-search-clear-btn::after { + border-left: 1px solid #dfe1e5; + height: 65%; + right: 0; + content: ''; + margin-right: 10px; + margin-left: 8px; + } + + :host([filled]) .google-search-clear-btn { + display: flex; + } + + * > ::slotted([slot='suffix']), + * > ::slotted([slot='clear-btn']) { + font: inherit; + margin: 0; + border: 0; + outline: 0; + padding: 0; + color: inherit; + background-color: transparent; + text-align: left; + white-space: normal; + overflow: visible; + + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + -webkit-tap-highlight-color: transparent; + + width: 25px; + height: 25px; + cursor: pointer; + } + + * > ::slotted([slot='suffix']) { + margin-right: 20px; + } + + * > ::slotted([slot='prefix']) { + height: 20px; + width: 20px; + margin-left: 12px; + margin-right: 16px; + } + + /* ============================= + block | .google-search-btns + ============================ */ + + .google-search-btns { + display: flex; + justify-content: center; + align-items: center; + } + + .google-search-btns__input-button { + background-image: -webkit-linear-gradient(top, #f8f9fa, #f8f9fa); + background-color: #f8f9fa; + border: 1px solid #f8f9fa; + border-radius: 4px; + color: #3c4043; + font-family: arial, sans-serif; + font-size: 14px; + margin: 11px 4px; + padding: 0 16px; + line-height: 27px; + height: 36px; + min-width: 54px; + text-align: center; + cursor: pointer; + user-select: none; + } + + .google-search-btns__input-button:hover { + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + background-image: -webkit-linear-gradient(top, #f8f8f8, #f1f1f1); + background-color: #f8f8f8; + border: 1px solid #c6c6c6; + color: #222; + } + + .google-search-btns__input-button:focus { + border: 1px solid #4d90fe; + outline: none; + } + + /* =============================== + block | .google-search-report + ============================== */ + + .google-search-report { + display: flex; + align-content: right; + color: #70757a; + font-style: italic; + font-size: 8pt; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + margin-bottom: 8px; + justify-content: flex-end; + margin-right: 20px; + } + + .google-search-report a { + color: inherit; + } + `, + ]; + } + + /** + * @enhance LionCombobox - add google search buttons + */ + _overlayListboxTemplate() { + return html` + + + `; + } + + /** + * @enhance FormControlMixin add clear-btn + */ + _inputGroupSuffixTemplate() { + return html` +
+
+ +
+ +
+ `; + } + + _googleSearchBtnsTemplate() { + return html`
+ + +
`; + } + + /** + * @enhance FormControlMixin - add google search buttons + */ + _groupTwoTemplate() { + return html`${super._groupTwoTemplate()} ${!this.opened ? this._googleSearchBtnsTemplate() : ''} `; + } + + get slots() { + return { + ...super.slots, + label: () => renderLitAsNode(html` Google Search`), + prefix: () => renderLitAsNode(html` ${googleSearchIcon} `), + suffix: () => + renderLitAsNode( + html` `, + ), + 'clear-btn': () => + renderLitAsNode( + html` + + `, + ), + }; + } + + /** + * @configure OverlayMixin + */ + get _overlayReferenceNode() { + return /** @type {ShadowRoot} */ (this.shadowRoot).querySelector('.input-group'); + } + + constructor() { + super(); + /** @configure LionCombobox */ + this.autocomplete = 'list'; + /** @configure LionCombobox */ + this.showAllOnEmpty = true; + + this.__resetFocus = this.__resetFocus.bind(this); + this.__clearText = this.__clearText.bind(this); + } + + firstUpdated(changedProperties) { + super.firstUpdated(changedProperties); + + this._overlayContentNode.addEventListener('mouseenter', this.__resetFocus); + } + + /** + * @override LionCombobox - always sync textbox when selected value changes + */ + // eslint-disable-next-line no-unused-vars + _syncToTextboxCondition() { + return true; + } + + _showOverlayCondition(options) { + return this.focused || super.showOverlayCondition(options); + } + + __resetFocus() { + this.activeIndex = -1; + this.checkedIndex = -1; + } + + __clearText() { + this._inputNode.value = ''; + } +} +customElements.define('google-combobox', GoogleCombobox); diff --git a/packages/combobox/docs/lm-option/lm-option.js b/packages/combobox/docs/lm-option/lm-option.js deleted file mode 100644 index 242aa2701..000000000 --- a/packages/combobox/docs/lm-option/lm-option.js +++ /dev/null @@ -1,6 +0,0 @@ -import { LionOption } from '@lion/listbox'; -import { LinkMixin } from '../LinkMixin.js'; - -export class LmOption extends LinkMixin(LionOption) {} - -customElements.define('lm-option', LmOption); diff --git a/packages/combobox/docs/wa-combobox/wa-combobox.js b/packages/combobox/docs/wa-combobox/wa-combobox.js index c876df32f..9d4162b0b 100644 --- a/packages/combobox/docs/wa-combobox/wa-combobox.js +++ b/packages/combobox/docs/wa-combobox/wa-combobox.js @@ -167,9 +167,9 @@ class WaOption extends LionOption { :host([is-user-text-read]) .wa-option__content-row2-text-inner-icon { color: lightblue; } - /* - .wa-option__content-row2-menu { - } */ + .wa-selected { + color: #009688; + } `, ]; } @@ -178,13 +178,7 @@ class WaOption extends LionOption { return html`