feat: update to latest overlay system
Co-authored-by: Thomas Allmer <Thomas.Allmer@ing.com> Co-authored-by: Joren Broekema <Joren.Broekema@ing.com> Co-authored-by: Mikhail Bashkirov <Mikhail.Bashkirov@ing.com> Co-authored-by: Alex Ghiu <Alex.Ghiu@ing.com>
This commit is contained in:
parent
364f185ad8
commit
4c26befaae
19 changed files with 243 additions and 259 deletions
|
|
@ -221,7 +221,6 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
super.firstUpdated();
|
super.firstUpdated();
|
||||||
this.__contentWrapperElement = this.shadowRoot.getElementById('js-content-wrapper');
|
this.__contentWrapperElement = this.shadowRoot.getElementById('js-content-wrapper');
|
||||||
|
|
||||||
this.__addEventDelegationForClickDate();
|
this.__addEventDelegationForClickDate();
|
||||||
this.__addEventDelegationForFocusDate();
|
this.__addEventDelegationForFocusDate();
|
||||||
this.__addEventDelegationForBlurDate();
|
this.__addEventDelegationForBlurDate();
|
||||||
|
|
@ -501,6 +500,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
__removeEventDelegations() {
|
__removeEventDelegations() {
|
||||||
|
if (!this.__contentWrapperElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.__contentWrapperElement.removeEventListener('click', this.__clickDateDelegation);
|
this.__contentWrapperElement.removeEventListener('click', this.__clickDateDelegation);
|
||||||
this.__contentWrapperElement.removeEventListener('focus', this.__focusDateDelegation);
|
this.__contentWrapperElement.removeEventListener('focus', this.__focusDateDelegation);
|
||||||
this.__contentWrapperElement.removeEventListener('blur', this.__blurDateDelegation);
|
this.__contentWrapperElement.removeEventListener('blur', this.__blurDateDelegation);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { html, render, ifDefined } from '@lion/core';
|
import { html, ifDefined, render } from '@lion/core';
|
||||||
import { LionInputDate } from '@lion/input-date';
|
import { LionInputDate } from '@lion/input-date';
|
||||||
import { overlays, ModalDialogController } from '@lion/overlays';
|
import { OverlayController, withModalDialogConfig, OverlayMixin } from '@lion/overlays';
|
||||||
import { Unparseable, isValidatorApplied } from '@lion/validate';
|
import { isValidatorApplied } from '@lion/validate';
|
||||||
import '@lion/calendar/lion-calendar.js';
|
import '@lion/calendar/lion-calendar.js';
|
||||||
import './lion-calendar-overlay-frame.js';
|
import './lion-calendar-overlay-frame.js';
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ import './lion-calendar-overlay-frame.js';
|
||||||
* @customElement lion-input-datepicker
|
* @customElement lion-input-datepicker
|
||||||
* @extends {LionInputDate}
|
* @extends {LionInputDate}
|
||||||
*/
|
*/
|
||||||
export class LionInputDatepicker extends LionInputDate {
|
export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,7 +46,11 @@ export class LionInputDatepicker extends LionInputDate {
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
[this._calendarInvokerSlot]: () => this.__createPickerAndReturnInvokerNode(),
|
[this._calendarInvokerSlot]: () => {
|
||||||
|
const renderParent = document.createElement('div');
|
||||||
|
render(this._invokerTemplate(), renderParent);
|
||||||
|
return renderParent.firstElementChild;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,12 +141,8 @@ export class LionInputDatepicker extends LionInputDate {
|
||||||
return this.querySelector(`#${this.__invokerId}`);
|
return this.querySelector(`#${this.__invokerId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
get _calendarOverlayElement() {
|
|
||||||
return this._overlayCtrl.contentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
get _calendarElement() {
|
get _calendarElement() {
|
||||||
return this._calendarOverlayElement.querySelector('#calendar');
|
return this._overlayCtrl.contentNode.querySelector('#calendar');
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -200,7 +200,12 @@ export class LionInputDatepicker extends LionInputDate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_calendarOverlayTemplate() {
|
/**
|
||||||
|
* Defining this overlay as a templates lets OverlayInteraceMixin
|
||||||
|
* this is our source to give as .contentNode to OverlayController.
|
||||||
|
* Important: do not change the name of this method.
|
||||||
|
*/
|
||||||
|
_overlayTemplate() {
|
||||||
return html`
|
return html`
|
||||||
<lion-calendar-overlay-frame @dialog-close=${() => this._overlayCtrl.hide()}>
|
<lion-calendar-overlay-frame @dialog-close=${() => this._overlayCtrl.hide()}>
|
||||||
<span slot="heading">${this.calendarHeading}</span>
|
<span slot="heading">${this.calendarHeading}</span>
|
||||||
|
|
@ -240,8 +245,6 @@ export class LionInputDatepicker extends LionInputDate {
|
||||||
type="button"
|
type="button"
|
||||||
@click="${this.__openCalendarOverlay}"
|
@click="${this.__openCalendarOverlay}"
|
||||||
id="${this.__invokerId}"
|
id="${this.__invokerId}"
|
||||||
aria-haspopup="dialog"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-label="${this.msgLit('lion-input-datepicker:openDatepickerLabel')}"
|
aria-label="${this.msgLit('lion-input-datepicker:openDatepickerLabel')}"
|
||||||
title="${this.msgLit('lion-input-datepicker:openDatepickerLabel')}"
|
title="${this.msgLit('lion-input-datepicker:openDatepickerLabel')}"
|
||||||
>
|
>
|
||||||
|
|
@ -250,27 +253,26 @@ export class LionInputDatepicker extends LionInputDate {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
__createPickerAndReturnInvokerNode() {
|
/**
|
||||||
const renderParent = document.createElement('div');
|
* @override Configures OverlayMixin
|
||||||
render(this._invokerTemplate(), renderParent);
|
* @desc returns an instance of a (dynamic) overlay controller
|
||||||
const invokerNode = renderParent.firstElementChild;
|
* @returns {OverlayController}
|
||||||
|
*/
|
||||||
// TODO: ModalDialogController could be replaced by a more flexible
|
// eslint-disable-next-line class-methods-use-this
|
||||||
// overlay, allowing the overlay to switch on smaller screens, for instance from dropdown to
|
_defineOverlay({ contentNode, invokerNode }) {
|
||||||
// bottom sheet via DynamicOverlayController
|
const ctrl = new OverlayController({
|
||||||
this._overlayCtrl = overlays.add(
|
...withModalDialogConfig(),
|
||||||
new ModalDialogController({
|
contentNode,
|
||||||
contentTemplate: () => this._calendarOverlayTemplate(),
|
invokerNode,
|
||||||
elementToFocusAfterHide: invokerNode,
|
elementToFocusAfterHide: invokerNode,
|
||||||
}),
|
});
|
||||||
);
|
return ctrl;
|
||||||
return invokerNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async __openCalendarOverlay() {
|
async __openCalendarOverlay() {
|
||||||
this._overlayCtrl.show();
|
this._overlayCtrl.show();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this._calendarOverlayElement.updateComplete,
|
this._overlayCtrl.contentNode.updateComplete,
|
||||||
this._calendarElement.updateComplete,
|
this._calendarElement.updateComplete,
|
||||||
]);
|
]);
|
||||||
this._onCalendarOverlayOpened();
|
this._onCalendarOverlayOpened();
|
||||||
|
|
@ -301,7 +303,7 @@ export class LionInputDatepicker extends LionInputDate {
|
||||||
* @returns {Date|undefined} a 'guarded' modelValue
|
* @returns {Date|undefined} a 'guarded' modelValue
|
||||||
*/
|
*/
|
||||||
static __getSyncDownValue(modelValue) {
|
static __getSyncDownValue(modelValue) {
|
||||||
return modelValue instanceof Unparseable ? undefined : modelValue;
|
return modelValue instanceof Date ? modelValue : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -327,4 +329,11 @@ export class LionInputDatepicker extends LionInputDate {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override Configures OverlayMixin
|
||||||
|
*/
|
||||||
|
get _overlayInvokerNode() {
|
||||||
|
return this._invokerElement;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ export class DatepickerInputObject {
|
||||||
return Promise.all(completePromises);
|
return Promise.all(completePromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async closeCalendar() {
|
||||||
|
this.overlayCloseButtonEl.click();
|
||||||
|
}
|
||||||
|
|
||||||
async selectMonthDay(day) {
|
async selectMonthDay(day) {
|
||||||
this.overlayController.show();
|
this.overlayController.show();
|
||||||
await this.calendarEl.updateComplete;
|
await this.calendarEl.updateComplete;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { expect, fixture, defineCE } from '@open-wc/testing';
|
import { expect, fixture, defineCE } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { localizeTearDown } from '@lion/localize/test-helpers.js';
|
|
||||||
import { html, LitElement } from '@lion/core';
|
import { html, LitElement } from '@lion/core';
|
||||||
import {
|
import {
|
||||||
maxDateValidator,
|
maxDateValidator,
|
||||||
|
|
@ -15,10 +14,6 @@ import { LionInputDatepicker } from '../src/LionInputDatepicker.js';
|
||||||
import '../lion-input-datepicker.js';
|
import '../lion-input-datepicker.js';
|
||||||
|
|
||||||
describe('<lion-input-datepicker>', () => {
|
describe('<lion-input-datepicker>', () => {
|
||||||
beforeEach(() => {
|
|
||||||
localizeTearDown();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Calendar Overlay', () => {
|
describe('Calendar Overlay', () => {
|
||||||
it('implements calendar-overlay Style component', async () => {
|
it('implements calendar-overlay Style component', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
|
|
@ -287,14 +282,16 @@ describe('<lion-input-datepicker>', () => {
|
||||||
expect(elObj.invokerEl.getAttribute('aria-label')).to.equal('Open date picker');
|
expect(elObj.invokerEl.getAttribute('aria-label')).to.equal('Open date picker');
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: move this functionality to GlobalOverlay
|
it('adds [aria-expanded] to invoker button', async () => {
|
||||||
it('adds aria-haspopup="dialog" and aria-expanded="true" to invoker button', async () => {
|
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<lion-input-datepicker></lion-input-datepicker>
|
<lion-input-datepicker></lion-input-datepicker>
|
||||||
`);
|
`);
|
||||||
const elObj = new DatepickerInputObject(el);
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
|
||||||
expect(elObj.invokerEl.getAttribute('aria-haspopup')).to.equal('dialog');
|
expect(elObj.invokerEl.getAttribute('aria-expanded')).to.equal('false');
|
||||||
|
await elObj.openCalendar();
|
||||||
|
expect(elObj.invokerEl.getAttribute('aria-expanded')).to.equal('true');
|
||||||
|
await elObj.closeCalendar();
|
||||||
expect(elObj.invokerEl.getAttribute('aria-expanded')).to.equal('false');
|
expect(elObj.invokerEl.getAttribute('aria-expanded')).to.equal('false');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([active]) {
|
:host([active]),
|
||||||
|
:host(:hover) {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +47,7 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.active = false;
|
this.active = false;
|
||||||
this.__registerEventListener();
|
this.__registerEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
_requestUpdate(name, oldValue) {
|
_requestUpdate(name, oldValue) {
|
||||||
|
|
@ -81,35 +82,16 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
|
||||||
this.setAttribute('role', 'option');
|
this.setAttribute('role', 'option');
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
__registerEventListeners() {
|
||||||
super.disconnectedCallback();
|
|
||||||
this.__unRegisterEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
__registerEventListener() {
|
|
||||||
this.__onClick = () => {
|
this.__onClick = () => {
|
||||||
if (!this.disabled) {
|
if (!this.disabled) {
|
||||||
this.checked = true;
|
this.checked = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.__onMouseEnter = () => {
|
|
||||||
if (!this.disabled) {
|
|
||||||
this.active = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.__onMouseLeave = () => {
|
|
||||||
if (!this.disabled) {
|
|
||||||
this.active = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.addEventListener('click', this.__onClick);
|
this.addEventListener('click', this.__onClick);
|
||||||
this.addEventListener('mouseenter', this.__onMouseEnter);
|
|
||||||
this.addEventListener('mouseleave', this.__onMouseLeave);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__unRegisterEventListeners() {
|
__unRegisterEventListeners() {
|
||||||
this.removeEventListener('click', this.__onClick);
|
this.removeEventListener('click', this.__onClick);
|
||||||
this.removeEventListener('mouseenter', this.__onMouseEnter);
|
|
||||||
this.removeEventListener('mouseleave', this.__onMouseLeave);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,24 +80,6 @@ describe('lion-option', () => {
|
||||||
expect(el.hasAttribute('active')).to.be.false;
|
expect(el.hasAttribute('active')).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does become active on [mouseenter]', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<lion-option .choiceValue=${10}></lion-option>
|
|
||||||
`);
|
|
||||||
expect(el.active).to.be.false;
|
|
||||||
el.dispatchEvent(new Event('mouseenter'));
|
|
||||||
expect(el.active).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does become un-active on [mouseleave]', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<lion-option .choiceValue=${10} active></lion-option>
|
|
||||||
`);
|
|
||||||
expect(el.active).to.be.true;
|
|
||||||
el.dispatchEvent(new Event('mouseleave'));
|
|
||||||
expect(el.active).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does become checked on [click]', async () => {
|
it('does become checked on [click]', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<lion-option .choiceValue=${10}></lion-option>
|
<lion-option .choiceValue=${10}></lion-option>
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,40 @@
|
||||||
import { UpdatingElement } from '@lion/core';
|
import { LitElement, html } from '@lion/core';
|
||||||
import { overlays, LocalOverlayController } from '@lion/overlays';
|
import { OverlayMixin, OverlayController } from '@lion/overlays';
|
||||||
|
|
||||||
export class LionPopup extends UpdatingElement {
|
export class LionPopup extends OverlayMixin(LitElement) {
|
||||||
static get properties() {
|
render() {
|
||||||
return {
|
return html`
|
||||||
popperConfig: {
|
<slot name="invoker"></slot>
|
||||||
type: Object,
|
<slot name="content"></slot>
|
||||||
},
|
`;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get popperConfig() {
|
get _overlayContentNode() {
|
||||||
return this._popperConfig;
|
return this.querySelector('[slot=content]');
|
||||||
}
|
}
|
||||||
|
|
||||||
set popperConfig(config) {
|
get _overlayInvokerNode() {
|
||||||
this._popperConfig = {
|
return this.querySelector('[slot=invoker]');
|
||||||
...this._popperConfig,
|
}
|
||||||
...config,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this._controller && this._controller._popper) {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
this._controller.updatePopperConfig(this._popperConfig);
|
_defineOverlay() {
|
||||||
}
|
return new OverlayController({
|
||||||
|
placementMode: 'local',
|
||||||
|
contentNode: this._overlayContentNode,
|
||||||
|
invokerNode: this._overlayInvokerNode,
|
||||||
|
handlesAccessibility: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.contentNode = this.querySelector('[slot="content"]');
|
this.__toggle = () => this._overlayCtrl.toggle();
|
||||||
this.invokerNode = this.querySelector('[slot="invoker"]');
|
this._overlayInvokerNode.addEventListener('click', this.__toggle);
|
||||||
|
|
||||||
this._controller = overlays.add(
|
|
||||||
new LocalOverlayController({
|
|
||||||
hidesOnEsc: true,
|
|
||||||
hidesOnOutsideClick: true,
|
|
||||||
popperConfig: this.popperConfig,
|
|
||||||
contentNode: this.contentNode,
|
|
||||||
invokerNode: this.invokerNode,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this._show = () => this._controller.show();
|
|
||||||
this._hide = () => this._controller.hide();
|
|
||||||
this._toggle = () => this._controller.toggle();
|
|
||||||
|
|
||||||
this.invokerNode.addEventListener('click', this._toggle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.invokerNode.removeEventListener('click', this._toggle);
|
this._overlayInvokerNode.removeEventListener('click', this._toggle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ describe('lion-popup', () => {
|
||||||
<lion-button slot="invoker">Popup button</lion-button>
|
<lion-button slot="invoker">Popup button</lion-button>
|
||||||
</lion-popup>
|
</lion-popup>
|
||||||
`);
|
`);
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('none');
|
expect(el._overlayCtrl.isShown).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should toggle to show content on click', async () => {
|
it('should toggle to show content on click', async () => {
|
||||||
|
|
@ -25,10 +25,10 @@ describe('lion-popup', () => {
|
||||||
invoker.click();
|
invoker.click();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
expect(el._overlayCtrl.isShown).to.be.true;
|
||||||
invoker.click();
|
invoker.click();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('none');
|
expect(el._overlayCtrl.isShown).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support popup containing html when specified in popup content body', async () => {
|
it('should support popup containing html when specified in popup content body', async () => {
|
||||||
|
|
@ -52,25 +52,12 @@ describe('lion-popup', () => {
|
||||||
<lion-button slot="invoker">Popup button</lion-button>
|
<lion-button slot="invoker">Popup button</lion-button>
|
||||||
</lion-popup>
|
</lion-popup>
|
||||||
`);
|
`);
|
||||||
await el._controller.show();
|
await el._overlayCtrl.show();
|
||||||
expect(el._controller._popper.options.placement).to.equal('top');
|
expect(el._overlayCtrl._popper.options.placement).to.equal('top');
|
||||||
|
|
||||||
el.popperConfig = { placement: 'left' };
|
el.popperConfig = { placement: 'left' };
|
||||||
await el._controller.show();
|
await el._overlayCtrl.show();
|
||||||
expect(el._controller._popper.options.placement).to.equal('left');
|
expect(el._overlayCtrl._popper.options.placement).to.equal('left');
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
|
||||||
it('should have aria-controls attribute set to the invoker', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<lion-popup>
|
|
||||||
<div slot="content" class="popup">Hey there</div>
|
|
||||||
<lion-button slot="invoker">Popup button</lion-button>
|
|
||||||
</lion-popup>
|
|
||||||
`);
|
|
||||||
const invoker = el.querySelector('[slot="invoker"]');
|
|
||||||
expect(invoker.getAttribute('aria-controls')).to.not.be.null;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
export { LionSelectRich } from './src/LionSelectRich.js';
|
export { LionSelectRich } from './src/LionSelectRich.js';
|
||||||
|
export { LionSelectInvoker } from './src/LionSelectInvoker.js';
|
||||||
|
export { LionOptions } from './src/LionOptions.js';
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
|
import { FormRegistrarPortalMixin } from '@lion/field';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LionOptions
|
* LionOptions
|
||||||
*
|
*
|
||||||
* @customElement
|
* @customElement lion-options
|
||||||
* @extends LitElement
|
* @extends LitElement
|
||||||
*/
|
*/
|
||||||
export class LionOptions extends LitElement {
|
export class LionOptions extends FormRegistrarPortalMixin(LitElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
role: {
|
role: {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { html } from '@lion/core';
|
||||||
/**
|
/**
|
||||||
* LionSelectInvoker: invoker button consuming a selected element
|
* LionSelectInvoker: invoker button consuming a selected element
|
||||||
*
|
*
|
||||||
* @customElement
|
* @customElement lion-select-invoker
|
||||||
* @extends LionButton
|
* @extends LionButton
|
||||||
*/
|
*/
|
||||||
export class LionSelectInvoker extends LionButton {
|
export class LionSelectInvoker extends LionButton {
|
||||||
|
|
@ -13,6 +13,11 @@ export class LionSelectInvoker extends LionButton {
|
||||||
selectedElement: {
|
selectedElement: {
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
attribute: 'readonly',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,6 +39,14 @@ export class LionSelectInvoker extends LionButton {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.selectedElement = null;
|
this.selectedElement = null;
|
||||||
|
this.type = 'button';
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestUpdate(name, oldValue) {
|
||||||
|
super._requestUpdate(name, oldValue);
|
||||||
|
if (name === 'readOnly') {
|
||||||
|
this.disabled = this.readOnly;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_contentTemplate() {
|
_contentTemplate() {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { html, css, LitElement, SlotMixin } from '@lion/core';
|
import { html, css, LitElement, SlotMixin } from '@lion/core';
|
||||||
import { LocalOverlayController, overlays } from '@lion/overlays';
|
import { OverlayController, withDropdownConfig, OverlayMixin } from '@lion/overlays';
|
||||||
import { FormControlMixin, InteractionStateMixin, FormRegistrarMixin } from '@lion/field';
|
import { FormControlMixin, InteractionStateMixin, FormRegistrarMixin } from '@lion/field';
|
||||||
import { ValidateMixin } from '@lion/validate';
|
import { ValidateMixin } from '@lion/validate';
|
||||||
import './differentKeyNamesShimIE.js';
|
import './differentKeyNamesShimIE.js';
|
||||||
|
|
@ -22,11 +22,11 @@ function detectInteractionMode() {
|
||||||
/**
|
/**
|
||||||
* LionSelectRich: wraps the <lion-listbox> element
|
* LionSelectRich: wraps the <lion-listbox> element
|
||||||
*
|
*
|
||||||
* @customElement
|
* @customElement lion-select-rich
|
||||||
* @extends LionField
|
* @extends LionField
|
||||||
*/
|
*/
|
||||||
export class LionSelectRich extends FormRegistrarMixin(
|
export class LionSelectRich extends OverlayMixin(
|
||||||
InteractionStateMixin(ValidateMixin(FormControlMixin(SlotMixin(LitElement)))),
|
FormRegistrarMixin(InteractionStateMixin(ValidateMixin(FormControlMixin(SlotMixin(LitElement))))),
|
||||||
) {
|
) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -39,9 +39,10 @@ export class LionSelectRich extends FormRegistrarMixin(
|
||||||
reflect: true,
|
reflect: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
opened: {
|
readOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
reflect: true,
|
reflect: true,
|
||||||
|
attribute: 'readonly',
|
||||||
},
|
},
|
||||||
|
|
||||||
interactionMode: {
|
interactionMode: {
|
||||||
|
|
@ -98,7 +99,9 @@ export class LionSelectRich extends FormRegistrarMixin(
|
||||||
}
|
}
|
||||||
|
|
||||||
get _listboxNode() {
|
get _listboxNode() {
|
||||||
return this.querySelector('[slot=input]');
|
return (
|
||||||
|
(this._overlayCtrl && this._overlayCtrl.contentNode) || this.querySelector('[slot=input]')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get _listboxActiveDescendantNode() {
|
get _listboxActiveDescendantNode() {
|
||||||
|
|
@ -132,7 +135,6 @@ export class LionSelectRich extends FormRegistrarMixin(
|
||||||
super();
|
super();
|
||||||
this.interactionMode = 'auto';
|
this.interactionMode = 'auto';
|
||||||
this.disabled = false;
|
this.disabled = false;
|
||||||
this.opened = false;
|
|
||||||
// for interaction states
|
// for interaction states
|
||||||
// we use a different event as 'model-value-changed' would bubble up from all options
|
// we use a different event as 'model-value-changed' would bubble up from all options
|
||||||
this._valueChangedEvent = 'select-model-value-changed';
|
this._valueChangedEvent = 'select-model-value-changed';
|
||||||
|
|
@ -143,6 +145,7 @@ export class LionSelectRich extends FormRegistrarMixin(
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
this._listboxNode.registrationTarget = this;
|
||||||
if (super.connectedCallback) {
|
if (super.connectedCallback) {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
}
|
}
|
||||||
|
|
@ -150,6 +153,8 @@ export class LionSelectRich extends FormRegistrarMixin(
|
||||||
this.__setupOverlay();
|
this.__setupOverlay();
|
||||||
this.__setupInvokerNode();
|
this.__setupInvokerNode();
|
||||||
this.__setupListboxNode();
|
this.__setupListboxNode();
|
||||||
|
|
||||||
|
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
|
@ -162,6 +167,11 @@ export class LionSelectRich extends FormRegistrarMixin(
|
||||||
this.__teardownListboxNode();
|
this.__teardownListboxNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
firstUpdated(c) {
|
||||||
|
super.firstUpdated(c);
|
||||||
|
this.__toggleInvokerDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
_requestUpdate(name, oldValue) {
|
_requestUpdate(name, oldValue) {
|
||||||
super._requestUpdate(name, oldValue);
|
super._requestUpdate(name, oldValue);
|
||||||
if (
|
if (
|
||||||
|
|
@ -185,17 +195,14 @@ export class LionSelectRich extends FormRegistrarMixin(
|
||||||
this.interactionMode = detectInteractionMode();
|
this.interactionMode = detectInteractionMode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'disabled' || name === 'readOnly') {
|
||||||
|
this.__toggleInvokerDisabled();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updated(changedProps) {
|
updated(changedProps) {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
if (changedProps.has('opened')) {
|
|
||||||
if (this.opened) {
|
|
||||||
this.__overlay.show();
|
|
||||||
} else {
|
|
||||||
this.__overlay.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changedProps.has('disabled')) {
|
if (changedProps.has('disabled')) {
|
||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
|
|
@ -293,15 +300,22 @@ export class LionSelectRich extends FormRegistrarMixin(
|
||||||
this.__onChildModelValueChanged = this.__onChildModelValueChanged.bind(this);
|
this.__onChildModelValueChanged = this.__onChildModelValueChanged.bind(this);
|
||||||
this.__onKeyUp = this.__onKeyUp.bind(this);
|
this.__onKeyUp = this.__onKeyUp.bind(this);
|
||||||
|
|
||||||
this.addEventListener('active-changed', this.__onChildActiveChanged);
|
this._listboxNode.addEventListener('active-changed', this.__onChildActiveChanged);
|
||||||
this.addEventListener('model-value-changed', this.__onChildModelValueChanged);
|
this._listboxNode.addEventListener('model-value-changed', this.__onChildModelValueChanged);
|
||||||
this.addEventListener('keyup', this.__onKeyUp);
|
this.addEventListener('keyup', this.__onKeyUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
__teardownEventListeners() {
|
__teardownEventListeners() {
|
||||||
this.removeEventListener('active-changed', this.__onChildActiveChanged);
|
this._listboxNode.removeEventListener('active-changed', this.__onChildActiveChanged);
|
||||||
this.removeEventListener('model-value-changed', this.__onChildModelValueChanged);
|
this._listboxNode.removeEventListener('model-value-changed', this.__onChildModelValueChanged);
|
||||||
this.removeEventListener('keyup', this.__onKeyUp);
|
this._listboxNode.removeEventListener('keyup', this.__onKeyUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
__toggleInvokerDisabled() {
|
||||||
|
if (this._invokerNode) {
|
||||||
|
this._invokerNode.disabled = this.disabled;
|
||||||
|
this._invokerNode.readOnly = this.readOnly;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
__onChildActiveChanged({ target }) {
|
__onChildActiveChanged({ target }) {
|
||||||
|
|
@ -448,6 +462,7 @@ export class LionSelectRich extends FormRegistrarMixin(
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
if (this.interactionMode === 'mac') {
|
if (this.interactionMode === 'mac') {
|
||||||
this.opened = true;
|
this.opened = true;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -492,7 +507,7 @@ export class LionSelectRich extends FormRegistrarMixin(
|
||||||
__setupInvokerNodeEventListener() {
|
__setupInvokerNodeEventListener() {
|
||||||
this.__invokerOnClick = () => {
|
this.__invokerOnClick = () => {
|
||||||
if (!this.disabled) {
|
if (!this.disabled) {
|
||||||
this.toggle();
|
this._overlayCtrl.toggle();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this._invokerNode.addEventListener('click', this.__invokerOnClick);
|
this._invokerNode.addEventListener('click', this.__invokerOnClick);
|
||||||
|
|
@ -548,55 +563,33 @@ export class LionSelectRich extends FormRegistrarMixin(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @overridable Subclassers can override the default
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_defineOverlay({ invokerNode, contentNode } = {}) {
|
_defineOverlay({ invokerNode, contentNode } = {}) {
|
||||||
return overlays.add(
|
return new OverlayController({
|
||||||
new LocalOverlayController({
|
...withDropdownConfig(),
|
||||||
contentNode,
|
contentNode,
|
||||||
invokerNode,
|
invokerNode,
|
||||||
hidesOnEsc: false,
|
});
|
||||||
hidesOnOutsideClick: true,
|
|
||||||
inheritsReferenceObjectWidth: true,
|
|
||||||
popperConfig: {
|
|
||||||
placement: 'bottom-start',
|
|
||||||
modifiers: {
|
|
||||||
offset: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__setupOverlay() {
|
__setupOverlay() {
|
||||||
this.__overlay = this._defineOverlay({
|
|
||||||
invokerNode: this._invokerNode,
|
|
||||||
contentNode: this._listboxNode,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.__overlayOnShow = () => {
|
this.__overlayOnShow = () => {
|
||||||
this.opened = true;
|
|
||||||
if (this.checkedIndex) {
|
if (this.checkedIndex) {
|
||||||
this.activeIndex = this.checkedIndex;
|
this.activeIndex = this.checkedIndex;
|
||||||
}
|
}
|
||||||
this._listboxNode.focus();
|
this._listboxNode.focus();
|
||||||
};
|
};
|
||||||
this.__overlay.addEventListener('show', this.__overlayOnShow);
|
this._overlayCtrl.addEventListener('show', this.__overlayOnShow);
|
||||||
|
|
||||||
this.__overlayOnHide = () => {
|
this.__overlayOnHide = () => {
|
||||||
this.opened = false;
|
|
||||||
this._invokerNode.focus();
|
this._invokerNode.focus();
|
||||||
};
|
};
|
||||||
this.__overlay.addEventListener('hide', this.__overlayOnHide);
|
this._overlayCtrl.addEventListener('hide', this.__overlayOnHide);
|
||||||
}
|
}
|
||||||
|
|
||||||
__teardownOverlay() {
|
__teardownOverlay() {
|
||||||
this.__overlay.removeEventListener('show', this.__overlayOnShow);
|
this._overlayCtrl.removeEventListener('show', this.__overlayOnShow);
|
||||||
this.__overlay.removeEventListener('hide', this.__overlayOnHide);
|
this._overlayCtrl.removeEventListener('hide', this.__overlayOnHide);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
|
@ -612,4 +605,18 @@ export class LionSelectRich extends FormRegistrarMixin(
|
||||||
(typeof value !== 'string' && value !== undefined && value !== null),
|
(typeof value !== 'string' && value !== undefined && value !== null),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override Configures OverlayMixin
|
||||||
|
*/
|
||||||
|
get _overlayInvokerNode() {
|
||||||
|
return this._invokerNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override Configures OverlayMixin
|
||||||
|
*/
|
||||||
|
get _overlayContentNode() {
|
||||||
|
return this._listboxNode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ import '../lion-options.js';
|
||||||
|
|
||||||
describe('lion-options', () => {
|
describe('lion-options', () => {
|
||||||
it('should have role="listbox"', async () => {
|
it('should have role="listbox"', async () => {
|
||||||
|
const registrationTargetEl = document.createElement('div');
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<lion-options></lion-options>
|
<lion-options .registrationTarget=${registrationTargetEl}></lion-options>
|
||||||
`);
|
`);
|
||||||
expect(el.role).to.equal('listbox');
|
expect(el.role).to.equal('listbox');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,13 @@ describe('lion-select-invoker', () => {
|
||||||
expect(el.getAttribute('tabindex')).to.equal('0');
|
expect(el.getAttribute('tabindex')).to.equal('0');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('delegates the readonly attribute to disabled', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-invoker readonly></lion-select-invoker>
|
||||||
|
`);
|
||||||
|
expect(el.hasAttribute('disabled')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
describe('Subclassers', () => {
|
describe('Subclassers', () => {
|
||||||
it('supports a custom _contentTemplate', async () => {
|
it('supports a custom _contentTemplate', async () => {
|
||||||
const myTag = defineCE(
|
const myTag = defineCE(
|
||||||
|
|
|
||||||
|
|
@ -362,13 +362,13 @@ describe('lion-select-rich interactions', () => {
|
||||||
</lion-options>
|
</lion-options>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
expect(el.activeIndex).to.equal(2);
|
|
||||||
|
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Home' }));
|
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
|
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'End' }));
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'End' }));
|
||||||
expect(el.activeIndex).to.equal(2);
|
expect(el.activeIndex).to.equal(2);
|
||||||
|
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Home' }));
|
||||||
|
expect(el.activeIndex).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checks the first enabled option', async () => {
|
it('checks the first enabled option', async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import { expect, fixture, html, aTimeout, defineCE, unsafeStatic } from '@open-wc/testing';
|
|
||||||
import '@lion/option/lion-option.js';
|
|
||||||
import {
|
import {
|
||||||
overlays,
|
expect,
|
||||||
LocalOverlayController,
|
fixture,
|
||||||
GlobalOverlayController,
|
html,
|
||||||
DynamicOverlayController,
|
aTimeout,
|
||||||
} from '@lion/overlays';
|
defineCE,
|
||||||
|
unsafeStatic,
|
||||||
|
nextFrame,
|
||||||
|
} from '@open-wc/testing';
|
||||||
|
import '@lion/option/lion-option.js';
|
||||||
|
import { OverlayController } from '@lion/overlays';
|
||||||
|
|
||||||
import './keyboardEventShimIE.js';
|
import './keyboardEventShimIE.js';
|
||||||
import '../lion-options.js';
|
import '../lion-options.js';
|
||||||
|
|
@ -49,6 +52,24 @@ describe('lion-select-rich', () => {
|
||||||
el.checkedIndex = 1;
|
el.checkedIndex = 1;
|
||||||
expect(el._invokerNode.selectedElement).to.equal(el.querySelectorAll('lion-option')[1]);
|
expect(el._invokerNode.selectedElement).to.equal(el.querySelectorAll('lion-option')[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('delegates readonly to the invoker, where disabled is added on top of this to disable opening', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich readonly>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(el.hasAttribute('readonly')).to.be.true;
|
||||||
|
// rich select is not disabled, so value is still serialized in forms when readonly
|
||||||
|
expect(el.hasAttribute('disabled')).to.be.false;
|
||||||
|
expect(el._invokerNode.hasAttribute('readonly')).to.be.true;
|
||||||
|
// invoker node has disabled, to disable it from being clicked
|
||||||
|
expect(el._invokerNode.hasAttribute('disabled')).to.be.true;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('overlay', () => {
|
describe('overlay', () => {
|
||||||
|
|
@ -69,16 +90,16 @@ describe('lion-select-rich', () => {
|
||||||
`);
|
`);
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el._listboxNode.style.display).to.be.equal('inline-block');
|
expect(el._overlayCtrl.isShown).to.be.true;
|
||||||
|
|
||||||
el.opened = false;
|
el.opened = false;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el._listboxNode.style.display).to.be.equal('none');
|
expect(el._overlayCtrl.isShown).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('syncs opened state with overlay shown', async () => {
|
it('syncs opened state with overlay shown', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<lion-select-rich opened>
|
<lion-select-rich .opened=${true}>
|
||||||
<lion-options slot="input"></lion-options>
|
<lion-options slot="input"></lion-options>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
|
@ -98,7 +119,7 @@ describe('lion-select-rich', () => {
|
||||||
<lion-options slot="input"></lion-options>
|
<lion-options slot="input"></lion-options>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
el.opened = true;
|
await el._overlayCtrl.show();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(document.activeElement === el._listboxNode).to.be.true;
|
expect(document.activeElement === el._listboxNode).to.be.true;
|
||||||
expect(document.activeElement === el._invokerNode).to.be.false;
|
expect(document.activeElement === el._invokerNode).to.be.false;
|
||||||
|
|
@ -118,7 +139,7 @@ describe('lion-select-rich', () => {
|
||||||
</lion-options>
|
</lion-options>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
el.opened = true;
|
await el._overlayCtrl.show();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
const options = Array.from(el.querySelectorAll('lion-option'));
|
const options = Array.from(el.querySelectorAll('lion-option'));
|
||||||
|
|
||||||
|
|
@ -194,6 +215,7 @@ describe('lion-select-rich', () => {
|
||||||
`);
|
`);
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
el._invokerNode.click();
|
el._invokerNode.click();
|
||||||
|
await nextFrame();
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -337,30 +359,20 @@ describe('lion-select-rich', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Subclassers', () => {
|
describe('Subclassers', () => {
|
||||||
it('allows to override the type of overlays', async () => {
|
it('allows to override the type of overlay', async () => {
|
||||||
const mySelectTagString = defineCE(
|
const mySelectTagString = defineCE(
|
||||||
class MySelect extends LionSelectRich {
|
class MySelect extends LionSelectRich {
|
||||||
_defineOverlay({ invokerNode, contentNode }) {
|
_defineOverlay({ invokerNode, contentNode }) {
|
||||||
// add a DynamicOverlayController
|
const ctrl = new OverlayController({
|
||||||
const dynamicCtrl = new DynamicOverlayController();
|
placementMode: 'global',
|
||||||
|
contentNode,
|
||||||
|
invokerNode,
|
||||||
|
});
|
||||||
|
|
||||||
const localCtrl = overlays.add(
|
this.addEventListener('switch', () => {
|
||||||
new LocalOverlayController({
|
ctrl.updateConfig({ placementMode: 'local' });
|
||||||
contentNode,
|
});
|
||||||
invokerNode,
|
return ctrl;
|
||||||
}),
|
|
||||||
);
|
|
||||||
dynamicCtrl.add(localCtrl);
|
|
||||||
|
|
||||||
const globalCtrl = overlays.add(
|
|
||||||
new GlobalOverlayController({
|
|
||||||
contentNode,
|
|
||||||
invokerNode,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
dynamicCtrl.add(globalCtrl);
|
|
||||||
|
|
||||||
return dynamicCtrl;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -379,7 +391,9 @@ describe('lion-select-rich', () => {
|
||||||
</${mySelectTag}>
|
</${mySelectTag}>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
expect(el.__overlay).to.be.instanceOf(DynamicOverlayController);
|
expect(el._overlayCtrl.placementMode).to.equal('global');
|
||||||
|
el.dispatchEvent(new Event('switch'));
|
||||||
|
expect(el._overlayCtrl.placementMode).to.equal('local');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export class LionTooltip extends LionPopup {
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.contentNode.setAttribute('role', 'tooltip');
|
this._overlayContentNode.setAttribute('role', 'tooltip');
|
||||||
|
|
||||||
this.__resetActive = () => {
|
this.__resetActive = () => {
|
||||||
this.mouseActive = false;
|
this.mouseActive = false;
|
||||||
|
|
@ -19,42 +19,42 @@ export class LionTooltip extends LionPopup {
|
||||||
this.__showMouse = () => {
|
this.__showMouse = () => {
|
||||||
if (!this.keyActive) {
|
if (!this.keyActive) {
|
||||||
this.mouseActive = true;
|
this.mouseActive = true;
|
||||||
this._controller.show();
|
this._overlayCtrl.show();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.__hideMouse = () => {
|
this.__hideMouse = () => {
|
||||||
if (!this.keyActive) {
|
if (!this.keyActive) {
|
||||||
this._controller.hide();
|
this._overlayCtrl.hide();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.__showKey = () => {
|
this.__showKey = () => {
|
||||||
if (!this.mouseActive) {
|
if (!this.mouseActive) {
|
||||||
this.keyActive = true;
|
this.keyActive = true;
|
||||||
this._controller.show();
|
this._overlayCtrl.show();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.__hideKey = () => {
|
this.__hideKey = () => {
|
||||||
if (!this.mouseActive) {
|
if (!this.mouseActive) {
|
||||||
this._controller.hide();
|
this._overlayCtrl.hide();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this._controller.addEventListener('hide', this.__resetActive);
|
this._overlayCtrl.addEventListener('hide', this.__resetActive);
|
||||||
this.addEventListener('mouseenter', this.__showMouse);
|
this.addEventListener('mouseenter', this.__showMouse);
|
||||||
this.addEventListener('mouseleave', this.__hideMouse);
|
this.addEventListener('mouseleave', this.__hideMouse);
|
||||||
this.invokerNode.addEventListener('focusin', this.__showKey);
|
this._overlayInvokerNode.addEventListener('focusin', this.__showKey);
|
||||||
this.invokerNode.addEventListener('focusout', this.__hideKey);
|
this._overlayInvokerNode.addEventListener('focusout', this.__hideKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this._controller.removeEventListener('hide', this.__resetActive);
|
this._overlayCtrl.removeEventListener('hide', this.__resetActive);
|
||||||
this.removeEventListener('mouseenter', this.__showMouse);
|
this.removeEventListener('mouseenter', this.__showMouse);
|
||||||
this.removeEventListener('mouseleave', this._hideMouse);
|
this.removeEventListener('mouseleave', this._hideMouse);
|
||||||
this.invokerNode.removeEventListener('focusin', this._showKey);
|
this._overlayInvokerNode.removeEventListener('focusin', this._showKey);
|
||||||
this.invokerNode.removeEventListener('focusout', this._hideKey);
|
this._overlayInvokerNode.removeEventListener('focusout', this._hideKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ storiesOf('Local Overlay System|Tooltip', module)
|
||||||
},
|
},
|
||||||
})}"
|
})}"
|
||||||
>
|
>
|
||||||
<lion-button slot="invoker">${text('Invoker text', 'Click me!')}</lion-button>
|
<lion-button slot="invoker">${text('Invoker text', 'Hover me!')}</lion-button>
|
||||||
<div slot="content" class="tooltip">${text('Content text', 'Hello, World!')}</div>
|
<div slot="content" class="tooltip">${text('Content text', 'Hello, World!')}</div>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ describe('lion-tooltip', () => {
|
||||||
<lion-button slot="invoker">Tooltip button</lion-button>
|
<lion-button slot="invoker">Tooltip button</lion-button>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
`);
|
`);
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('none');
|
expect(el._overlayCtrl.isShown).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show content on mouseenter and hide on mouseleave', async () => {
|
it('should show content on mouseenter and hide on mouseleave', async () => {
|
||||||
|
|
@ -24,11 +24,11 @@ describe('lion-tooltip', () => {
|
||||||
const eventMouseEnter = new Event('mouseenter');
|
const eventMouseEnter = new Event('mouseenter');
|
||||||
el.dispatchEvent(eventMouseEnter);
|
el.dispatchEvent(eventMouseEnter);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
expect(el._overlayCtrl.isShown).to.equal(true);
|
||||||
const eventMouseLeave = new Event('mouseleave');
|
const eventMouseLeave = new Event('mouseleave');
|
||||||
el.dispatchEvent(eventMouseLeave);
|
el.dispatchEvent(eventMouseLeave);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('none');
|
expect(el._overlayCtrl.isShown).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show content on mouseenter and remain shown on focusout', async () => {
|
it('should show content on mouseenter and remain shown on focusout', async () => {
|
||||||
|
|
@ -41,11 +41,11 @@ describe('lion-tooltip', () => {
|
||||||
const eventMouseEnter = new Event('mouseenter');
|
const eventMouseEnter = new Event('mouseenter');
|
||||||
el.dispatchEvent(eventMouseEnter);
|
el.dispatchEvent(eventMouseEnter);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
expect(el._overlayCtrl.isShown).to.equal(true);
|
||||||
const eventFocusOut = new Event('focusout');
|
const eventFocusOut = new Event('focusout');
|
||||||
el.dispatchEvent(eventFocusOut);
|
el.dispatchEvent(eventFocusOut);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
expect(el._overlayCtrl.isShown).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show content on focusin and hide on focusout', async () => {
|
it('should show content on focusin and hide on focusout', async () => {
|
||||||
|
|
@ -59,11 +59,11 @@ describe('lion-tooltip', () => {
|
||||||
const eventFocusIn = new Event('focusin');
|
const eventFocusIn = new Event('focusin');
|
||||||
invoker.dispatchEvent(eventFocusIn);
|
invoker.dispatchEvent(eventFocusIn);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
expect(el._overlayCtrl.isShown).to.equal(true);
|
||||||
const eventFocusOut = new Event('focusout');
|
const eventFocusOut = new Event('focusout');
|
||||||
invoker.dispatchEvent(eventFocusOut);
|
invoker.dispatchEvent(eventFocusOut);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('none');
|
expect(el._overlayCtrl.isShown).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show content on focusin and remain shown on mouseleave', async () => {
|
it('should show content on focusin and remain shown on mouseleave', async () => {
|
||||||
|
|
@ -77,11 +77,11 @@ describe('lion-tooltip', () => {
|
||||||
const eventFocusIn = new Event('focusin');
|
const eventFocusIn = new Event('focusin');
|
||||||
invoker.dispatchEvent(eventFocusIn);
|
invoker.dispatchEvent(eventFocusIn);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
expect(el._overlayCtrl.isShown).to.equal(true);
|
||||||
const eventMouseLeave = new Event('mouseleave');
|
const eventMouseLeave = new Event('mouseleave');
|
||||||
invoker.dispatchEvent(eventMouseLeave);
|
invoker.dispatchEvent(eventMouseLeave);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
expect(el._overlayCtrl.isShown).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should tooltip contains html when specified in tooltip content body', async () => {
|
it('should tooltip contains html when specified in tooltip content body', async () => {
|
||||||
|
|
@ -112,16 +112,5 @@ describe('lion-tooltip', () => {
|
||||||
const invoker = el.querySelector('[slot="content"]');
|
const invoker = el.querySelector('[slot="content"]');
|
||||||
expect(invoker.getAttribute('role')).to.be.equal('tooltip');
|
expect(invoker.getAttribute('role')).to.be.equal('tooltip');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have aria-controls attribute set to the invoker', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<lion-tooltip>
|
|
||||||
<div slot="content">Hey there</div>
|
|
||||||
<lion-button slot="invoker">Tooltip button</lion-button>
|
|
||||||
</lion-tooltip>
|
|
||||||
`);
|
|
||||||
const invoker = el.querySelector('[slot="invoker"]');
|
|
||||||
expect(invoker.getAttribute('aria-controls')).to.not.be.null;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue