Skip to content

Commit 6502d17

Browse files
authored
Merge pull request #10 from sjsjsj1246/codex/add-customization-options
[-]: 커스터마이징 옵션 확장
2 parents 53af379 + ab48522 commit 6502d17

9 files changed

Lines changed: 173 additions & 13 deletions

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,18 @@ const App = () => {
5252
options: {
5353
highLightPadding: 12,
5454
infoBoxHeight: 220,
55+
infoBoxWidth: '24rem',
5556
infoBoxMargin: 24,
57+
overlayColor: 'rgba(15, 23, 42, 0.6)',
58+
highlightBorderColor: '#22c55e',
59+
highlightBorderRadius: 16,
60+
zIndex: 3000,
61+
labels: {
62+
prev: 'Back',
63+
next: 'Continue',
64+
skip: 'Dismiss',
65+
done: 'Finish',
66+
},
5667
keyboardNavigation: true,
5768
closeOnOverlayClick: true,
5869
onClose: () => {
@@ -87,6 +98,10 @@ const App = () => {
8798

8899
`highLightPadding` expands the highlight frame around the target element. It defaults to `8` pixels and applies to the rendered highlight box as well as the info box anchor position.
89100

101+
Use `overlayColor`, `highlightBorderColor`, `highlightBorderRadius`, `zIndex`, and `infoBoxWidth` to match the built-in UI to your product without changing the overlay, highlight, or info box structure. When omitted, the existing defaults remain in place.
102+
103+
Use `labels` to override the built-in button text. The default labels are `이전`, `다음`, `건너뛰기`, and `완료`.
104+
90105
Keyboard navigation is enabled by default while the overlay is open:
91106

92107
- `Escape` closes the tutorial.

packages/document/src/pages/docs/tutorial-overlay.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ Because `tutorial.open()` returns a Promise, you can keep `<TutorialOverlay />`
2828

2929
The highlight frame uses `options.highLightPadding` to expand around the target. If you do not provide it, the overlay uses an `8px` padding by default.
3030

31+
Visual customization stays global for now. Use `options.overlayColor`, `options.highlightBorderColor`, `options.highlightBorderRadius`, `options.zIndex`, and `options.infoBoxWidth` when you need the built-in overlay UI to better match your product chrome.
32+
33+
Use `options.labels` to replace the built-in `이전`, `다음`, `건너뛰기`, and `완료` button text without replacing the UI itself.
34+
3135
By default, the mounted overlay listens for `Escape`, `ArrowLeft`, and `ArrowRight` while it is open. You can disable that with `options.keyboardNavigation = false`.
3236

3337
Backdrop clicks are ignored by default. Set `options.closeOnOverlayClick = true` if you want clicking the dimmed overlay area to close the tutorial.

packages/document/src/pages/docs/tutorial.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,18 @@ function App() {
2929
options: {
3030
highLightPadding: 12,
3131
infoBoxHeight: 220,
32+
infoBoxWidth: '24rem',
3233
infoBoxMargin: 24,
34+
overlayColor: 'rgba(15, 23, 42, 0.6)',
35+
highlightBorderColor: '#22c55e',
36+
highlightBorderRadius: 16,
37+
zIndex: 3000,
38+
labels: {
39+
prev: 'Back',
40+
next: 'Continue',
41+
skip: 'Dismiss',
42+
done: 'Finish',
43+
},
3344
keyboardNavigation: true,
3445
closeOnOverlayClick: true,
3546
},
@@ -76,7 +87,13 @@ function App() {
7687

7788
- `highLightPadding`: expands the highlight frame around the target in pixels. Defaults to `8`.
7889
- `infoBoxHeight`: sets the info box height in pixels.
90+
- `infoBoxWidth`: sets the info box width with a CSS length such as `360` or `'24rem'`. Defaults to `'20rem'`.
7991
- `infoBoxMargin`: controls the vertical gap between the target and the info box.
92+
- `overlayColor`: sets the backdrop color. Defaults to `rgba(0, 0, 0, 0.5)`.
93+
- `highlightBorderColor`: sets the highlight border color. Defaults to `#ff0000`.
94+
- `highlightBorderRadius`: sets the highlight border radius with a CSS length. Defaults to the current padding-based radius.
95+
- `zIndex`: sets the overlay stack level. Defaults to `9999`.
96+
- `labels`: overrides the built-in `prev`, `next`, `skip`, and `done` button labels.
8097
- `keyboardNavigation`: enables `Escape`, `ArrowLeft`, and `ArrowRight` shortcuts while the overlay is open. Defaults to `true`.
8198
- `closeOnOverlayClick`: closes the tutorial when the backdrop itself is clicked. Defaults to `false`.
8299
- `onClose`: runs when the tutorial is closed.

packages/main/src/components/content.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ import React, { useId } from 'react';
22
import { useTutorialStore } from '../core/store';
33
import { skipTutorial, tutorial } from '../core/tutorial';
44
import { styled } from 'goober';
5+
import { DEFAULT_INFO_BOX_WIDTH, INFO_BOX_Z_INDEX_OFFSET, getBaseZIndex, getLabels } from '../core/options';
56

67
export const Content = React.forwardRef<HTMLDivElement>((_, ref) => {
78
const {
89
index,
9-
tutorial: { steps },
10+
tutorial: { steps, options },
1011
} = useTutorialStore();
1112
const currentStep = steps[index];
1213
const titleId = useId();
1314
const contentId = useId();
15+
const labels = getLabels(options);
1416

1517
const handlePrev = () => {
1618
tutorial.prev();
@@ -31,11 +33,15 @@ export const Content = React.forwardRef<HTMLDivElement>((_, ref) => {
3133
aria-labelledby={titleId}
3234
aria-describedby={currentStep.content ? contentId : undefined}
3335
tabIndex={-1}
36+
style={{
37+
width: options?.infoBoxWidth ?? DEFAULT_INFO_BOX_WIDTH,
38+
zIndex: getBaseZIndex(options) + INFO_BOX_Z_INDEX_OFFSET,
39+
}}
3440
>
3541
<Heander>
3642
<InfoTitle>
3743
<Title id={titleId}>{currentStep.title}</Title>
38-
<button onClick={handleClose}>건너뛰기</button>
44+
<button onClick={handleClose}>{labels.skip}</button>
3945
</InfoTitle>
4046
<InfoContent id={contentId}>{currentStep.content ?? ''}</InfoContent>
4147
</Heander>
@@ -44,8 +50,8 @@ export const Content = React.forwardRef<HTMLDivElement>((_, ref) => {
4450
<span>{`${index + 1} / ${steps.length}`}</span>
4551
</InfoSteps>
4652
<ButtonWrapper className="flex gap-[.625rem]">
47-
{index !== 0 && <button onClick={handlePrev}>이전</button>}
48-
<button onClick={handleNext}>{index === steps.length - 1 ? '완료' : '다음'}</button>
53+
{index !== 0 && <button onClick={handlePrev}>{labels.prev}</button>}
54+
<button onClick={handleNext}>{index === steps.length - 1 ? labels.done : labels.next}</button>
4955
</ButtonWrapper>
5056
</Footer>
5157
</Wrapper>
@@ -56,7 +62,7 @@ Content.displayName = 'Content';
5662
const Wrapper = styled('div', React.forwardRef)`
5763
position: absolute;
5864
top: 6.25rem;
59-
z-index: 999;
65+
z-index: 10001;
6066
width: 20rem;
6167
min-height: 7.5rem;
6268
display: flex;

packages/main/src/components/tutorial-overlay.tsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@ import { useTutorialStore } from '../core/store';
44
import { setup, styled } from 'goober';
55
import { Content } from './content';
66
import { tutorial } from '../core/tutorial';
7+
import {
8+
DEFAULT_HIGHLIGHT_BORDER_COLOR,
9+
DEFAULT_HIGHLIGHT_PADDING,
10+
DEFAULT_OVERLAY_COLOR,
11+
DEFAULT_Z_INDEX,
12+
HIGHLIGHT_Z_INDEX_OFFSET,
13+
getBaseZIndex,
14+
} from '../core/options';
715

816
setup(React.createElement);
917

10-
const DEFAULT_HIGHLIGHT_PADDING = 8;
1118
const MIN_VIEWPORT_OFFSET = 10;
1219
const FOCUSABLE_SELECTOR = [
1320
'button:not([disabled])',
@@ -34,6 +41,7 @@ export const TutorialOverlay = React.memo(({}: TutorialOverlayProps) => {
3441
const previouslyFocusedElement = useRef<HTMLElement | null>(null);
3542
const wasOpen = useRef(open);
3643
const timeout = useRef<number | undefined>();
44+
const baseZIndex = getBaseZIndex(options);
3745

3846
function shouldIgnoreKeyboardEvent(): boolean {
3947
const activeElement = document.activeElement;
@@ -284,14 +292,26 @@ export const TutorialOverlay = React.memo(({}: TutorialOverlayProps) => {
284292
};
285293

286294
return open ? (
287-
<Wrapper data-testid="tutorial-overlay-backdrop" onClick={handleBackdropClick}>
295+
<Wrapper
296+
data-testid="tutorial-overlay-backdrop"
297+
onClick={handleBackdropClick}
298+
style={{
299+
backgroundColor: options?.overlayColor ?? DEFAULT_OVERLAY_COLOR,
300+
zIndex: baseZIndex,
301+
}}
302+
>
288303
<Content ref={infoBoxElement} />
289304
{rectStyles.map((style) => (
290305
<Hightlight
291306
aria-hidden="true"
292307
data-testid={`tutorial-overlay-highlight-${style.id}`}
293308
key={style.id}
294-
style={style}
309+
style={{
310+
...style,
311+
borderColor: options?.highlightBorderColor ?? DEFAULT_HIGHLIGHT_BORDER_COLOR,
312+
borderRadius: options?.highlightBorderRadius ?? style.borderRadius,
313+
zIndex: baseZIndex + HIGHLIGHT_Z_INDEX_OFFSET,
314+
}}
295315
/>
296316
))}
297317
</Wrapper>
@@ -302,16 +322,16 @@ const Wrapper = styled('div')`
302322
position: fixed;
303323
top: 0;
304324
left: 0;
305-
z-index: 9999;
325+
z-index: ${DEFAULT_Z_INDEX};
306326
height: 100vh;
307327
width: 100vw;
308-
background-color: rgba(0, 0, 0, 0.5);
328+
background-color: ${DEFAULT_OVERLAY_COLOR};
309329
`;
310330

311331
const Hightlight = styled('div')`
312332
position: absolute;
313-
z-index: 9999;
333+
z-index: ${DEFAULT_Z_INDEX + HIGHLIGHT_Z_INDEX_OFFSET};
314334
box-sizing: border-box;
315-
border: 2px solid #ff0000;
335+
border: 2px solid ${DEFAULT_HIGHLIGHT_BORDER_COLOR};
316336
border-radius: 0.625rem;
317337
`;

packages/main/src/core/options.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Labels, Options } from './types';
2+
3+
export const DEFAULT_HIGHLIGHT_PADDING = 8;
4+
export const DEFAULT_OVERLAY_COLOR = 'rgba(0, 0, 0, 0.5)';
5+
export const DEFAULT_HIGHLIGHT_BORDER_COLOR = '#ff0000';
6+
export const DEFAULT_INFO_BOX_WIDTH = '20rem';
7+
export const DEFAULT_Z_INDEX = 9999;
8+
export const HIGHLIGHT_Z_INDEX_OFFSET = 1;
9+
export const INFO_BOX_Z_INDEX_OFFSET = 2;
10+
11+
export const DEFAULT_LABELS: Required<Labels> = {
12+
prev: '이전',
13+
next: '다음',
14+
skip: '건너뛰기',
15+
done: '완료',
16+
};
17+
18+
export function getLabels(options?: Options): Required<Labels> {
19+
return {
20+
...DEFAULT_LABELS,
21+
...options?.labels,
22+
};
23+
}
24+
25+
export function getBaseZIndex(options?: Options): number {
26+
return options?.zIndex ?? DEFAULT_Z_INDEX;
27+
}

packages/main/src/core/types.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export type StyleValue = number | string;
2+
13
export interface Step {
24
targetIds: string[];
35
content?: string;
@@ -7,10 +9,23 @@ export interface Step {
79
onNextStep?: () => void;
810
}
911

12+
export interface Labels {
13+
prev?: string;
14+
next?: string;
15+
skip?: string;
16+
done?: string;
17+
}
18+
1019
export interface Options {
1120
highLightPadding?: number;
1221
infoBoxHeight?: number;
22+
infoBoxWidth?: StyleValue;
1323
infoBoxMargin?: number;
24+
overlayColor?: string;
25+
highlightBorderColor?: string;
26+
highlightBorderRadius?: StyleValue;
27+
zIndex?: number;
28+
labels?: Labels;
1429
keyboardNavigation?: boolean;
1530
closeOnOverlayClick?: boolean;
1631
onClose?: () => void;
@@ -33,5 +48,5 @@ export interface ElementStyle {
3348
top: number;
3449
width: number;
3550
height: number;
36-
borderRadius?: number;
51+
borderRadius?: StyleValue;
3752
}

packages/main/test/content.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,35 @@ function renderOverlay() {
1414
}
1515

1616
describe('Content', () => {
17+
test('reads built-in button labels from options.labels', () => {
18+
renderOverlay();
19+
20+
act(() => {
21+
tutorial.open({
22+
steps: [
23+
{ title: 'Step 1', content: 'Step 1 content', targetIds: ['first-target'] },
24+
{ title: 'Step 2', content: 'Step 2 content', targetIds: ['second-target'] },
25+
],
26+
options: {
27+
labels: {
28+
prev: 'Back',
29+
next: 'Continue',
30+
skip: 'Dismiss',
31+
done: 'Finish',
32+
},
33+
},
34+
});
35+
});
36+
37+
expect(screen.getByRole('button', { name: 'Dismiss' })).toBeInTheDocument();
38+
expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument();
39+
40+
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
41+
42+
expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument();
43+
expect(screen.getByRole('button', { name: 'Finish' })).toBeInTheDocument();
44+
});
45+
1746
test('button navigation invokes each step callback once', () => {
1847
const onNextStep = jest.fn();
1948
const onPrevStep = jest.fn();

packages/main/test/tutorial-overlay.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,4 +345,31 @@ describe('TutorialOverlay', () => {
345345

346346
jest.useRealTimers();
347347
});
348+
349+
test('applies custom overlay, highlight, and info box styles from options', () => {
350+
renderOverlay();
351+
mockTargetRect('first-target', { left: 120, top: 96, width: 140, height: 48 });
352+
353+
openTutorial({
354+
overlayColor: 'rgba(12, 34, 56, 0.7)',
355+
highlightBorderColor: 'rgb(0, 255, 136)',
356+
highlightBorderRadius: 24,
357+
zIndex: 4321,
358+
infoBoxWidth: '28rem',
359+
});
360+
361+
expect(screen.getByTestId('tutorial-overlay-backdrop')).toHaveStyle({
362+
backgroundColor: 'rgba(12, 34, 56, 0.7)',
363+
zIndex: '4321',
364+
});
365+
expect(screen.getByRole('dialog', { name: 'Step 1' })).toHaveStyle({
366+
width: '28rem',
367+
zIndex: '4323',
368+
});
369+
expect(screen.getByTestId('tutorial-overlay-highlight-first-target')).toHaveStyle({
370+
borderColor: 'rgb(0, 255, 136)',
371+
borderRadius: '24px',
372+
zIndex: '4322',
373+
});
374+
});
348375
});

0 commit comments

Comments
 (0)