feat(overlays): abstract arrow logic from tooltip into ArrowMixin
This commit is contained in:
parent
de0e0d52b5
commit
a9d6971c67
13 changed files with 384 additions and 141 deletions
6
.changeset/nine-actors-occur.md
Normal file
6
.changeset/nine-actors-occur.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
'@lion/overlays': minor
|
||||||
|
'@lion/tooltip': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Abstracted the tooltip arrow related logic to a mixin, so it can be used in other overlays. Also created some demos to show this.
|
||||||
|
|
@ -111,7 +111,6 @@ class MyOverlayComponent extends LitElement {
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<slot name="invoker"></slot>
|
<slot name="invoker"></slot>
|
||||||
<slot name="_overlay-shadow-outlet"></slot>
|
|
||||||
<div id="overlay-content-node-wrapper">
|
<div id="overlay-content-node-wrapper">
|
||||||
<slot name="content"></slot>
|
<slot name="content"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
|
|
||||||
```js script
|
```js script
|
||||||
import { html } from 'lit-html';
|
import { html } from 'lit-html';
|
||||||
|
import { render, LitElement } from '@lion/core';
|
||||||
import {
|
import {
|
||||||
|
ArrowMixin,
|
||||||
OverlayMixin,
|
OverlayMixin,
|
||||||
withBottomSheetConfig,
|
withBottomSheetConfig,
|
||||||
withDropdownConfig,
|
withDropdownConfig,
|
||||||
|
|
@ -94,8 +96,8 @@ or in your Web Component with `OverlayMixin`, make sure you override these metho
|
||||||
- Handle the tearing down of those event listeners
|
- Handle the tearing down of those event listeners
|
||||||
- Define a template which includes:
|
- Define a template which includes:
|
||||||
- invoker slot for your user to provide the invoker node (the element that invokes the overlay content)
|
- invoker slot for your user to provide the invoker node (the element that invokes the overlay content)
|
||||||
- content slot for your user to provide the content that shows when the overlay is opened
|
- content slot for your user to provide the content that shows when the overlay is opened,
|
||||||
- \_overlay-shadow-outlet, this slot is currently necessary under the hood for acting as a wrapper element for placement purposes, but is not something your end user should be concerned with, unless they are extending your component.
|
make sure to put it inside a div with id `overlay-content-node-wrapper` which is necessary for positioning logic to work properly.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
_defineOverlayConfig() {
|
_defineOverlayConfig() {
|
||||||
|
|
@ -126,7 +128,6 @@ _teardownOpenCloseListeners() {
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<slot name="invoker"></slot>
|
<slot name="invoker"></slot>
|
||||||
<slot name="_overlay-shadow-outlet"></slot>
|
|
||||||
<div id="overlay-content-node-wrapper">
|
<div id="overlay-content-node-wrapper">
|
||||||
<slot name="content"></slot>
|
<slot name="content"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -356,7 +357,7 @@ Under the hood, the `OverlayMixin` will instantiate an OverlayController with th
|
||||||
|
|
||||||
By default, there are only a few `OverlayMixin` methods you need to override to create a working Web Component using an overlay:
|
By default, there are only a few `OverlayMixin` methods you need to override to create a working Web Component using an overlay:
|
||||||
|
|
||||||
- `render`, the template needs to include a `<slot name="content">`, `<slot name="invoker">` and `<slot name="_overlay-shadow-outlet">`.
|
- `render`, the template needs to include a `<slot name="invoker">` and `<slot name="content">` inside a div with id `overlay-content-node-wrapper` (for positioning).
|
||||||
- `_defineOverlayConfig`, in this protected method, return an object that contains the default configuration for your Web Component's overlay. See configuration section of OverlayController.
|
- `_defineOverlayConfig`, in this protected method, return an object that contains the default configuration for your Web Component's overlay. See configuration section of OverlayController.
|
||||||
- `_setupOpenCloseListeners`, use this lifecycle hook to setup the open and close event listeners on your `_overlayInvokerNode`.
|
- `_setupOpenCloseListeners`, use this lifecycle hook to setup the open and close event listeners on your `_overlayInvokerNode`.
|
||||||
- `_teardownOpenCloseListeners`, use this lifecycle hook to ensure that the listeners are removed when the OverlayController is tearing down. For example when the Web Component is disconnected from the DOM.
|
- `_teardownOpenCloseListeners`, use this lifecycle hook to ensure that the listeners are removed when the OverlayController is tearing down. For example when the Web Component is disconnected from the DOM.
|
||||||
|
|
@ -397,7 +398,6 @@ class MyOverlayWC extends OverlayMixin(LitElement) {
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<slot name="invoker"></slot>
|
<slot name="invoker"></slot>
|
||||||
<slot name="_overlay-shadow-outlet"></slot>
|
|
||||||
<div id="overlay-content-node-wrapper">
|
<div id="overlay-content-node-wrapper">
|
||||||
<slot name="content"></slot>
|
<slot name="content"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -568,3 +568,57 @@ export const nestedOverlays = () => {
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Local overlay with an arrow
|
||||||
|
|
||||||
|
To add an arrow to the localOverlay you can add `ArrowMixin` to your component.
|
||||||
|
And add the `arrowPopperConfig` to the `_defineOverlayConfig`.
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const LocalWithArrow = () => {
|
||||||
|
class ArrowExample extends ArrowMixin(OverlayMixin(LitElement)) {
|
||||||
|
// Alternatively, set `this.config = { popperConfig: { placement: 'bottom' } }` on connectedCallback
|
||||||
|
_defineOverlayConfig() {
|
||||||
|
return {
|
||||||
|
...super._defineOverlayConfig(),
|
||||||
|
popperConfig: {
|
||||||
|
...super._defineOverlayConfig().popperConfig,
|
||||||
|
placement: 'bottom',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.__toggle = this.__toggle.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
__toggle() {
|
||||||
|
this.opened = !this.opened;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupOpenCloseListeners() {
|
||||||
|
super._setupOpenCloseListeners();
|
||||||
|
if (this._overlayInvokerNode) {
|
||||||
|
this._overlayInvokerNode.addEventListener('click', this.__toggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_teardownOpenCloseListeners() {
|
||||||
|
super._teardownOpenCloseListeners();
|
||||||
|
if (this._overlayInvokerNode) {
|
||||||
|
this._overlayInvokerNode.removeEventListener('click', this.__toggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!customElements.get('arrow-example')) {
|
||||||
|
customElements.define('arrow-example', ArrowExample);
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<arrow-example>
|
||||||
|
<button slot="invoker">Click me to open the overlay!</button>
|
||||||
|
<div slot="content">This is a tooltip with an arrow<div>
|
||||||
|
</arrow-example>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ class DemoOverlaySystem extends OverlayMixin(LitElement) {
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<slot name="invoker"></slot>
|
<slot name="invoker"></slot>
|
||||||
<slot name="_overlay-shadow-outlet"></slot>
|
|
||||||
<div id="overlay-content-node-wrapper">
|
<div id="overlay-content-node-wrapper">
|
||||||
<slot name="content"></slot>
|
<slot name="content"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export { overlays, setOverlays } from './src/overlays.js';
|
||||||
export { OverlaysManager } from './src/OverlaysManager.js';
|
export { OverlaysManager } from './src/OverlaysManager.js';
|
||||||
export { OverlayController } from './src/OverlayController.js';
|
export { OverlayController } from './src/OverlayController.js';
|
||||||
export { OverlayMixin } from './src/OverlayMixin.js';
|
export { OverlayMixin } from './src/OverlayMixin.js';
|
||||||
|
export { ArrowMixin } from './src/ArrowMixin.js';
|
||||||
|
|
||||||
export { withBottomSheetConfig } from './src/configurations/withBottomSheetConfig.js';
|
export { withBottomSheetConfig } from './src/configurations/withBottomSheetConfig.js';
|
||||||
export { withModalDialogConfig } from './src/configurations/withModalDialogConfig.js';
|
export { withModalDialogConfig } from './src/configurations/withModalDialogConfig.js';
|
||||||
|
|
|
||||||
162
packages/overlays/src/ArrowMixin.js
Normal file
162
packages/overlays/src/ArrowMixin.js
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
import { css, html, dedupeMixin } from '@lion/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
|
||||||
|
* @typedef {import('../types/ArrowMixinTypes').ArrowMixin} ArrowMixin
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {ArrowMixin}
|
||||||
|
*/
|
||||||
|
export const ArrowMixinImplementation = superclass =>
|
||||||
|
class ArrowMixin extends superclass {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hasArrow: {
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
attribute: 'has-arrow',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
--tooltip-arrow-width: 12px;
|
||||||
|
--tooltip-arrow-height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
width: var(--tooltip-arrow-width);
|
||||||
|
height: var(--tooltip-arrow-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([has-arrow]) .arrow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[x-placement^='bottom'] .arrow {
|
||||||
|
top: calc(-1 * var(--tooltip-arrow-height));
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[x-placement^='left'] .arrow {
|
||||||
|
right: calc(
|
||||||
|
-1 * (var(--tooltip-arrow-height) +
|
||||||
|
(var(--tooltip-arrow-width) - var(--tooltip-arrow-height)) / 2)
|
||||||
|
);
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[x-placement^='right'] .arrow {
|
||||||
|
left: calc(
|
||||||
|
-1 * (var(--tooltip-arrow-height) +
|
||||||
|
(var(--tooltip-arrow-width) - var(--tooltip-arrow-height)) / 2)
|
||||||
|
);
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.hasArrow = true;
|
||||||
|
this.__setupRepositionCompletePromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<slot name="invoker"></slot>
|
||||||
|
<div id="overlay-content-node-wrapper">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
<div class="arrow" x-arrow>${this._arrowTemplate()}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_arrowTemplate() {
|
||||||
|
return html`
|
||||||
|
<svg viewBox="0 0 12 8">
|
||||||
|
<path d="M 0,0 h 12 L 6,8 z"></path>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable method `_defineOverlay`
|
||||||
|
* @desc Overrides arrow and keepTogether modifier to be enabled,
|
||||||
|
* and adds onCreate and onUpdate hooks to sync from popper state
|
||||||
|
* @returns {OverlayConfig}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line
|
||||||
|
_defineOverlayConfig() {
|
||||||
|
if (!this.hasArrow) {
|
||||||
|
return super._defineOverlayConfig();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...super._defineOverlayConfig(),
|
||||||
|
popperConfig: {
|
||||||
|
...super._defineOverlayConfig()?.popperConfig,
|
||||||
|
placement: 'top',
|
||||||
|
|
||||||
|
modifiers: {
|
||||||
|
...super._defineOverlayConfig()?.popperConfig?.modifiers,
|
||||||
|
keepTogether: {
|
||||||
|
...super._defineOverlayConfig()?.popperConfig?.modifiers?.keepTogether,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
arrow: {
|
||||||
|
...super._defineOverlayConfig()?.popperConfig?.modifiers?.arrow,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/** @param {import("popper.js").default.Data} data */
|
||||||
|
onCreate: data => {
|
||||||
|
this.__syncFromPopperState(data);
|
||||||
|
},
|
||||||
|
/** @param {import("popper.js").default.Data} data */
|
||||||
|
onUpdate: data => {
|
||||||
|
this.__syncFromPopperState(data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
__setupRepositionCompletePromise() {
|
||||||
|
this.repositionComplete = new Promise(resolve => {
|
||||||
|
this.__repositionCompleteResolver = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get _arrowNode() {
|
||||||
|
return /** @type {ShadowRoot} */ (this.shadowRoot).querySelector('[x-arrow]');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("popper.js").default.Data} data
|
||||||
|
*/
|
||||||
|
__syncFromPopperState(data) {
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this._arrowNode &&
|
||||||
|
data.placement !== /** @type {Element & {placement:string}} */ (this._arrowNode).placement
|
||||||
|
) {
|
||||||
|
/** @type {function} */ (this.__repositionCompleteResolver)(data.placement);
|
||||||
|
this.__setupRepositionCompletePromise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ArrowMixin = dedupeMixin(ArrowMixinImplementation);
|
||||||
|
|
@ -895,7 +895,6 @@ export class OverlayController extends EventTargetShim {
|
||||||
/** @type {HTMLElement} */
|
/** @type {HTMLElement} */
|
||||||
(this.backdropNode).classList.add('local-overlays__backdrop');
|
(this.backdropNode).classList.add('local-overlays__backdrop');
|
||||||
}
|
}
|
||||||
this.backdropNode.slot = '_overlay-shadow-outlet';
|
|
||||||
/** @type {HTMLElement} */
|
/** @type {HTMLElement} */
|
||||||
(this.contentNode.parentNode).insertBefore(this.backdropNode, this.contentNode);
|
(this.contentNode.parentNode).insertBefore(this.backdropNode, this.contentNode);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
106
packages/overlays/test/ArrowMixin.test.js
Normal file
106
packages/overlays/test/ArrowMixin.test.js
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { expect, fixture } from '@open-wc/testing';
|
||||||
|
import { LitElement, html } from '@lion/core';
|
||||||
|
import { ArrowMixin, OverlayMixin } from '../index.js';
|
||||||
|
|
||||||
|
describe('ArrowMixin', () => {
|
||||||
|
class ArrowTest extends ArrowMixin(OverlayMixin(LitElement)) {
|
||||||
|
/**
|
||||||
|
* @overridable method `_defineOverlay`
|
||||||
|
* @desc Overrides arrow and keepTogether modifier to be enabled,
|
||||||
|
* and adds onCreate and onUpdate hooks to sync from popper state
|
||||||
|
* @returns {import('../types/OverlayConfig').OverlayConfig}
|
||||||
|
*/
|
||||||
|
_defineOverlayConfig() {
|
||||||
|
return {
|
||||||
|
...super._defineOverlayConfig(),
|
||||||
|
placementMode: 'local',
|
||||||
|
popperConfig: {
|
||||||
|
...super._defineOverlayConfig().popperConfig,
|
||||||
|
placement: 'bottom',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.__toggle = this.__toggle.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
__toggle() {
|
||||||
|
this.opened = !this.opened;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupOpenCloseListeners() {
|
||||||
|
super._setupOpenCloseListeners();
|
||||||
|
if (this._overlayInvokerNode) {
|
||||||
|
this._overlayInvokerNode.addEventListener('click', this.__toggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_teardownOpenCloseListeners() {
|
||||||
|
super._teardownOpenCloseListeners();
|
||||||
|
if (this._overlayInvokerNode) {
|
||||||
|
this._overlayInvokerNode.removeEventListener('click', this.__toggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
before(() => {
|
||||||
|
customElements.define('arrow-test', ArrowTest);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows by default', async () => {
|
||||||
|
const el = /** @type {ArrowTest} */ (await fixture(html`
|
||||||
|
<arrow-test>
|
||||||
|
<div slot="content">This is a tooltip</div>
|
||||||
|
<button slot="invoker">Tooltip button</button>
|
||||||
|
</arrow-test>
|
||||||
|
`));
|
||||||
|
expect(el.hasAttribute('has-arrow')).to.be.true;
|
||||||
|
|
||||||
|
const arrowNode = /** @type {Element} */ (el._arrowNode);
|
||||||
|
expect(window.getComputedStyle(arrowNode).getPropertyValue('display')).to.equal('block');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides the arrow when has-arrow is false', async () => {
|
||||||
|
const el = /** @type {ArrowTest} */ (await fixture(html`
|
||||||
|
<arrow-test>
|
||||||
|
<div slot="content">This is a tooltip</div>
|
||||||
|
<button slot="invoker">Tooltip button</button>
|
||||||
|
</arrow-test>
|
||||||
|
`));
|
||||||
|
el.hasArrow = false;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.hasAttribute('has-arrow')).to.be.false;
|
||||||
|
const arrowNode = /** @type {Element} */ (el._arrowNode);
|
||||||
|
expect(window.getComputedStyle(arrowNode).getPropertyValue('display')).to.equal('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('makes sure positioning of the arrow is correct', async () => {
|
||||||
|
const el = /** @type {ArrowTest} */ (await fixture(html`
|
||||||
|
<arrow-test
|
||||||
|
.config="${/** @type {import('../types/OverlayConfig').OverlayConfig} */ ({
|
||||||
|
popperConfig: {
|
||||||
|
placement: 'right',
|
||||||
|
},
|
||||||
|
})}"
|
||||||
|
style="position: relative; top: 10px;"
|
||||||
|
>
|
||||||
|
<div slot="content" style="height: 30px; background-color: red;">Hey there</div>
|
||||||
|
<button slot="invoker" style="height: 30px;">Tooltip button</button>
|
||||||
|
</arrow-test>
|
||||||
|
`));
|
||||||
|
|
||||||
|
el.opened = true;
|
||||||
|
|
||||||
|
await el.repositionComplete;
|
||||||
|
expect(
|
||||||
|
getComputedStyle(/** @type {HTMLElement} */ (el._arrowNode)).getPropertyValue('left'),
|
||||||
|
).to.equal(
|
||||||
|
'-10px',
|
||||||
|
`
|
||||||
|
arrow height is 8px so this offset should be taken into account to align the arrow properly,
|
||||||
|
as well as half the difference between width and height ((12 - 8) / 2 = 2)
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -8,7 +8,6 @@ const tagString = defineCE(
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<button slot="invoker">invoker button</button>
|
<button slot="invoker">invoker button</button>
|
||||||
<slot name="_overlay-shadow-outlet"></slot>
|
|
||||||
<div id="overlay-content-node-wrapper">
|
<div id="overlay-content-node-wrapper">
|
||||||
<div slot="content">content of the overlay</div>
|
<div slot="content">content of the overlay</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
32
packages/overlays/types/ArrowMixinTypes.d.ts
vendored
Normal file
32
packages/overlays/types/ArrowMixinTypes.d.ts
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
|
import { LitElement, TemplateResult } from '@lion/core';
|
||||||
|
import { CSSResultArray } from 'lit-element';
|
||||||
|
import Data from 'popper.js';
|
||||||
|
import { OverlayConfig } from '../types/OverlayConfig';
|
||||||
|
|
||||||
|
export declare class ArrowHost {
|
||||||
|
static get properties(): {
|
||||||
|
hasArrow: {
|
||||||
|
type: BooleanConstructor;
|
||||||
|
reflect: boolean;
|
||||||
|
attribute: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
hasArrow: boolean;
|
||||||
|
repositionComplete: Promise<void>;
|
||||||
|
|
||||||
|
static styles: CSSResultArray;
|
||||||
|
|
||||||
|
render(): TemplateResult;
|
||||||
|
_arrowTemplate(): TemplateResult;
|
||||||
|
_defineOverlayConfig(): OverlayConfig;
|
||||||
|
__setupRepositionCompletePromise(): void;
|
||||||
|
get _arrowNode(): Element | null;
|
||||||
|
__syncFromPopperState(data: Data): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function ArrowImplementation<T extends Constructor<LitElement>>(
|
||||||
|
superclass: T,
|
||||||
|
): T & Constructor<ArrowHost> & ArrowHost;
|
||||||
|
|
||||||
|
export type ArrowMixin = typeof ArrowImplementation;
|
||||||
|
|
@ -180,7 +180,7 @@ Modifier explanations:
|
||||||
|
|
||||||
By default, the arrow is disabled for our tooltip. Via the `has-arrow` property it can be enabled.
|
By default, the arrow is disabled for our tooltip. Via the `has-arrow` property it can be enabled.
|
||||||
|
|
||||||
> As a Subclasser, you can decide to turn the arrow on by default if this fits your Design System
|
> As a Subclasser, you can decide to turn the arrow on by default if this fits your Design System, by setting `this.hasArrow = true;` in the constructor.
|
||||||
|
|
||||||
```js preview-story
|
```js preview-story
|
||||||
export const arrow = () => html`
|
export const arrow = () => html`
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,18 @@
|
||||||
import { css, html, LitElement } from '@lion/core';
|
import { css, LitElement } from '@lion/core';
|
||||||
import { OverlayMixin } from '@lion/overlays';
|
import { ArrowMixin, OverlayMixin } from '@lion/overlays';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@lion/overlays/types/OverlayConfig').OverlayConfig} OverlayConfig
|
* @typedef {import('@lion/overlays/types/OverlayConfig').OverlayConfig} OverlayConfig
|
||||||
|
* @typedef {import('@lion/core').CSSResult} CSSResult
|
||||||
|
* @typedef {import('lit-element').CSSResultArray} CSSResultArray
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @customElement lion-tooltip
|
* @customElement lion-tooltip
|
||||||
*/
|
*/
|
||||||
export class LionTooltip extends OverlayMixin(LitElement) {
|
export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
hasArrow: {
|
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
attribute: 'has-arrow',
|
|
||||||
},
|
|
||||||
invokerRelation: {
|
invokerRelation: {
|
||||||
type: String,
|
type: String,
|
||||||
attribute: 'invoker-relation',
|
attribute: 'invoker-relation',
|
||||||
|
|
@ -24,56 +21,18 @@ export class LionTooltip extends OverlayMixin(LitElement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return css`
|
return [
|
||||||
:host {
|
/** @type {CSSResult | CSSStyleSheet | CSSResultArray} */ (super.styles),
|
||||||
--tooltip-arrow-width: 12px;
|
css`
|
||||||
--tooltip-arrow-height: 8px;
|
:host {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([hidden]) {
|
:host([hidden]) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
.arrow {
|
];
|
||||||
position: absolute;
|
|
||||||
width: var(--tooltip-arrow-width);
|
|
||||||
height: var(--tooltip-arrow-height);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow svg {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
[x-placement^='bottom'] .arrow {
|
|
||||||
top: calc(-1 * var(--tooltip-arrow-height));
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
[x-placement^='left'] .arrow {
|
|
||||||
right: calc(
|
|
||||||
-1 * (var(--tooltip-arrow-height) +
|
|
||||||
(var(--tooltip-arrow-width) - var(--tooltip-arrow-height)) / 2)
|
|
||||||
);
|
|
||||||
transform: rotate(270deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
[x-placement^='right'] .arrow {
|
|
||||||
left: calc(
|
|
||||||
-1 * (var(--tooltip-arrow-height) +
|
|
||||||
(var(--tooltip-arrow-width) - var(--tooltip-arrow-height)) / 2)
|
|
||||||
);
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([has-arrow]) .arrow {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -91,85 +50,22 @@ export class LionTooltip extends OverlayMixin(LitElement) {
|
||||||
this.invokerRelation = 'description';
|
this.invokerRelation = 'description';
|
||||||
this._mouseActive = false;
|
this._mouseActive = false;
|
||||||
this._keyActive = false;
|
this._keyActive = false;
|
||||||
this.__setupRepositionCompletePromise();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`
|
|
||||||
<slot name="invoker"></slot>
|
|
||||||
<slot name="_overlay-shadow-outlet"></slot>
|
|
||||||
<div id="overlay-content-node-wrapper">
|
|
||||||
<slot name="content"></slot>
|
|
||||||
<div class="arrow" x-arrow>${this._arrowTemplate()}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
_arrowTemplate() {
|
|
||||||
return html`
|
|
||||||
<svg viewBox="0 0 12 8">
|
|
||||||
<path d="M 0,0 h 12 L 6,8 z"></path>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_defineOverlayConfig() {
|
_defineOverlayConfig() {
|
||||||
return /** @type {OverlayConfig} */ ({
|
return /** @type {OverlayConfig} */ ({
|
||||||
|
...super._defineOverlayConfig(),
|
||||||
placementMode: 'local',
|
placementMode: 'local',
|
||||||
elementToFocusAfterHide: undefined,
|
elementToFocusAfterHide: undefined,
|
||||||
hidesOnEsc: true,
|
hidesOnEsc: true,
|
||||||
hidesOnOutsideEsc: true,
|
hidesOnOutsideEsc: true,
|
||||||
popperConfig: {
|
|
||||||
placement: 'top', // default
|
|
||||||
modifiers: {
|
|
||||||
keepTogether: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
arrow: {
|
|
||||||
enabled: this.hasArrow,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onCreate: data => {
|
|
||||||
this.__syncFromPopperState(data);
|
|
||||||
},
|
|
||||||
onUpdate: data => {
|
|
||||||
this.__syncFromPopperState(data);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
handlesAccessibility: true,
|
handlesAccessibility: true,
|
||||||
isTooltip: true,
|
isTooltip: true,
|
||||||
invokerRelation: this.invokerRelation,
|
invokerRelation: this.invokerRelation,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
__setupRepositionCompletePromise() {
|
|
||||||
this.repositionComplete = new Promise(resolve => {
|
|
||||||
this.__repositionCompleteResolver = resolve;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get _arrowNode() {
|
|
||||||
return /** @type {ShadowRoot} */ (this.shadowRoot).querySelector('[x-arrow]');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import("popper.js").default.Data} data
|
|
||||||
*/
|
|
||||||
__syncFromPopperState(data) {
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this._arrowNode &&
|
|
||||||
data.placement !== /** @type {Element & {placement:string}} */ (this._arrowNode).placement
|
|
||||||
) {
|
|
||||||
/** @type {function} */ (this.__repositionCompleteResolver)(data.placement);
|
|
||||||
this.__setupRepositionCompletePromise();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupOpenCloseListeners() {
|
_setupOpenCloseListeners() {
|
||||||
super._setupOpenCloseListeners();
|
super._setupOpenCloseListeners();
|
||||||
this.__resetActive = this.__resetActive.bind(this);
|
this.__resetActive = this.__resetActive.bind(this);
|
||||||
|
|
|
||||||
|
|
@ -185,11 +185,6 @@ describe('lion-tooltip', () => {
|
||||||
const initialPopperModifiers = el._overlayCtrl.config.popperConfig.modifiers;
|
const initialPopperModifiers = el._overlayCtrl.config.popperConfig.modifiers;
|
||||||
// @ts-expect-error allow protected props in tests
|
// @ts-expect-error allow protected props in tests
|
||||||
expect(el._overlayCtrl.config.popperConfig.placement).to.equal('top');
|
expect(el._overlayCtrl.config.popperConfig.placement).to.equal('top');
|
||||||
// TODO: this fails in CI, we need to investigate why in CI
|
|
||||||
// the value of the transform is: translate3d(16px, -26px, 0px)'
|
|
||||||
// expect(el.querySelector('[slot=_overlay-shadow-outlet]').style.transform).to.equal(
|
|
||||||
// 'translate3d(15px, -26px, 0px)',
|
|
||||||
// );
|
|
||||||
|
|
||||||
el.config = {
|
el.config = {
|
||||||
popperConfig: {
|
popperConfig: {
|
||||||
|
|
@ -205,11 +200,6 @@ describe('lion-tooltip', () => {
|
||||||
expect(updatedPopperModifiers).to.deep.equal(initialPopperModifiers);
|
expect(updatedPopperModifiers).to.deep.equal(initialPopperModifiers);
|
||||||
// @ts-expect-error allow protected props in tests
|
// @ts-expect-error allow protected props in tests
|
||||||
expect(el._overlayCtrl.config.popperConfig.placement).to.equal('bottom');
|
expect(el._overlayCtrl.config.popperConfig.placement).to.equal('bottom');
|
||||||
// TODO: this fails in CI, we need to investigate why in CI
|
|
||||||
// the value of the transform is: translate3d(16px, 26px, 0px)'
|
|
||||||
// expect(el.querySelector('[slot=_overlay-shadow-outlet]').style.transform).to.equal(
|
|
||||||
// 'translate3d(15px, 26px, 0px)',
|
|
||||||
// );
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue