Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace popperJS with Floating UI #2037

Merged
merged 35 commits into from
Oct 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d9c35eb
first try, tooltip show up, position issues when out of screen
theodoreb Aug 25, 2022
c1afcb4
make focus more reliable
theodoreb Aug 26, 2022
97ad33c
make arrow position work
theodoreb Aug 26, 2022
69da7be
fix shift config
theodoreb Aug 26, 2022
ac41f4c
make smaller functions
theodoreb Aug 26, 2022
b28b572
make smaller functions
theodoreb Aug 26, 2022
4a6a10a
Help pass some tests
theodoreb Aug 26, 2022
5dd8647
always return something
theodoreb Aug 26, 2022
6661538
remove extra console.log
theodoreb Aug 26, 2022
c9e57e6
working on tests
theodoreb Aug 29, 2022
2eb9aa0
Fix offset middleware usage in test
theodoreb Aug 30, 2022
2c3a596
update general.spec.js
theodoreb Aug 30, 2022
ed37e99
work on tour.spec.js
theodoreb Aug 30, 2022
6aff45d
fix destroying-elements.cy.js test
theodoreb Aug 31, 2022
096a563
fix step.spec.js
theodoreb Aug 31, 2022
517ed05
effectively remove popperjs dependency
theodoreb Aug 31, 2022
b12f6be
fix step.spec.js
theodoreb Aug 31, 2022
3720c97
woops
theodoreb Aug 31, 2022
6c91b70
fix promise chain parameters
theodoreb Aug 31, 2022
2be574b
fix complexity?
theodoreb Aug 31, 2022
5a62d9a
docs
theodoreb Aug 31, 2022
73c083b
complexity
theodoreb Aug 31, 2022
a79eab8
woops
theodoreb Aug 31, 2022
3817a95
Separate floating ui code from the rest of the implementation
theodoreb Aug 31, 2022
8fffe8d
lint
theodoreb Aug 31, 2022
53ef7b0
fix tests imports
theodoreb Aug 31, 2022
52ba0f0
fix config merge
theodoreb Sep 1, 2022
b9938c4
fix more tests
theodoreb Sep 1, 2022
9076e76
always return a promise from show for consistency
theodoreb Sep 1, 2022
ad261cd
Use cy.get
theodoreb Sep 15, 2022
3886914
removed Cypress.$ use
theodoreb Sep 15, 2022
00152aa
unskip test so it'll fail
theodoreb Sep 16, 2022
b5a0f54
Fix test to test middleware stacking instead of direct modifier overw…
theodoreb Oct 19, 2022
9812e08
Merge branch 'master' into floating-ui
theodoreb Oct 19, 2022
4524537
remove unnecessary library
theodoreb Oct 19, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"watch": "yarn clean && rollup -c --environment DEVELOPMENT --watch"
},
"dependencies": {
"@popperjs/core": "^2.11.6",
"@floating-ui/dom": "^1.0.1",
"deepmerge": "^4.2.2"
},
"devDependencies": {
Expand Down Expand Up @@ -88,6 +88,7 @@
"release-it": "^15.5.0",
"release-it-lerna-changelog": "^5.0.0",
"replace": "^1.2.1",
"resize-observer-polyfill": "^1.5.1",
RobbieTheWagner marked this conversation as resolved.
Show resolved Hide resolved
"rimraf": "^3.0.2",
"rollup": "^2.79.1",
"rollup-plugin-analyzer": "^4.0.0",
Expand Down
32 changes: 16 additions & 16 deletions src/js/step.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import {
isUndefined
} from './utils/type-check.js';
import { bindAdvance } from './utils/bind.js';
import { parseAttachTo, normalizePrefix, uuid } from './utils/general.js';
import {
setupTooltip,
parseAttachTo,
normalizePrefix,
uuid
} from './utils/general.js';
destroyTooltip,
mergeTooltipConfig
} from './utils/floating-ui.js';
import ShepherdElement from './components/shepherd-element.svelte';

/**
Expand Down Expand Up @@ -155,13 +155,10 @@ export class Step extends Evented {
* Triggers `destroy` event
*/
destroy() {
if (this.tooltip) {
this.tooltip.destroy();
this.tooltip = null;
}
destroyTooltip(this);
theodoreb marked this conversation as resolved.
Show resolved Hide resolved

if (isHTMLElement(this.el) && this.el.parentNode) {
this.el.parentNode.removeChild(this.el);
if (isHTMLElement(this.el)) {
this.el.remove();
theodoreb marked this conversation as resolved.
Show resolved Hide resolved
this.el = null;
}

Expand Down Expand Up @@ -232,12 +229,11 @@ export class Step extends Evented {
*/
show() {
if (isFunction(this.options.beforeShowPromise)) {
const beforeShowPromise = this.options.beforeShowPromise();
if (!isUndefined(beforeShowPromise)) {
return beforeShowPromise.then(() => this._show());
}
return Promise.resolve(this.options.beforeShowPromise()).then(() =>
theodoreb marked this conversation as resolved.
Show resolved Hide resolved
this._show()
);
}
this._show();
return Promise.resolve(this._show());
theodoreb marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -353,7 +349,8 @@ export class Step extends Evented {
arrow: true
},
tourOptions,
options
options,
mergeTooltipConfig(tourOptions, options)
theodoreb marked this conversation as resolved.
Show resolved Hide resolved
);

const { when } = this.options;
Expand Down Expand Up @@ -384,6 +381,9 @@ export class Step extends Evented {
if (this.options.advanceOn) {
bindAdvance(this);
}

// The tooltip implementation details are handled outside of the Step
// object.
setupTooltip(this);
}

Expand Down
203 changes: 203 additions & 0 deletions src/js/utils/floating-ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import merge from 'deepmerge';
import { shouldCenterStep } from './general';
import {
computePosition,
autoUpdate,
shift,
arrow,
limitShift
} from '@floating-ui/dom';

/**
* Floating UI Options
*
* @typedef {object} FloatingUIOptions
*/

/**
* Determines options for the tooltip and initializes event listeners.
*
* @param {Step} step The step instance
*
* @return {FloatingUIOptions}
*/
export function setupTooltip(step) {
if (step.cleanup) {
step.cleanup();
}

const attachToOptions = step._getResolvedAttachToOptions();

let target = attachToOptions.element;
const floatingUIOptions = getFloatingUIOptions(attachToOptions, step);

if (shouldCenterStep(attachToOptions)) {
theodoreb marked this conversation as resolved.
Show resolved Hide resolved
target = document.body;
const content = step.shepherdElementComponent.getElement();
content.classList.add('shepherd-centered');
}

step.cleanup = autoUpdate(target, step.el, () => {
// The element might have already been removed by the end of the tour.
if (!step.el) {
step.cleanup();
return;
}

setPosition(target, step, floatingUIOptions);
});

step.target = attachToOptions.element;

return floatingUIOptions;
}

/**
* Merge tooltip options handling nested keys.
*
* @param tourOptions - The default tour options.
* @param options - Step specific options.
*
* @return {floatingUIOptions: FloatingUIOptions}
*/
export function mergeTooltipConfig(tourOptions, options) {
return {
floatingUIOptions: merge(
tourOptions.floatingUIOptions || {},
options.floatingUIOptions || {}
)
};
}

/**
* Cleanup function called when the step is closed/destroyed.
*
* @param {Step} step
*/
export function destroyTooltip(step) {
if (step.cleanup) {
step.cleanup();
}

step.cleanup = null;
}

/**
*
* @return {Promise<*>}
*/
function setPosition(target, step, floatingUIOptions) {
return (
computePosition(target, step.el, floatingUIOptions)
.then(floatingUIposition(step))
// Wait before forcing focus.
.then(
(step) =>
new Promise((resolve) => {
setTimeout(() => resolve(step), 300);
})
)
// Replaces focusAfterRender modifier.
.then((step) => {
if (step && step.el) {
step.el.focus({ preventScroll: true });
}
})
);
}

/**
*
* @param step
* @return {function({x: *, y: *, placement: *, middlewareData: *}): Promise<unknown>}
*/
function floatingUIposition(step) {
return ({ x, y, placement, middlewareData }) => {
if (!step.el) {
return step;
}

Object.assign(step.el.style, {
position: 'absolute',
left: `${x}px`,
top: `${y}px`
});

step.el.dataset.popperPlacement = placement;

placeArrow(step.el, placement, middlewareData);

return step;
};
}

/**
*
* @param el
* @param placement
* @param middlewareData
*/
function placeArrow(el, placement, middlewareData) {
const arrowEl = el.querySelector('.shepherd-arrow');
if (arrowEl) {
const { x: arrowX, y: arrowY } = middlewareData.arrow;

const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right'
}[placement.split('-')[0]];

Object.assign(arrowEl.style, {
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: '-35px'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably should be a config/CSS rule somewhere.

});
}
}

/**
* Gets the `Floating UI` options from a set of base `attachTo` options
* @param attachToOptions
* @param {Step} step The step instance
* @return {Object}
* @private
*/
export function getFloatingUIOptions(attachToOptions, step) {
const options = {
strategy: 'absolute',
middleware: [
// Replicate PopperJS default behavior.
shift({
limiter: limitShift(),
crossAxis: true
})
]
};

const arrowEl = addArrow(step);
if (arrowEl) {
options.middleware.push(arrow({ element: arrowEl }));
}

if (!shouldCenterStep(attachToOptions)) {
options.placement = attachToOptions.on;
}

return merge(step.options.floatingUIOptions || {}, options);
}

/**
* @param {Step} step
* @return {HTMLElement|false|null}
*/
function addArrow(step) {
if (step.options.arrow && step.el) {
return step.el.querySelector('.shepherd-arrow');
}

return false;
}
Loading