Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion shepherd.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
"del": "^8.0.1",
"dts-bundle-generator": "^9.5.1",
"eslint": "^10.1.0",
"eslint-plugin-cypress": "^5.3.0",
"eslint-plugin-cypress": "5.3.0",
"eslint-plugin-vitest": "^0.5.4",
"execa": "^9.6.1",
"globals": "^17.4.0",
Expand Down
26 changes: 22 additions & 4 deletions shepherd.js/src/utils/floating-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function setupTooltip(step: Step): ComputePositionConfig {
let target = attachToOptions.element as HTMLElement;
const floatingUIOptions = getFloatingUIOptions(attachToOptions, step);
const shouldCenter = shouldCenterStep(attachToOptions);
let shouldFocusAfterRender = true;

if (shouldCenter) {
target = document.body;
Expand All @@ -45,7 +46,14 @@ export function setupTooltip(step: Step): ComputePositionConfig {
return;
}

setPosition(target, step, floatingUIOptions, shouldCenter);
setPosition(
target,
step,
floatingUIOptions,
shouldCenter,
shouldFocusAfterRender
);
shouldFocusAfterRender = false;
});

step.target = attachToOptions.element as HTMLElement;
Expand Down Expand Up @@ -90,11 +98,21 @@ function setPosition(
target: HTMLElement,
step: Step,
floatingUIOptions: ComputePositionConfig,
shouldCenter: boolean
shouldCenter: boolean,
shouldFocusAfterRender: boolean
) {
const positionPromise = computePosition(
target,
step.el as HTMLElement,
floatingUIOptions
).then(floatingUIposition(step, shouldCenter));

if (!shouldFocusAfterRender) {
return positionPromise;
}

return (
computePosition(target, step.el as HTMLElement, floatingUIOptions)
.then(floatingUIposition(step, shouldCenter))
positionPromise
// Wait before forcing focus.
.then(
(step: Step) =>
Expand Down
103 changes: 103 additions & 0 deletions shepherd.js/test/unit/utils/floating-ui.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const floatingUIMock = vi.hoisted(() => ({
autoUpdate: vi.fn(),
computePosition: vi.fn(),
updateCallbacks: []
}));

vi.mock('@floating-ui/dom', async (importOriginal) => {
const actual = await importOriginal();

return {
...actual,
autoUpdate: floatingUIMock.autoUpdate,
computePosition: floatingUIMock.computePosition
};
});

import { setupTooltip } from '../../../src/utils/floating-ui';

describe('Floating UI Utils', function () {
let input;
let step;
let stepElement;
let target;

beforeEach(() => {
vi.useFakeTimers();

floatingUIMock.updateCallbacks.length = 0;
floatingUIMock.autoUpdate.mockImplementation(
(_target, _stepElement, update) => {
floatingUIMock.updateCallbacks.push(update);
update();

return vi.fn();
}
);
floatingUIMock.computePosition.mockResolvedValue({
middlewareData: {},
placement: 'bottom',
x: 12,
y: 34
});

target = document.createElement('div');
input = document.createElement('input');
target.appendChild(input);
document.body.appendChild(target);

stepElement = document.createElement('div');
document.body.appendChild(stepElement);

step = {
cleanup: null,
el: stepElement,
options: {
arrow: false,
attachTo: { element: target, on: 'bottom' },
floatingUIOptions: {}
},
shepherdElementComponent: {
element: stepElement
},
_getResolvedAttachToOptions() {
return this.options.attachTo;
}
};
});

afterEach(() => {
document.body.innerHTML = '';
vi.clearAllTimers();
vi.useRealTimers();
vi.restoreAllMocks();
});

it('only focuses the step element after the first render', async () => {
const focusSpy = vi.spyOn(stepElement, 'focus');

setupTooltip(step);

await flushPositioning();

expect(focusSpy).toHaveBeenCalledTimes(1);

input.focus();
expect(document.activeElement).toBe(input);

floatingUIMock.updateCallbacks[0]();

await flushPositioning();

expect(focusSpy).toHaveBeenCalledTimes(1);
expect(document.activeElement).toBe(input);
});
});

async function flushPositioning() {
await Promise.resolve();
await vi.advanceTimersByTimeAsync(300);
await Promise.resolve();
}