Skip to content

Commit

Permalink
feat: add close hint reason
Browse files Browse the repository at this point in the history
  • Loading branch information
vanilla-wave committed Feb 27, 2025
1 parent 43c0a0d commit 2f98d34
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 66 deletions.
24 changes: 14 additions & 10 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
PresetStatus,
ResolvedOptions,
UserPreset,
HintCloseSource,
} from './types';
import {HintStore} from './hints/hintStore';
import {createLogger, Logger} from './logger';
Expand Down Expand Up @@ -203,7 +204,7 @@ export class Controller<HintParams, Presets extends string, Steps extends string

if (step?.passMode !== 'onShowHint') {
this.logger.debug('Close hint on step', stepSlug);
this.closeHintByUser();
this.closeHintByUser(undefined, 'stepPassed');
this.checkReachedHints();
}
});
Expand Down Expand Up @@ -317,11 +318,11 @@ export class Controller<HintParams, Presets extends string, Steps extends string
const step = this.getStepBySlug(stepSlug);

if (step?.closeOnElementUnmount !== false) {
this.closeHint(stepSlug);
this.closeHint(stepSlug, 'elementHidden');
}
};

closeHintByUser = (stepSlug?: Steps) => {
closeHintByUser = (stepSlug?: Steps, eventSource: HintCloseSource = 'closedByUser') => {
const currentHintStep = this.hintStore.state.hint?.step.slug;
this.logger.debug('Close hint(internal)', currentHintStep);
if (stepSlug && stepSlug !== currentHintStep) {
Expand All @@ -332,15 +333,18 @@ export class Controller<HintParams, Presets extends string, Steps extends string
if (currentHintStep) {
this.closedHints.add(currentHintStep);
const step = this.getStepBySlug(currentHintStep);
step?.hooks?.onCloseHintByUser?.();
step?.hooks?.onCloseHint?.();
step?.hooks?.onCloseHintByUser?.({eventSource});
step?.hooks?.onCloseHint?.({eventSource});
}

if (this.hintStore.state.hint) {
this.events.emit('closeHintByUser', {hint: this.hintStore.state.hint});
this.events.emit('closeHintByUser', {
hint: this.hintStore.state.hint,
eventSource,
});
}

this.hintStore.closeHint();
this.hintStore.closeHint(eventSource);
this.checkReachedHints();
};

Expand Down Expand Up @@ -613,7 +617,7 @@ export class Controller<HintParams, Presets extends string, Steps extends string
await this.updateProgress();
}

closeHint = (stepSlug?: Steps) => {
closeHint = (stepSlug?: Steps, eventSource: HintCloseSource = 'externalEvent') => {
const currentHintStep = this.hintStore.state.hint?.step.slug;
this.logger.debug('Close hint(internal)', currentHintStep);
if (stepSlug && stepSlug !== currentHintStep) {
Expand All @@ -623,10 +627,10 @@ export class Controller<HintParams, Presets extends string, Steps extends string

if (currentHintStep) {
const step = this.getStepBySlug(currentHintStep);
step?.hooks?.onCloseHint?.();
step?.hooks?.onCloseHint?.({eventSource});
}

this.hintStore.closeHint();
this.hintStore.closeHint(eventSource);
};

emitStateChange = () => {
Expand Down
7 changes: 3 additions & 4 deletions src/hints/hintStore.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {MutableRefObject} from 'react';
import type {ShowHintParams} from '../types';
import type {HintCloseSource, ShowHintParams, EventListener, EventsMap, EventTypes} from '../types';
import {EventEmitter} from '../event-emitter';
import {EventListener, EventsMap, EventTypes} from '../types';

export type HintState<HintParams, Preset extends string, Steps extends string> = {
anchorRef: MutableRefObject<HTMLElement | null>;
Expand Down Expand Up @@ -45,9 +44,9 @@ export class HintStore<HintParams, Preset extends string, Steps extends string>
this.emitter.emit('hintDataChanged', {state: this.state});
};

closeHint = () => {
closeHint = (eventSource: HintCloseSource = 'externalEvent') => {
if (this.state.hint) {
this.emitter.emit('closeHint', {hint: this.state.hint});
this.emitter.emit('closeHint', {hint: this.state.hint, eventSource});
}

this.state = {
Expand Down
35 changes: 34 additions & 1 deletion src/tests/event-hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ describe('event subscriptions', function () {
description: '',
},
},
eventSource: 'stepPassed',
},
controller,
);
Expand Down Expand Up @@ -355,6 +356,7 @@ describe('event subscriptions', function () {
description: '',
},
},
eventSource: 'elementHidden',
},
controller,
);
Expand All @@ -370,7 +372,35 @@ describe('event subscriptions', function () {
stepSlug: 'createSprint',
element: getAnchorElement(),
});
controller.closeHint('createSprint');
controller.closeHintByUser('createSprint');

expect(mock).toHaveBeenCalledWith(
{
hint: {
preset: 'createProject',
step: {
slug: 'createSprint',
name: '',
description: '',
},
},
eventSource: 'closedByUser',
},
controller,
);
});

it('call closeHint -> trigger with eventSource=externalEvent', async function () {
const controller = new Controller(getOptions());

const mock = jest.fn();
controller.events.subscribe('closeHint', mock);

await controller.stepElementReached({
stepSlug: 'createSprint',
element: getAnchorElement(),
});
controller.closeHint();

expect(mock).toHaveBeenCalledWith(
{
Expand All @@ -382,6 +412,7 @@ describe('event subscriptions', function () {
description: '',
},
},
eventSource: 'externalEvent',
},
controller,
);
Expand Down Expand Up @@ -437,6 +468,7 @@ describe('event subscriptions', function () {
description: '',
},
},
eventSource: 'stepPassed',
},
controller,
);
Expand Down Expand Up @@ -479,6 +511,7 @@ describe('event subscriptions', function () {
description: '',
},
},
eventSource: 'closedByUser',
},
controller,
);
Expand Down
117 changes: 70 additions & 47 deletions src/tests/steps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,67 +221,90 @@ describe('pass step', function () {
});
});

it('user close hint -> call onCloseHintByUser and onCloseHint', async function () {
const options = getOptions();
const onCloseHintByUserMock = jest.fn();
const onCloseHintMock = jest.fn();
describe('closeHint hooks', () => {
it('user close hint -> call onCloseHintByUser and onCloseHint', async function () {
const options = getOptions();
const onCloseHintByUserMock = jest.fn();
const onCloseHintMock = jest.fn();

options.config.presets.createProject.steps[1].hooks = {
onCloseHintByUser: onCloseHintByUserMock,
onCloseHint: onCloseHintMock,
};
options.config.presets.createProject.steps[1].hooks = {
onCloseHintByUser: onCloseHintByUserMock,
onCloseHint: onCloseHintMock,
};

const controller = new Controller(options);
await controller.stepElementReached({
stepSlug: 'createSprint',
element: getAnchorElement(),
const controller = new Controller(options);
await controller.stepElementReached({
stepSlug: 'createSprint',
element: getAnchorElement(),
});
await controller.closeHintByUser();

expect(onCloseHintByUserMock).toHaveBeenCalledWith({eventSource: 'closedByUser'});
expect(onCloseHintMock).toHaveBeenCalledWith({eventSource: 'closedByUser'});
});
await controller.closeHintByUser();

expect(onCloseHintByUserMock).toHaveBeenCalled();
expect(onCloseHintMock).toHaveBeenCalled();
});
it('element disappear -> call only onCloseHint', async function () {
const options = getOptions();
const onCloseHintByUserMock = jest.fn();
const onCloseHintMock = jest.fn();

it('element disappear -> call only onCloseHint', async function () {
const options = getOptions();
const onCloseHintByUserMock = jest.fn();
const onCloseHintMock = jest.fn();
options.config.presets.createProject.steps[1].hooks = {
onCloseHintByUser: onCloseHintByUserMock,
onCloseHint: onCloseHintMock,
};

options.config.presets.createProject.steps[1].hooks = {
onCloseHintByUser: onCloseHintByUserMock,
onCloseHint: onCloseHintMock,
};
const controller = new Controller(options);
await controller.stepElementReached({
stepSlug: 'createSprint',
element: getAnchorElement(),
});
await controller.stepElementDisappeared('createSprint');

const controller = new Controller(options);
await controller.stepElementReached({
stepSlug: 'createSprint',
element: getAnchorElement(),
expect(onCloseHintByUserMock).not.toHaveBeenCalled();
expect(onCloseHintMock).toHaveBeenCalledWith({eventSource: 'elementHidden'});
});
await controller.stepElementDisappeared('createSprint');

expect(onCloseHintByUserMock).not.toHaveBeenCalled();
expect(onCloseHintMock).toHaveBeenCalled();
});
it('pass step -> call onCloseHint + onCloseByUser', async function () {
const options = getOptions();
const onCloseHintByUserMock = jest.fn();
const onCloseHintMock = jest.fn();

it('pass step -> call onCloseHint + onCloseByUser', async function () {
const options = getOptions();
const onCloseHintByUserMock = jest.fn();
const onCloseHintMock = jest.fn();
options.config.presets.createProject.steps[1].hooks = {
onCloseHintByUser: onCloseHintByUserMock,
onCloseHint: onCloseHintMock,
};

options.config.presets.createProject.steps[1].hooks = {
onCloseHintByUser: onCloseHintByUserMock,
onCloseHint: onCloseHintMock,
};
const controller = new Controller(options);
await controller.stepElementReached({
stepSlug: 'createSprint',
element: getAnchorElement(),
});
await controller.passStep('createSprint');

const controller = new Controller(options);
await controller.stepElementReached({
stepSlug: 'createSprint',
element: getAnchorElement(),
expect(onCloseHintByUserMock).toHaveBeenCalledWith({eventSource: 'stepPassed'});
expect(onCloseHintMock).toHaveBeenCalledWith({eventSource: 'stepPassed'});
});
await controller.passStep('createSprint');

expect(onCloseHintByUserMock).toHaveBeenCalled();
expect(onCloseHintMock).toHaveBeenCalled();
it('call closeHint -> call onCloseHint with eventSource=externalEvent', async function () {
const options = getOptions();
const onCloseHintByUserMock = jest.fn();
const onCloseHintMock = jest.fn();

options.config.presets.createProject.steps[1].hooks = {
onCloseHintByUser: onCloseHintByUserMock,
onCloseHint: onCloseHintMock,
};

const controller = new Controller(options);
await controller.stepElementReached({
stepSlug: 'createSprint',
element: getAnchorElement(),
});
await controller.closeHint();

expect(onCloseHintByUserMock).not.toHaveBeenCalled();
expect(onCloseHintMock).toHaveBeenCalledWith({eventSource: 'externalEvent'});
});
});
});
});
Expand Down
16 changes: 12 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type HintPlacement =
| 'left-start'
| 'left-end';

export type HintCloseSource = 'stepPassed' | 'closedByUser' | 'elementHidden' | 'externalEvent';

export type PresetStatus = 'unPassed' | 'inProgress' | 'finished';

export type PresetStep<Steps extends string, HintParams> = {
Expand All @@ -35,8 +37,8 @@ export type PresetStep<Steps extends string, HintParams> = {
passRestriction?: 'afterPrevious';
hooks?: {
onStepPass?: () => void;
onCloseHint?: () => void;
onCloseHintByUser?: () => void;
onCloseHint?: (params: {eventSource: HintCloseSource}) => void;
onCloseHintByUser?: (params: {eventSource: HintCloseSource}) => void;
};
};

Expand Down Expand Up @@ -182,8 +184,14 @@ export type EventsMap<
beforeShowHint: {stepData: ReachElementParams<Presets, Steps>};
stateChange: {state: Controller<any, any, any>['state']};
hintDataChanged: {state: HintState<HintParams, Presets, Steps>};
closeHint: {hint: Pick<ShowHintParams<HintParams, Presets, Steps>, 'preset' | 'step'>};
closeHintByUser: {hint: Pick<ShowHintParams<HintParams, Presets, Steps>, 'preset' | 'step'>};
closeHint: {
hint: Pick<ShowHintParams<HintParams, Presets, Steps>, 'preset' | 'step'>;
eventSource: HintCloseSource;
};
closeHintByUser: {
hint: Pick<ShowHintParams<HintParams, Presets, Steps>, 'preset' | 'step'>;
eventSource: HintCloseSource;
};
init: {};
wizardStateChanged: {wizardState: BaseState['wizardState']};
};
Expand Down

0 comments on commit 2f98d34

Please sign in to comment.