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() {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
/**
|
||||
* @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 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
if (this._controller && this._controller._popper) {
|
||||
this._controller.updatePopperConfig(this._popperConfig);
|
||||
get _overlayInvokerNode() {
|
||||
return this.querySelector('[slot=invoker]');
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
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 { 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: {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
return new OverlayController({
|
||||
...withDropdownConfig(),
|
||||
contentNode,
|
||||
invokerNode,
|
||||
hidesOnEsc: false,
|
||||
hidesOnOutsideClick: true,
|
||||
inheritsReferenceObjectWidth: true,
|
||||
popperConfig: {
|
||||
placement: 'bottom-start',
|
||||
modifiers: {
|
||||
offset: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
__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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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 localCtrl = overlays.add(
|
||||
new LocalOverlayController({
|
||||
const ctrl = new OverlayController({
|
||||
placementMode: 'global',
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue