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:
Thijs Louisse 2019-10-10 16:42:40 +02:00
parent 364f185ad8
commit 4c26befaae
19 changed files with 243 additions and 259 deletions

View file

@ -221,7 +221,6 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
firstUpdated() {
super.firstUpdated();
this.__contentWrapperElement = this.shadowRoot.getElementById('js-content-wrapper');
this.__addEventDelegationForClickDate();
this.__addEventDelegationForFocusDate();
this.__addEventDelegationForBlurDate();
@ -501,6 +500,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
}
__removeEventDelegations() {
if (!this.__contentWrapperElement) {
return;
}
this.__contentWrapperElement.removeEventListener('click', this.__clickDateDelegation);
this.__contentWrapperElement.removeEventListener('focus', this.__focusDateDelegation);
this.__contentWrapperElement.removeEventListener('blur', this.__blurDateDelegation);

View file

@ -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 { overlays, ModalDialogController } from '@lion/overlays';
import { Unparseable, isValidatorApplied } from '@lion/validate';
import { OverlayController, withModalDialogConfig, OverlayMixin } from '@lion/overlays';
import { isValidatorApplied } from '@lion/validate';
import '@lion/calendar/lion-calendar.js';
import './lion-calendar-overlay-frame.js';
@ -9,7 +9,7 @@ import './lion-calendar-overlay-frame.js';
* @customElement lion-input-datepicker
* @extends {LionInputDate}
*/
export class LionInputDatepicker extends LionInputDate {
export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
static get properties() {
return {
/**
@ -46,7 +46,11 @@ export class LionInputDatepicker extends LionInputDate {
get slots() {
return {
...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}`);
}
get _calendarOverlayElement() {
return this._overlayCtrl.contentNode;
}
get _calendarElement() {
return this._calendarOverlayElement.querySelector('#calendar');
return this._overlayCtrl.contentNode.querySelector('#calendar');
}
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`
<lion-calendar-overlay-frame @dialog-close=${() => this._overlayCtrl.hide()}>
<span slot="heading">${this.calendarHeading}</span>
@ -240,8 +245,6 @@ export class LionInputDatepicker extends LionInputDate {
type="button"
@click="${this.__openCalendarOverlay}"
id="${this.__invokerId}"
aria-haspopup="dialog"
aria-expanded="false"
aria-label="${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');
render(this._invokerTemplate(), renderParent);
const invokerNode = renderParent.firstElementChild;
// TODO: ModalDialogController could be replaced by a more flexible
// overlay, allowing the overlay to switch on smaller screens, for instance from dropdown to
// bottom sheet via DynamicOverlayController
this._overlayCtrl = overlays.add(
new ModalDialogController({
contentTemplate: () => this._calendarOverlayTemplate(),
elementToFocusAfterHide: invokerNode,
}),
);
return invokerNode;
/**
* @override Configures OverlayMixin
* @desc returns an instance of a (dynamic) overlay controller
* @returns {OverlayController}
*/
// eslint-disable-next-line class-methods-use-this
_defineOverlay({ contentNode, invokerNode }) {
const ctrl = new OverlayController({
...withModalDialogConfig(),
contentNode,
invokerNode,
elementToFocusAfterHide: invokerNode,
});
return ctrl;
}
async __openCalendarOverlay() {
this._overlayCtrl.show();
await Promise.all([
this._calendarOverlayElement.updateComplete,
this._overlayCtrl.contentNode.updateComplete,
this._calendarElement.updateComplete,
]);
this._onCalendarOverlayOpened();
@ -301,7 +303,7 @@ export class LionInputDatepicker extends LionInputDate {
* @returns {Date|undefined} a 'guarded' 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;
}
}

View file

@ -24,6 +24,10 @@ export class DatepickerInputObject {
return Promise.all(completePromises);
}
async closeCalendar() {
this.overlayCloseButtonEl.click();
}
async selectMonthDay(day) {
this.overlayController.show();
await this.calendarEl.updateComplete;

View file

@ -1,6 +1,5 @@
import { expect, fixture, defineCE } from '@open-wc/testing';
import sinon from 'sinon';
import { localizeTearDown } from '@lion/localize/test-helpers.js';
import { html, LitElement } from '@lion/core';
import {
maxDateValidator,
@ -15,10 +14,6 @@ import { LionInputDatepicker } from '../src/LionInputDatepicker.js';
import '../lion-input-datepicker.js';
describe('<lion-input-datepicker>', () => {
beforeEach(() => {
localizeTearDown();
});
describe('Calendar Overlay', () => {
it('implements calendar-overlay Style component', async () => {
const el = await fixture(html`
@ -287,14 +282,16 @@ describe('<lion-input-datepicker>', () => {
expect(elObj.invokerEl.getAttribute('aria-label')).to.equal('Open date picker');
});
// TODO: move this functionality to GlobalOverlay
it('adds aria-haspopup="dialog" and aria-expanded="true" to invoker button', async () => {
it('adds [aria-expanded] to invoker button', async () => {
const el = await fixture(html`
<lion-input-datepicker></lion-input-datepicker>
`);
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');
});
});

View file

@ -28,7 +28,8 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
padding: 4px;
}
:host([active]) {
:host([active]),
:host(:hover) {
background-color: #ddd;
}
@ -46,7 +47,7 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
constructor() {
super();
this.active = false;
this.__registerEventListener();
this.__registerEventListeners();
}
_requestUpdate(name, oldValue) {
@ -81,35 +82,16 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
this.setAttribute('role', 'option');
}
disconnectedCallback() {
super.disconnectedCallback();
this.__unRegisterEventListeners();
}
__registerEventListener() {
__registerEventListeners() {
this.__onClick = () => {
if (!this.disabled) {
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('mouseenter', this.__onMouseEnter);
this.addEventListener('mouseleave', this.__onMouseLeave);
}
__unRegisterEventListeners() {
this.removeEventListener('click', this.__onClick);
this.removeEventListener('mouseenter', this.__onMouseEnter);
this.removeEventListener('mouseleave', this.__onMouseLeave);
}
}

View file

@ -80,24 +80,6 @@ describe('lion-option', () => {
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 () => {
const el = await fixture(html`
<lion-option .choiceValue=${10}></lion-option>

View file

@ -1,53 +1,40 @@
import { UpdatingElement } from '@lion/core';
import { overlays, LocalOverlayController } from '@lion/overlays';
import { LitElement, html } from '@lion/core';
import { OverlayMixin, OverlayController } from '@lion/overlays';
export class LionPopup extends UpdatingElement {
static get properties() {
return {
popperConfig: {
type: Object,
},
};
export class LionPopup extends OverlayMixin(LitElement) {
render() {
return html`
<slot name="invoker"></slot>
<slot name="content"></slot>
`;
}
get popperConfig() {
return this._popperConfig;
get _overlayContentNode() {
return this.querySelector('[slot=content]');
}
set popperConfig(config) {
this._popperConfig = {
...this._popperConfig,
...config,
};
get _overlayInvokerNode() {
return this.querySelector('[slot=invoker]');
}
if (this._controller && this._controller._popper) {
this._controller.updatePopperConfig(this._popperConfig);
}
// eslint-disable-next-line class-methods-use-this
_defineOverlay() {
return new OverlayController({
placementMode: 'local',
contentNode: this._overlayContentNode,
invokerNode: this._overlayInvokerNode,
handlesAccessibility: true,
});
}
connectedCallback() {
super.connectedCallback();
this.contentNode = this.querySelector('[slot="content"]');
this.invokerNode = this.querySelector('[slot="invoker"]');
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);
this.__toggle = () => this._overlayCtrl.toggle();
this._overlayInvokerNode.addEventListener('click', this.__toggle);
}
disconnectedCallback() {
super.disconnectedCallback();
this.invokerNode.removeEventListener('click', this._toggle);
this._overlayInvokerNode.removeEventListener('click', this._toggle);
}
}

View file

@ -11,7 +11,7 @@ describe('lion-popup', () => {
<lion-button slot="invoker">Popup button</lion-button>
</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 () => {
@ -25,10 +25,10 @@ describe('lion-popup', () => {
invoker.click();
await el.updateComplete;
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
expect(el._overlayCtrl.isShown).to.be.true;
invoker.click();
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 () => {
@ -52,25 +52,12 @@ describe('lion-popup', () => {
<lion-button slot="invoker">Popup button</lion-button>
</lion-popup>
`);
await el._controller.show();
expect(el._controller._popper.options.placement).to.equal('top');
await el._overlayCtrl.show();
expect(el._overlayCtrl._popper.options.placement).to.equal('top');
el.popperConfig = { placement: 'left' };
await el._controller.show();
expect(el._controller._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;
await el._overlayCtrl.show();
expect(el._overlayCtrl._popper.options.placement).to.equal('left');
});
});
});

View file

@ -1 +1,3 @@
export { LionSelectRich } from './src/LionSelectRich.js';
export { LionSelectInvoker } from './src/LionSelectInvoker.js';
export { LionOptions } from './src/LionOptions.js';

View file

@ -1,12 +1,13 @@
import { LitElement } from '@lion/core';
import { FormRegistrarPortalMixin } from '@lion/field';
/**
* LionOptions
*
* @customElement
* @customElement lion-options
* @extends LitElement
*/
export class LionOptions extends LitElement {
export class LionOptions extends FormRegistrarPortalMixin(LitElement) {
static get properties() {
return {
role: {

View file

@ -4,7 +4,7 @@ import { html } from '@lion/core';
/**
* LionSelectInvoker: invoker button consuming a selected element
*
* @customElement
* @customElement lion-select-invoker
* @extends LionButton
*/
export class LionSelectInvoker extends LionButton {
@ -13,6 +13,11 @@ export class LionSelectInvoker extends LionButton {
selectedElement: {
type: Object,
},
readOnly: {
type: Boolean,
reflect: true,
attribute: 'readonly',
},
};
}
@ -34,6 +39,14 @@ export class LionSelectInvoker extends LionButton {
constructor() {
super();
this.selectedElement = null;
this.type = 'button';
}
_requestUpdate(name, oldValue) {
super._requestUpdate(name, oldValue);
if (name === 'readOnly') {
this.disabled = this.readOnly;
}
}
_contentTemplate() {

View file

@ -1,5 +1,5 @@
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 { ValidateMixin } from '@lion/validate';
import './differentKeyNamesShimIE.js';
@ -22,11 +22,11 @@ function detectInteractionMode() {
/**
* LionSelectRich: wraps the <lion-listbox> element
*
* @customElement
* @customElement lion-select-rich
* @extends LionField
*/
export class LionSelectRich extends FormRegistrarMixin(
InteractionStateMixin(ValidateMixin(FormControlMixin(SlotMixin(LitElement)))),
export class LionSelectRich extends OverlayMixin(
FormRegistrarMixin(InteractionStateMixin(ValidateMixin(FormControlMixin(SlotMixin(LitElement))))),
) {
static get properties() {
return {
@ -39,9 +39,10 @@ export class LionSelectRich extends FormRegistrarMixin(
reflect: true,
},
opened: {
readOnly: {
type: Boolean,
reflect: true,
attribute: 'readonly',
},
interactionMode: {
@ -98,7 +99,9 @@ export class LionSelectRich extends FormRegistrarMixin(
}
get _listboxNode() {
return this.querySelector('[slot=input]');
return (
(this._overlayCtrl && this._overlayCtrl.contentNode) || this.querySelector('[slot=input]')
);
}
get _listboxActiveDescendantNode() {
@ -132,7 +135,6 @@ export class LionSelectRich extends FormRegistrarMixin(
super();
this.interactionMode = 'auto';
this.disabled = false;
this.opened = false;
// for interaction states
// we use a different event as 'model-value-changed' would bubble up from all options
this._valueChangedEvent = 'select-model-value-changed';
@ -143,6 +145,7 @@ export class LionSelectRich extends FormRegistrarMixin(
}
connectedCallback() {
this._listboxNode.registrationTarget = this;
if (super.connectedCallback) {
super.connectedCallback();
}
@ -150,6 +153,8 @@ export class LionSelectRich extends FormRegistrarMixin(
this.__setupOverlay();
this.__setupInvokerNode();
this.__setupListboxNode();
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
}
disconnectedCallback() {
@ -162,6 +167,11 @@ export class LionSelectRich extends FormRegistrarMixin(
this.__teardownListboxNode();
}
firstUpdated(c) {
super.firstUpdated(c);
this.__toggleInvokerDisabled();
}
_requestUpdate(name, oldValue) {
super._requestUpdate(name, oldValue);
if (
@ -185,17 +195,14 @@ export class LionSelectRich extends FormRegistrarMixin(
this.interactionMode = detectInteractionMode();
}
}
if (name === 'disabled' || name === 'readOnly') {
this.__toggleInvokerDisabled();
}
}
updated(changedProps) {
super.updated(changedProps);
if (changedProps.has('opened')) {
if (this.opened) {
this.__overlay.show();
} else {
this.__overlay.hide();
}
}
if (changedProps.has('disabled')) {
if (this.disabled) {
@ -293,15 +300,22 @@ export class LionSelectRich extends FormRegistrarMixin(
this.__onChildModelValueChanged = this.__onChildModelValueChanged.bind(this);
this.__onKeyUp = this.__onKeyUp.bind(this);
this.addEventListener('active-changed', this.__onChildActiveChanged);
this.addEventListener('model-value-changed', this.__onChildModelValueChanged);
this._listboxNode.addEventListener('active-changed', this.__onChildActiveChanged);
this._listboxNode.addEventListener('model-value-changed', this.__onChildModelValueChanged);
this.addEventListener('keyup', this.__onKeyUp);
}
__teardownEventListeners() {
this.removeEventListener('active-changed', this.__onChildActiveChanged);
this.removeEventListener('model-value-changed', this.__onChildModelValueChanged);
this.removeEventListener('keyup', this.__onKeyUp);
this._listboxNode.removeEventListener('active-changed', this.__onChildActiveChanged);
this._listboxNode.removeEventListener('model-value-changed', this.__onChildModelValueChanged);
this._listboxNode.removeEventListener('keyup', this.__onKeyUp);
}
__toggleInvokerDisabled() {
if (this._invokerNode) {
this._invokerNode.disabled = this.disabled;
this._invokerNode.readOnly = this.readOnly;
}
}
__onChildActiveChanged({ target }) {
@ -448,6 +462,7 @@ export class LionSelectRich extends FormRegistrarMixin(
switch (key) {
case 'ArrowUp':
ev.preventDefault();
if (this.interactionMode === 'mac') {
this.opened = true;
} else {
@ -492,7 +507,7 @@ export class LionSelectRich extends FormRegistrarMixin(
__setupInvokerNodeEventListener() {
this.__invokerOnClick = () => {
if (!this.disabled) {
this.toggle();
this._overlayCtrl.toggle();
}
};
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
_defineOverlay({ invokerNode, contentNode } = {}) {
return overlays.add(
new LocalOverlayController({
contentNode,
invokerNode,
hidesOnEsc: false,
hidesOnOutsideClick: true,
inheritsReferenceObjectWidth: true,
popperConfig: {
placement: 'bottom-start',
modifiers: {
offset: {
enabled: false,
},
},
},
}),
);
return new OverlayController({
...withDropdownConfig(),
contentNode,
invokerNode,
});
}
__setupOverlay() {
this.__overlay = this._defineOverlay({
invokerNode: this._invokerNode,
contentNode: this._listboxNode,
});
this.__overlayOnShow = () => {
this.opened = true;
if (this.checkedIndex) {
this.activeIndex = this.checkedIndex;
}
this._listboxNode.focus();
};
this.__overlay.addEventListener('show', this.__overlayOnShow);
this._overlayCtrl.addEventListener('show', this.__overlayOnShow);
this.__overlayOnHide = () => {
this.opened = false;
this._invokerNode.focus();
};
this.__overlay.addEventListener('hide', this.__overlayOnHide);
this._overlayCtrl.addEventListener('hide', this.__overlayOnHide);
}
__teardownOverlay() {
this.__overlay.removeEventListener('show', this.__overlayOnShow);
this.__overlay.removeEventListener('hide', this.__overlayOnHide);
this._overlayCtrl.removeEventListener('show', this.__overlayOnShow);
this._overlayCtrl.removeEventListener('hide', this.__overlayOnHide);
}
// eslint-disable-next-line class-methods-use-this
@ -612,4 +605,18 @@ export class LionSelectRich extends FormRegistrarMixin(
(typeof value !== 'string' && value !== undefined && value !== null),
};
}
/**
* @override Configures OverlayMixin
*/
get _overlayInvokerNode() {
return this._invokerNode;
}
/**
* @override Configures OverlayMixin
*/
get _overlayContentNode() {
return this._listboxNode;
}
}

View file

@ -4,8 +4,9 @@ import '../lion-options.js';
describe('lion-options', () => {
it('should have role="listbox"', async () => {
const registrationTargetEl = document.createElement('div');
const el = await fixture(html`
<lion-options></lion-options>
<lion-options .registrationTarget=${registrationTargetEl}></lion-options>
`);
expect(el.role).to.equal('listbox');
});

View file

@ -48,6 +48,13 @@ describe('lion-select-invoker', () => {
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', () => {
it('supports a custom _contentTemplate', async () => {
const myTag = defineCE(

View file

@ -362,13 +362,13 @@ describe('lion-select-rich interactions', () => {
</lion-options>
</lion-select-rich>
`);
expect(el.activeIndex).to.equal(2);
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Home' }));
expect(el.activeIndex).to.equal(1);
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'End' }));
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 () => {

View file

@ -1,11 +1,14 @@
import { expect, fixture, html, aTimeout, defineCE, unsafeStatic } from '@open-wc/testing';
import '@lion/option/lion-option.js';
import {
overlays,
LocalOverlayController,
GlobalOverlayController,
DynamicOverlayController,
} from '@lion/overlays';
expect,
fixture,
html,
aTimeout,
defineCE,
unsafeStatic,
nextFrame,
} from '@open-wc/testing';
import '@lion/option/lion-option.js';
import { OverlayController } from '@lion/overlays';
import './keyboardEventShimIE.js';
import '../lion-options.js';
@ -49,6 +52,24 @@ describe('lion-select-rich', () => {
el.checkedIndex = 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', () => {
@ -69,16 +90,16 @@ describe('lion-select-rich', () => {
`);
el.opened = true;
await el.updateComplete;
expect(el._listboxNode.style.display).to.be.equal('inline-block');
expect(el._overlayCtrl.isShown).to.be.true;
el.opened = false;
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 () => {
const el = await fixture(html`
<lion-select-rich opened>
<lion-select-rich .opened=${true}>
<lion-options slot="input"></lion-options>
</lion-select-rich>
`);
@ -98,7 +119,7 @@ describe('lion-select-rich', () => {
<lion-options slot="input"></lion-options>
</lion-select-rich>
`);
el.opened = true;
await el._overlayCtrl.show();
await el.updateComplete;
expect(document.activeElement === el._listboxNode).to.be.true;
expect(document.activeElement === el._invokerNode).to.be.false;
@ -118,7 +139,7 @@ describe('lion-select-rich', () => {
</lion-options>
</lion-select-rich>
`);
el.opened = true;
await el._overlayCtrl.show();
await el.updateComplete;
const options = Array.from(el.querySelectorAll('lion-option'));
@ -194,6 +215,7 @@ describe('lion-select-rich', () => {
`);
expect(el.opened).to.be.false;
el._invokerNode.click();
await nextFrame();
expect(el.opened).to.be.true;
});
@ -337,30 +359,20 @@ describe('lion-select-rich', () => {
});
describe('Subclassers', () => {
it('allows to override the type of overlays', async () => {
it('allows to override the type of overlay', async () => {
const mySelectTagString = defineCE(
class MySelect extends LionSelectRich {
_defineOverlay({ invokerNode, contentNode }) {
// add a DynamicOverlayController
const dynamicCtrl = new DynamicOverlayController();
const ctrl = new OverlayController({
placementMode: 'global',
contentNode,
invokerNode,
});
const localCtrl = overlays.add(
new LocalOverlayController({
contentNode,
invokerNode,
}),
);
dynamicCtrl.add(localCtrl);
const globalCtrl = overlays.add(
new GlobalOverlayController({
contentNode,
invokerNode,
}),
);
dynamicCtrl.add(globalCtrl);
return dynamicCtrl;
this.addEventListener('switch', () => {
ctrl.updateConfig({ placementMode: 'local' });
});
return ctrl;
}
},
);
@ -379,7 +391,9 @@ describe('lion-select-rich', () => {
</${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');
});
});
});

View file

@ -9,7 +9,7 @@ export class LionTooltip extends LionPopup {
connectedCallback() {
super.connectedCallback();
this.contentNode.setAttribute('role', 'tooltip');
this._overlayContentNode.setAttribute('role', 'tooltip');
this.__resetActive = () => {
this.mouseActive = false;
@ -19,42 +19,42 @@ export class LionTooltip extends LionPopup {
this.__showMouse = () => {
if (!this.keyActive) {
this.mouseActive = true;
this._controller.show();
this._overlayCtrl.show();
}
};
this.__hideMouse = () => {
if (!this.keyActive) {
this._controller.hide();
this._overlayCtrl.hide();
}
};
this.__showKey = () => {
if (!this.mouseActive) {
this.keyActive = true;
this._controller.show();
this._overlayCtrl.show();
}
};
this.__hideKey = () => {
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('mouseleave', this.__hideMouse);
this.invokerNode.addEventListener('focusin', this.__showKey);
this.invokerNode.addEventListener('focusout', this.__hideKey);
this._overlayInvokerNode.addEventListener('focusin', this.__showKey);
this._overlayInvokerNode.addEventListener('focusout', this.__hideKey);
}
disconnectedCallback() {
super.disconnectedCallback();
this._controller.removeEventListener('hide', this.__resetActive);
this._overlayCtrl.removeEventListener('hide', this.__resetActive);
this.removeEventListener('mouseenter', this.__showMouse);
this.removeEventListener('mouseleave', this._hideMouse);
this.invokerNode.removeEventListener('focusin', this._showKey);
this.invokerNode.removeEventListener('focusout', this._hideKey);
this._overlayInvokerNode.removeEventListener('focusin', this._showKey);
this._overlayInvokerNode.removeEventListener('focusout', this._hideKey);
}
}

View file

@ -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>
</lion-tooltip>
</div>

View file

@ -11,7 +11,7 @@ describe('lion-tooltip', () => {
<lion-button slot="invoker">Tooltip button</lion-button>
</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 () => {
@ -24,11 +24,11 @@ describe('lion-tooltip', () => {
const eventMouseEnter = new Event('mouseenter');
el.dispatchEvent(eventMouseEnter);
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');
el.dispatchEvent(eventMouseLeave);
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 () => {
@ -41,11 +41,11 @@ describe('lion-tooltip', () => {
const eventMouseEnter = new Event('mouseenter');
el.dispatchEvent(eventMouseEnter);
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');
el.dispatchEvent(eventFocusOut);
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 () => {
@ -59,11 +59,11 @@ describe('lion-tooltip', () => {
const eventFocusIn = new Event('focusin');
invoker.dispatchEvent(eventFocusIn);
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');
invoker.dispatchEvent(eventFocusOut);
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 () => {
@ -77,11 +77,11 @@ describe('lion-tooltip', () => {
const eventFocusIn = new Event('focusin');
invoker.dispatchEvent(eventFocusIn);
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');
invoker.dispatchEvent(eventMouseLeave);
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 () => {
@ -112,16 +112,5 @@ describe('lion-tooltip', () => {
const invoker = el.querySelector('[slot="content"]');
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;
});
});
});