From b9770e7e73b6929bf44f57465eba7f4085c0dd9b Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Thu, 3 Mar 2022 15:48:20 -0500 Subject: [PATCH] replace popper with floating ui --- cspell.json | 1 - docs/assets/plugins/search/search.js | 6 +- docs/getting-started/overview.md | 2 +- docs/resources/changelog.md | 3 + package-lock.json | 42 ++++--- package.json | 2 +- scripts/build.js | 4 +- src/components/dropdown/dropdown.styles.ts | 9 +- src/components/dropdown/dropdown.ts | 116 +++++++++-------- src/components/menu/menu.ts | 9 +- src/components/tooltip/tooltip.styles.ts | 118 ++++-------------- src/components/tooltip/tooltip.ts | 137 +++++++++++---------- src/declaration.d.ts | 4 - src/themes/dark.css | 3 +- src/themes/light.css | 3 +- 15 files changed, 213 insertions(+), 246 deletions(-) diff --git a/cspell.json b/cspell.json index e39a68ee..a6027071 100644 --- a/cspell.json +++ b/cspell.json @@ -80,7 +80,6 @@ "ParamagicDev", "peta", "petabit", - "popperjs", "progressbar", "radiogroup", "Railsbyte", diff --git a/docs/assets/plugins/search/search.js b/docs/assets/plugins/search/search.js index 220a1a4a..a99ee72f 100644 --- a/docs/assets/plugins/search/search.js +++ b/docs/assets/plugins/search/search.js @@ -219,7 +219,11 @@ await searchIndex; const hasQuery = query.length > 0; - const matches = hasQuery ? searchIndex.search(`t:*${query}*^20 h:*${query}*^10 ${query}~1^5 *${query}*^0`) : []; + const searchQuery = query + .split(' ') + .map((term, index, arr) => `${term}${index === arr.length - 1 ? `* ${term}~1` : '~1'}`) + .join(' '); + const matches = hasQuery ? searchIndex.search(searchQuery) : []; const hasResults = hasQuery && matches.length > 0; siteSearch.classList.toggle('site-search--has-results', hasQuery && hasResults); siteSearch.classList.toggle('site-search--no-results', hasQuery && !hasResults); diff --git a/docs/getting-started/overview.md b/docs/getting-started/overview.md index 99ca874c..0f23927b 100644 --- a/docs/getting-started/overview.md +++ b/docs/getting-started/overview.md @@ -124,7 +124,7 @@ Special thanks to the following projects and individuals that help make Shoelace - Color primitives are inspired by [Tailwind](https://tailwindcss.com/) - Icons are courtesy of [Bootstrap Icons](https://icons.getbootstrap.com/) - The homepage illustration is courtesy of [unDraw](https://undraw.co/) -- Positioning of menus, tooltips, et al is handled by [Popper.js](https://popper.js.org/) +- Positioning of dropdowns, tooltips, et al is handled by [Floating UI](https://floating-ui.com/) - Animations are courtesy of [animate.css](https://animate.style/) - QR codes are generated with [qr-creator](https://github.com/nimiq/qr-creator) - Search is powered by [Lunr](https://lunrjs.com/) diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 9629d3a6..04aac275 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -13,7 +13,10 @@ _During the beta period, these restrictions may be relaxed in the event of a mis - Fixed a bug that prevented forms from submitting when pressing Enter inside of an `` [#700](https://github.com/shoelace-style/shoelace/issues/700) - Fixed a bug in `` that prevented the `valueAsDate` and `valueAsNumber` properties from working when set before the component was initialized - Improved `autofocus` behavior in Safari for `` and `` [#693](https://github.com/shoelace-style/shoelace/issues/693) +- Improved type to select logic in `` so it supports Backspace and gives users more time before resetting - Removed feature detection for `focus({ preventScroll })` since it no longer works in Safari +- Removed the `--sl-tooltip-arrow-start-end-offset` design token +- Replaced Popper positioning dependency with Floating UI in `` and `` ## 2.0.0-beta.70 diff --git a/package-lock.json b/package-lock.json index c65c601a..805145b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "2.0.0-beta.70", "license": "MIT", "dependencies": { + "@floating-ui/dom": "^0.3.1", "@lit-labs/react": "^1.0.2", - "@popperjs/core": "^2.11.2", "@shoelace-style/animations": "^1.1.0", "@shoelace-style/localize": "^2.1.3", "color": "4.2", @@ -663,6 +663,19 @@ "@types/chai": "^4.2.12" } }, + "node_modules/@floating-ui/core": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.5.1.tgz", + "integrity": "sha512-LOFg65vdU4o4eZjVKNhu7VXbjdnY0amMw9sydLtKZiCNwj0eMD06rmktbWp9L/f6w6s9Qqe22lkks8PbwYwy4Q==" + }, + "node_modules/@floating-ui/dom": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.3.1.tgz", + "integrity": "sha512-HJX/2xgGPt01YHQNAya2taqg5HDnY6ipF0B0Bk/Th2s4FD9R89Ecw3TEegLLO+VrQnPEGbuHOGJ6Fr3BPM0DYg==", + "dependencies": { + "@floating-ui/core": "^0.5.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -892,15 +905,6 @@ "lit-html": "^2.0.0" } }, - "node_modules/@popperjs/core": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz", - "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@rollup/plugin-node-resolve": { "version": "11.2.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", @@ -14821,6 +14825,19 @@ "@types/chai": "^4.2.12" } }, + "@floating-ui/core": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.5.1.tgz", + "integrity": "sha512-LOFg65vdU4o4eZjVKNhu7VXbjdnY0amMw9sydLtKZiCNwj0eMD06rmktbWp9L/f6w6s9Qqe22lkks8PbwYwy4Q==" + }, + "@floating-ui/dom": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.3.1.tgz", + "integrity": "sha512-HJX/2xgGPt01YHQNAya2taqg5HDnY6ipF0B0Bk/Th2s4FD9R89Ecw3TEegLLO+VrQnPEGbuHOGJ6Fr3BPM0DYg==", + "requires": { + "@floating-ui/core": "^0.5.0" + } + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -15031,11 +15048,6 @@ "lit-html": "^2.0.0" } }, - "@popperjs/core": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz", - "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==" - }, "@rollup/plugin-node-resolve": { "version": "11.2.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", diff --git a/package.json b/package.json index 445552fd..a4305d00 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,8 @@ "node": ">=14.17.0" }, "dependencies": { + "@floating-ui/dom": "^0.3.1", "@lit-labs/react": "^1.0.2", - "@popperjs/core": "^2.11.2", "@shoelace-style/animations": "^1.1.0", "@shoelace-style/localize": "^2.1.3", "color": "4.2", diff --git a/scripts/build.js b/scripts/build.js index c6a217a8..a8a7638f 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -64,7 +64,7 @@ fs.mkdirSync(outdir, { recursive: true }); chunkNames: 'chunks/[name].[hash]', incremental: serve, define: { - // Popper.js requires this to be set + // Floating UI requires this to be set 'process.env.NODE_ENV': '"production"' }, bundle: true, @@ -76,7 +76,7 @@ fs.mkdirSync(outdir, { recursive: true }); // external: bundle ? alwaysExternal - : [...alwaysExternal, '@popperjs/core', '@shoelace-style/animations', 'lit', 'qr-creator'], + : [...alwaysExternal, '@floating-ui/dom', '@shoelace-style/animations', 'lit', 'qr-creator'], splitting: true, plugins: [] }) diff --git a/src/components/dropdown/dropdown.styles.ts b/src/components/dropdown/dropdown.styles.ts index a5578f23..14a426d1 100644 --- a/src/components/dropdown/dropdown.styles.ts +++ b/src/components/dropdown/dropdown.styles.ts @@ -22,7 +22,6 @@ export default css` } .dropdown__panel { - max-height: 75vh; font-family: var(--sl-font-sans); font-size: var(--sl-font-size-medium); font-weight: var(--sl-font-weight-normal); @@ -40,19 +39,19 @@ export default css` pointer-events: all; } - .dropdown__positioner[data-popper-placement^='top'] .dropdown__panel { + .dropdown__positioner[data-placement^='top'] .dropdown__panel { transform-origin: bottom; } - .dropdown__positioner[data-popper-placement^='bottom'] .dropdown__panel { + .dropdown__positioner[data-placement^='bottom'] .dropdown__panel { transform-origin: top; } - .dropdown__positioner[data-popper-placement^='left'] .dropdown__panel { + .dropdown__positioner[data-placement^='left'] .dropdown__panel { transform-origin: right; } - .dropdown__positioner[data-popper-placement^='right'] .dropdown__panel { + .dropdown__positioner[data-placement^='right'] .dropdown__panel { transform-origin: left; } `; diff --git a/src/components/dropdown/dropdown.ts b/src/components/dropdown/dropdown.ts index 04bcc168..185a00cb 100644 --- a/src/components/dropdown/dropdown.ts +++ b/src/components/dropdown/dropdown.ts @@ -1,4 +1,4 @@ -import { createPopper } from '@popperjs/core/dist/esm'; +import { autoUpdate, computePosition, flip, offset, shift, size } 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'; @@ -11,7 +11,6 @@ import { getTabbableBoundary } from '~/internal/tabbable'; import { watch } from '~/internal/watch'; import { getAnimation, setDefaultAnimation } from '~/utilities/animation-registry'; import styles from './dropdown.styles'; -import type { Instance as PopperInstance } from '@popperjs/core/dist/esm'; /** * @since 2.0 @@ -40,7 +39,7 @@ export default class SlDropdown extends LitElement { @query('.dropdown__panel') panel: HTMLElement; @query('.dropdown__positioner') positioner: HTMLElement; - private popover?: PopperInstance; + private positionerCleanup: ReturnType | undefined; /** Indicates whether or not the dropdown is open. You can use this in lieu of the show/hide methods. */ @property({ type: Boolean, reflect: true }) open = false; @@ -49,7 +48,7 @@ export default class SlDropdown extends LitElement { * The preferred placement of the dropdown panel. Note that the actual placement may vary as needed to keep the panel * inside of the viewport. */ - @property() placement: + @property({ reflect: true }) placement: | 'top' | 'top-start' | 'top-end' @@ -97,39 +96,22 @@ export default class SlDropdown extends LitElement { if (!this.containingElement) { this.containingElement = this; } - - // Create the popover after render - this.updateComplete.then(() => { - this.popover = createPopper(this.trigger, this.positioner, { - placement: this.placement, - strategy: this.hoist ? 'fixed' : 'absolute', - modifiers: [ - { - name: 'flip', - options: { - boundary: 'viewport' - } - }, - { - name: 'offset', - options: { - offset: [this.skidding, this.distance] - } - } - ] - }); - }); } - firstUpdated() { + async firstUpdated() { this.panel.hidden = !this.open; + + // If the dropdown is visible on init, update its position + if (this.open) { + await this.updateComplete; + this.startPositioner(); + } } disconnectedCallback() { super.disconnectedCallback(); this.hide(); - - this.popover?.destroy(); + this.stopPositioner(); } focusOnTrigger() { @@ -213,24 +195,7 @@ export default class SlDropdown extends LitElement { @watch('placement') @watch('skidding') handlePopoverOptionsChange() { - this.popover?.setOptions({ - placement: this.placement, - strategy: this.hoist ? 'fixed' : 'absolute', - modifiers: [ - { - name: 'flip', - options: { - boundary: 'viewport' - } - }, - { - name: 'offset', - options: { - offset: [this.skidding, this.distance] - } - } - ] - }); + this.updatePositioner(); } handleTriggerClick() { @@ -351,11 +316,7 @@ export default class SlDropdown extends LitElement { * is activated. */ reposition() { - if (!this.open) { - return; - } - - this.popover?.update(); + this.updatePositioner(); } @watch('open', { waitUntilFirstUpdate: true }) @@ -376,7 +337,7 @@ export default class SlDropdown extends LitElement { document.addEventListener('mousedown', this.handleDocumentMouseDown); await stopAnimations(this); - this.popover?.update(); + this.startPositioner(); this.panel.hidden = false; const { keyframes, options } = getAnimation(this, 'dropdown.show'); await animateTo(this.panel, keyframes, options); @@ -394,11 +355,60 @@ export default class SlDropdown extends LitElement { const { keyframes, options } = getAnimation(this, 'dropdown.hide'); await animateTo(this.panel, keyframes, options); this.panel.hidden = true; + this.stopPositioner(); emit(this, 'sl-after-hide'); } } + private startPositioner() { + this.stopPositioner(); + this.updatePositioner(); + this.positionerCleanup = autoUpdate(this.trigger, this.positioner, this.updatePositioner.bind(this)); + } + + private updatePositioner() { + if (!this.open || !this.trigger || !this.positioner) { + return; + } + + computePosition(this.trigger, this.positioner, { + placement: this.placement, + middleware: [ + offset({ mainAxis: this.distance, crossAxis: this.skidding }), + flip(), + shift(), + size({ + apply: ({ width, height }) => { + // Ensure the panel stays within the viewport when we have lots of menu items + Object.assign(this.panel.style, { + maxWidth: `${width}px`, + maxHeight: `${height}px` + }); + }, + padding: 8 + }) + ], + strategy: this.hoist ? 'fixed' : 'absolute' + }).then(({ x, y, placement }) => { + this.positioner.setAttribute('data-placement', placement); + + Object.assign(this.positioner.style, { + position: this.hoist ? 'fixed' : 'absolute', + left: `${x}px`, + top: `${y}px` + }); + }); + } + + private stopPositioner() { + if (this.positionerCleanup) { + this.positionerCleanup(); + this.positionerCleanup = undefined; + this.positioner.removeAttribute('data-placement'); + } + } + render() { return html`
(this.typeToSelectString = ''), 750); - this.typeToSelectString += key.toLowerCase(); + this.typeToSelectTimeout = window.setTimeout(() => (this.typeToSelectString = ''), 1000); + + if (key === 'Backspace') { + this.typeToSelectString = this.typeToSelectString.slice(0, -1); + } else { + this.typeToSelectString += key.toLowerCase(); + } // Restore focus in browsers that don't support :focus-visible when using the keyboard if (!hasFocusVisible) { diff --git a/src/components/tooltip/tooltip.styles.ts b/src/components/tooltip/tooltip.styles.ts index cea11433..a78017fc 100644 --- a/src/components/tooltip/tooltip.styles.ts +++ b/src/components/tooltip/tooltip.styles.ts @@ -12,7 +12,7 @@ export default css` display: contents; } - .tooltip-content { + .tooltip-target { display: contents; } @@ -22,7 +22,23 @@ export default css` pointer-events: none; } - .tooltip { + .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); border-radius: var(--sl-tooltip-border-radius); background-color: var(--sl-tooltip-background-color); @@ -34,98 +50,12 @@ export default css` padding: var(--sl-tooltip-padding); } - .tooltip:after { - content: ''; + .tooltip__arrow { position: absolute; - width: 0; - height: 0; - } - - .tooltip-positioner[data-popper-placement^='top'] .tooltip { - transform-origin: bottom; - } - - .tooltip-positioner[data-popper-placement^='bottom'] .tooltip { - transform-origin: top; - } - - .tooltip-positioner[data-popper-placement^='left'] .tooltip { - transform-origin: right; - } - - .tooltip-positioner[data-popper-placement^='right'] .tooltip { - transform-origin: left; - } - - /* Arrow + bottom */ - .tooltip-positioner[data-popper-placement^='bottom'] .tooltip:after { - bottom: 100%; - left: calc(50% - var(--sl-tooltip-arrow-size)); - border-bottom: var(--sl-tooltip-arrow-size) solid var(--sl-tooltip-background-color); - border-left: var(--sl-tooltip-arrow-size) solid transparent; - border-right: var(--sl-tooltip-arrow-size) solid transparent; - } - - .tooltip-positioner[data-popper-placement='bottom-start'] .tooltip:after { - left: var(--sl-tooltip-arrow-start-end-offset); - } - - .tooltip-positioner[data-popper-placement='bottom-end'] .tooltip:after { - right: var(--sl-tooltip-arrow-start-end-offset); - left: auto; - } - - /* Arrow + top */ - .tooltip-positioner[data-popper-placement^='top'] .tooltip:after { - top: 100%; - left: calc(50% - var(--sl-tooltip-arrow-size)); - border-top: var(--sl-tooltip-arrow-size) solid var(--sl-tooltip-background-color); - border-left: var(--sl-tooltip-arrow-size) solid transparent; - border-right: var(--sl-tooltip-arrow-size) solid transparent; - } - - .tooltip-positioner[data-popper-placement='top-start'] .tooltip:after { - left: var(--sl-tooltip-arrow-start-end-offset); - } - - .tooltip-positioner[data-popper-placement='top-end'] .tooltip:after { - right: var(--sl-tooltip-arrow-start-end-offset); - left: auto; - } - - /* Arrow + left */ - .tooltip-positioner[data-popper-placement^='left'] .tooltip:after { - top: calc(50% - var(--sl-tooltip-arrow-size)); - left: 100%; - border-left: var(--sl-tooltip-arrow-size) solid var(--sl-tooltip-background-color); - border-top: var(--sl-tooltip-arrow-size) solid transparent; - border-bottom: var(--sl-tooltip-arrow-size) solid transparent; - } - - .tooltip-positioner[data-popper-placement='left-start'] .tooltip:after { - top: var(--sl-tooltip-arrow-start-end-offset); - } - - .tooltip-positioner[data-popper-placement='left-end'] .tooltip:after { - top: auto; - bottom: var(--sl-tooltip-arrow-start-end-offset); - } - - /* Arrow + right */ - .tooltip-positioner[data-popper-placement^='right'] .tooltip:after { - top: calc(50% - var(--sl-tooltip-arrow-size)); - right: 100%; - border-right: var(--sl-tooltip-arrow-size) solid var(--sl-tooltip-background-color); - border-top: var(--sl-tooltip-arrow-size) solid transparent; - border-bottom: var(--sl-tooltip-arrow-size) solid transparent; - } - - .tooltip-positioner[data-popper-placement='right-start'] .tooltip:after { - top: var(--sl-tooltip-arrow-start-end-offset); - } - - .tooltip-positioner[data-popper-placement='right-end'] .tooltip:after { - top: auto; - bottom: var(--sl-tooltip-arrow-start-end-offset); + 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; } `; diff --git a/src/components/tooltip/tooltip.ts b/src/components/tooltip/tooltip.ts index 0827775a..c34bca35 100644 --- a/src/components/tooltip/tooltip.ts +++ b/src/components/tooltip/tooltip.ts @@ -1,4 +1,4 @@ -import { createPopper } from '@popperjs/core/dist/esm'; +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'; @@ -7,7 +7,6 @@ import { emit, waitForEvent } from '~/internal/event'; import { watch } from '~/internal/watch'; import { getAnimation, setDefaultAnimation } from '~/utilities/animation-registry'; import styles from './tooltip.styles'; -import type { Instance as PopperInstance } from '@popperjs/core/dist/esm'; /** * @since 2.0 @@ -36,10 +35,11 @@ export default class SlTooltip extends LitElement { @query('.tooltip-positioner') positioner: HTMLElement; @query('.tooltip') tooltip: HTMLElement; + @query('.tooltip__arrow') arrow: HTMLElement; private target: HTMLElement; - private popover?: PopperInstance; private hoverTimeout: number; + private positionerCleanup: ReturnType | undefined; /** The tooltip's content. Alternatively, you can use the content slot. */ @property() content = ''; @@ -103,14 +103,18 @@ 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(); - this.syncOptions(); }); } - firstUpdated() { + async firstUpdated() { this.tooltip.hidden = !this.open; + + // If the tooltip is visible on init, update its position + if (this.open) { + await this.updateComplete; + this.updatePositioner(); + } } disconnectedCallback() { @@ -121,8 +125,7 @@ export default class SlTooltip extends LitElement { this.removeEventListener('keydown', this.handleKeyDown); this.removeEventListener('mouseover', this.handleMouseOver); this.removeEventListener('mouseout', this.handleMouseOut); - - this.popover?.destroy(); + this.stopPositioner(); } /** Shows the tooltip. */ @@ -215,28 +218,7 @@ export default class SlTooltip extends LitElement { emit(this, 'sl-show'); await stopAnimations(this.tooltip); - - this.popover?.destroy(); - - this.popover = createPopper(this.target, this.positioner, { - placement: this.placement, - strategy: this.hoist ? 'fixed' : 'absolute', - modifiers: [ - { - name: 'flip', - options: { - boundary: 'viewport' - } - }, - { - name: 'offset', - options: { - offset: [this.skidding, this.distance] - } - } - ] - }); - + this.startPositioner(); this.tooltip.hidden = false; const { keyframes, options } = getAnimation(this, 'tooltip.show'); await animateTo(this.tooltip, keyframes, options); @@ -250,26 +232,19 @@ export default class SlTooltip extends LitElement { const { keyframes, options } = getAnimation(this, 'tooltip.hide'); await animateTo(this.tooltip, keyframes, options); this.tooltip.hidden = true; - - this.popover?.destroy(); + this.stopPositioner(); emit(this, 'sl-after-hide'); } } - @watch('placement') - @watch('distance') - @watch('skidding') - @watch('hoist') - handleOptionsChange() { - this.syncOptions(); - } - @watch('content') - handleContentChange() { - if (this.open) { - this.popover?.update(); - } + @watch('distance') + @watch('hoist') + @watch('placement') + @watch('skidding') + handleOptionsChange() { + this.updatePositioner(); } @watch('disabled') @@ -284,30 +259,63 @@ export default class SlTooltip extends LitElement { return triggers.includes(triggerType); } - syncOptions() { - this.popover?.setOptions({ + private startPositioner() { + this.stopPositioner(); + 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, - strategy: this.hoist ? 'fixed' : 'absolute', - modifiers: [ - { - name: 'flip', - options: { - boundary: 'viewport' - } - }, - { - name: 'offset', - options: { - offset: [this.skidding, this.distance] - } - } - ] + 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` -
+
@@ -322,7 +330,10 @@ export default class SlTooltip extends LitElement { role="tooltip" aria-hidden=${this.open ? 'false' : 'true'} > - ${this.content} +
+
+ ${this.content} +
`; diff --git a/src/declaration.d.ts b/src/declaration.d.ts index 08227969..d16b948d 100644 --- a/src/declaration.d.ts +++ b/src/declaration.d.ts @@ -1,7 +1,3 @@ -declare module '@popperjs/core/dist/esm' { - export * from '@popperjs/core/lib'; -} - declare module '*.css' { const styles: string; export default styles; diff --git a/src/themes/dark.css b/src/themes/dark.css index cc3a07b6..a45f4b08 100644 --- a/src/themes/dark.css +++ b/src/themes/dark.css @@ -505,8 +505,7 @@ --sl-tooltip-font-size: var(--sl-font-size-small); --sl-tooltip-line-height: var(--sl-line-height-dense); --sl-tooltip-padding: var(--sl-spacing-2x-small) var(--sl-spacing-x-small); - --sl-tooltip-arrow-size: 5px; - --sl-tooltip-arrow-start-end-offset: 8px; + --sl-tooltip-arrow-size: 4px; /* * Z-indexes diff --git a/src/themes/light.css b/src/themes/light.css index acbfc3eb..874670ff 100644 --- a/src/themes/light.css +++ b/src/themes/light.css @@ -505,8 +505,7 @@ --sl-tooltip-font-size: var(--sl-font-size-small); --sl-tooltip-line-height: var(--sl-line-height-dense); --sl-tooltip-padding: var(--sl-spacing-2x-small) var(--sl-spacing-x-small); - --sl-tooltip-arrow-size: 5px; - --sl-tooltip-arrow-start-end-offset: 8px; + --sl-tooltip-arrow-size: 4px; /* * Z-indexes