Skip to content

Commit

Permalink
feat: add mask and base component container API (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
yuzhanglong authored Sep 20, 2021
1 parent 57fbdf8 commit 4c90286
Show file tree
Hide file tree
Showing 13 changed files with 241 additions and 75 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ example

yarn.lock
package.lock.json

storybook-static
6 changes: 3 additions & 3 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { Config } from '@jest/types';
const config: Config.InitialOptions = {
testEnvironment: 'jsdom',
preset: 'ts-jest',
testRegex: ['(/__tests__/.*\\.(test|spec))\\.[jt]sx?$'],
testRegex: ['(./src/__tests__/.*\\.(test|spec))\\.[jt]sx?$'],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}'
'./src/**/*.{js,jsx,ts,tsx}'
],
coveragePathIgnorePatterns: [
'.stories.tsx',
'./src/stories/*',
'./src/utils/mask-checker/animation-frame.ts'
],
setupFiles: [
Expand Down
8 changes: 4 additions & 4 deletions src/__tests__/__snapshots__/onboarding.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ exports[`引导组件相关测试 测试步骤的切换能力 1`] = `
>
<div>
<div
class="ant-popover"
style="opacity: 0; pointer-events: none;"
class="ant-popover ant-popover-placement-top "
style="pointer-events: none; left: 5px; top: -4px; transform-origin: 50% 4px;"
>
<div
class="ant-popover-content"
Expand Down Expand Up @@ -91,8 +91,8 @@ exports[`引导组件相关测试 测试步骤的切换能力 2`] = `
>
<div>
<div
class="ant-popover"
style="opacity: 0; pointer-events: none;"
class="ant-popover ant-popover-placement-top "
style="pointer-events: none; left: 5px; top: -4px; transform-origin: 50% 4px;"
>
<div
class="ant-popover-content"
Expand Down
12 changes: 2 additions & 10 deletions src/__tests__/mask.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,14 @@ import { createTestContainer } from '../utils/create-test-container';
import { MASK_ANIMATION_TIME } from '../';
import { act } from '@testing-library/react';

jest.useFakeTimers();

describe('引导组件遮罩层相关测试', () => {
const testBaseElement = document.createElement('div');
testBaseElement.style.height = '200px';
testBaseElement.style.width = '200px';
document.body.appendChild(testBaseElement);


beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
// 退出时进行清理
jest.useRealTimers();
});

test('mask 配置的监听器应该按预期销毁', () => {
const destroyCallback = jest.fn();
const container = createTestContainer(
Expand Down
16 changes: 4 additions & 12 deletions src/__tests__/onboarding.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
import { createTestContainer } from '../utils/create-test-container';
import React from 'react';
import { OnBoarding, OnBoardingRef } from '../';
import { act, fireEvent, screen } from '@testing-library/react';
import { act } from '@testing-library/react';
import { OnBoardingStepConfig } from '../';
import userEvent from '@testing-library/user-event';
import zhCN from '../locale/zh-CN';

describe('引导组件相关测试', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
// 退出时进行清理
jest.useRealTimers();
});

jest.useFakeTimers();

describe('引导组件相关测试', () => {
test('测试没有配置任何步骤的场景, 组件返回一个 null', () => {
const container = createTestContainer(
<OnBoarding steps={[]} />
Expand Down Expand Up @@ -66,7 +58,6 @@ describe('引导组件相关测试', () => {
});

expect(document.documentElement).toMatchSnapshot();
container.unmount();
});

test('测试 ref API', async () => {
Expand Down Expand Up @@ -113,6 +104,7 @@ describe('引导组件相关测试', () => {
await ref.current.forward();
jest.advanceTimersByTime(10000);
});

expect(document.querySelector('.step2')).toBeTruthy();

await act(async () => {
Expand Down
31 changes: 31 additions & 0 deletions src/hooks/use-dom-observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect, useState } from 'react';
import { isFunction } from 'lodash';

export const useDomObserver = (
getter: () => HTMLElement,
onGet?: (element: HTMLElement) => void,
deps?: any[]
) => {
const [element, setElement] = useState<HTMLElement>(null);

// 用于渲染
const [renderTick, setRenderTick] = useState<number>(0);

useEffect(() => {
if (!isFunction(getter)) {
return;
}

const el = getter();
if (!el) {
setRenderTick(renderTick + 1);
} else {
setElement(el);
onGet(element);
}
}, [renderTick, ...deps]);

return {
element
};
};
16 changes: 11 additions & 5 deletions src/mask/mask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export const Mask: React.FC<MaskProps> = (props) => {
visible = true,
styleChecker,
onAnimationEnd = noop,
onAnimationStart = noop
onAnimationStart = noop,
container
} = props;

const [style, setStyle] = useState<Record<string, any>>({});
Expand All @@ -48,7 +49,7 @@ export const Mask: React.FC<MaskProps> = (props) => {
block: 'nearest'
});

const style = getMaskStyle(element, document.documentElement);
const style = getMaskStyle(element, container || document.documentElement);
setStyle(style);
};

Expand All @@ -58,7 +59,7 @@ export const Mask: React.FC<MaskProps> = (props) => {
}

checkStyle();
}, [element]);
}, [element, container]);

useEffect(() => {
if (!element) {
Expand All @@ -71,13 +72,18 @@ export const Mask: React.FC<MaskProps> = (props) => {
return () => {
o.destroy();
};
}, [element]);
}, [element, container]);

useEffect(() => {
onAnimationStart();
setTimeout(() => {
let t = null;
t = setTimeout(() => {
onAnimationEnd();
}, MASK_ANIMATION_TIME);

return () => {
window.clearTimeout(t);
};
}, [element]);

const maskClasses = classNames(
Expand Down
46 changes: 17 additions & 29 deletions src/onboarding/onboarding.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { forwardRef, Fragment, useCallback, useEffect, useImperativeHandle, useState } from 'react';
import React, { forwardRef, Fragment, useEffect, useImperativeHandle, useState } from 'react';
import { Mask } from '../mask/mask';
import ReactDOM from 'react-dom';
import { Button, Popover } from 'antd';
Expand All @@ -7,6 +7,7 @@ import Content, { PopoverContentProps } from './content';
import { MaskStyleChecker, OnBoardingLocale, OnBoardingRef, OnBoardingStatus, OnBoardingStepConfig } from '../types';
import { isNil, noop } from 'lodash';
import enUS from '../locale/en-US';
import { useDomObserver } from '../hooks/use-dom-observer';

export interface OnBoardingProps {
// 初始化步骤
Expand Down Expand Up @@ -37,7 +38,7 @@ export interface OnBoardingProps {
supportKeyboard?: boolean;

// 挂载节点,默认为 document.body
mountedElement?: HTMLElement;
getContainer?: () => HTMLElement;
}

export const OnBoarding = forwardRef<OnBoardingRef, OnBoardingProps>((props, ref) => {
Expand All @@ -51,7 +52,7 @@ export const OnBoarding = forwardRef<OnBoardingRef, OnBoardingProps>((props, ref
styleChecker,
locale = enUS,
supportKeyboard = true,
mountedElement = document.body
getContainer
} = props;

// 当前状态
Expand All @@ -60,30 +61,20 @@ export const OnBoarding = forwardRef<OnBoardingRef, OnBoardingProps>((props, ref
// 当前步骤(step index)
const [currentStep, setCurrentStep] = useState<number>(initialStep || 0);

// 用于渲染
const [renderTick, setRenderTick] = useState<number>(0);

// mask 是否在移动
const [isMaskMoving, setIsMaskMoving] = useState<boolean>(false);

// 选择配置的元素
const getCurrentTargetElement = useCallback(() => {
return steps[currentStep]?.selector();
}, [steps, currentStep]);

const initCurrentSelectedElement = () => {
const currentElement = getCurrentTargetElement();
// 当前目标 DOM 的监听
const currentSelectedElement = useDomObserver(
steps.length ? () => {
return steps[currentStep]?.selector();
} : null,
() => {
setCurrentStatus(OnBoardingStatus.READY);
}, [steps, currentStep]);

if (!steps.length) {
return;
}
const currentContainerElement = useDomObserver(getContainer, noop, [getContainer]);

if (!currentElement) {
setRenderTick(renderTick + 1);
} else {
setCurrentStatus(OnBoardingStatus.READY);
}
};
// 获取当前步骤
const getCurrentStep = () => {
return steps[currentStep];
Expand Down Expand Up @@ -113,10 +104,6 @@ export const OnBoarding = forwardRef<OnBoardingRef, OnBoardingProps>((props, ref
setCurrentStep(currentStep + 1);
};

useEffect(() => {
initCurrentSelectedElement();
}, [steps, renderTick]);

useEffect(() => {
if (!isNil(step)) {
// 不触发相关生命周期
Expand Down Expand Up @@ -203,7 +190,7 @@ export const OnBoarding = forwardRef<OnBoardingRef, OnBoardingProps>((props, ref
await back();
}
};
}, [currentStep]);
}, [step, currentStep]);

return ReactDOM.createPortal(
currentStatus === OnBoardingStatus.READY ? (
Expand All @@ -214,11 +201,12 @@ export const OnBoarding = forwardRef<OnBoardingRef, OnBoardingProps>((props, ref
onAnimationEnd={() => {
setIsMaskMoving(false);
}}
container={currentContainerElement.element}
styleChecker={styleChecker}
visible={isShowMask}
element={getCurrentTargetElement() || mountedElement}
element={currentSelectedElement.element || currentContainerElement.element || document.body}
renderMaskContent={(wrapper) => renderPopover(wrapper)} />
) : <Fragment />,
mountedElement
currentContainerElement.element || document.body
);
});
2 changes: 1 addition & 1 deletion src/stories/basic.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const Basic: React.FC = () => {
return (
<OnBoardingContent
title={'🎉 欢迎使用 antd-onboarding!'}
content={'按钮有五种类型:主按钮、次按钮、虚线按钮、文本按钮和链接按钮。主按钮在同一个操作区域最多出现一次。'} />
content={'点击上一步或者下一步按钮以开始,你也可以通过键盘的左方向键和右方向键进行操作'} />
);
},
placement: 'bottom'
Expand Down
Loading

1 comment on commit 4c90286

@vercel
Copy link

@vercel vercel bot commented on 4c90286 Sep 20, 2021

Choose a reason for hiding this comment

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

Please sign in to comment.