chore: types button and combobox

This commit is contained in:
Thijs Louisse 2020-10-08 11:28:52 +02:00
parent 15b6079282
commit 6679fe7743
13 changed files with 150 additions and 139 deletions

View file

@ -0,0 +1,6 @@
---
'@lion/button': patch
'@lion/combobox': patch
---
Types button and combobox

View file

@ -1,2 +1,5 @@
/**
* @param {Function} tag
*/
export default tag =>
tag`<svg focusable="false" style="width: 24px; height: 24px;" version="1.1" viewBox="0 0 100 100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g><path d="M44.8,44.2c-0.3-0.1-6.9-2.3-10-8c-0.9-1.7-1.7-3.6-2.4-5.4c-1.4-3.4-2.7-6.6-4.6-6.8l0.1-1.9c3.1,0.2,4.5,3.8,6.2,7.9 c0.7,1.7,1.4,3.5,2.3,5.2c2.7,5.1,8.9,7.1,8.9,7.1L44.8,44.2z"/></g><g><path d="M19.8,61.7v-1.9c4.7,0,10.7-6.6,13.4-10.7c2.6-4,11.2-2.1,12.2-1.9l-0.4,1.8c-2.3-0.5-8.6-1.4-10.2,1.1 C32.7,53.4,25.9,61.7,19.8,61.7z"/></g><g><path d="M25.1,86.5l-0.3-1.9c6-0.9,6.9-9.7,7.6-16.8c0.2-1.9,0.4-3.6,0.6-5.1c1.4-7.4,11.6-10.3,12.1-10.4l0.5,1.8 c-0.1,0-9.6,2.7-10.7,8.9c-0.3,1.4-0.4,3.1-0.6,5C33.5,75.3,32.5,85.4,25.1,86.5z"/></g><g><path d="M45.8,25.4c-0.1-1.4-0.1-4.7,0.9-5.7c0.3-0.3,0.6-0.4,1-0.4v1.9c0.2,0,0.3-0.1,0.4-0.2c-0.3,0.4-0.5,2.4-0.4,4.4 L45.8,25.4z"/></g><g><path d="M43.6,28c-0.3-0.3-6.8-6.2-6.8-11.4c0-5,1.4-8.7,1.5-8.8L40,8.5c0,0-1.3,3.6-1.3,8.1c0,4.4,6.2,10,6.2,10L43.6,28z"/></g><g><path d="M54.6,54.5c0-2.7,0.8-13-0.3-13c-1.1,0-1.8-2.2-1.8-3.4c0-1.1,4.1-3.4,4.1-8.1c0-4.8-6.6-5.6-6.6-5.6s-6.6,0.8-6.6,5.6 c0,4.8,4.1,7,4.1,8.1c0,1.1-0.7,3.4-1.8,3.4c-1.1,0-0.3,10.4-0.3,13c0,2.7-5.3,7.7-5.3,17.2s5.9,19.5,9.9,19.5 c4.1,0,9.9-9.9,9.9-19.5S54.6,57.2,54.6,54.5z"/></g><g><path d="M50,92.2c-5,0-10.9-11.1-10.9-20.4c0-6.5,2.4-11,4-14c0.7-1.4,1.4-2.6,1.4-3.3c0-0.6-0.1-1.8-0.1-3.1 c-0.3-7.6-0.2-9.8,0.5-10.5c0.2-0.2,0.5-0.4,0.8-0.4c0.3-0.1,0.8-1.4,0.9-2.3c-0.1-0.2-0.5-0.6-0.8-1c-1.2-1.4-3.3-3.7-3.3-7.3 c0-5.5,7.1-6.5,7.4-6.5c0.1,0,0.2,0,0.2,0c0.3,0,7.4,1,7.4,6.5c0,3.6-2.1,5.9-3.3,7.3c-0.3,0.3-0.7,0.8-0.8,1 c0,0.9,0.6,2.2,0.9,2.3c0.3,0,0.6,0.1,0.8,0.4c0.7,0.8,0.8,2.9,0.5,10.5c-0.1,1.3-0.1,2.4-0.1,3.1c0,0.7,0.6,1.9,1.4,3.3 c1.6,3,4,7.5,4,14C60.9,81.1,55,92.2,50,92.2z M46.2,42.4c-0.3,1.4-0.1,6.5,0,9c0.1,1.3,0.1,2.5,0.1,3.2c0,1.2-0.7,2.4-1.6,4.1 c-1.6,3-3.7,7.1-3.7,13.1c0,9.4,5.7,18.5,9,18.5s9-9.2,9-18.5c0-6-2.2-10.1-3.7-13.1c-0.9-1.7-1.6-3-1.6-4.1 c0-0.7,0.1-1.8,0.1-3.2c0.1-2.5,0.3-7.6,0-9c-1.6-0.5-2.2-3.1-2.2-4.2c0-0.7,0.5-1.3,1.3-2.1c1.2-1.3,2.8-3.1,2.8-6 c0-3.6-4.8-4.5-5.6-4.6c-0.8,0.1-5.6,1.1-5.6,4.6c0,2.8,1.6,4.7,2.8,6c0.7,0.8,1.3,1.4,1.3,2.1C48.4,39.3,47.8,41.9,46.2,42.4z"/></g><g><path d="M55.2,44.2l-0.6-1.8c0.1,0,6.2-2.1,9-7.1c0.9-1.7,1.6-3.4,2.3-5.2c1.7-4.2,3.2-7.7,6.2-7.9l0.1,1.9 c-1.9,0.1-3.2,3.3-4.6,6.8c-0.7,1.8-1.5,3.6-2.4,5.4C62.1,41.9,55.4,44.1,55.2,44.2z"/></g><g><path d="M80.2,61.7c-6.1,0-12.9-8.4-15-11.6c-1.6-2.5-8-1.6-10.2-1.1l-0.4-1.8c1-0.2,9.6-2.1,12.2,1.9c2.7,4.1,8.7,10.7,13.4,10.7 V61.7z"/></g><g><path d="M74.9,86.5c-7.4-1.1-8.5-11.2-9.2-18.5c-0.2-1.8-0.4-3.6-0.6-5c-1.1-6.3-10.6-8.9-10.7-8.9l0.5-1.8 c0.4,0.1,10.7,3,12.1,10.4c0.3,1.5,0.4,3.2,0.6,5.1c0.7,7.1,1.6,16,7.6,16.9L74.9,86.5z"/></g><g><path d="M54.2,25.4l-1.9-0.1c0.1-1.9-0.1-4-0.4-4.4c0,0,0.2,0.2,0.4,0.2v-1.9c0.4,0,0.7,0.2,1,0.4C54.3,20.7,54.3,24.1,54.2,25.4z "/></g><g><path d="M56.4,28l-1.3-1.4c0.1-0.1,6.2-5.7,6.2-10c0-4.6-1.3-8.1-1.3-8.1l1.8-0.7c0.1,0.2,1.5,3.9,1.5,8.8 C63.2,21.8,56.6,27.8,56.4,28z"/></g></g></svg>`;

View file

@ -6,12 +6,20 @@ import {
LitElement,
SlotMixin,
} from '@lion/core';
import '@lion/core/src/differentKeyEventNamesShimIE.js';
const isKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) =>
e.keyCode === 32 /* space */ || e.keyCode === 13; /* enter */
const isSpaceKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) =>
// @ts-expect-error
e.keyCode === 32 || e.key === 32; /* space */
// TODO: several improvements:
// [1] remove click-area
// [2] remove the native _button slot. We can detect and submit parent form without the slot.
// [3] reduce css so that extending styles makes sense. Merge .btn with host
// [4] reduce the template and remove the if else construction inside the template (an extra
// div by default to support IE is fine) => <div id="${this._buttonId}"><slot></slot></div>
// should be all needed
// [5] do we need the before and after templates? Could be added by subclasser
// [6] extract all functionality (except for form submission) into LionButtonMixin
const isKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' ' || e.key === 'Enter';
const isSpaceKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' ';
export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement)) {
static get properties() {
@ -145,6 +153,7 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
return this._nativeButtonNode.form;
}
// @ts-ignore
get slots() {
return {
...super.slots,
@ -165,8 +174,8 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
this.active = false;
this.__setupDelegationInConstructor();
this._buttonId = `button-${Math.random().toString(36).substr(2, 10)}`;
if (browserDetection.isIE11) {
this._buttonId = `button-${Math.random().toString(36).substr(2, 10)}`;
this.updateComplete.then(() => this.setAttribute('aria-labelledby', this._buttonId));
}
}

View file

@ -1,3 +1,4 @@
// @ts-nocheck
/* globals capture getStoryPage */
const selector = 'lion-button';

View file

@ -1,22 +1,13 @@
import { browserDetection } from '@lion/core';
import { aTimeout, expect, fixture, html, oneEvent } from '@open-wc/testing';
import sinon from 'sinon';
import '@lion/core/src/differentKeyEventNamesShimIE.js';
import '../lion-button.js';
/**
* @typedef {import('@lion/button/src/LionButton').LionButton} LionButton
*/
/**
* @param {HTMLElement} el
*/
function getClickArea(el) {
if (el.shadowRoot) {
return el.shadowRoot.querySelector('.click-area');
}
return undefined;
}
describe('lion-button', () => {
it('behaves like native `button` in terms of a11y', async () => {
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
@ -103,12 +94,12 @@ describe('lion-button', () => {
it('updates "active" attribute on host when space keydown/keyup on button', async () => {
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
el.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 32 }));
el.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
expect(el.active).to.be.true;
await el.updateComplete;
expect(el.hasAttribute('active')).to.be.true;
el.dispatchEvent(new KeyboardEvent('keyup', { keyCode: 32 }));
el.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
expect(el.active).to.be.false;
await el.updateComplete;
expect(el.hasAttribute('active')).to.be.false;
@ -117,12 +108,12 @@ describe('lion-button', () => {
it('updates "active" attribute on host when space keydown on button and space keyup anywhere else', async () => {
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
el.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 32 }));
el.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
expect(el.active).to.be.true;
await el.updateComplete;
expect(el.hasAttribute('active')).to.be.true;
el.dispatchEvent(new KeyboardEvent('keyup', { keyCode: 32 }));
el.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
expect(el.active).to.be.false;
await el.updateComplete;
expect(el.hasAttribute('active')).to.be.false;
@ -131,12 +122,12 @@ describe('lion-button', () => {
it('updates "active" attribute on host when enter keydown/keyup on button', async () => {
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
el.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 13 }));
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
expect(el.active).to.be.true;
await el.updateComplete;
expect(el.hasAttribute('active')).to.be.true;
el.dispatchEvent(new KeyboardEvent('keyup', { keyCode: 13 }));
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
expect(el.active).to.be.false;
await el.updateComplete;
expect(el.hasAttribute('active')).to.be.false;
@ -145,12 +136,12 @@ describe('lion-button', () => {
it('updates "active" attribute on host when enter keydown on button and space keyup anywhere else', async () => {
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
el.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 13 }));
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
expect(el.active).to.be.true;
await el.updateComplete;
expect(el.hasAttribute('active')).to.be.true;
document.body.dispatchEvent(new KeyboardEvent('keyup', { keyCode: 13 }));
document.body.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
expect(el.active).to.be.false;
await el.updateComplete;
expect(el.hasAttribute('active')).to.be.false;
@ -213,8 +204,8 @@ describe('lion-button', () => {
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
expect(el.hasAttribute('aria-labelledby')).to.be.true;
const wrapperId = el.getAttribute('aria-labelledby');
expect(el.shadowRoot.querySelector(`#${wrapperId}`)).to.exist;
expect(el.shadowRoot.querySelector(`#${wrapperId}`)).dom.to.equal(
expect(/** @type {ShadowRoot} */ (el.shadowRoot).querySelector(`#${wrapperId}`)).to.exist;
expect(/** @type {ShadowRoot} */ (el.shadowRoot).querySelector(`#${wrapperId}`)).dom.to.equal(
`<div id="${wrapperId}"><slot></slot></div>`,
);
browserDetectionStub.restore();
@ -248,11 +239,11 @@ describe('lion-button', () => {
<lion-button type="submit">foo</lion-button>
</form>
`);
const button = form.querySelector('lion-button');
getClickArea(button).click();
expect(formSubmitSpy.callCount).to.equal(1);
const button = /** @type {LionButton} */ (
/** @type {LionButton} */ (form.querySelector('lion-button'))
);
button.click();
expect(formSubmitSpy).to.have.been.calledOnce;
});
it('behaves like native `button` when interacted with keyboard space', async () => {
@ -262,13 +253,13 @@ describe('lion-button', () => {
<lion-button type="submit">foo</lion-button>
</form>
`);
form
.querySelector('lion-button')
.dispatchEvent(new KeyboardEvent('keyup', { keyCode: 32 }));
await aTimeout();
await aTimeout();
expect(formSubmitSpy.callCount).to.equal(1);
const button = /** @type {LionButton} */ (
/** @type {LionButton} */ (form.querySelector('lion-button'))
);
button.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
await aTimeout(0);
await aTimeout(0);
expect(formSubmitSpy).to.have.been.calledOnce;
});
it('behaves like native `button` when interacted with keyboard enter', async () => {
@ -279,13 +270,12 @@ describe('lion-button', () => {
</form>
`);
form
.querySelector('lion-button')
.dispatchEvent(new KeyboardEvent('keyup', { keyCode: 13 }));
await aTimeout();
await aTimeout();
const button = /** @type {LionButton} */ (form.querySelector('lion-button'));
button.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
await aTimeout(0);
await aTimeout(0);
expect(formSubmitSpy.callCount).to.equal(1);
expect(formSubmitSpy).to.have.been.calledOnce;
});
it('supports resetting form inputs in a native form', async () => {
@ -296,9 +286,15 @@ describe('lion-button', () => {
<lion-button type="reset">reset</lion-button>
</form>
`);
const btn = form.querySelector('lion-button');
const firstName = form.querySelector('input[name=firstName]');
const lastName = form.querySelector('input[name=lastName]');
const btn = /** @type {LionButton} */ (
/** @type {LionButton} */ (form.querySelector('lion-button'))
);
const firstName = /** @type {HTMLInputElement} */ (form.querySelector(
'input[name=firstName]',
));
const lastName = /** @type {HTMLInputElement} */ (form.querySelector(
'input[name=lastName]',
));
firstName.value = 'Foo';
lastName.value = 'Bar';
@ -322,13 +318,12 @@ describe('lion-button', () => {
</form>
`);
form
.querySelector('input[name="foo2"]')
.dispatchEvent(new KeyboardEvent('keyup', { key: 13 }));
await aTimeout();
await aTimeout();
const input2 = /** @type {HTMLInputElement} */ (form.querySelector('input[name="foo2"]'));
input2.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
await aTimeout(0);
await aTimeout(0);
expect(formSubmitSpy.callCount).to.equal(1);
expect(formSubmitSpy).to.have.been.calledOnce;
});
});
@ -336,69 +331,67 @@ describe('lion-button', () => {
it('behaves like native `button` when clicked', async () => {
const formButtonClickedSpy = /** @type {EventListener} */ (sinon.spy());
const form = await fixture(html`
<form @submit=${ev => ev.preventDefault()}>
<form @submit=${/** @type {EventListener} */ ev => ev.preventDefault()}>
<lion-button @click="${formButtonClickedSpy}" type="submit">foo</lion-button>
</form>
`);
const button = form.querySelector('lion-button');
getClickArea(button).click();
const button = /** @type {LionButton} */ (form.querySelector('lion-button'));
button.click();
expect(formButtonClickedSpy.callCount).to.equal(1);
expect(formButtonClickedSpy).to.have.been.calledOnce;
});
it('behaves like native `button` when interacted with keyboard space', async () => {
const formButtonClickedSpy = /** @type {EventListener} */ (sinon.spy());
const form = await fixture(html`
<form @submit=${ev => ev.preventDefault()}>
<form @submit=${/** @type {EventListener} */ ev => ev.preventDefault()}>
<lion-button @click="${formButtonClickedSpy}" type="submit">foo</lion-button>
</form>
`);
form
.querySelector('lion-button')
.dispatchEvent(new KeyboardEvent('keyup', { keyCode: 32 }));
await aTimeout();
await aTimeout();
/** @type {LionButton} */ (form.querySelector('lion-button')).dispatchEvent(
new KeyboardEvent('keyup', { key: ' ' }),
);
await aTimeout(0);
await aTimeout(0);
expect(formButtonClickedSpy.callCount).to.equal(1);
expect(formButtonClickedSpy).to.have.been.calledOnce;
});
it('behaves like native `button` when interacted with keyboard enter', async () => {
const formButtonClickedSpy = /** @type {EventListener} */ (sinon.spy());
const form = await fixture(html`
<form @submit=${ev => ev.preventDefault()}>
<form @submit=${/** @type {EventListener} */ ev => ev.preventDefault()}>
<lion-button @click="${formButtonClickedSpy}" type="submit">foo</lion-button>
</form>
`);
form
.querySelector('lion-button')
.dispatchEvent(new KeyboardEvent('keyup', { keyCode: 13 }));
await aTimeout();
await aTimeout();
const button = /** @type {LionButton} */ (form.querySelector('lion-button'));
button.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
await aTimeout(0);
await aTimeout(0);
expect(formButtonClickedSpy.callCount).to.equal(1);
expect(formButtonClickedSpy).to.have.been.calledOnce;
});
// input "enter" keypress mock doesn't seem to work right now, but should be tested in the future (maybe with Selenium)
it.skip('works with implicit form submission on-enter inside an input', async () => {
const formButtonClickedSpy = /** @type {EventListener} */ (sinon.spy());
const form = await fixture(html`
<form @submit=${ev => ev.preventDefault()}>
<form @submit=${/** @type {EventListener} */ ev => ev.preventDefault()}>
<input name="foo" />
<input name="foo2" />
<lion-button @click="${formButtonClickedSpy}" type="submit">foo</lion-button>
</form>
`);
form
.querySelector('input[name="foo2"]')
.dispatchEvent(new KeyboardEvent('keyup', { key: 13 }));
await aTimeout();
await aTimeout();
const input2 = /** @type {HTMLInputElement} */ (form.querySelector('input[name="foo2"]'));
input2.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
await aTimeout(0);
await aTimeout(0);
expect(formButtonClickedSpy.callCount).to.equal(1);
expect(formButtonClickedSpy).to.have.been.calledOnce;
});
});
});
@ -410,28 +403,28 @@ describe('lion-button', () => {
html`<lion-button @click="${clickSpy}">foo</lion-button>`,
));
getClickArea(el).click();
el.click();
// trying to wait for other possible redispatched events
await aTimeout();
await aTimeout();
await aTimeout(0);
await aTimeout(0);
expect(clickSpy.callCount).to.equal(1);
expect(clickSpy).to.have.been.calledOnce;
});
describe('native button behavior', async () => {
/**
* @param {HTMLButtonElement | LionButton} el
*/
async function prepareClickEvent(el) {
setTimeout(() => {
if (getClickArea(el)) {
getClickArea(el).click();
} else {
el.click();
}
el.click();
});
return oneEvent(el, 'click');
}
/** @type {Event} */
let nativeButtonEvent;
/** @type {Event} */
let lionButtonEvent;
before(async () => {

View file

@ -1,6 +1,7 @@
import { css, html } from '@lion/core';
import { LionButton } from '@lion/button';
// @ts-expect-error
export class GhButton extends LionButton {
static get properties() {
return {
@ -65,5 +66,10 @@ export class GhButton extends LionButton {
<slot name="after"></slot>
<slot name="_button"></slot>`;
}
constructor() {
super();
this.value = '';
}
}
customElements.define('gh-button', GhButton);

View file

@ -5,6 +5,7 @@ import { renderLitAsNode } from '@lion/helpers';
import { LionCombobox } from '../../src/LionCombobox.js';
import './gh-button.js';
// @ts-expect-error
export class GhOption extends LionOption {
static get properties() {
return {

View file

@ -1,4 +1,4 @@
// eslint-disable-next-line max-classes-per-file
// @ts-nocheck there's an error in cli that cannot be reproduced locally
import { html, css, browserDetection } from '@lion/core';
import { OverlayMixin, withDropdownConfig } from '@lion/overlays';
import { LionListbox } from '@lion/listbox';
@ -12,12 +12,14 @@ import { LionListbox } from '@lion/listbox';
* @typedef {import('@lion/listbox').LionOptions} LionOptions
* @typedef {import('@lion/overlays/types/OverlayConfig').OverlayConfig} OverlayConfig
* @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap
* @typedef {import('../types/SelectionDisplay').SelectionDisplay} SelectionDisplay
*/
/**
* LionCombobox: implements the wai-aria combobox design pattern and integrates it as a Lion
* FormControl
*/
// @ts-ignore
export class LionCombobox extends OverlayMixin(LionListbox) {
static get properties() {
return {
@ -73,6 +75,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
*/
// eslint-disable-next-line class-methods-use-this
_inputGroupInputTemplate() {
// @ts-ignore
return html`
<div class="input-group__input">
<slot name="selection-display"></slot>
@ -101,6 +104,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @type {SlotsMap}
*/
// @ts-ignore
get slots() {
return {
...super.slots,
@ -158,7 +162,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
}
/**
* @type {HTMLElement | null}
* @type {SelectionDisplay | null}
*/
get _selectionDisplayNode() {
return this.querySelector('[slot="selection-display"]');
@ -187,7 +191,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @configure OverlayMixin
*/
get _overlayReferenceNode() {
return this.shadowRoot.querySelector('.input-group__container');
return /** @type {ShadowRoot} */ (this.shadowRoot).querySelector('.input-group__container');
}
/**
@ -358,7 +362,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
}
/**
* @param {Event} ev
* @param {KeyboardEvent} ev
*/
_textboxOnKeydown(ev) {
if (ev.key === 'Tab') {
@ -379,6 +383,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
this._inputNode.focus();
}
/**
* @param {string} v
*/
_setTextboxValue(v) {
this._inputNode.value = v;
}
@ -439,6 +446,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* Computes whether a user intends to autofill (inline autocomplete textbox)
* @overridable
* @param {{ prevValue:string, curValue:string }} config
*/
_computeUserIntendsAutoFill({ prevValue, curValue }) {
const userIsAddingChars = prevValue.length < curValue.length;
@ -474,6 +482,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
let hasAutoFilled = false;
const userIntendsAutoFill = this._computeUserIntendsAutoFill({ prevValue, curValue });
const isCandidate = this.autocomplete === 'both' || this.autocomplete === 'inline';
// @ts-ignore this.autocomplete === 'none' needs to be there if statement above is removed
const noFilter = this.autocomplete === 'inline' || this.autocomplete === 'none';
/** @typedef {LionOption & { onFilterUnmatch?:function, onFilterMatch?:function }} OptionWithFilterFn */

View file

@ -7,6 +7,7 @@ import { browserDetection, LitElement } from '@lion/core';
/**
* @typedef {import('../src/LionCombobox.js').LionCombobox} LionCombobox
* @typedef {import('../types/SelectionDisplay').SelectionDisplay} SelectionDisplay
*/
/**
@ -23,10 +24,10 @@ function mimicUserTyping(el, value) {
/**
* @param {LionCombobox} el
* @param {string[]} value
* @param {string[]} values
*/
async function mimicUserTypingAdvanced(el, values) {
const inputNode = el._inputNode;
const inputNode = /** @type {HTMLInputElement & {selectionStart:number, selectionEnd:number}} */ (el._inputNode);
inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
let hasSelection = inputNode.selectionStart !== inputNode.selectionEnd;
@ -439,8 +440,15 @@ describe('lion-combobox', () => {
describe('Selection display', () => {
class MySelectionDisplay extends LitElement {
/**
* @param {import('lit-element').PropertyValues } changedProperties
*/
onComboboxElementUpdated(changedProperties) {
if (changedProperties.has('modelValue') && this.comboboxElement.multipleChoice) {
if (
changedProperties.has('modelValue') &&
// @ts-ignore
this.comboboxElement.multipleChoice
) {
// do smth..
}
}
@ -464,6 +472,7 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
</lion-combobox>
`));
// @ts-ignore allow protected members
expect(el._selectionDisplayNode.comboboxElement).to.equal(el);
});
@ -474,6 +483,7 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
</lion-combobox>
`));
// @ts-expect-error sinon not typed correctly?
const spy = sinon.spy(el._selectionDisplayNode, 'onComboboxElementUpdated');
el.requestUpdate('modelValue');
await el.updateComplete;
@ -974,14 +984,14 @@ describe('lion-combobox', () => {
});
it('updates aria-activedescendant on textbox node', async () => {
let el = await fixture(html`
let el = /** @type {LionCombobox} */ (await fixture(html`
<lion-combobox name="foo" autocomplete="none">
<lion-option .choiceValue="${'Artichoke'}" id="artichoke-option">Artichoke</lion-option>
<lion-option .choiceValue="${'Chard'}" id="chard-option">Chard</lion-option>
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`);
`));
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(null);
expect(el.formElements[1].active).to.equal(false);
@ -995,14 +1005,14 @@ describe('lion-combobox', () => {
);
expect(el.formElements[1].active).to.equal(false);
el = await fixture(html`
el = /** @type {LionCombobox} */ (await fixture(html`
<lion-combobox name="foo" autocomplete="both" match-mode="begin">
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`);
`));
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
await el.updateComplete;
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(

View file

@ -0,0 +1,6 @@
import { LionCombobox } from '../src/LionCombobox.js';
export interface SelectionDisplay extends HTMLElement {
comboboxElement: LionCombobox;
onComboboxElementUpdated: Function;
}

View file

@ -44,9 +44,9 @@ export declare class ListboxHost {
/** Reset interaction states and modelValue */
public reset(): void;
protected _scrollTargetNode: LionOptions;
protected get _scrollTargetNode(): LionOptions;
protected _listboxNode: LionOptions;
protected get _listboxNode(): LionOptions;
protected _listboxReceivesNoFocus: boolean;
@ -72,7 +72,7 @@ export declare class ListboxHost {
protected _onChildActiveChanged(ev: Event): void;
protected _activeDescendantOwnerNode: HTMLElement;
protected get _activeDescendantOwnerNode(): HTMLElement;
}
export declare function ListboxImplementation<T extends Constructor<LitElement>>(

View file

@ -1,3 +1,4 @@
// @ts-nocheck
/* globals capture getStoryPage */
const selector = 'lion-select-rich';
@ -5,21 +6,16 @@ const selector = 'lion-select-rich';
describe('forms-select-rich', () => {
it('main', async () => {
const id = `forms-select-rich--main`;
// @ts-expect-error
const page = await getStoryPage(id);
// @ts-expect-error
await capture({ selector, id, page });
});
it('main-opened', async () => {
const id = `forms-select-rich--main`;
// @ts-expect-error
const page = await getStoryPage(id);
await page.evaluate(() => {
const el = document.querySelector('lion-select-rich');
// @ts-expect-error
el.opened = true;
});
// @ts-expect-error
await capture({
selector,
id: `${id}-opened`,
@ -29,21 +25,16 @@ describe('forms-select-rich', () => {
});
it('options-with-html', async () => {
const id = `forms-select-rich--options-with-html`;
// @ts-expect-error
const page = await getStoryPage(id);
// @ts-expect-error
await capture({ selector, id, page });
});
it('options-with-html-opened', async () => {
const id = `forms-select-rich--options-with-html`;
// @ts-expect-error
const page = await getStoryPage(id);
await page.evaluate(() => {
const el = document.querySelector('lion-select-rich');
// @ts-expect-error
el.opened = true;
});
// @ts-expect-error
await capture({
selector,
id: `${id}-opened`,
@ -53,14 +44,11 @@ describe('forms-select-rich', () => {
});
it('many-options-with-scrolling-opened', async () => {
const id = `forms-select-rich--many-options-with-scrolling`;
// @ts-expect-error
const page = await getStoryPage(id);
await page.evaluate(() => {
const el = document.querySelector('lion-select-rich');
// @ts-expect-error
el.opened = true;
});
// @ts-expect-error
await capture({
selector,
id: `${id}-opened`,
@ -70,28 +58,21 @@ describe('forms-select-rich', () => {
});
it('read-only-prefilled', async () => {
const id = `forms-select-rich--read-only-prefilled`;
// @ts-expect-error
const page = await getStoryPage(id);
// @ts-expect-error
await capture({ selector, id, page });
});
it('disabled-select', async () => {
const id = `forms-select-rich--disabled-select`;
// @ts-expect-error
const page = await getStoryPage(id);
// @ts-expect-error
await capture({ selector, id, page });
});
it('disabled-option-opened', async () => {
const id = `forms-select-rich--disabled-option`;
// @ts-expect-error
const page = await getStoryPage(id);
await page.evaluate(() => {
const el = document.querySelector('lion-select-rich');
// @ts-expect-error
el.opened = true;
});
// @ts-expect-error
await capture({
selector,
id: `${id}-opened`,
@ -101,35 +82,26 @@ describe('forms-select-rich', () => {
});
it('validation', async () => {
const id = `forms-select-rich--validation`;
// @ts-expect-error
const page = await getStoryPage(id);
await page.evaluate(() => {
const el = document.querySelector('lion-select-rich');
// @ts-expect-error
el.updateComplete.then(() => {
// @ts-expect-error
el.touched = true;
// @ts-expect-error
el.dirty = true;
});
});
// @ts-expect-error
await capture({ selector, id, page });
});
it('render-options', async () => {
const id = `forms-select-rich--render-options`;
// @ts-expect-error
const page = await getStoryPage(id);
// @ts-expect-error
await capture({ selector, id, page });
});
it('interaction-mode-mac', async () => {
const id = `forms-select-rich--interaction-mode`;
// @ts-expect-error
const page = await getStoryPage(id);
await page.click('lion-select-rich');
await page.keyboard.press('ArrowDown');
// @ts-expect-error
await capture({
selector,
id: `${id}-mac`,
@ -138,11 +110,9 @@ describe('forms-select-rich', () => {
});
it('interaction-mode-windows-linux', async () => {
const id = `forms-select-rich--interaction-mode`;
// @ts-expect-error
const page = await getStoryPage(id);
await page.click('lion-select-rich:last-of-type');
await page.keyboard.press('ArrowDown');
// @ts-expect-error
await capture({
selector: 'lion-select-rich:last-of-type',
id: `${id}-windows-linux`,
@ -151,16 +121,12 @@ describe('forms-select-rich', () => {
});
it('no-default-selection', async () => {
const id = `forms-select-rich--no-default-selection`;
// @ts-expect-error
const page = await getStoryPage(id);
// @ts-expect-error
await capture({ selector, id, page });
});
it('single-option', async () => {
const id = `forms-select-rich--single-option`;
// @ts-expect-error
const page = await getStoryPage(id);
// @ts-expect-error
await capture({ selector, id, page });
});
});

View file

@ -16,12 +16,13 @@
},
"include": [
"packages/accordion/**/*.js",
"packages/button/src/**/*.js",
"packages/button/**/*.js",
"packages/calendar/**/*.js",
"packages/button/index.js",
"packages/checkbox-group/**/*.js",
"packages/collapsible/**/*.js",
"packages/core/**/*.js",
"packages/combobox/(test|src)/**/*.js",
"packages/combobox/*.js",
"packages/dialog/**/*.js",
"packages/fieldset/**/*.js",
"packages/form/**/*.js",