Skip to content
Merged
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
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,34 @@ yarn add react-tutorial-overlay
```

```jsx
import { TutorialOverlay, tutorial } from "../src";
import { TutorialOverlay, tutorial } from 'react-tutorial-overlay';

const App = () => {
const handleClick = () => {
tutorial.open([
{
targetIds: ["target1"],
title: "title",
content: "content",
},
]);
tutorial.open({
steps: [
{
targetIds: ['target1'],
title: 'title',
content: 'content',
},
],
options: {},
});
};

return (
<div>
<button onClick={handleClick}>open</button>
<div id="target1">target</div>
<TutorialOverlay />
</div>
);
};
```

`content` is rendered as a plain string. HTML markup in the string is not interpreted.

## Documentation

- [Document](https://react-tutorial-overlay.vercel.app/docs)
Expand Down
14 changes: 6 additions & 8 deletions packages/main/src/components/content.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { ActionType, dispatch, useTutorialStore } from '../core/store';
import { useTutorialStore } from '../core/store';
import { tutorial } from '../core/tutorial';
import { styled } from 'goober';

export const Content = React.forwardRef((_, ref?: React.ForwardedRef<HTMLInputElement>) => {
Expand All @@ -8,20 +9,17 @@ export const Content = React.forwardRef((_, ref?: React.ForwardedRef<HTMLInputEl
tutorial: { steps },
} = useTutorialStore();
const currentStep = steps[index];
const { onPrevStep, onNextStep } = currentStep;

const handlePrev = () => {
onPrevStep?.();
dispatch({ type: ActionType.PREV });
tutorial.prev();
};

const handleNext = () => {
onNextStep?.();
dispatch({ type: ActionType.NEXT });
tutorial.next();
};

const handleClose = () => {
dispatch({ type: ActionType.CLOSE });
tutorial.close();
};

return (
Expand All @@ -31,7 +29,7 @@ export const Content = React.forwardRef((_, ref?: React.ForwardedRef<HTMLInputEl
<p>{currentStep.title}</p>
<button onClick={handleClose}>건너뛰기</button>
</InfoTitle>
<InfoContent dangerouslySetInnerHTML={{ __html: currentStep.content ?? '' }} />
<InfoContent>{currentStep.content ?? ''}</InfoContent>
</Heander>
<Footer className="flex items-center justify-between">
<InfoSteps className="text-[.75rem] font-medium text-sub-2.5">
Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ const reducer = (state: State, action: Action): State => {
};
}
case ActionType.PREV:
state.tutorial.steps[state.index]?.onPrevStep?.();
if (state.index > 0) {
state.tutorial.steps[state.index]?.onPrevStep?.();
return {
...state,
index: state.index - 1,
Expand Down
63 changes: 63 additions & 0 deletions packages/main/test/content.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import { TutorialOverlay } from '../src/components/tutorial-overlay';
import { tutorial } from '../src/core/tutorial';

function renderOverlay() {
render(
<div>
<div id="first-target">First target</div>
<div id="second-target">Second target</div>
<TutorialOverlay />
</div>
);
}

describe('Content', () => {
test('button navigation invokes each step callback once', () => {
const onNextStep = jest.fn();
const onPrevStep = jest.fn();

renderOverlay();

act(() => {
tutorial.open({
steps: [
{ title: 'Step 1', content: 'Step 1 content', targetIds: ['first-target'], onNextStep },
{ title: 'Step 2', content: 'Step 2 content', targetIds: ['second-target'], onPrevStep },
],
options: {},
});
});

fireEvent.click(screen.getByRole('button', { name: '다음' }));

expect(onNextStep).toHaveBeenCalledTimes(1);
expect(screen.getByText('Step 2 content')).toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: '이전' }));

expect(onPrevStep).toHaveBeenCalledTimes(1);
expect(screen.getByText('Step 1 content')).toBeInTheDocument();
});

test('renders content strings as text instead of interpreting html', () => {
renderOverlay();

act(() => {
tutorial.open({
steps: [
{
title: 'HTML content',
content: '<strong>Literal HTML</strong>',
targetIds: ['first-target'],
},
],
options: {},
});
});

expect(screen.getByText('<strong>Literal HTML</strong>')).toBeInTheDocument();
expect(document.querySelector('strong')).toBeNull();
});
});
66 changes: 66 additions & 0 deletions packages/main/test/store.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import { act, render, screen } from '@testing-library/react';
import { useTutorialStore } from '../src/core/store';
import { tutorial } from '../src/core/tutorial';

function StateProbe() {
const {
index,
tutorial: { steps },
} = useTutorialStore();

return <span data-testid="title">{steps[index]?.title ?? 'none'}</span>;
}

describe('store step callbacks', () => {
test('tutorial.next and tutorial.prev invoke step callbacks once per transition', () => {
const onNextStep = jest.fn();
const onPrevStep = jest.fn();

render(<StateProbe />);

act(() => {
tutorial.open({
steps: [
{ title: 'Step 1', targetIds: ['first-target'], onNextStep },
{ title: 'Step 2', targetIds: ['second-target'], onPrevStep },
],
options: {},
});
});

act(() => {
tutorial.next();
});

expect(onNextStep).toHaveBeenCalledTimes(1);
expect(screen.getByTestId('title')).toHaveTextContent('Step 2');

act(() => {
tutorial.prev();
});

expect(onPrevStep).toHaveBeenCalledTimes(1);
expect(screen.getByTestId('title')).toHaveTextContent('Step 1');
});

test('tutorial.prev does not invoke onPrevStep when already on the first step', () => {
const onPrevStep = jest.fn();

render(<StateProbe />);

act(() => {
tutorial.open({
steps: [{ title: 'Step 1', targetIds: ['first-target'], onPrevStep }],
options: {},
});
});

act(() => {
tutorial.prev();
});

expect(onPrevStep).not.toHaveBeenCalled();
expect(screen.getByTestId('title')).toHaveTextContent('Step 1');
});
});
Loading