Browse Source

feat: add tour (#6332)

* feat v4 add tour

* fix type error

* sync tour from antd5.4.6 & fix type error

* fix error
pull/6607/head
果冻橙 2 years ago committed by GitHub
parent
commit
698c0ff3b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      components/_util/hooks/useId.ts
  2. 3
      components/components.ts
  3. 5
      components/locale/en_US.ts
  4. 2
      components/locale/index.tsx
  5. 5
      components/locale/pt_BR.ts
  6. 5
      components/locale/zh_CN.ts
  7. 5
      components/locale/zh_HK.ts
  8. 5
      components/locale/zh_TW.ts
  9. 1
      components/theme/interface/alias.ts
  10. 4
      components/theme/interface/components.ts
  11. 5
      components/theme/util/alias.ts
  12. 478
      components/tour/__tests__/__snapshots__/demo-extend.test.ts.snap
  13. 478
      components/tour/__tests__/__snapshots__/demo.test.ts.snap
  14. 706
      components/tour/__tests__/__snapshots__/index.test.tsx.snap
  15. 3
      components/tour/__tests__/demo-extend.test.ts
  16. 3
      components/tour/__tests__/demo.test.ts
  17. 5
      components/tour/__tests__/image.test.ts
  18. 287
      components/tour/__tests__/index.test.tsx
  19. 70
      components/tour/demo/basic.vue
  20. 35
      components/tour/demo/index.vue
  21. 70
      components/tour/demo/indicator.vue
  22. 86
      components/tour/demo/mask.vue
  23. 69
      components/tour/demo/non-modal.vue
  24. 57
      components/tour/demo/placement.vue
  25. 59
      components/tour/index.en-US.md
  26. 82
      components/tour/index.tsx
  27. 60
      components/tour/index.zh-CN.md
  28. 41
      components/tour/interface.ts
  29. 154
      components/tour/panelRender.tsx
  30. 250
      components/tour/style/index.ts
  31. 35
      components/tour/useMergedType.ts
  32. 130
      components/vc-tour/Mask.tsx
  33. 243
      components/vc-tour/Tour.tsx
  34. 55
      components/vc-tour/TourStep/DefaultPanel.tsx
  35. 25
      components/vc-tour/TourStep/index.tsx
  36. 44
      components/vc-tour/hooks/useScrollLocker.tsx
  37. 100
      components/vc-tour/hooks/useTarget.ts
  38. 6
      components/vc-tour/index.tsx
  39. 36
      components/vc-tour/interface.ts
  40. 135
      components/vc-tour/placements.tsx
  41. 7
      components/vc-tour/util.ts
  42. 56
      components/vc-trigger/Trigger.tsx
  43. 5
      components/vc-trigger/index.ts
  44. 59
      components/vc-trigger/interface.ts

30
components/_util/hooks/useId.ts

@ -0,0 +1,30 @@
import { ref } from 'vue';
import canUseDom from '../../_util/canUseDom';
let uuid = 0;
/** Is client side and not jsdom */
export const isBrowserClient = process.env.NODE_ENV !== 'test' && canUseDom();
/** Get unique id for accessibility usage */
export function getUUID(): number | string {
let retId: string | number;
// Test never reach
/* istanbul ignore if */
if (isBrowserClient) {
retId = uuid;
uuid += 1;
} else {
retId = 'TEST_OR_SSR';
}
return retId;
}
export default function useId(id = ref('')) {
// Inner id for accessibility usage. Only work in client side
const innerId = `vc_unique_${getUUID()}`;
return id.value || innerId;
}

3
components/components.ts

@ -258,3 +258,6 @@ export { default as Segmented } from './segmented';
export type { QRCodeProps } from './qrcode';
export { default as QRCode } from './qrcode';
export type { TourProps, TourStepProps } from './tour';
export { default as Tour } from './tour';

5
components/locale/en_US.ts

@ -35,6 +35,11 @@ const localeValues: Locale = {
triggerAsc: 'Click to sort ascending',
cancelSort: 'Click to cancel sorting',
},
Tour: {
Next: 'Next',
Previous: 'Previous',
Finish: 'Finish',
},
Modal: {
okText: 'OK',
cancelText: 'Cancel',

2
components/locale/index.tsx

@ -10,6 +10,7 @@ import type { PickerLocale as DatePickerLocale } from '../date-picker/generatePi
import type { PaginationLocale } from '../pagination/Pagination';
import type { TableLocale } from '../table/interface';
import type { UploadLocale } from '../upload/interface';
import type { TourLocale } from '../tour/interface';
interface TransferLocaleForEmpty {
description: string;
@ -43,6 +44,7 @@ export interface Locale {
copied?: any;
expand?: any;
};
Tour?: TourLocale;
QRCode?: {
expired?: string;
refresh?: string;

5
components/locale/pt_BR.ts

@ -33,6 +33,11 @@ const localeValues: Locale = {
triggerAsc: 'Clique organiza por ascendente',
cancelSort: 'Clique para cancelar organização',
},
Tour: {
Next: 'Próximo',
Previous: 'Anterior',
Finish: 'Finalizar',
},
Modal: {
okText: 'OK',
cancelText: 'Cancelar',

5
components/locale/zh_CN.ts

@ -35,6 +35,11 @@ const localeValues: Locale = {
triggerAsc: '点击升序',
cancelSort: '取消排序',
},
Tour: {
Next: '下一步',
Previous: '上一步',
Finish: '结束导览',
},
Modal: {
okText: '确定',
cancelText: '取消',

5
components/locale/zh_HK.ts

@ -32,6 +32,11 @@ const localeValues: Locale = {
triggerAsc: '點擊升序',
cancelSort: '取消排序',
},
Tour: {
Next: '下一步',
Previous: '上一步',
Finish: '結束導覽',
},
Modal: {
okText: '確定',
cancelText: '取消',

5
components/locale/zh_TW.ts

@ -32,6 +32,11 @@ const localeValues: Locale = {
triggerAsc: '點擊升序',
cancelSort: '取消排序',
},
Tour: {
Next: '下一步',
Previous: '上一步',
Finish: '結束導覽',
},
Modal: {
okText: '確定',
cancelText: '取消',

1
components/theme/interface/alias.ts

@ -94,6 +94,7 @@ export interface AliasToken extends MapToken {
boxShadow: string;
boxShadowSecondary: string;
boxShadowTertiary: string;
linkDecoration: CSSProperties['textDecoration'];
linkHoverDecoration: CSSProperties['textDecoration'];

4
components/theme/interface/components.ts

@ -44,7 +44,7 @@ import type { ComponentToken as TooltipComponentToken } from '../../tooltip/styl
import type { ComponentToken as TransferComponentToken } from '../../transfer/style';
import type { ComponentToken as TypographyComponentToken } from '../../typography/style';
import type { ComponentToken as UploadComponentToken } from '../../upload/style';
// import type { ComponentToken as TourComponentToken } from '../../tour/style';
import type { ComponentToken as TourComponentToken } from '../../tour/style';
import type { ComponentToken as QRCodeComponentToken } from '../../qrcode/style';
// import type { ComponentToken as AppComponentToken } from '../../app/style';
import type { ComponentToken as WaveToken } from '../../_util/wave/style';
@ -110,7 +110,7 @@ export interface ComponentTokenMap {
Table?: TableComponentToken;
Space?: SpaceComponentToken;
Progress?: ProgressComponentToken;
// Tour?: TourComponentToken;
Tour?: TourComponentToken;
QRCode?: QRCodeComponentToken;
// App?: AppComponentToken;

5
components/theme/util/alias.ts

@ -134,6 +134,11 @@ export default function formatToken(derivativeToken: RawMergedToken): AliasToken
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05)
`,
boxShadowTertiary: `
0 1px 2px 0 rgba(0, 0, 0, 0.03),
0 1px 6px -1px rgba(0, 0, 0, 0.02),
0 2px 4px 0 rgba(0, 0, 0, 0.02)
`,
screenXS,
screenXSMin: screenXS,

478
components/tour/__tests__/__snapshots__/demo-extend.test.ts.snap

@ -0,0 +1,478 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/tour/demo/basic.tsx extend context correctly 1`] = `
Array [
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Begin Tour
</span>
</button>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Upload
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Save
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-btn-icon-only"
type="button"
>
<span
aria-label="ellipsis"
class="anticon anticon-ellipsis"
role="img"
>
<svg
aria-hidden="true"
data-icon="ellipsis"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"
/>
</svg>
</span>
</button>
</div>
</div>,
]
`;
exports[`renders ./components/tour/demo/non-modal.tsx extend context correctly 1`] = `
Array [
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Begin non-modal Tour
</span>
</button>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Upload
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Save
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-btn-icon-only"
type="button"
>
<span
aria-label="ellipsis"
class="anticon anticon-ellipsis"
role="img"
>
<svg
aria-hidden="true"
data-icon="ellipsis"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"
/>
</svg>
</span>
</button>
</div>
</div>,
]
`;
exports[`renders ./components/tour/demo/placement.tsx extend context correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Begin Tour
</span>
</button>
`;
exports[`renders ./components/tour/demo/render-panel.tsx extend context correctly 1`] = `
<div
style="display:flex;flex-direction:column;row-gap:16px;background:rgba(50,0,0,0.65);padding:8px"
>
<div
class="ant-tour ant-tour-pure ant-tour-placement-top ant-tour-pure"
>
<div
class="ant-tour-content"
>
<div
class="ant-tour-arrow"
/>
<div
class="ant-tour-inner"
role="tooltip"
>
<div
class="ant-tour-content"
>
<div
class="ant-tour-inner"
>
<span
aria-label="close"
class="anticon anticon-close ant-tour-close"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
<div
class="ant-tour-header"
>
<div
class="ant-tour-title"
>
Hello World!
</div>
</div>
<div
class="ant-tour-description"
>
Hello World?!
</div>
<div
class="ant-tour-footer"
>
<div
class="ant-tour-sliders"
>
<span
class="ant-tour-slider-active ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
</div>
<div
class="ant-tour-buttons"
>
<button
class="ant-btn ant-btn-primary ant-btn-sm ant-tour-next-btn"
type="button"
>
<span>
Next
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-tour ant-tour-pure ant-tour-placement-top ant-tour-pure"
>
<div
class="ant-tour-content"
>
<div
class="ant-tour-arrow"
/>
<div
class="ant-tour-inner"
role="tooltip"
>
<div
class="ant-tour-content"
>
<div
class="ant-tour-inner"
>
<span
aria-label="close"
class="anticon anticon-close ant-tour-close"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
<div
class="ant-tour-cover"
>
<img
alt="tour.png"
src="https://user-images.githubusercontent.com/5378891/197385811-55df8480-7ff4-44bd-9d43-a7dade598d70.png"
/>
</div>
<div
class="ant-tour-header"
>
<div
class="ant-tour-title"
>
Hello World!
</div>
</div>
<div
class="ant-tour-description"
>
Hello World?!
</div>
<div
class="ant-tour-footer"
>
<div
class="ant-tour-sliders"
>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider-active ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
</div>
<div
class="ant-tour-buttons"
>
<button
class="ant-btn ant-btn-default ant-btn-sm ant-tour-prev-btn"
type="button"
>
<span>
Previous
</span>
</button>
<button
class="ant-btn ant-btn-primary ant-btn-sm ant-tour-next-btn"
type="button"
>
<span>
Next
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-tour ant-tour-pure ant-tour-placement-top ant-tour-pure ant-tour-primary"
>
<div
class="ant-tour-content"
>
<div
class="ant-tour-arrow"
/>
<div
class="ant-tour-inner"
role="tooltip"
>
<div
class="ant-tour-primary ant-tour-content"
>
<div
class="ant-tour-inner"
>
<span
aria-label="close"
class="anticon anticon-close ant-tour-close"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
<div
class="ant-tour-header"
>
<div
class="ant-tour-title"
>
Hello World!
</div>
</div>
<div
class="ant-tour-description"
>
Hello World?!
</div>
<div
class="ant-tour-footer"
>
<div
class="ant-tour-sliders"
>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider-active ant-tour-slider"
/>
</div>
<div
class="ant-tour-buttons"
>
<button
class="ant-btn ant-btn-default ant-btn-sm ant-btn-background-ghost ant-tour-prev-btn"
type="button"
>
<span>
Previous
</span>
</button>
<button
class="ant-btn ant-btn-default ant-btn-sm ant-tour-next-btn"
type="button"
>
<span>
Finish
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

478
components/tour/__tests__/__snapshots__/demo.test.ts.snap

@ -0,0 +1,478 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/tour/demo/basic.tsx correctly 1`] = `
Array [
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Begin Tour
</span>
</button>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Upload
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Save
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-btn-icon-only"
type="button"
>
<span
aria-label="ellipsis"
class="anticon anticon-ellipsis"
role="img"
>
<svg
aria-hidden="true"
data-icon="ellipsis"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"
/>
</svg>
</span>
</button>
</div>
</div>,
]
`;
exports[`renders ./components/tour/demo/non-modal.tsx correctly 1`] = `
Array [
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Begin non-modal Tour
</span>
</button>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Upload
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Save
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-btn-icon-only"
type="button"
>
<span
aria-label="ellipsis"
class="anticon anticon-ellipsis"
role="img"
>
<svg
aria-hidden="true"
data-icon="ellipsis"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"
/>
</svg>
</span>
</button>
</div>
</div>,
]
`;
exports[`renders ./components/tour/demo/placement.tsx correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Begin Tour
</span>
</button>
`;
exports[`renders ./components/tour/demo/render-panel.tsx correctly 1`] = `
<div
style="display:flex;flex-direction:column;row-gap:16px;background:rgba(50,0,0,0.65);padding:8px"
>
<div
class="ant-tour ant-tour-pure ant-tour-placement-top ant-tour-pure"
>
<div
class="ant-tour-content"
>
<div
class="ant-tour-arrow"
/>
<div
class="ant-tour-inner"
role="tooltip"
>
<div
class="ant-tour-content"
>
<div
class="ant-tour-inner"
>
<span
aria-label="close"
class="anticon anticon-close ant-tour-close"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
<div
class="ant-tour-header"
>
<div
class="ant-tour-title"
>
Hello World!
</div>
</div>
<div
class="ant-tour-description"
>
Hello World?!
</div>
<div
class="ant-tour-footer"
>
<div
class="ant-tour-sliders"
>
<span
class="ant-tour-slider-active ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
</div>
<div
class="ant-tour-buttons"
>
<button
class="ant-btn ant-btn-primary ant-btn-sm ant-tour-next-btn"
type="button"
>
<span>
Next
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-tour ant-tour-pure ant-tour-placement-top ant-tour-pure"
>
<div
class="ant-tour-content"
>
<div
class="ant-tour-arrow"
/>
<div
class="ant-tour-inner"
role="tooltip"
>
<div
class="ant-tour-content"
>
<div
class="ant-tour-inner"
>
<span
aria-label="close"
class="anticon anticon-close ant-tour-close"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
<div
class="ant-tour-cover"
>
<img
alt="tour.png"
src="https://user-images.githubusercontent.com/5378891/197385811-55df8480-7ff4-44bd-9d43-a7dade598d70.png"
/>
</div>
<div
class="ant-tour-header"
>
<div
class="ant-tour-title"
>
Hello World!
</div>
</div>
<div
class="ant-tour-description"
>
Hello World?!
</div>
<div
class="ant-tour-footer"
>
<div
class="ant-tour-sliders"
>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider-active ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
</div>
<div
class="ant-tour-buttons"
>
<button
class="ant-btn ant-btn-default ant-btn-sm ant-tour-prev-btn"
type="button"
>
<span>
Previous
</span>
</button>
<button
class="ant-btn ant-btn-primary ant-btn-sm ant-tour-next-btn"
type="button"
>
<span>
Next
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-tour ant-tour-pure ant-tour-placement-top ant-tour-pure ant-tour-primary"
>
<div
class="ant-tour-content"
>
<div
class="ant-tour-arrow"
/>
<div
class="ant-tour-inner"
role="tooltip"
>
<div
class="ant-tour-primary ant-tour-content"
>
<div
class="ant-tour-inner"
>
<span
aria-label="close"
class="anticon anticon-close ant-tour-close"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
<div
class="ant-tour-header"
>
<div
class="ant-tour-title"
>
Hello World!
</div>
</div>
<div
class="ant-tour-description"
>
Hello World?!
</div>
<div
class="ant-tour-footer"
>
<div
class="ant-tour-sliders"
>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider-active ant-tour-slider"
/>
</div>
<div
class="ant-tour-buttons"
>
<button
class="ant-btn ant-btn-default ant-btn-sm ant-btn-background-ghost ant-tour-prev-btn"
type="button"
>
<span>
Previous
</span>
</button>
<button
class="ant-btn ant-btn-default ant-btn-sm ant-tour-next-btn"
type="button"
>
<span>
Finish
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

706
components/tour/__tests__/__snapshots__/index.test.tsx.snap

@ -0,0 +1,706 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Tour Primary 1`] = `
<body>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div>
<button
disabled=""
type="button"
>
Cover
</button>
<div>
<div
class="ant-tour"
style="z-index: 1090; opacity: 0;"
>
<div
class="ant-tour-primary ant-tour-content"
>
<div
class="ant-tour-arrow"
/>
<div
class="ant-tour-inner"
>
<span
aria-label="close"
class="anticon anticon-close ant-tour-close"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
<div
class="ant-tour-header"
>
<div
class="ant-tour-title"
>
primary title
</div>
</div>
<div
class="ant-tour-description"
>
primary description.
</div>
<div
class="ant-tour-footer"
>
<div
class="ant-tour-sliders"
/>
<div
class="ant-tour-buttons"
>
<button
class="ant-btn ant-btn-default ant-btn-sm ant-tour-next-btn"
type="button"
>
<span>
Finish
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<div
class="ant-tour-target-placeholder"
style="left: -6px; top: -6px; width: 12px; height: 12px; position: fixed; pointer-events: none;"
/>
</div>
<div>
<div
class="ant-tour-mask"
style="position: fixed; left: 0px; right: 0px; top: 0px; bottom: 0px; z-index: 900; pointer-events: none;"
>
<svg
style="width: 100%; height: 100%;"
>
<defs>
<mask
id="ant-tour-mask-test-id"
>
<rect
fill="white"
height="100%"
width="100%"
x="0"
y="0"
/>
<rect
class="ant-tour-placeholder-animated"
fill="black"
height="12"
rx="2"
width="12"
x="-6"
y="-6"
/>
</mask>
</defs>
<rect
fill="rgba(0,0,0,0.5)"
height="100%"
mask="url(#ant-tour-mask-test-id)"
width="100%"
x="0"
y="0"
/>
<rect
fill="transparent"
height="-6"
pointer-events="auto"
width="100%"
x="0"
y="0"
/>
<rect
fill="transparent"
height="100%"
pointer-events="auto"
width="-6"
x="0"
y="0"
/>
<rect
fill="transparent"
height="calc(100vh - 6px)"
pointer-events="auto"
width="100%"
x="0"
y="6"
/>
<rect
fill="transparent"
height="100%"
pointer-events="auto"
width="calc(100vw - 6px)"
x="6"
y="0"
/>
</svg>
</div>
</div>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
</body>
`;
exports[`Tour basic 1`] = `
<body>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div>
<div>
<button
type="button"
>
Show
</button>
<button
disabled=""
type="button"
>
Cover
</button>
<button
disabled=""
type="button"
>
Placement
</button>
</div>
<div />
</div>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
</body>
`;
exports[`Tour button props onClick 1`] = `
<body>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div>
<span
id="btnName"
>
finishButton
</span>
<button
disabled=""
type="button"
>
target
</button>
<div />
</div>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
</body>
`;
exports[`Tour custom step pre btn & next btn className & style 1`] = `
<div>
<div
class="ant-tour"
style="z-index: 1090; opacity: 0;"
>
<div
class="ant-tour-content"
>
<div
class="ant-tour-inner"
>
<span
aria-label="close"
class="anticon anticon-close ant-tour-close"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
<div
class="ant-tour-header"
>
<div
class="ant-tour-title"
>
Show in Center
</div>
</div>
<div
class="ant-tour-description"
>
Here is the content of Tour.
</div>
<div
class="ant-tour-footer"
>
<div
class="ant-tour-sliders"
>
<span
class="ant-tour-slider-active ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
</div>
<div
class="ant-tour-buttons"
>
<button
class="ant-btn ant-btn-primary ant-btn-sm ant-tour-next-btn customClassName"
style="background-color: rgb(69, 69, 255);"
type="button"
>
<span>
Next
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Tour rtl render component should be rendered correctly in RTL direction 1`] = `null`;
exports[`Tour single 1`] = `
<body>
<div>
<button
disabled=""
type="button"
>
Cover
</button>
<div>
<div
class="ant-tour"
style="z-index: 1090; opacity: 0;"
>
<div
class="ant-tour-content"
>
<div
class="ant-tour-arrow"
/>
<div
class="ant-tour-inner"
>
<span
aria-label="close"
class="anticon anticon-close ant-tour-close"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
<div
class="ant-tour-header"
>
<div
class="ant-tour-title"
>
cover title
</div>
</div>
<div
class="ant-tour-description"
>
cover description.
</div>
<div
class="ant-tour-footer"
>
<div
class="ant-tour-sliders"
/>
<div
class="ant-tour-buttons"
>
<button
class="ant-btn ant-btn-primary ant-btn-sm ant-tour-next-btn"
type="button"
>
<span>
Finish
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<div
class="ant-tour-target-placeholder"
style="left: -6px; top: -6px; width: 12px; height: 12px; position: fixed; pointer-events: none;"
/>
</div>
<div>
<div
class="ant-tour-mask"
style="position: fixed; left: 0px; right: 0px; top: 0px; bottom: 0px; z-index: 900; pointer-events: none;"
>
<svg
style="width: 100%; height: 100%;"
>
<defs>
<mask
id="ant-tour-mask-test-id"
>
<rect
fill="white"
height="100%"
width="100%"
x="0"
y="0"
/>
<rect
class="ant-tour-placeholder-animated"
fill="black"
height="12"
rx="2"
width="12"
x="-6"
y="-6"
/>
</mask>
</defs>
<rect
fill="rgba(0,0,0,0.5)"
height="100%"
mask="url(#ant-tour-mask-test-id)"
width="100%"
x="0"
y="0"
/>
<rect
fill="transparent"
height="-6"
pointer-events="auto"
width="100%"
x="0"
y="0"
/>
<rect
fill="transparent"
height="100%"
pointer-events="auto"
width="-6"
x="0"
y="0"
/>
<rect
fill="transparent"
height="calc(100vh - 6px)"
pointer-events="auto"
width="100%"
x="0"
y="6"
/>
<rect
fill="transparent"
height="100%"
pointer-events="auto"
width="calc(100vw - 6px)"
x="6"
y="0"
/>
</svg>
</div>
</div>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
</body>
`;
exports[`Tour step support Primary 1`] = `
<body>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div>
<button
disabled=""
type="button"
>
Cover
</button>
<div>
<div
class="ant-tour"
style="z-index: 1090; opacity: 0;"
>
<div
class="ant-tour-primary ant-tour-content"
>
<div
class="ant-tour-arrow"
/>
<div
class="ant-tour-inner"
>
<span
aria-label="close"
class="anticon anticon-close ant-tour-close"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
<div
class="ant-tour-header"
>
<div
class="ant-tour-title"
>
primary title
</div>
</div>
<div
class="ant-tour-description"
>
primary description.
</div>
<div
class="ant-tour-footer"
>
<div
class="ant-tour-sliders"
>
<span
class="ant-tour-slider"
/>
<span
class="ant-tour-slider-active ant-tour-slider"
/>
</div>
<div
class="ant-tour-buttons"
>
<button
class="ant-btn ant-btn-default ant-btn-sm ant-btn-background-ghost ant-tour-prev-btn"
type="button"
>
<span>
Previous
</span>
</button>
<button
class="ant-btn ant-btn-default ant-btn-sm ant-tour-next-btn"
type="button"
>
<span>
Finish
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<div
class="ant-tour-target-placeholder"
style="left: -6px; top: -6px; width: 12px; height: 12px; position: fixed; pointer-events: none;"
/>
</div>
<div>
<div
class="ant-tour-mask"
style="position: fixed; left: 0px; right: 0px; top: 0px; bottom: 0px; z-index: 900; pointer-events: none;"
>
<svg
style="width: 100%; height: 100%;"
>
<defs>
<mask
id="ant-tour-mask-test-id"
>
<rect
fill="white"
height="100%"
width="100%"
x="0"
y="0"
/>
<rect
class="ant-tour-placeholder-animated"
fill="black"
height="12"
rx="2"
width="12"
x="-6"
y="-6"
/>
</mask>
</defs>
<rect
fill="rgba(0,0,0,0.5)"
height="100%"
mask="url(#ant-tour-mask-test-id)"
width="100%"
x="0"
y="0"
/>
<rect
fill="transparent"
height="-6"
pointer-events="auto"
width="100%"
x="0"
y="0"
/>
<rect
fill="transparent"
height="100%"
pointer-events="auto"
width="-6"
x="0"
y="0"
/>
<rect
fill="transparent"
height="calc(100vh - 6px)"
pointer-events="auto"
width="100%"
x="0"
y="6"
/>
<rect
fill="transparent"
height="100%"
pointer-events="auto"
width="calc(100vw - 6px)"
x="6"
y="0"
/>
</svg>
</div>
</div>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
</body>
`;
exports[`Tour steps is empty 1`] = `
<body>
<div
style="position: absolute; top: 0px; left: 0px; width: 100%;"
/>
<div>
<button
disabled=""
type="button"
>
Cover
</button>
</div>
</body>
`;

3
components/tour/__tests__/demo-extend.test.ts

@ -0,0 +1,3 @@
import { extendTest } from '../../../tests/shared/demoTest';
extendTest('tour');

3
components/tour/__tests__/demo.test.ts

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('tour');

5
components/tour/__tests__/image.test.ts

@ -0,0 +1,5 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('Tooltip tour', () => {
imageDemoTest('tour');
});

287
components/tour/__tests__/index.test.tsx

@ -0,0 +1,287 @@
import React, { useRef, useEffect } from 'react';
import Tour from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render, screen } from '../../../tests/utils';
import panelRender from '../panelRender';
describe('Tour', () => {
mountTest(Tour);
rtlTest(Tour);
it('single', () => {
const App: React.FC = () => {
const coverBtnRef = useRef<HTMLButtonElement>(null);
return (
<>
<button disabled ref={coverBtnRef} type="button">
Cover
</button>
<Tour
steps={[
{
title: 'cover title',
description: 'cover description.',
target: () => coverBtnRef.current!,
},
]}
/>
</>
);
};
const { getByText, baseElement } = render(<App />);
expect(getByText('cover title')).toBeTruthy();
expect(getByText('cover description.')).toBeTruthy();
expect(baseElement).toMatchSnapshot();
});
it('steps is empty', () => {
const App: React.FC = () => {
const coverBtnRef = useRef<HTMLButtonElement>(null);
return (
<>
<button disabled ref={coverBtnRef} type="button">
Cover
</button>
<Tour steps={[]} />
<Tour />
</>
);
};
const { baseElement } = render(<App />);
expect(baseElement).toMatchSnapshot();
});
it('button props onClick', () => {
const App: React.FC = () => {
const coverBtnRef = useRef<HTMLButtonElement>(null);
const [btnName, steBtnName] = React.useState<string>('defaultBtn');
return (
<>
<span id="btnName">{btnName}</span>
<button disabled ref={coverBtnRef} type="button">
target
</button>
<Tour
steps={[
{
title: '',
description: '',
target: () => coverBtnRef.current!,
nextButtonProps: {
onClick: () => steBtnName('nextButton'),
},
},
{
title: '',
target: () => coverBtnRef.current!,
prevButtonProps: {
onClick: () => steBtnName('prevButton'),
},
nextButtonProps: {
onClick: () => steBtnName('finishButton'),
},
},
]}
/>
</>
);
};
const { baseElement } = render(<App />);
expect(baseElement.querySelector('#btnName')).toHaveTextContent('defaultBtn');
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
expect(baseElement.querySelector('#btnName')).toHaveTextContent('nextButton');
fireEvent.click(screen.getByRole('button', { name: 'Previous' }));
expect(baseElement.querySelector('#btnName')).toHaveTextContent('prevButton');
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
fireEvent.click(screen.getByRole('button', { name: 'Finish' }));
expect(baseElement.querySelector('#btnName')).toHaveTextContent('finishButton');
expect(baseElement).toMatchSnapshot();
});
it('Primary', () => {
const App: React.FC = () => {
const coverBtnRef = useRef<HTMLButtonElement>(null);
return (
<>
<button disabled ref={coverBtnRef} type="button">
Cover
</button>
<Tour
type="primary"
steps={[
{
title: 'primary title',
description: 'primary description.',
target: () => coverBtnRef.current!,
},
]}
/>
</>
);
};
const { getByText, baseElement } = render(<App />);
expect(getByText('primary description.')).toBeTruthy();
expect(baseElement.querySelector('.ant-tour-content')).toHaveClass('ant-tour-primary');
expect(baseElement).toMatchSnapshot();
});
it('step support Primary', () => {
const App: React.FC = () => {
const coverBtnRef = useRef<HTMLButtonElement>(null);
return (
<>
<button disabled ref={coverBtnRef} type="button">
Cover
</button>
<Tour
type="default"
steps={[
{
title: 'cover title',
description: 'cover description.',
target: () => coverBtnRef.current!,
},
{
title: 'primary title',
description: 'primary description.',
target: () => coverBtnRef.current!,
type: 'primary',
},
]}
/>
</>
);
};
const { getByText, container, baseElement } = render(<App />);
expect(getByText('cover description.')).toBeTruthy();
expect(container.querySelector('.ant-tour-content.ant-tour-primary')).toBeFalsy();
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
expect(getByText('primary description.')).toBeTruthy();
expect(container.querySelector('.ant-tour-content.ant-tour-primary')).toBeTruthy();
expect(baseElement).toMatchSnapshot();
});
it('basic', () => {
const App: React.FC = () => {
const coverBtnRef = useRef<HTMLButtonElement>(null);
const placementBtnRef = useRef<HTMLButtonElement>(null);
const [show, setShow] = React.useState<boolean>();
useEffect(() => {
if (show === false) {
setShow(true);
}
}, [show]);
return (
<>
<div>
<button
type="button"
onClick={() => {
setShow(false);
}}
>
Show
</button>
<button disabled ref={coverBtnRef} type="button">
Cover
</button>
<button disabled ref={placementBtnRef} type="button">
Placement
</button>
</div>
{show && (
<Tour
steps={[
{
title: 'Show in Center',
description: 'Here is the content of Tour.',
target: null,
},
{
title: 'With Cover',
description: 'Here is the content of Tour.',
target: () => coverBtnRef.current!,
cover: (
<img
alt="tour.png"
src="https://user-images.githubusercontent.com/5378891/197385811-55df8480-7ff4-44bd-9d43-a7dade598d70.png"
/>
),
},
{
title: 'Adjust Placement',
description: 'Here is the content of Tour which show on the right.',
placement: 'right',
target: () => placementBtnRef.current!,
},
]}
/>
)}
</>
);
};
const { getByText, container, baseElement } = render(<App />);
fireEvent.click(screen.getByRole('button', { name: 'Show' }));
expect(getByText('Show in Center')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
expect(getByText('Here is the content of Tour.')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
expect(getByText('Adjust Placement')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Finish' }));
expect(container.querySelector('.ant-tour')).toBeFalsy();
expect(baseElement).toMatchSnapshot();
});
it('panelRender should correct render when total is undefined', () => {
expect(() => {
panelRender({ total: undefined, title: <div>test</div> }, 0, 'default');
}).not.toThrow();
});
it('custom step pre btn & next btn className & style', () => {
const App: React.FC = () => (
<Tour
steps={[
{
title: 'Show in Center',
description: 'Here is the content of Tour.',
nextButtonProps: {
className: 'customClassName',
style: {
backgroundColor: 'rgb(69,69,255)',
},
},
},
{
title: 'With Cover',
description: 'Here is the content of Tour.',
cover: (
<img
alt="tour.png"
src="https://user-images.githubusercontent.com/5378891/197385811-55df8480-7ff4-44bd-9d43-a7dade598d70.png"
/>
),
},
]}
/>
);
const { container } = render(<App />);
// className
expect(
screen.getByRole('button', { name: 'Next' }).className.includes('customClassName'),
).toEqual(true);
// style
expect(screen.getByRole('button', { name: 'Next' }).style.backgroundColor).toEqual(
'rgb(69, 69, 255)',
);
expect(container.firstChild).toMatchSnapshot();
});
});

70
components/tour/demo/basic.vue

@ -0,0 +1,70 @@
<docs>
---
order: 0
title:
zh-CN: 基本用法
en-US: Basic usage
---
## zh-CN
最简单的用法
## en-US
The most basic usage.
</docs>
<template>
<a-button type="primary" @click="handleOpen(true)">Begin Tour</a-button>
<a-divider />
<a-space>
<a-button ref="ref1">Upload</a-button>
<a-button ref="ref2" type="primary">Save</a-button>
<a-button ref="ref3"><EllipsisOutlined /></a-button>
</a-space>
<a-tour :open="open" :steps="steps" @close="handleOpen(false)" />
</template>
<script lang="ts" setup>
import { ref, createVNode } from 'vue';
import { EllipsisOutlined } from '@ant-design/icons-vue';
import type { TourProps } from 'ant-design-vue';
const open = ref<boolean>(false);
const ref1 = ref(null);
const ref2 = ref(null);
const ref3 = ref(null);
const steps: TourProps['steps'] = [
{
title: 'Upload File',
description: 'Put your files here.',
cover: createVNode('img', {
alt: 'tour.png',
src: 'https://user-images.githubusercontent.com/5378891/197385811-55df8480-7ff4-44bd-9d43-a7dade598d70.png',
}),
target: () => ref1.value && ref1.value.$el,
},
{
title: 'Save',
description: 'Save your changes.',
target: () => ref2.value && ref2.value.$el,
},
{
title: 'Other Actions',
description: 'Click to see other actions.',
target: () => ref3.value && ref3.value.$el,
},
];
const handleOpen = (val: boolean): void => {
open.value = val;
};
</script>

35
components/tour/demo/index.vue

@ -0,0 +1,35 @@
<template>
<demo-sort>
<basic />
<non-modal />
<placement />
<Mask />
<indicator />
</demo-sort>
</template>
<script lang="ts">
import Basic from './basic.vue';
import NonModal from './non-modal.vue';
import Placement from './placement.vue';
import Mask from './mask.vue';
import Indicator from './indicator.vue';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
import { defineComponent } from 'vue';
export default defineComponent({
CN,
US,
components: {
Basic,
NonModal,
Placement,
Mask,
Indicator,
},
setup() {
return {};
},
});
</script>

70
components/tour/demo/indicator.vue

@ -0,0 +1,70 @@
<docs>
---
order: 4
title:
zh-CN: 自定义指示器
en-US: custom indicator
---
## zh-CN
自定义指示器
## en-US
Custom indicator.
</docs>
<template>
<a-button type="primary" @click="handleOpen(true)">Begin Tour</a-button>
<a-divider />
<a-space>
<a-button ref="ref1">Upload</a-button>
<a-button ref="ref2" type="primary">Save</a-button>
<a-button ref="ref3"><EllipsisOutlined /></a-button>
</a-space>
<a-tour :open="open" :steps="steps" @close="handleOpen(false)">
<template #indicatorsRender="{ current, total }">
<span>{{ current + 1 }} / {{ total }}</span>
</template>
</a-tour>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { EllipsisOutlined } from '@ant-design/icons-vue';
import type { TourProps } from 'ant-design-vue';
const open = ref<boolean>(false);
const ref1 = ref(null);
const ref2 = ref(null);
const ref3 = ref(null);
const steps: TourProps['steps'] = [
{
title: 'Upload File',
description: 'Put your files here.',
target: () => ref1.value && ref1.value.$el,
},
{
title: 'Save',
description: 'Save your changes.',
target: () => ref2.value && ref2.value.$el,
},
{
title: 'Other Actions',
description: 'Click to see other actions.',
target: () => ref3.value && ref3.value.$el,
},
];
const handleOpen = (val: boolean): void => {
open.value = val;
};
</script>

86
components/tour/demo/mask.vue

@ -0,0 +1,86 @@
<docs>
---
order: 3
title:
zh-CN: 自定义遮罩样式
en-US: custom mask style
---
## zh-CN
自定义遮罩样式
## en-US
custom mask style.
</docs>
<template>
<a-button type="primary" @click="handleOpen(true)">Begin Tour</a-button>
<a-divider />
<a-space>
<a-button ref="ref1">Upload</a-button>
<a-button ref="ref2" type="primary">Save</a-button>
<a-button ref="ref3"><EllipsisOutlined /></a-button>
</a-space>
<a-tour
:open="open"
:steps="steps"
:mask="{
style: {
boxShadow: 'inset 0 0 15px #333',
},
color: 'rgba(80, 255, 255, .4)',
}"
@close="handleOpen(false)"
/>
</template>
<script lang="ts" setup>
import { ref, createVNode } from 'vue';
import { EllipsisOutlined } from '@ant-design/icons-vue';
import type { TourProps } from 'ant-design-vue';
const open = ref<boolean>(false);
const ref1 = ref(null);
const ref2 = ref(null);
const ref3 = ref(null);
const steps: TourProps['steps'] = [
{
title: 'Upload File',
description: 'Put your files here.',
cover: createVNode('img', {
alt: 'tour.png',
src: 'https://user-images.githubusercontent.com/5378891/197385811-55df8480-7ff4-44bd-9d43-a7dade598d70.png',
}),
target: () => ref1.value && ref1.value.$el,
},
{
title: 'Save',
description: 'Save your changes.',
target: () => ref2.value && ref2.value.$el,
mask: {
style: {
boxShadow: 'inset 0 0 15px #fff',
},
color: 'rgba(40, 0, 255, .4)',
},
},
{
title: 'Other Actions',
description: 'Click to see other actions.',
target: () => ref3.value && ref3.value.$el,
mask: false,
},
];
const handleOpen = (val: boolean): void => {
open.value = val;
};
</script>

69
components/tour/demo/non-modal.vue

@ -0,0 +1,69 @@
<docs>
---
order: 1
title:
zh-CN: 非模态
en-US: Non modal
---
## zh-CN
使用 `mask={false}` 可以将引导变为非模态同时为了强调引导本身建议与 `type="primary"` 组合使用
## en-US
Use `mask={false}` to make Tour non-modal. At the meantime it is recommended to use with `type="primary"` to emphasize the guide itself.
</docs>
<template>
<a-button type="primary" @click="handleOpen(true)">Begin Tour</a-button>
<a-divider />
<a-space>
<a-button ref="ref1">Upload</a-button>
<a-button ref="ref2" type="primary">Save</a-button>
<a-button ref="ref3"><EllipsisOutlined /></a-button>
</a-space>
<a-tour :open="open" :mask="false" type="primary" :steps="steps" @close="handleOpen(false)" />
</template>
<script lang="ts" setup>
import { ref, createVNode } from 'vue';
import { EllipsisOutlined } from '@ant-design/icons-vue';
import type { TourProps } from 'ant-design-vue';
const open = ref<boolean>(false);
const ref1 = ref(null);
const ref2 = ref(null);
const ref3 = ref(null);
const steps: TourProps['steps'] = [
{
title: 'Upload File',
description: 'Put your files here.',
cover: createVNode('img', {
alt: 'tour.png',
src: 'https://user-images.githubusercontent.com/5378891/197385811-55df8480-7ff4-44bd-9d43-a7dade598d70.png',
}),
target: () => ref1.value && ref1.value.$el,
},
{
title: 'Save',
description: 'Save your changes.',
target: () => ref2.value && ref2.value.$el,
},
{
title: 'Other Actions',
description: 'Click to see other actions.',
target: () => ref3.value && ref3.value.$el,
},
];
const handleOpen = (val: boolean): void => {
open.value = val;
};
</script>

57
components/tour/demo/placement.vue

@ -0,0 +1,57 @@
<docs>
---
order: 2
title:
zh-CN: 位置
en-US: Placement
---
## zh-CN
改变引导相对于目标的位置共有 12 种位置可供选择 `target={null}` 时引导将会展示在正中央
## en-US
Change the placement of the guide relative to the target, there are 12 placements available. When `target={null}` the guide will show in the center.
</docs>
<template>
<a-button ref="btnRef" type="primary" @click="handleOpen(true)">Begin Tour</a-button>
<a-tour :open="open" :steps="steps" @close="handleOpen(false)" />
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import type { TourProps } from 'ant-design-vue';
const open = ref<boolean>(false);
const btnRef = ref(null);
const steps: TourProps['steps'] = [
{
title: 'Center',
description: 'Displayed in the center of screen.',
target: null,
},
{
title: 'Right',
description: 'On the right of target.',
placement: 'right',
target: () => btnRef.value && btnRef.value.$el,
},
{
title: 'Top',
description: 'On the top of target.',
placement: 'top',
target: () => btnRef.value && btnRef.value.$el,
},
];
const handleOpen = (val: boolean): void => {
open.value = val;
};
</script>

59
components/tour/index.en-US.md

@ -0,0 +1,59 @@
---
category: Components
type: Data Display
title: Tour
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*8CC_Tbe3_e4AAAAAAAAAAAAADrJ8AQ/original
coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*nF6hQpM0XtEAAAAAAAAAAAAADrJ8AQ/original
---
A popup component for guiding users through a product. Available since `4.0.0`.
## When To Use
Use when you want to guide users through a product.
## API
### Tour
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| arrow | Whether to show the arrow, including the configuration whether to point to the center of the element | `boolean`\|`{ pointAtCenter: boolean}` | `true` | |
| placement | Position of the guide card relative to the target element | `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` | `bottom` | |
| mask | Whether to enable masking, change mask style and fill color by pass custom props | `boolean` \| `{ style?: CSSProperties; color?: string; }` | `true` | |
| type | Type, affects the background color and text color | `default` `primary` | `default` | |
| open | Open tour | `boolean` | - | |
| current | What is the current step | `number` | - | |
| scrollIntoViewOptions | support pass custom scrollIntoView options | `boolean` \| `ScrollIntoViewOptions` | `true` | |
| indicatorsRender | custom indicator | `v-slot:indicatorsRender="{current, total}"` | - | |
| zIndex | Tour's zIndex | `number` | `1001` | |
### Tour events
| Events Name | Description | Arguments | Version |
| --- | --- | --- | --- | --- |
| close | Callback function on shutdown | `Function` | - | |
| finish | Callback function on finished | `Function` | - | |
| change | Callback when the step changes. Current is the previous step | `(current: number) => void` |
### TourStep
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| target | Get the element the guide card points to. Empty makes it show in center of screen | `() => HTMLElement` `HTMLElement` | - | |
| arrow | Whether to show the arrow, including the configuration whether to point to the center of the element | `boolean` `{ pointAtCenter: boolean}` | `true` | |
| cover | Displayed pictures or videos | `VueNode` | - | |
| title | title | `VueNode` | - | |
| description | description | `VueNode` | - | |
| placement | Position of the guide card relative to the target element | `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` | `bottom` | |
| mask | Whether to enable masking, change mask style and fill color by pass custom props, the default follows the `mask` property of Tour | `boolean` \| `{ style?: CSSProperties; color?: string; }` | `true` | |
| type | Type, affects the background color and text color | `default` `primary` | `default` | |
| nextButtonProps | Properties of the Next button | `{ children: VueNode; onClick: Function }` | - | |
| prevButtonProps | Properties of the previous button | `{ children: VueNode; onClick: Function }` | - | |
| scrollIntoViewOptions | support pass custom scrollIntoView options, the default follows the `scrollIntoViewOptions` property of Tour | `boolean` \| `ScrollIntoViewOptions` | `true` | |
### TourStep events
| Events Name | Description | Arguments | Version |
| ----------- | ----------------------------- | ---------- | ------- | --- |
| close | Callback function on shutdown | `Function` | - | |

82
components/tour/index.tsx

@ -0,0 +1,82 @@
import { defineComponent, toRefs } from 'vue';
import VCTour from '../vc-tour';
import classNames from '../_util/classNames';
import TourPanel from './panelRender';
import type { TourProps, TourStepProps } from './interface';
import { tourProps } from './interface';
import useConfigInject from '../config-provider/hooks/useConfigInject';
import type { VueNode } from '../_util/type';
import { withInstall } from '../_util/type';
import useMergedType from './useMergedType';
// CSSINJS
import useStyle from './style';
export { TourProps, TourStepProps };
const Tour = defineComponent({
name: 'ATour',
props: tourProps(),
setup(props, { attrs, emit, slots }) {
const { current } = toRefs(props);
const { prefixCls, direction } = useConfigInject('tour', props);
// style
const [wrapSSR, hashId] = useStyle(prefixCls);
const { currentMergedType, updateInnerCurrent } = useMergedType({
defaultType: props.type,
steps: props.steps,
current,
defaultCurrent: props.defaultCurrent,
});
return () => {
const { steps, current, type, rootClassName, ...restProps } = props;
const customClassName = classNames(
{
[`${prefixCls.value}-primary`]: currentMergedType.value === 'primary',
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
},
hashId.value,
rootClassName,
);
const mergedRenderPanel = (stepProps: TourStepProps, stepCurrent: number): VueNode => {
return (
<TourPanel
{...stepProps}
type={type}
current={stepCurrent}
v-slots={{
indicatorsRender: slots.indicatorsRender,
}}
></TourPanel>
);
};
const onStepChange = (stepCurrent: number) => {
updateInnerCurrent(stepCurrent);
emit('change', stepCurrent);
};
return wrapSSR(
<VCTour
{...attrs}
{...restProps}
rootClassName={customClassName}
prefixCls={prefixCls.value}
current={current}
defaultCurrent={props.defaultCurrent}
animated
renderPanel={mergedRenderPanel}
onChange={onStepChange}
steps={steps}
/>,
);
};
},
});
export default withInstall(Tour);

60
components/tour/index.zh-CN.md

@ -0,0 +1,60 @@
---
category: Components
type: 数据展示
title: Tour
subtitle: 漫游式引导
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*8CC_Tbe3_e4AAAAAAAAAAAAADrJ8AQ/original
coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*nF6hQpM0XtEAAAAAAAAAAAAADrJ8AQ/original
---
用于分步引导用户了解产品功能的气泡组件。自 `4.0.0` 版本开始提供该组件。
## 何时使用
常用于引导用户了解产品功能。
## API
### Tour
| 属性 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| arrow | 是否显示箭头,包含是否指向元素中心的配置 | `boolean` \| `{ pointAtCenter: boolean}` | `true` | |
| placement | 引导卡片相对于目标元素的位置 | `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` | `bottom` | |
| mask | 是否启用蒙层,也可传入配置改变蒙层样式和填充色 | `boolean` \| `{ style?: CSSProperties; color?: string; }` | `true` | |
| type | 类型,影响底色与文字颜色 | `default` \| `primary` | `default` | |
| open | 打开引导 | `boolean` | - | |
| current | 当前处于哪一步 | `number` | - | |
| scrollIntoViewOptions | 是否支持当前元素滚动到视窗内,也可传入配置指定滚动视窗的相关参数 | `boolean` \| `ScrollIntoViewOptions` | `true` | |
| indicatorsRender | 自定义指示器 | `v-slot:indicatorsRender="{current, total}"` | - | |
| zIndex | Tour 的层级 | `number` | `1001` | |
### Tour events
| 事件名称 | 说明 | 回调参数 | 版本 |
| -------- | ---------------------------------------- | --------------------------- | ---- | --- |
| close | 关闭引导时的回调函数 | `Function` | - | |
| finish | 引导完成时的回调 | `Function` | - | |
| change | 步骤改变时的回调,current 为当前前的步骤 | `(current: number) => void` | - | |
### TourStep 引导步骤卡片
| 属性 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| target | 获取引导卡片指向的元素,为空时居中于屏幕 | `() => HTMLElement` \| `HTMLElement` | - | |
| arrow | 是否显示箭头,包含是否指向元素中心的配置 | `boolean` \| `{ pointAtCenter: boolean}` | `true` | |
| cover | 展示的图片或者视频 | `VueNode` | - | |
| title | 标题 | `VueNode` | - | |
| description | 主要描述部分 | `VueNode` | - | |
| placement | 引导卡片相对于目标元素的位置 | `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` `bottom` | | |
| mask | 是否启用蒙层,也可传入配置改变蒙层样式和填充色,默认跟随 Tour 的 `mask` 属性 | `boolean` \| `{ style?: CSSProperties; color?: string; }` | `true` | |
| type | 类型,影响底色与文字颜色 | `default` \| `primary` | `default` | |
| nextButtonProps | 下一步按钮的属性 | `{ children: VueNode; onClick: Function }` | - | |
| prevButtonProps | 上一步按钮的属性 | `{ children: VueNode; onClick: Function }` | - | |
| scrollIntoViewOptions | 是否支持当前元素滚动到视窗内,也可传入配置指定滚动视窗的相关参数,默认跟随 Tour 的 `scrollIntoViewOptions` 属性 | `boolean` \| `ScrollIntoViewOptions` | `true` | |
### TourStep events
| 事件名称 | 说明 | 回调参数 | 版本 |
| -------- | -------------------- | ---------- | ---- | --- |
| close | 关闭引导时的回调函数 | `Function` | - | |

41
components/tour/interface.ts

@ -0,0 +1,41 @@
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue';
import { tourProps as VCTourProps, tourStepProps as VCTourStepProps } from '../vc-tour';
import type { VueNode } from '../_util/type';
export const tourProps = () => ({
...VCTourProps(),
steps: { type: Array as PropType<TourStepProps[]> },
prefixCls: { type: String },
current: { type: Number },
type: { type: String as PropType<'default' | 'primary'> }, // default 类型,影响底色与文字颜色
});
export type TourProps = Partial<ExtractPropTypes<ReturnType<typeof tourProps>>>;
export interface TourBtnProps {
children?: () => VueNode;
onClick?: () => void;
className?: string;
style?: CSSProperties;
}
export const tourStepProps = () => ({
...VCTourStepProps(),
cover: { type: Object as PropType<VueNode> }, // 展示的图片或者视频
nextButtonProps: {
type: Object as PropType<TourBtnProps>,
},
prevButtonProps: {
type: Object as PropType<TourBtnProps>,
},
current: { type: Number },
type: { type: String as PropType<'default' | 'primary'> }, // default 类型,影响底色与文字颜色
});
export type TourStepProps = Partial<ExtractPropTypes<ReturnType<typeof tourStepProps>>>;
export interface TourLocale {
Next: string;
Previous: string;
Finish: string;
}

154
components/tour/panelRender.tsx

@ -0,0 +1,154 @@
import { computed, defineComponent, toRefs } from 'vue';
import classNames from '../_util/classNames';
import CloseOutlined from '@ant-design/icons-vue/CloseOutlined';
import { tourStepProps } from './interface';
import type { TourBtnProps } from './interface';
import LocaleReceiver from '../locale/LocaleReceiver';
import Button from '../button';
import type { ButtonProps } from '../button';
import defaultLocale from '../locale/en_US';
import type { VueNode } from '../_util/type';
const panelRender = defineComponent({
props: tourStepProps(),
setup(props, { attrs, slots }) {
const { current, total } = toRefs(props);
const isLastStep = computed(() => current.value === total.value - 1);
const prevButtonProps = props.prevButtonProps as TourBtnProps;
const nextButtonProps = props.nextButtonProps as TourBtnProps;
const prevBtnClick = e => {
props.onPrev?.(e);
if (typeof prevButtonProps?.onClick === 'function') {
prevButtonProps?.onClick();
}
};
const nextBtnClick = e => {
if (isLastStep.value) {
props.onFinish?.(e);
} else {
props.onNext?.(e);
}
if (typeof nextButtonProps?.onClick === 'function') {
nextButtonProps?.onClick();
}
};
return () => {
const {
prefixCls,
title,
onClose,
cover,
description,
type: stepType,
arrow,
} = props;
const prevButtonProps = props.prevButtonProps as TourBtnProps;
const nextButtonProps = props.nextButtonProps as TourBtnProps;
let headerNode: VueNode;
if (title) {
headerNode = (
<div class={`${prefixCls}-header`}>
<div class={`${prefixCls}-title`}>{title}</div>
</div>
);
}
let descriptionNode: VueNode;
if (description) {
descriptionNode = <div class={`${prefixCls}-description`}>{description}</div>;
}
let coverNode: VueNode;
if (cover) {
coverNode = <div class={`${prefixCls}-cover`}>{cover}</div>;
}
let mergeIndicatorNode: VueNode;
if (slots.indicatorsRender) {
mergeIndicatorNode = slots.indicatorsRender({ current: current.value, total });
} else {
mergeIndicatorNode = [...Array.from({ length: total.value }).keys()].map(
(stepItem, index) => (
<span
key={stepItem}
class={classNames(
index === current.value && `${prefixCls}-indicator-active`,
`${prefixCls}-indicator`,
)}
/>
),
);
}
const mainBtnType = stepType === 'primary' ? 'default' : 'primary';
const secondaryBtnProps: ButtonProps = {
type: 'default',
ghost: stepType === 'primary',
};
return (
<LocaleReceiver componentName="Tour" defaultLocale={defaultLocale.Tour}>
{contextLocale => (
<div
{...attrs}
class={classNames(
stepType === 'primary' ? `${prefixCls}-primary` : '',
attrs.class,
`${prefixCls}-content`,
)}
>
{arrow && <div class={`${prefixCls}-arrow`} key="arrow" />}
<div class={`${prefixCls}-inner`}>
<CloseOutlined class={`${prefixCls}-close`} onClick={onClose} />
{coverNode}
{headerNode}
{descriptionNode}
<div class={`${prefixCls}-footer`}>
{total.value > 1 && (
<div class={`${prefixCls}-indicators`}>{mergeIndicatorNode}</div>
)}
<div class={`${prefixCls}-buttons`}>
{current.value !== 0 ? (
<Button
{...secondaryBtnProps}
{...prevButtonProps}
onClick={prevBtnClick}
size="small"
class={classNames(`${prefixCls}-prev-btn`, prevButtonProps?.className)}
>
{prevButtonProps?.children ?? contextLocale.Previous}
</Button>
) : null}
<Button
type={mainBtnType}
{...nextButtonProps}
onClick={nextBtnClick}
size="small"
class={classNames(`${prefixCls}-next-btn`, nextButtonProps?.className)}
>
{nextButtonProps?.children ??
(isLastStep.value ? contextLocale.Finish : contextLocale.Next)}
</Button>
</div>
</div>
</div>
</div>
)}
</LocaleReceiver>
);
};
},
});
export default panelRender;

250
components/tour/style/index.ts

@ -0,0 +1,250 @@
import { TinyColor } from '@ctrl/tinycolor';
import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import { resetComponent } from '../../style';
import getArrowStyle, { MAX_VERTICAL_CONTENT_RADIUS } from '../../style/placementArrow';
export interface ComponentToken {}
interface TourToken extends FullToken<'Tour'> {
tourZIndexPopup: number;
indicatorWidth: number;
indicatorHeight: number;
tourBorderRadius: number;
tourCloseSize: number;
}
// =============================== Base ===============================
const genBaseStyle: GenerateStyle<TourToken> = token => {
const {
componentCls,
lineHeight,
padding,
paddingXS,
borderRadius,
borderRadiusXS,
colorPrimary,
colorText,
colorFill,
indicatorHeight,
indicatorWidth,
boxShadowTertiary,
tourZIndexPopup,
fontSize,
colorBgContainer,
fontWeightStrong,
marginXS,
colorTextLightSolid,
tourBorderRadius,
colorWhite,
colorBgTextHover,
tourCloseSize,
motionDurationSlow,
antCls,
} = token;
return [
{
[componentCls]: {
...resetComponent(token),
color: colorText,
position: 'absolute',
zIndex: tourZIndexPopup,
display: 'block',
visibility: 'visible',
fontSize,
lineHeight,
width: 520,
'--antd-arrow-background-color': colorBgContainer,
'&-pure': {
maxWidth: '100%',
position: 'relative',
},
[`&${componentCls}-hidden`]: {
display: 'none',
},
// ============================= panel content ============================
[`${componentCls}-content`]: {
position: 'relative',
},
[`${componentCls}-inner`]: {
textAlign: 'start',
textDecoration: 'none',
borderRadius: tourBorderRadius,
boxShadow: boxShadowTertiary,
position: 'relative',
backgroundColor: colorBgContainer,
border: 'none',
backgroundClip: 'padding-box',
[`${componentCls}-close`]: {
position: 'absolute',
top: padding,
insetInlineEnd: padding,
color: token.colorIcon,
outline: 'none',
width: tourCloseSize,
height: tourCloseSize,
borderRadius: token.borderRadiusSM,
transition: `background-color ${token.motionDurationMid}, color ${token.motionDurationMid}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&:hover': {
color: token.colorIconHover,
backgroundColor: token.wireframe ? 'transparent' : token.colorFillContent,
},
},
[`${componentCls}-cover`]: {
textAlign: 'center',
padding: `${padding + tourCloseSize + paddingXS}px ${padding}px 0`,
img: {
width: '100%',
},
},
[`${componentCls}-header`]: {
padding: `${padding}px ${padding}px ${paddingXS}px`,
[`${componentCls}-title`]: {
lineHeight,
fontSize,
fontWeight: fontWeightStrong,
},
},
[`${componentCls}-description`]: {
padding: `0 ${padding}px`,
lineHeight,
wordWrap: 'break-word',
},
[`${componentCls}-footer`]: {
padding: `${paddingXS}px ${padding}px ${padding}px`,
textAlign: 'end',
borderRadius: `0 0 ${borderRadiusXS}px ${borderRadiusXS}px`,
display: 'flex',
[`${componentCls}-indicators`]: {
display: 'inline-block',
[`${componentCls}-indicator`]: {
width: indicatorWidth,
height: indicatorHeight,
display: 'inline-block',
borderRadius: '50%',
background: colorFill,
'&:not(:last-child)': {
marginInlineEnd: indicatorHeight,
},
'&-active': {
background: colorPrimary,
},
},
},
[`${componentCls}-buttons`]: {
marginInlineStart: 'auto',
[`${antCls}-btn`]: {
marginInlineStart: marginXS,
},
},
},
},
// ============================= primary type ===========================
// `$` for panel, `&$` for pure panel
[`${componentCls}-primary, &${componentCls}-primary`]: {
'--antd-arrow-background-color': colorPrimary,
[`${componentCls}-inner`]: {
color: colorTextLightSolid,
textAlign: 'start',
textDecoration: 'none',
backgroundColor: colorPrimary,
borderRadius,
boxShadow: boxShadowTertiary,
[`${componentCls}-close`]: {
color: colorTextLightSolid,
},
[`${componentCls}-indicators`]: {
[`${componentCls}-indicator`]: {
background: new TinyColor(colorTextLightSolid).setAlpha(0.15).toRgbString(),
'&-active': {
background: colorTextLightSolid,
},
},
},
[`${componentCls}-prev-btn`]: {
color: colorTextLightSolid,
borderColor: new TinyColor(colorTextLightSolid).setAlpha(0.15).toRgbString(),
backgroundColor: colorPrimary,
'&:hover': {
backgroundColor: new TinyColor(colorTextLightSolid).setAlpha(0.15).toRgbString(),
borderColor: 'transparent',
},
},
[`${componentCls}-next-btn`]: {
color: colorPrimary,
borderColor: 'transparent',
background: colorWhite,
'&:hover': {
background: new TinyColor(colorBgTextHover).onBackground(colorWhite).toRgbString(),
},
},
},
},
},
// ============================= mask ===========================
[`${componentCls}-mask`]: {
[`${componentCls}-placeholder-animated`]: {
transition: `all ${motionDurationSlow}`,
},
},
// =========== Limit left and right placement radius ==============
[[
'&-placement-left',
'&-placement-leftTop',
'&-placement-leftBottom',
'&-placement-right',
'&-placement-rightTop',
'&-placement-rightBottom',
].join(',')]: {
[`${componentCls}-inner`]: {
borderRadius: Math.min(tourBorderRadius, MAX_VERTICAL_CONTENT_RADIUS),
},
},
},
// ============================= Arrow ===========================
getArrowStyle<TourToken>(token, {
colorBg: 'var(--antd-arrow-background-color)',
contentRadius: tourBorderRadius,
limitVerticalRadius: true,
}),
];
};
// ============================== Export ==============================
export default genComponentStyleHook('Tour', token => {
const { borderRadiusLG, fontSize, lineHeight } = token;
const TourToken = mergeToken<TourToken>(token, {
tourZIndexPopup: token.zIndexPopupBase + 70,
indicatorWidth: 6,
indicatorHeight: 6,
tourBorderRadius: borderRadiusLG,
tourCloseSize: fontSize * lineHeight,
});
return [genBaseStyle(TourToken)];
});

35
components/tour/useMergedType.ts

@ -0,0 +1,35 @@
import useMergedState from '../_util/hooks/useMergedState';
import type { TourProps } from './interface';
import type { Ref } from 'vue';
import { computed, watch } from 'vue';
interface Props {
defaultType?: string;
steps?: TourProps['steps'];
current?: Ref<number>;
defaultCurrent?: number;
}
/**
* returns the merged type of a step or the default type.
*/
const useMergedType = ({ defaultType, steps = [], current, defaultCurrent }: Props) => {
const [innerCurrent, updateInnerCurrent] = useMergedState<number | undefined>(defaultCurrent, {
value: current,
});
watch(current, val => {
if (val === undefined) return;
updateInnerCurrent(val);
});
const innerType = computed(() => {
return typeof innerCurrent.value === 'number' ? steps[innerCurrent.value]?.type : defaultType;
});
const currentMergedType = computed(() => innerType.value ?? defaultType);
return { currentMergedType, updateInnerCurrent };
};
export default useMergedType;

130
components/vc-tour/Mask.tsx

@ -0,0 +1,130 @@
import type { CSSProperties } from 'vue';
import { defineComponent } from 'vue';
import classNames from '../_util/classNames';
import type { PosInfo } from './hooks/useTarget';
import useId from '../_util/hooks/useId';
import Portal from '../_util/PortalWrapper';
import { someType, objectType, booleanType } from '../_util/type';
const COVER_PROPS = {
fill: 'transparent',
pointerEvents: 'auto',
};
export interface MaskProps {
prefixCls?: string;
pos: PosInfo; //
rootClassName?: string;
showMask?: boolean;
style?: CSSProperties;
fill?: string;
open?: boolean;
animated?: boolean | { placeholder: boolean };
zIndex?: number;
}
const Mask = defineComponent({
name: 'Mask',
props: {
prefixCls: { type: String },
pos: objectType<PosInfo>(), //
rootClassName: { type: String },
showMask: booleanType(),
fill: { type: String, default: 'rgba(0,0,0,0.5)' },
open: booleanType(),
animated: someType<boolean | { placeholder: boolean }>([Boolean, Object]),
zIndex: { type: Number },
},
setup(props, { attrs }) {
return () => {
const { prefixCls, open, rootClassName, pos, showMask, fill, animated, zIndex } = props;
const id = useId();
const maskId = `${prefixCls}-mask-${id}`;
const mergedAnimated = typeof animated === 'object' ? animated?.placeholder : animated;
console.log(open);
return (
<Portal
visible={open}
v-slots={{
default: () =>
open && (
<div
{...attrs}
class={classNames(`${prefixCls}-mask`, rootClassName, attrs.class)}
style={[
{
position: 'fixed',
left: 0,
right: 0,
top: 0,
bottom: 0,
zIndex,
},
]}
>
{showMask ? (
<svg
style={{
width: '100%',
height: '100%',
}}
>
<defs>
<mask id={maskId}>
<rect x="0" y="0" width="100vw" height="100vh" fill="white" />
{pos && (
<rect
x={pos.left}
y={pos.top}
rx={pos.radius}
width={pos.width}
height={pos.height}
fill="black"
class={mergedAnimated ? `${prefixCls}-placeholder-animated` : ''}
/>
)}
</mask>
</defs>
<rect
x="0"
y="0"
width="100%"
height="100%"
fill={fill}
mask={`url(#${maskId})`}
/>
{/* Block click region */}
{pos && (
<>
<rect {...COVER_PROPS} x="0" y="0" width="100%" height={pos.top} />
<rect {...COVER_PROPS} x="0" y="0" width={pos.left} height="100%" />
<rect
{...COVER_PROPS}
x="0"
y={pos.top + pos.height}
width="100%"
height={`calc(100vh - ${pos.top + pos.height}px)`}
/>
<rect
{...COVER_PROPS}
x={pos.left + pos.width}
y="0"
width={`calc(100vw - ${pos.left + pos.width}px)`}
height="100%"
/>
</>
)}
</svg>
) : null}
</div>
),
}}
/>
);
};
},
});
export default Mask;

243
components/vc-tour/Tour.tsx

@ -0,0 +1,243 @@
import { ref, computed, watch, watchEffect, defineComponent, toRefs, shallowRef } from 'vue';
import type { CSSProperties, ExtractPropTypes } from 'vue';
import type { VueNode } from '../_util/type';
import Trigger, { triggerProps } from '../vc-trigger';
import classNames from '../_util/classNames';
import useMergedState from '../_util/hooks/useMergedState';
import useTarget from './hooks/useTarget';
import type { Gap } from './hooks/useTarget';
import TourStep from './TourStep';
import type { TourStepInfo, TourStepProps } from './interface';
import Mask from './Mask';
import { getPlacements } from './placements';
import type { PlacementType } from './placements';
import { initDefaultProps } from '../_util/props-util';
import useScrollLocker from './hooks/useScrollLocker';
import canUseDom from '../_util/canUseDom';
import {
someType,
stringType,
arrayType,
objectType,
functionType,
booleanType,
} from '../_util/type';
const CENTER_PLACEHOLDER: CSSProperties = {
left: '50%',
top: '50%',
width: 1,
height: 1,
};
export const tourProps = () => {
const { builtinPlacements, ...pickedTriggerProps } = triggerProps();
return {
...pickedTriggerProps,
steps: arrayType<TourStepInfo[]>(),
open: booleanType(),
defaultCurrent: { type: Number },
current: { type: Number },
onChange: functionType<(current: number) => void>(),
onClose: functionType<(current: number) => void>(),
onFinish: functionType<() => void>(),
mask: someType<boolean | { style?: CSSProperties; color?: string }>([Boolean, Object], true),
arrow: someType<boolean | { pointAtCenter: boolean }>([Boolean, Object], true),
rootClassName: { type: String },
placement: stringType<PlacementType>('bottom'),
prefixCls: { type: String, default: 'rc-tour' },
renderPanel: functionType<(props: TourStepProps, current: number) => VueNode>(),
gap: objectType<Gap>(),
animated: someType<boolean | { placeholder: boolean }>([Boolean, Object]),
scrollIntoViewOptions: someType<boolean | ScrollIntoViewOptions>([Boolean, Object], true),
zIndex: { type: Number, default: 1001 },
};
};
export type TourProps = Partial<ExtractPropTypes<ReturnType<typeof tourProps>>>;
const Tour = defineComponent({
name: 'Tour',
props: initDefaultProps(tourProps(), {}),
setup(props) {
const { defaultCurrent, placement, mask, scrollIntoViewOptions, open, gap, arrow } =
toRefs(props);
const triggerRef = ref();
const [mergedCurrent, setMergedCurrent] = useMergedState(0, {
value: computed(() => props.current),
defaultValue: defaultCurrent.value,
});
const [mergedOpen, setMergedOpen] = useMergedState(undefined, {
value: computed(() => props.open),
postState: origin =>
mergedCurrent.value < 0 || mergedCurrent.value >= props.steps.length
? false
: origin ?? true,
});
const openRef = shallowRef(mergedOpen.value);
watchEffect(() => {
if (mergedOpen.value && !openRef.value) {
setMergedCurrent(0);
}
openRef.value = mergedOpen.value;
});
const curStep = computed(() => (props.steps[mergedCurrent.value] || {}) as TourStepInfo);
const mergedPlacement = computed(() => curStep.value.placement ?? placement.value);
const mergedMask = computed(() => mergedOpen.value && (curStep.value.mask ?? mask.value));
const mergedScrollIntoViewOptions = computed(
() => curStep.value.scrollIntoViewOptions ?? scrollIntoViewOptions.value,
);
const [posInfo, targetElement] = useTarget(
computed(() => curStep.value.target),
open,
gap,
mergedScrollIntoViewOptions,
);
// ========================= arrow =========================
const mergedArrow = computed(() =>
targetElement.value
? typeof curStep.value.arrow === 'undefined'
? arrow.value
: curStep.value.arrow
: false,
);
const arrowPointAtCenter = computed(() =>
typeof mergedArrow.value === 'object' ? mergedArrow.value.pointAtCenter : false,
);
watch(arrowPointAtCenter, () => {
triggerRef.value?.forcePopupAlign();
});
watch(mergedCurrent, () => {
triggerRef.value?.forcePopupAlign();
});
// ========================= Change =========================
const onInternalChange = (nextCurrent: number) => {
setMergedCurrent(nextCurrent);
props.onChange?.(nextCurrent);
};
// ========================= lock scroll =========================
const lockScroll = computed(() => mergedOpen.value && canUseDom());
useScrollLocker(lockScroll);
return () => {
const {
prefixCls,
steps,
onClose,
onFinish,
rootClassName,
renderPanel,
animated,
zIndex,
...restProps
} = props;
// ========================= Render =========================
// Skip if not init yet
if (targetElement.value === undefined) {
return null;
}
const handleClose = () => {
setMergedOpen(false);
onClose?.(mergedCurrent.value);
};
const mergedShowMask =
typeof mergedMask.value === 'boolean' ? mergedMask.value : !!mergedMask.value;
const mergedMaskStyle = typeof mergedMask.value === 'boolean' ? undefined : mergedMask.value;
// when targetElement is not exist, use body as triggerDOMNode
const getTriggerDOMNode = () => {
return targetElement.value || document.body;
};
const getPopupElement = () => (
<TourStep
arrow={mergedArrow.value}
key="content"
prefixCls={prefixCls}
total={steps.length}
renderPanel={renderPanel}
onPrev={() => {
onInternalChange(mergedCurrent.value - 1);
}}
onNext={() => {
onInternalChange(mergedCurrent.value + 1);
}}
onClose={handleClose}
current={mergedCurrent.value}
onFinish={() => {
handleClose();
onFinish?.();
}}
{...curStep.value}
/>
);
return (
<>
<Mask
zIndex={zIndex}
prefixCls={prefixCls}
pos={posInfo.value}
showMask={mergedShowMask}
style={mergedMaskStyle?.style}
fill={mergedMaskStyle?.color}
open={mergedOpen.value}
animated={animated}
rootClassName={rootClassName}
/>
<Trigger
builtinPlacements={getPlacements(arrowPointAtCenter.value)}
{...restProps}
ref={triggerRef}
popupStyle={
!curStep.value.target
? {
...curStep.value.style,
position: 'fixed',
left: CENTER_PLACEHOLDER.left,
top: CENTER_PLACEHOLDER.top,
transform: 'translate(-50%, -50%)',
}
: curStep.value.style
}
popupPlacement={!curStep.value.target ? 'center' : mergedPlacement.value}
popupVisible={mergedOpen.value}
popupClassName={classNames(rootClassName, curStep.value.className)}
prefixCls={prefixCls}
popup={getPopupElement}
forceRender={false}
destroyPopupOnHide
zIndex={zIndex}
mask={false}
getTriggerDOMNode={getTriggerDOMNode}
>
<div
class={classNames(rootClassName, `${prefixCls}-target-placeholder`)}
style={{
...(posInfo.value || CENTER_PLACEHOLDER),
position: 'fixed',
pointerEvents: 'none',
}}
/>
</Trigger>
</>
);
};
},
});
export default Tour;

55
components/vc-tour/TourStep/DefaultPanel.tsx

@ -0,0 +1,55 @@
import { defineComponent } from 'vue';
import classNames from '../../_util/classNames';
import { tourStepProps } from '../interface';
import type { TourStepProps } from '../interface';
const DefaultPanel = defineComponent({
name: 'DefaultPanel',
props: tourStepProps(),
setup(props, { attrs }) {
return () => {
const { prefixCls, current, total, title, description, onClose, onPrev, onNext, onFinish } =
props as TourStepProps;
return (
<div {...attrs} class={classNames(`${prefixCls}-content`, attrs.class)}>
<div class={`${prefixCls}-inner`}>
<button type="button" onClick={onClose} aria-label="Close" class={`${prefixCls}-close`}>
<span class={`${prefixCls}-close-x`}>&times;</span>
</button>
<div class={`${prefixCls}-header`}>
<div class={`${prefixCls}-title`}>{title}</div>
</div>
<div class={`${prefixCls}-description`}>{description}</div>
<div class={`${prefixCls}-footer`}>
<div class={`${prefixCls}-sliders`}>
{total > 1
? [...Array.from({ length: total }).keys()].map((item, index) => {
return <span key={item} class={index === current ? 'active' : ''} />;
})
: null}
</div>
<div class={`${prefixCls}-buttons`}>
{current !== 0 ? (
<button class={`${prefixCls}-prev-btn`} onClick={onPrev}>
Prev
</button>
) : null}
{current === total - 1 ? (
<button class={`${prefixCls}-finish-btn`} onClick={onFinish}>
Finish
</button>
) : (
<button class={`${prefixCls}-next-btn`} onClick={onNext}>
Next
</button>
)}
</div>
</div>
</div>
</div>
);
};
},
});
export default DefaultPanel;

25
components/vc-tour/TourStep/index.tsx

@ -0,0 +1,25 @@
import { defineComponent } from 'vue';
import DefaultPanel from './DefaultPanel';
import { tourStepProps } from '../interface';
const TourStep = defineComponent({
name: 'TourStep',
props: tourStepProps(),
setup(props, { attrs }) {
return () => {
const { current, renderPanel } = props;
return (
<>
{typeof renderPanel === 'function' ? (
renderPanel({ ...attrs, ...props }, current)
) : (
<DefaultPanel {...attrs} {...props} />
)}
</>
);
};
},
});
export default TourStep;

44
components/vc-tour/hooks/useScrollLocker.tsx

@ -0,0 +1,44 @@
import type { Ref } from 'vue';
import { computed, watchEffect } from 'vue';
import { updateCSS, removeCSS } from '../../vc-util/Dom/dynamicCSS';
import getScrollBarSize from '../../_util/getScrollBarSize';
const UNIQUE_ID = `vc-util-locker-${Date.now()}`;
let uuid = 0;
/**../vc-util/Dom/dynam
* Test usage export. Do not use in your production
*/
export function isBodyOverflowing() {
return (
document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight) &&
window.innerWidth > document.body.offsetWidth
);
}
export default function useScrollLocker(lock?: Ref<boolean>) {
const mergedLock = computed(() => !!lock && !!lock.value);
const id = computed(() => {
uuid += 1;
return `${UNIQUE_ID}_${uuid}`;
});
watchEffect(() => {
if (mergedLock.value) {
const scrollbarSize = getScrollBarSize();
const isOverflow = isBodyOverflowing();
updateCSS(
`
html body {
overflow-y: hidden;
${isOverflow ? `width: calc(100% - ${scrollbarSize}px);` : ''}
}`,
id.value,
);
} else {
removeCSS(id.value);
}
});
}

100
components/vc-tour/hooks/useTarget.ts

@ -0,0 +1,100 @@
import { computed, watchEffect, watch } from 'vue';
import type { Ref } from 'vue';
import { isInViewPort } from '../util';
import type { TourStepInfo } from '..';
import useState from '../../_util/hooks/useState';
export interface Gap {
offset?: number;
radius?: number;
}
export interface PosInfo {
left: number;
top: number;
height: number;
width: number;
radius: number;
}
export default function useTarget(
target: Ref<TourStepInfo['target']>,
open: Ref<boolean>,
gap?: Ref<Gap>,
scrollIntoViewOptions?: Ref<boolean | ScrollIntoViewOptions>,
): [Ref<PosInfo>, Ref<HTMLElement>] {
// ========================= Target =========================
// We trade `undefined` as not get target by function yet.
// `null` as empty target.
const [targetElement, setTargetElement] = useState<null | HTMLElement | undefined>(undefined);
watchEffect(() => {
const nextElement = typeof target.value === 'function' ? (target.value as any)() : target.value;
setTargetElement(nextElement || null);
});
// ========================= Align ==========================
const [posInfo, setPosInfo] = useState<PosInfo>(null);
const updatePos = () => {
if (targetElement.value) {
// Exist target element. We should scroll and get target position
if (!isInViewPort(targetElement.value) && open.value) {
targetElement.value.scrollIntoView(scrollIntoViewOptions.value);
}
const { left, top, width, height } = targetElement.value.getBoundingClientRect();
const nextPosInfo: PosInfo = { left, top, width, height, radius: 0 };
setPosInfo(nextPosInfo);
} else {
// Not exist target which means we just show in center
setPosInfo(null);
}
};
watchEffect(() => {
updatePos();
// update when window resize
window.addEventListener('resize', updatePos);
return () => {
window.removeEventListener('resize', updatePos);
};
});
watch(
open,
val => {
updatePos();
// update when window resize
if (val) {
window.addEventListener('resize', updatePos);
} else {
window.removeEventListener('resize', updatePos);
}
},
{ immediate: true },
);
// ======================== PosInfo =========================
const mergedPosInfo = computed(() => {
if (!posInfo.value) {
return posInfo.value;
}
const gapOffset = gap.value?.offset || 6;
const gapRadius = gap.value?.radius || 2;
return {
left: posInfo.value.left - gapOffset,
top: posInfo.value.top - gapOffset,
width: posInfo.value.width + gapOffset * 2,
height: posInfo.value.height + gapOffset * 2,
radius: gapRadius,
};
});
return [mergedPosInfo, targetElement];
}

6
components/vc-tour/index.tsx

@ -0,0 +1,6 @@
import Tour from './Tour';
export type { TourProps } from './Tour';
export { tourProps } from './Tour';
export type { TourStepInfo, TourStepProps } from './interface';
export { tourStepInfo, tourStepProps } from './interface';
export default Tour;

36
components/vc-tour/interface.ts

@ -0,0 +1,36 @@
import type { ExtractPropTypes, CSSProperties } from 'vue';
import type { PlacementType } from './placements';
import type { VueNode } from '../_util/type';
import { someType, stringType, objectType, functionType } from '../_util/type';
export const tourStepInfo = () => ({
arrow: someType<boolean | { pointAtCenter: boolean }>([Boolean, Object]),
target: someType<HTMLElement | (() => HTMLElement) | null | (() => null)>([
String,
Function,
Object,
]),
title: someType<string | VueNode>([String, Object]),
description: someType<string | VueNode>([String, Object]),
placement: stringType<PlacementType>(),
mask: someType<boolean | { style?: CSSProperties; color?: string }>([Object, Boolean], true),
className: { type: String },
style: objectType<CSSProperties>(),
scrollIntoViewOptions: someType<boolean | ScrollIntoViewOptions>([Boolean, Object]),
});
export type TourStepInfo = Partial<ExtractPropTypes<ReturnType<typeof tourStepInfo>>>;
export const tourStepProps = () => ({
...tourStepInfo(),
prefixCls: { type: String },
total: { type: Number },
current: { type: Number },
onClose: functionType<(e: MouseEvent) => void>(),
onFinish: functionType<(e: MouseEvent) => void>(),
renderPanel: functionType<(step: any, current: number) => VueNode>(),
onPrev: functionType<(e: MouseEvent) => void>(),
onNext: functionType<(e: MouseEvent) => void>(),
});
export type TourStepProps = Partial<ExtractPropTypes<ReturnType<typeof tourStepProps>>>;

135
components/vc-tour/placements.tsx

@ -0,0 +1,135 @@
export type PlacementType =
| 'left'
| 'leftTop'
| 'leftBottom'
| 'right'
| 'rightTop'
| 'rightBottom'
| 'top'
| 'topLeft'
| 'topRight'
| 'bottom'
| 'bottomLeft'
| 'bottomRight'
| 'center';
const targetOffset = [0, 0];
export type AlignPointTopBottom = 't' | 'b' | 'c';
export type AlignPointLeftRight = 'l' | 'r' | 'c';
/** Two char of 't' 'b' 'c' 'l' 'r'. Example: 'lt' */
export type AlignPoint = `${AlignPointTopBottom}${AlignPointLeftRight}`;
export interface AlignType {
/**
* move point of source node to align with point of target node.
* Such as ['tr','cc'], align top right point of source node with center point of target node.
* Point can be 't'(top), 'b'(bottom), 'c'(center), 'l'(left), 'r'(right) */
points?: (string | AlignPoint)[];
/**
* offset source node by offset[0] in x and offset[1] in y.
* If offset contains percentage string value, it is relative to sourceNode region.
*/
offset?: number[];
/**
* offset target node by offset[0] in x and offset[1] in y.
* If targetOffset contains percentage string value, it is relative to targetNode region.
*/
targetOffset?: number[];
/**
* If adjustX field is true, will adjust source node in x direction if source node is invisible.
* If adjustY field is true, will adjust source node in y direction if source node is invisible.
*/
overflow?: {
adjustX?: boolean | number;
adjustY?: boolean | number;
shiftX?: boolean | number;
shiftY?: boolean | number;
};
/** Auto adjust arrow position */
autoArrow?: boolean;
/**
* Config visible region check of html node. Default `visible`:
* - `visible`: The visible region of user browser window. Use `clientHeight` for check.
* - `scroll`: The whole region of the html scroll area. Use `scrollHeight` for check.
*/
htmlRegion?: 'visible' | 'scroll';
/**
* Whether use css right instead of left to position
*/
useCssRight?: boolean;
/**
* Whether use css bottom instead of top to position
*/
useCssBottom?: boolean;
/**
* Whether use css transform instead of left/top/right/bottom to position if browser supports.
* Defaults to false.
*/
useCssTransform?: boolean;
ignoreShake?: boolean;
}
export type BuildInPlacements = Record<string, AlignType>;
const basePlacements: BuildInPlacements = {
left: {
points: ['cr', 'cl'],
offset: [-8, 0],
},
right: {
points: ['cl', 'cr'],
offset: [8, 0],
},
top: {
points: ['bc', 'tc'],
offset: [0, -8],
},
bottom: {
points: ['tc', 'bc'],
offset: [0, 8],
},
topLeft: {
points: ['bl', 'tl'],
offset: [0, -8],
},
leftTop: {
points: ['tr', 'tl'],
offset: [-8, 0],
},
topRight: {
points: ['br', 'tr'],
offset: [0, -8],
},
rightTop: {
points: ['tl', 'tr'],
offset: [8, 0],
},
bottomRight: {
points: ['tr', 'br'],
offset: [0, 8],
},
rightBottom: {
points: ['bl', 'br'],
offset: [8, 0],
},
bottomLeft: {
points: ['tl', 'bl'],
offset: [0, 8],
},
leftBottom: {
points: ['br', 'bl'],
offset: [-8, 0],
},
};
export function getPlacements(arrowPointAtCenter = false) {
const placements: BuildInPlacements = {};
Object.keys(basePlacements).forEach(key => {
placements[key] = { ...basePlacements[key], autoArrow: arrowPointAtCenter, targetOffset };
});
return placements;
}
export const placements = getPlacements();

7
components/vc-tour/util.ts

@ -0,0 +1,7 @@
export function isInViewPort(element: HTMLElement) {
const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
const { top, right, bottom, left } = element.getBoundingClientRect();
return top >= 0 && left >= 0 && right <= viewWidth && bottom <= viewHeight;
}

56
components/vc-trigger/Trigger.tsx

@ -1,6 +1,6 @@
import type { CSSProperties, HTMLAttributes, PropType } from 'vue';
import type { HTMLAttributes } from 'vue';
import { computed, defineComponent, inject, provide, shallowRef } from 'vue';
import PropTypes from '../_util/vue-types';
import { triggerProps, noop } from './interface';
import contains from '../vc-util/Dom/contains';
import raf from '../_util/raf';
import {
@ -21,17 +21,6 @@ import { cloneElement } from '../_util/vnode';
import supportsPassive from '../_util/supportsPassive';
import { useInjectTrigger, useProvidePortal } from './context';
function noop() {}
function returnEmptyString() {
return '';
}
function returnDocument(element) {
if (element) {
return element.ownerDocument;
}
return window.document;
}
const ALL_HANDLERS = [
'onClick',
'onMousedown',
@ -47,46 +36,7 @@ export default defineComponent({
name: 'Trigger',
mixins: [BaseMixin],
inheritAttrs: false,
props: {
action: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]).def([]),
showAction: PropTypes.any.def([]),
hideAction: PropTypes.any.def([]),
getPopupClassNameFromAlign: PropTypes.any.def(returnEmptyString),
onPopupVisibleChange: Function as PropType<(open: boolean) => void>,
afterPopupVisibleChange: PropTypes.func.def(noop),
popup: PropTypes.any,
popupStyle: { type: Object as PropType<CSSProperties>, default: undefined as CSSProperties },
prefixCls: PropTypes.string.def('rc-trigger-popup'),
popupClassName: PropTypes.string.def(''),
popupPlacement: String,
builtinPlacements: PropTypes.object,
popupTransitionName: String,
popupAnimation: PropTypes.any,
mouseEnterDelay: PropTypes.number.def(0),
mouseLeaveDelay: PropTypes.number.def(0.1),
zIndex: Number,
focusDelay: PropTypes.number.def(0),
blurDelay: PropTypes.number.def(0.15),
getPopupContainer: Function,
getDocument: PropTypes.func.def(returnDocument),
forceRender: { type: Boolean, default: undefined },
destroyPopupOnHide: { type: Boolean, default: false },
mask: { type: Boolean, default: false },
maskClosable: { type: Boolean, default: true },
// onPopupAlign: PropTypes.func.def(noop),
popupAlign: PropTypes.object.def(() => ({})),
popupVisible: { type: Boolean, default: undefined },
defaultPopupVisible: { type: Boolean, default: false },
maskTransitionName: String,
maskAnimation: String,
stretch: String,
alignPoint: { type: Boolean, default: undefined }, // Maybe we can support user pass position in the future
autoDestroy: { type: Boolean, default: false },
mobile: Object,
getTriggerDOMNode: Function as PropType<(d?: HTMLElement) => HTMLElement>,
// portal context will change
tryPopPortal: Boolean, // no need reactive
},
props: triggerProps(),
setup(props) {
const align = computed(() => {
const { popupPlacement, popupAlign, builtinPlacements } = props;

5
components/vc-trigger/index.ts

@ -1,3 +1,8 @@
// based on rc-trigger 5.2.10
import Trigger from './Trigger';
import { triggerProps } from './interface';
import type { TriggerProps } from './interface';
export { triggerProps };
export type { TriggerProps };
export default Trigger;

59
components/vc-trigger/interface.ts

@ -1,5 +1,6 @@
import type { CSSProperties, TransitionProps } from 'vue';
import type { CSSProperties, ExtractPropTypes, TransitionProps, PropType } from 'vue';
import type { VueNode } from '../_util/type';
import PropTypes from '../_util/vue-types';
/** Two char of 't' 'b' 'c' 'l' 'r'. Example: 'lt' */
export type AlignPoint = string;
@ -70,3 +71,59 @@ export interface MobileConfig {
popupStyle?: CSSProperties;
popupRender?: (originNode: VueNode) => VueNode;
}
function returnEmptyString() {
return '';
}
function returnDocument(element) {
if (element) {
return element.ownerDocument;
}
return window.document;
}
export function noop() {}
export const triggerProps = () => ({
action: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]).def([]),
showAction: PropTypes.any.def([]),
hideAction: PropTypes.any.def([]),
getPopupClassNameFromAlign: PropTypes.any.def(returnEmptyString),
onPopupVisibleChange: Function as PropType<(open: boolean) => void>,
afterPopupVisibleChange: PropTypes.func.def(noop),
popup: PropTypes.any,
popupStyle: { type: Object as PropType<CSSProperties>, default: undefined as CSSProperties },
prefixCls: PropTypes.string.def('rc-trigger-popup'),
popupClassName: PropTypes.string.def(''),
popupPlacement: String,
builtinPlacements: PropTypes.object,
popupTransitionName: String,
popupAnimation: PropTypes.any,
mouseEnterDelay: PropTypes.number.def(0),
mouseLeaveDelay: PropTypes.number.def(0.1),
zIndex: Number,
focusDelay: PropTypes.number.def(0),
blurDelay: PropTypes.number.def(0.15),
getPopupContainer: Function,
getDocument: PropTypes.func.def(returnDocument),
forceRender: { type: Boolean, default: undefined },
destroyPopupOnHide: { type: Boolean, default: false },
mask: { type: Boolean, default: false },
maskClosable: { type: Boolean, default: true },
// onPopupAlign: PropTypes.func.def(noop),
popupAlign: PropTypes.object.def(() => ({})),
popupVisible: { type: Boolean, default: undefined },
defaultPopupVisible: { type: Boolean, default: false },
maskTransitionName: String,
maskAnimation: String,
stretch: String,
alignPoint: { type: Boolean, default: undefined }, // Maybe we can support user pass position in the future
autoDestroy: { type: Boolean, default: false },
mobile: Object,
getTriggerDOMNode: Function as PropType<(d?: HTMLElement) => HTMLElement>,
// portal context will change
tryPopPortal: Boolean, // no need reactive
});
export type TriggerProps = Partial<ExtractPropTypes<ReturnType<typeof triggerProps>>>;

Loading…
Cancel
Save