kopia lustrzana https://github.com/shoelace-style/shoelace
use popup in tooltip
rodzic
ab0fdd66b4
commit
d14b9e12ed
|
@ -94,6 +94,7 @@ Use the `placement` attribute to set the preferred placement of the tooltip.
|
||||||
<style>
|
<style>
|
||||||
.tooltip-placement-example {
|
.tooltip-placement-example {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-placement-example-row:after {
|
.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
|
```html preview
|
||||||
<div style="--sl-tooltip-arrow-size: 0;">
|
<sl-tooltip content="This is a tooltip" style="--sl-tooltip-arrow-size: 0;">
|
||||||
<sl-tooltip content="This is a tooltip">
|
<sl-button>No Arrow</sl-button>
|
||||||
<sl-button>Above</sl-button>
|
</sl-tooltip>
|
||||||
</sl-tooltip>
|
|
||||||
|
|
||||||
<sl-tooltip content="This is a tooltip" placement="bottom">
|
|
||||||
<sl-button>Below</sl-button>
|
|
||||||
</sl-tooltip>
|
|
||||||
</div>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```jsx react
|
```jsx react
|
||||||
|
|
|
@ -12,33 +12,12 @@ export default css`
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-target {
|
.tooltip {
|
||||||
display: contents;
|
--arrow-size: var(--sl-tooltip-arrow-size);
|
||||||
|
--arrow-color: var(--sl-tooltip-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-positioner {
|
.tooltip__body {
|
||||||
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 {
|
|
||||||
max-width: var(--max-width);
|
max-width: var(--max-width);
|
||||||
border-radius: var(--sl-tooltip-border-radius);
|
border-radius: var(--sl-tooltip-border-radius);
|
||||||
background-color: var(--sl-tooltip-background-color);
|
background-color: var(--sl-tooltip-background-color);
|
||||||
|
@ -48,14 +27,23 @@ export default css`
|
||||||
line-height: var(--sl-tooltip-line-height);
|
line-height: var(--sl-tooltip-line-height);
|
||||||
color: var(--sl-tooltip-color);
|
color: var(--sl-tooltip-color);
|
||||||
padding: var(--sl-tooltip-padding);
|
padding: var(--sl-tooltip-padding);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: var(--sl-z-index-tooltip);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip__arrow {
|
:host([placement^='top']) .tooltip-popup::part(popup) {
|
||||||
position: absolute;
|
transform-origin: bottom;
|
||||||
background: var(--sl-tooltip-background-color);
|
}
|
||||||
width: calc(var(--sl-tooltip-arrow-size) * 2);
|
|
||||||
height: calc(var(--sl-tooltip-arrow-size) * 2);
|
:host([placement^='bottom']) .tooltip-popup::part(popup) {
|
||||||
transform: rotate(45deg);
|
transform-origin: top;
|
||||||
z-index: -1;
|
}
|
||||||
|
|
||||||
|
:host([placement^='left']) .tooltip-popup::part(popup) {
|
||||||
|
transform-origin: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([placement^='right']) .tooltip-popup::part(popup) {
|
||||||
|
transform-origin: left;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -9,7 +9,7 @@ describe('<sl-tooltip>', () => {
|
||||||
<sl-button>Hover Me</sl-button>
|
<sl-button>Hover Me</sl-button>
|
||||||
</sl-tooltip>
|
</sl-tooltip>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||||
|
|
||||||
expect(base.hidden).to.be.false;
|
expect(base.hidden).to.be.false;
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,7 @@ describe('<sl-tooltip>', () => {
|
||||||
<sl-button>Hover Me</sl-button>
|
<sl-button>Hover Me</sl-button>
|
||||||
</sl-tooltip>
|
</sl-tooltip>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||||
|
|
||||||
expect(base.hidden).to.be.true;
|
expect(base.hidden).to.be.true;
|
||||||
});
|
});
|
||||||
|
@ -31,7 +31,7 @@ describe('<sl-tooltip>', () => {
|
||||||
<sl-button>Hover Me</sl-button>
|
<sl-button>Hover Me</sl-button>
|
||||||
</sl-tooltip>
|
</sl-tooltip>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||||
const showHandler = sinon.spy();
|
const showHandler = sinon.spy();
|
||||||
const afterShowHandler = sinon.spy();
|
const afterShowHandler = sinon.spy();
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ describe('<sl-tooltip>', () => {
|
||||||
<sl-button>Hover Me</sl-button>
|
<sl-button>Hover Me</sl-button>
|
||||||
</sl-tooltip>
|
</sl-tooltip>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||||
const hideHandler = sinon.spy();
|
const hideHandler = sinon.spy();
|
||||||
const afterHideHandler = sinon.spy();
|
const afterHideHandler = sinon.spy();
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ describe('<sl-tooltip>', () => {
|
||||||
<sl-button>Hover Me</sl-button>
|
<sl-button>Hover Me</sl-button>
|
||||||
</sl-tooltip>
|
</sl-tooltip>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||||
const showHandler = sinon.spy();
|
const showHandler = sinon.spy();
|
||||||
const afterShowHandler = sinon.spy();
|
const afterShowHandler = sinon.spy();
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ describe('<sl-tooltip>', () => {
|
||||||
<sl-button>Hover Me</sl-button>
|
<sl-button>Hover Me</sl-button>
|
||||||
</sl-tooltip>
|
</sl-tooltip>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||||
const hideHandler = sinon.spy();
|
const hideHandler = sinon.spy();
|
||||||
const afterHideHandler = sinon.spy();
|
const afterHideHandler = sinon.spy();
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ describe('<sl-tooltip>', () => {
|
||||||
<sl-button>Hover Me</sl-button>
|
<sl-button>Hover Me</sl-button>
|
||||||
</sl-tooltip>
|
</sl-tooltip>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||||
const hideHandler = sinon.spy();
|
const hideHandler = sinon.spy();
|
||||||
const afterHideHandler = sinon.spy();
|
const afterHideHandler = sinon.spy();
|
||||||
|
|
||||||
|
@ -134,18 +134,4 @@ describe('<sl-tooltip>', () => {
|
||||||
expect(afterHideHandler).to.have.been.calledOnce;
|
expect(afterHideHandler).to.have.been.calledOnce;
|
||||||
expect(base.hidden).to.be.true;
|
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');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { arrow, autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
|
|
||||||
import { html, LitElement } from 'lit';
|
import { html, LitElement } from 'lit';
|
||||||
import { customElement, property, query } from 'lit/decorators.js';
|
import { customElement, property, query } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.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 { LocalizeController } from '../../utilities/localize';
|
||||||
import '../popup/popup';
|
import '../popup/popup';
|
||||||
import styles from './tooltip.styles';
|
import styles from './tooltip.styles';
|
||||||
|
import type SlPopup from '../popup/popup';
|
||||||
import type { CSSResultGroup } from 'lit';
|
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-hide - Emitted when the tooltip begins to hide.
|
||||||
* @event sl-after-hide - Emitted after the tooltip has hidden and all animations are complete.
|
* @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 --max-width - The maximum width of the tooltip.
|
||||||
* @cssproperty --hide-delay - The amount of time to wait before hiding the tooltip when hovering.
|
* @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 {
|
export default class SlTooltip extends LitElement {
|
||||||
static styles: CSSResultGroup = styles;
|
static styles: CSSResultGroup = styles;
|
||||||
|
|
||||||
@query('.tooltip-positioner') positioner: HTMLElement;
|
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
|
||||||
@query('.tooltip') tooltip: HTMLElement;
|
@query('.tooltip__body') body: HTMLElement;
|
||||||
@query('.tooltip__arrow') arrow: HTMLElement;
|
@query('sl-popup') popup: SlPopup;
|
||||||
|
|
||||||
private target: HTMLElement;
|
|
||||||
private hoverTimeout: number;
|
private hoverTimeout: number;
|
||||||
private readonly localize = new LocalizeController(this);
|
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. */
|
/** The tooltip's content. If you need to display HTML, you can use the `content` slot instead. */
|
||||||
@property() content = '';
|
@property() content = '';
|
||||||
|
@ -72,7 +71,7 @@ export default class SlTooltip extends LitElement {
|
||||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||||
|
|
||||||
/** The distance in pixels from which to offset the tooltip away from its target. */
|
/** 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. */
|
/** 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;
|
@property({ type: Boolean, reflect: true }) open = false;
|
||||||
|
@ -109,17 +108,16 @@ export default class SlTooltip extends LitElement {
|
||||||
this.addEventListener('keydown', this.handleKeyDown);
|
this.addEventListener('keydown', this.handleKeyDown);
|
||||||
this.addEventListener('mouseover', this.handleMouseOver);
|
this.addEventListener('mouseover', this.handleMouseOver);
|
||||||
this.addEventListener('mouseout', this.handleMouseOut);
|
this.addEventListener('mouseout', this.handleMouseOut);
|
||||||
this.target = this.getTarget();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async firstUpdated() {
|
async firstUpdated() {
|
||||||
this.tooltip.hidden = !this.open;
|
this.body.hidden = !this.open;
|
||||||
|
|
||||||
// If the tooltip is visible on init, update its position
|
// If the tooltip is visible on init, update its position
|
||||||
if (this.open) {
|
if (this.open) {
|
||||||
await this.updateComplete;
|
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('keydown', this.handleKeyDown);
|
||||||
this.removeEventListener('mouseover', this.handleMouseOver);
|
this.removeEventListener('mouseover', this.handleMouseOver);
|
||||||
this.removeEventListener('mouseout', this.handleMouseOut);
|
this.removeEventListener('mouseout', this.handleMouseOut);
|
||||||
this.stopPositioner();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shows the tooltip. */
|
/** Shows the tooltip. */
|
||||||
|
@ -223,22 +220,22 @@ export default class SlTooltip extends LitElement {
|
||||||
// Show
|
// Show
|
||||||
emit(this, 'sl-show');
|
emit(this, 'sl-show');
|
||||||
|
|
||||||
await stopAnimations(this.tooltip);
|
await stopAnimations(this.body);
|
||||||
this.startPositioner();
|
this.body.hidden = false;
|
||||||
this.tooltip.hidden = false;
|
this.popup.active = true;
|
||||||
const { keyframes, options } = getAnimation(this, 'tooltip.show', { dir: this.localize.dir() });
|
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');
|
emit(this, 'sl-after-show');
|
||||||
} else {
|
} else {
|
||||||
// Hide
|
// Hide
|
||||||
emit(this, 'sl-hide');
|
emit(this, 'sl-hide');
|
||||||
|
|
||||||
await stopAnimations(this.tooltip);
|
await stopAnimations(this.body);
|
||||||
const { keyframes, options } = getAnimation(this, 'tooltip.hide', { dir: this.localize.dir() });
|
const { keyframes, options } = getAnimation(this, 'tooltip.hide', { dir: this.localize.dir() });
|
||||||
await animateTo(this.tooltip, keyframes, options);
|
await animateTo(this.popup.popup, keyframes, options);
|
||||||
this.tooltip.hidden = true;
|
this.popup.active = false;
|
||||||
this.stopPositioner();
|
this.body.hidden = true;
|
||||||
|
|
||||||
emit(this, 'sl-after-hide');
|
emit(this, 'sl-after-hide');
|
||||||
}
|
}
|
||||||
|
@ -249,8 +246,11 @@ export default class SlTooltip extends LitElement {
|
||||||
@watch('hoist')
|
@watch('hoist')
|
||||||
@watch('placement')
|
@watch('placement')
|
||||||
@watch('skidding')
|
@watch('skidding')
|
||||||
handleOptionsChange() {
|
async handleOptionsChange() {
|
||||||
this.updatePositioner();
|
if (this.hasUpdated) {
|
||||||
|
await this.updateComplete;
|
||||||
|
this.popup.reposition();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@watch('disabled')
|
@watch('disabled')
|
||||||
|
@ -265,87 +265,28 @@ export default class SlTooltip extends LitElement {
|
||||||
return triggers.includes(triggerType);
|
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() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="tooltip-target" aria-describedby="tooltip">
|
<sl-popup
|
||||||
<slot @slotchange=${this.handleSlotChange}></slot>
|
part="base"
|
||||||
</div>
|
class=${classMap({
|
||||||
|
tooltip: true,
|
||||||
|
'tooltip--open': this.open
|
||||||
|
})}
|
||||||
|
placement=${this.placement}
|
||||||
|
distance=${this.distance}
|
||||||
|
skidding=${this.skidding}
|
||||||
|
strategy=${this.hoist ? 'fixed' : 'absolute'}
|
||||||
|
flip
|
||||||
|
shift
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<slot slot="anchor" aria-describedby="tooltip"></slot>
|
||||||
|
|
||||||
<div class="tooltip-positioner">
|
<div part="body" id="tooltip" class="tooltip__body" role="tooltip" aria-hidden=${this.open ? 'false' : 'true'}>
|
||||||
<div
|
<slot name="content" aria-live=${this.open ? 'polite' : 'off'}> ${this.content} </slot>
|
||||||
part="base"
|
|
||||||
id="tooltip"
|
|
||||||
class=${classMap({
|
|
||||||
tooltip: true,
|
|
||||||
'tooltip--open': this.open
|
|
||||||
})}
|
|
||||||
role="tooltip"
|
|
||||||
aria-hidden=${this.open ? 'false' : 'true'}
|
|
||||||
>
|
|
||||||
<div class="tooltip__arrow"></div>
|
|
||||||
<div class="tooltip__content" aria-live=${this.open ? 'polite' : 'off'}>
|
|
||||||
<slot name="content"> ${this.content} </slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</sl-popup>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue