From 4c26befaae0621e0c10b4c857d243c4fa45c9b6c Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Thu, 10 Oct 2019 16:42:40 +0200 Subject: [PATCH] feat: update to latest overlay system Co-authored-by: Thomas Allmer Co-authored-by: Joren Broekema Co-authored-by: Mikhail Bashkirov Co-authored-by: Alex Ghiu --- packages/calendar/src/LionCalendar.js | 4 +- .../src/LionInputDatepicker.js | 69 ++++++----- .../test-helpers/DatepickerInputObject.js | 4 + .../test/lion-input-datepicker.test.js | 13 +-- packages/option/src/LionOption.js | 26 +---- packages/option/test/lion-option.test.js | 18 --- packages/popup/src/LionPopup.js | 61 ++++------ packages/popup/test/lion-popup.test.js | 27 ++--- packages/select-rich/index.js | 2 + packages/select-rich/src/LionOptions.js | 5 +- packages/select-rich/src/LionSelectInvoker.js | 15 ++- packages/select-rich/src/LionSelectRich.js | 109 ++++++++++-------- .../select-rich/test/lion-options.test.js | 3 +- .../test/lion-select-invoker.test.js | 7 ++ .../test/lion-select-rich-interaction.test.js | 6 +- .../select-rich/test/lion-select-rich.test.js | 80 +++++++------ packages/tooltip/src/LionTooltip.js | 22 ++-- packages/tooltip/stories/index.stories.js | 2 +- packages/tooltip/test/lion-tooltip.test.js | 29 ++--- 19 files changed, 243 insertions(+), 259 deletions(-) diff --git a/packages/calendar/src/LionCalendar.js b/packages/calendar/src/LionCalendar.js index 9fa203d36..e37f998d3 100644 --- a/packages/calendar/src/LionCalendar.js +++ b/packages/calendar/src/LionCalendar.js @@ -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); diff --git a/packages/input-datepicker/src/LionInputDatepicker.js b/packages/input-datepicker/src/LionInputDatepicker.js index 2ce4d3c0a..e7a2e0fd5 100644 --- a/packages/input-datepicker/src/LionInputDatepicker.js +++ b/packages/input-datepicker/src/LionInputDatepicker.js @@ -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` this._overlayCtrl.hide()}> ${this.calendarHeading} @@ -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; + } } diff --git a/packages/input-datepicker/test-helpers/DatepickerInputObject.js b/packages/input-datepicker/test-helpers/DatepickerInputObject.js index ff8cc4275..380ca3b1b 100644 --- a/packages/input-datepicker/test-helpers/DatepickerInputObject.js +++ b/packages/input-datepicker/test-helpers/DatepickerInputObject.js @@ -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; diff --git a/packages/input-datepicker/test/lion-input-datepicker.test.js b/packages/input-datepicker/test/lion-input-datepicker.test.js index 440a5593f..07cef9130 100644 --- a/packages/input-datepicker/test/lion-input-datepicker.test.js +++ b/packages/input-datepicker/test/lion-input-datepicker.test.js @@ -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('', () => { - beforeEach(() => { - localizeTearDown(); - }); - describe('Calendar Overlay', () => { it('implements calendar-overlay Style component', async () => { const el = await fixture(html` @@ -287,14 +282,16 @@ describe('', () => { 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` `); 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'); }); }); diff --git a/packages/option/src/LionOption.js b/packages/option/src/LionOption.js index 4d299b181..c990bd746 100644 --- a/packages/option/src/LionOption.js +++ b/packages/option/src/LionOption.js @@ -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); } } diff --git a/packages/option/test/lion-option.test.js b/packages/option/test/lion-option.test.js index e034c586c..964a484b9 100644 --- a/packages/option/test/lion-option.test.js +++ b/packages/option/test/lion-option.test.js @@ -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` - - `); - 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` - - `); - 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` diff --git a/packages/popup/src/LionPopup.js b/packages/popup/src/LionPopup.js index e42b098e7..a8f8f9241 100644 --- a/packages/popup/src/LionPopup.js +++ b/packages/popup/src/LionPopup.js @@ -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` + + + `; } - 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); } } diff --git a/packages/popup/test/lion-popup.test.js b/packages/popup/test/lion-popup.test.js index 9c6fd461a..6184ad006 100644 --- a/packages/popup/test/lion-popup.test.js +++ b/packages/popup/test/lion-popup.test.js @@ -11,7 +11,7 @@ describe('lion-popup', () => { Popup button `); - 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', () => { Popup button `); - 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` - - - Popup button - - `); - 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'); }); }); }); diff --git a/packages/select-rich/index.js b/packages/select-rich/index.js index f052e9010..dc995bd9c 100644 --- a/packages/select-rich/index.js +++ b/packages/select-rich/index.js @@ -1 +1,3 @@ export { LionSelectRich } from './src/LionSelectRich.js'; +export { LionSelectInvoker } from './src/LionSelectInvoker.js'; +export { LionOptions } from './src/LionOptions.js'; diff --git a/packages/select-rich/src/LionOptions.js b/packages/select-rich/src/LionOptions.js index b78a4fced..3dc34b897 100644 --- a/packages/select-rich/src/LionOptions.js +++ b/packages/select-rich/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: { diff --git a/packages/select-rich/src/LionSelectInvoker.js b/packages/select-rich/src/LionSelectInvoker.js index 063f5712d..40f9d1243 100644 --- a/packages/select-rich/src/LionSelectInvoker.js +++ b/packages/select-rich/src/LionSelectInvoker.js @@ -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() { diff --git a/packages/select-rich/src/LionSelectRich.js b/packages/select-rich/src/LionSelectRich.js index 0e1a70db1..e24ec9ef3 100644 --- a/packages/select-rich/src/LionSelectRich.js +++ b/packages/select-rich/src/LionSelectRich.js @@ -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 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; + } } diff --git a/packages/select-rich/test/lion-options.test.js b/packages/select-rich/test/lion-options.test.js index 903ba7fd6..b3793a749 100644 --- a/packages/select-rich/test/lion-options.test.js +++ b/packages/select-rich/test/lion-options.test.js @@ -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` - + `); expect(el.role).to.equal('listbox'); }); diff --git a/packages/select-rich/test/lion-select-invoker.test.js b/packages/select-rich/test/lion-select-invoker.test.js index fdec095ac..a9a9b50b2 100644 --- a/packages/select-rich/test/lion-select-invoker.test.js +++ b/packages/select-rich/test/lion-select-invoker.test.js @@ -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` + + `); + expect(el.hasAttribute('disabled')).to.be.true; + }); + describe('Subclassers', () => { it('supports a custom _contentTemplate', async () => { const myTag = defineCE( diff --git a/packages/select-rich/test/lion-select-rich-interaction.test.js b/packages/select-rich/test/lion-select-rich-interaction.test.js index c39026e76..a95479f78 100644 --- a/packages/select-rich/test/lion-select-rich-interaction.test.js +++ b/packages/select-rich/test/lion-select-rich-interaction.test.js @@ -362,13 +362,13 @@ describe('lion-select-rich interactions', () => { `); - 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 () => { diff --git a/packages/select-rich/test/lion-select-rich.test.js b/packages/select-rich/test/lion-select-rich.test.js index 4015f05e3..50705c0c1 100644 --- a/packages/select-rich/test/lion-select-rich.test.js +++ b/packages/select-rich/test/lion-select-rich.test.js @@ -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` + + + Item 1 + Item 2 + + + `); + + 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` - + `); @@ -98,7 +119,7 @@ describe('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', () => { `); - 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', () => { `); - 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'); }); }); }); diff --git a/packages/tooltip/src/LionTooltip.js b/packages/tooltip/src/LionTooltip.js index 05cf948bd..540426d08 100644 --- a/packages/tooltip/src/LionTooltip.js +++ b/packages/tooltip/src/LionTooltip.js @@ -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); } } diff --git a/packages/tooltip/stories/index.stories.js b/packages/tooltip/stories/index.stories.js index 9be5424ce..dfb0bb2fc 100644 --- a/packages/tooltip/stories/index.stories.js +++ b/packages/tooltip/stories/index.stories.js @@ -123,7 +123,7 @@ storiesOf('Local Overlay System|Tooltip', module) }, })}" > - ${text('Invoker text', 'Click me!')} + ${text('Invoker text', 'Hover me!')}
${text('Content text', 'Hello, World!')}
diff --git a/packages/tooltip/test/lion-tooltip.test.js b/packages/tooltip/test/lion-tooltip.test.js index 249f247ac..b233b0a21 100644 --- a/packages/tooltip/test/lion-tooltip.test.js +++ b/packages/tooltip/test/lion-tooltip.test.js @@ -11,7 +11,7 @@ describe('lion-tooltip', () => { Tooltip button `); - 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` - -
Hey there
- Tooltip button -
- `); - const invoker = el.querySelector('[slot="invoker"]'); - expect(invoker.getAttribute('aria-controls')).to.not.be.null; - }); }); });