use popup in tooltip

pull/853/head
Cory LaViska 2022-08-08 15:41:19 -04:00
rodzic ab0fdd66b4
commit d14b9e12ed
4 zmienionych plików z 73 dodań i 163 usunięć

Wyświetl plik

@ -94,6 +94,7 @@ Use the `placement` attribute to set the preferred placement of the tooltip.
<style>
.tooltip-placement-example {
width: 250px;
margin: 1rem;
}
.tooltip-placement-example-row:after {
@ -282,20 +283,14 @@ const App = () => {
};
```
### Remove Arrows
### Removing Arrows
You can control the size of tooltip arrows by overriding the `--sl-tooltip-arrow-size` design token.
You can control the size of tooltip arrows by overriding the `--sl-tooltip-arrow-size` design token. To remove them, set the value to `0` as shown below.
```html preview
<div style="--sl-tooltip-arrow-size: 0;">
<sl-tooltip content="This is a tooltip">
<sl-button>Above</sl-button>
<sl-tooltip content="This is a tooltip" style="--sl-tooltip-arrow-size: 0;">
<sl-button>No Arrow</sl-button>
</sl-tooltip>
<sl-tooltip content="This is a tooltip" placement="bottom">
<sl-button>Below</sl-button>
</sl-tooltip>
</div>
```
```jsx react

Wyświetl plik

@ -12,33 +12,12 @@ export default css`
display: contents;
}
.tooltip-target {
display: contents;
.tooltip {
--arrow-size: var(--sl-tooltip-arrow-size);
--arrow-color: var(--sl-tooltip-background-color);
}
.tooltip-positioner {
position: absolute;
z-index: var(--sl-z-index-tooltip);
pointer-events: none;
}
.tooltip-positioner[data-placement^='top'] .tooltip {
transform-origin: bottom;
}
.tooltip-positioner[data-placement^='bottom'] .tooltip {
transform-origin: top;
}
.tooltip-positioner[data-placement^='left'] .tooltip {
transform-origin: right;
}
.tooltip-positioner[data-placement^='right'] .tooltip {
transform-origin: left;
}
.tooltip__content {
.tooltip__body {
max-width: var(--max-width);
border-radius: var(--sl-tooltip-border-radius);
background-color: var(--sl-tooltip-background-color);
@ -48,14 +27,23 @@ export default css`
line-height: var(--sl-tooltip-line-height);
color: var(--sl-tooltip-color);
padding: var(--sl-tooltip-padding);
pointer-events: none;
z-index: var(--sl-z-index-tooltip);
}
.tooltip__arrow {
position: absolute;
background: var(--sl-tooltip-background-color);
width: calc(var(--sl-tooltip-arrow-size) * 2);
height: calc(var(--sl-tooltip-arrow-size) * 2);
transform: rotate(45deg);
z-index: -1;
:host([placement^='top']) .tooltip-popup::part(popup) {
transform-origin: bottom;
}
:host([placement^='bottom']) .tooltip-popup::part(popup) {
transform-origin: top;
}
:host([placement^='left']) .tooltip-popup::part(popup) {
transform-origin: right;
}
:host([placement^='right']) .tooltip-popup::part(popup) {
transform-origin: left;
}
`;

Wyświetl plik

@ -9,7 +9,7 @@ describe('<sl-tooltip>', () => {
<sl-button>Hover Me</sl-button>
</sl-tooltip>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
expect(base.hidden).to.be.false;
});
@ -20,7 +20,7 @@ describe('<sl-tooltip>', () => {
<sl-button>Hover Me</sl-button>
</sl-tooltip>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
expect(base.hidden).to.be.true;
});
@ -31,7 +31,7 @@ describe('<sl-tooltip>', () => {
<sl-button>Hover Me</sl-button>
</sl-tooltip>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
const showHandler = sinon.spy();
const afterShowHandler = sinon.spy();
@ -53,7 +53,7 @@ describe('<sl-tooltip>', () => {
<sl-button>Hover Me</sl-button>
</sl-tooltip>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy();
@ -75,7 +75,7 @@ describe('<sl-tooltip>', () => {
<sl-button>Hover Me</sl-button>
</sl-tooltip>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
const showHandler = sinon.spy();
const afterShowHandler = sinon.spy();
@ -97,7 +97,7 @@ describe('<sl-tooltip>', () => {
<sl-button>Hover Me</sl-button>
</sl-tooltip>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy();
@ -119,7 +119,7 @@ describe('<sl-tooltip>', () => {
<sl-button>Hover Me</sl-button>
</sl-tooltip>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy();
@ -134,18 +134,4 @@ describe('<sl-tooltip>', () => {
expect(afterHideHandler).to.have.been.calledOnce;
expect(base.hidden).to.be.true;
});
it('should recalculate its target when the slotted element changes', async () => {
const el = await fixture<SlTooltip>(html`
<sl-tooltip content="This is a tooltip" open>
<sl-button>Hover me</sl-button>
</sl-tooltip>
`);
el.innerHTML = '<sl-button>New element</sl-button>';
await el.updateComplete;
// @ts-expect-error - target is a private property
expect(el.target.innerHTML).to.equal('New element');
});
});

Wyświetl plik

@ -1,4 +1,3 @@
import { arrow, autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
import { html, LitElement } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@ -9,6 +8,7 @@ import { getAnimation, setDefaultAnimation } from '../../utilities/animation-reg
import { LocalizeController } from '../../utilities/localize';
import '../popup/popup';
import styles from './tooltip.styles';
import type SlPopup from '../popup/popup';
import type { CSSResultGroup } from 'lit';
/**
@ -25,7 +25,8 @@ import type { CSSResultGroup } from 'lit';
* @event sl-hide - Emitted when the tooltip begins to hide.
* @event sl-after-hide - Emitted after the tooltip has hidden and all animations are complete.
*
* @csspart base - The component's internal wrapper.
* @csspart base - The component's base wrapper, an `<sl-popup>` element.
* @csspart body - The tooltip's body.
*
* @cssproperty --max-width - The maximum width of the tooltip.
* @cssproperty --hide-delay - The amount of time to wait before hiding the tooltip when hovering.
@ -38,14 +39,12 @@ import type { CSSResultGroup } from 'lit';
export default class SlTooltip extends LitElement {
static styles: CSSResultGroup = styles;
@query('.tooltip-positioner') positioner: HTMLElement;
@query('.tooltip') tooltip: HTMLElement;
@query('.tooltip__arrow') arrow: HTMLElement;
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
@query('.tooltip__body') body: HTMLElement;
@query('sl-popup') popup: SlPopup;
private target: HTMLElement;
private hoverTimeout: number;
private readonly localize = new LocalizeController(this);
private positionerCleanup: ReturnType<typeof autoUpdate> | undefined;
/** The tooltip's content. If you need to display HTML, you can use the `content` slot instead. */
@property() content = '';
@ -72,7 +71,7 @@ export default class SlTooltip extends LitElement {
@property({ type: Boolean, reflect: true }) disabled = false;
/** The distance in pixels from which to offset the tooltip away from its target. */
@property({ type: Number }) distance = 10;
@property({ type: Number }) distance = 8;
/** Indicates whether or not the tooltip is open. You can use this in lieu of the show/hide methods. */
@property({ type: Boolean, reflect: true }) open = false;
@ -109,17 +108,16 @@ export default class SlTooltip extends LitElement {
this.addEventListener('keydown', this.handleKeyDown);
this.addEventListener('mouseover', this.handleMouseOver);
this.addEventListener('mouseout', this.handleMouseOut);
this.target = this.getTarget();
});
}
async firstUpdated() {
this.tooltip.hidden = !this.open;
this.body.hidden = !this.open;
// If the tooltip is visible on init, update its position
if (this.open) {
await this.updateComplete;
this.updatePositioner();
this.popup.reposition();
}
}
@ -131,7 +129,6 @@ export default class SlTooltip extends LitElement {
this.removeEventListener('keydown', this.handleKeyDown);
this.removeEventListener('mouseover', this.handleMouseOver);
this.removeEventListener('mouseout', this.handleMouseOut);
this.stopPositioner();
}
/** Shows the tooltip. */
@ -223,22 +220,22 @@ export default class SlTooltip extends LitElement {
// Show
emit(this, 'sl-show');
await stopAnimations(this.tooltip);
this.startPositioner();
this.tooltip.hidden = false;
await stopAnimations(this.body);
this.body.hidden = false;
this.popup.active = true;
const { keyframes, options } = getAnimation(this, 'tooltip.show', { dir: this.localize.dir() });
await animateTo(this.tooltip, keyframes, options);
await animateTo(this.popup.popup, keyframes, options);
emit(this, 'sl-after-show');
} else {
// Hide
emit(this, 'sl-hide');
await stopAnimations(this.tooltip);
await stopAnimations(this.body);
const { keyframes, options } = getAnimation(this, 'tooltip.hide', { dir: this.localize.dir() });
await animateTo(this.tooltip, keyframes, options);
this.tooltip.hidden = true;
this.stopPositioner();
await animateTo(this.popup.popup, keyframes, options);
this.popup.active = false;
this.body.hidden = true;
emit(this, 'sl-after-hide');
}
@ -249,8 +246,11 @@ export default class SlTooltip extends LitElement {
@watch('hoist')
@watch('placement')
@watch('skidding')
handleOptionsChange() {
this.updatePositioner();
async handleOptionsChange() {
if (this.hasUpdated) {
await this.updateComplete;
this.popup.reposition();
}
}
@watch('disabled')
@ -265,87 +265,28 @@ export default class SlTooltip extends LitElement {
return triggers.includes(triggerType);
}
handleSlotChange() {
this.target = this.getTarget();
}
private startPositioner() {
this.stopPositioner();
requestAnimationFrame(() => this.updatePositioner());
this.positionerCleanup = autoUpdate(this.target, this.positioner, this.updatePositioner.bind(this));
}
private updatePositioner() {
if (!this.open || !this.target || !this.positioner) {
return;
}
computePosition(this.target, this.positioner, {
placement: this.placement,
middleware: [
offset({ mainAxis: this.distance, crossAxis: this.skidding }),
flip(),
shift(),
arrow({
element: this.arrow,
padding: 10 // min distance from the edge
})
],
strategy: this.hoist ? 'fixed' : 'absolute'
}).then(({ x, y, middlewareData, placement }) => {
const arrowX = middlewareData.arrow!.x;
const arrowY = middlewareData.arrow!.y;
const staticSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' }[placement.split('-')[0]]!;
this.positioner.setAttribute('data-placement', placement);
Object.assign(this.positioner.style, {
position: this.hoist ? 'fixed' : 'absolute',
left: `${x}px`,
top: `${y}px`
});
Object.assign(this.arrow.style, {
left: typeof arrowX === 'number' ? `${arrowX}px` : '',
top: typeof arrowY === 'number' ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: 'calc(var(--sl-tooltip-arrow-size) * -1)'
});
});
}
private stopPositioner() {
if (this.positionerCleanup) {
this.positionerCleanup();
this.positionerCleanup = undefined;
this.positioner.removeAttribute('data-placement');
}
}
render() {
return html`
<div class="tooltip-target" aria-describedby="tooltip">
<slot @slotchange=${this.handleSlotChange}></slot>
</div>
<div class="tooltip-positioner">
<div
<sl-popup
part="base"
id="tooltip"
class=${classMap({
tooltip: true,
'tooltip--open': this.open
})}
role="tooltip"
aria-hidden=${this.open ? 'false' : 'true'}
placement=${this.placement}
distance=${this.distance}
skidding=${this.skidding}
strategy=${this.hoist ? 'fixed' : 'absolute'}
flip
shift
arrow
>
<div class="tooltip__arrow"></div>
<div class="tooltip__content" aria-live=${this.open ? 'polite' : 'off'}>
<slot name="content"> ${this.content} </slot>
</div>
</div>
<slot slot="anchor" aria-describedby="tooltip"></slot>
<div part="body" id="tooltip" class="tooltip__body" role="tooltip" aria-hidden=${this.open ? 'false' : 'true'}>
<slot name="content" aria-live=${this.open ? 'polite' : 'off'}> ${this.content} </slot>
</div>
</sl-popup>
`;
}
}