Skip to content

Commit

Permalink
feat: Tour support aria-* in closable (#52)
Browse files Browse the repository at this point in the history
* feat: Tour support aria-* in closable

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* refactor: simplify code

* feat: optimize code

---------

Co-authored-by: 二货机器人 <[email protected]>
  • Loading branch information
kiner-tang and zombieJ authored Mar 6, 2024
1 parent 2116e35 commit dc93652
Show file tree
Hide file tree
Showing 9 changed files with 553 additions and 88 deletions.
26 changes: 26 additions & 0 deletions docs/examples/closable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,32 @@ const App = () => {
mask: true,
style: { color: 'red' },
},
{
title: '删除',
closable: true,
description: (
<div>
<span>危险操作:删除一条数据</span>
<button>帮助文档</button>
</div>
),
target: () => deleteBtnRef.current,
mask: true,
style: { color: 'red' },
},
{
title: '删除',
closable: { closeIcon: <span className='custom-close'>X</span>, 'aria-label': 'CloseBtn' },
description: (
<div>
<span>危险操作:删除一条数据</span>
<button>帮助文档</button>
</div>
),
target: () => deleteBtnRef.current,
mask: true,
style: { color: 'red' },
},
]}
/>
</div>
Expand Down
71 changes: 26 additions & 45 deletions src/Tour.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import type { ReactNode } from 'react';
import * as React from 'react';

import Portal from '@rc-component/portal';
import type { TriggerProps, TriggerRef } from '@rc-component/trigger';
import type { TriggerRef } from '@rc-component/trigger';
import Trigger from '@rc-component/trigger';
import classNames from 'classnames';
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import type { Gap } from './hooks/useTarget';
import { useMemo } from 'react';
import { useClosable } from './hooks/useClosable';
import useTarget from './hooks/useTarget';
import type { TourProps } from './interface';
import Mask from './Mask';
import type { PlacementType } from './placements';
import { getPlacements } from './placements';
import type { TourStepInfo, TourStepProps } from './TourStep';
import TourStep from './TourStep';
import { getPlacement } from './util';
import { useMemo } from 'react';

const CENTER_PLACEHOLDER: React.CSSProperties = {
left: '50%',
Expand All @@ -24,40 +22,11 @@ const CENTER_PLACEHOLDER: React.CSSProperties = {
height: 1,
};
const defaultScrollIntoViewOptions: ScrollIntoViewOptions = {
block: "center",
inline: "center"
}

export interface TourProps
extends Pick<TriggerProps, 'onPopupAlign'> {
steps?: TourStepInfo[];
open?: boolean;
defaultCurrent?: number;
current?: number;
onChange?: (current: number) => void;
onClose?: (current: number) => void;
onFinish?: () => void;
closeIcon?: TourStepProps['closeIcon'];
mask?:
| boolean
| {
style?: React.CSSProperties;
// to fill mask color, e.g. rgba(80,0,0,0.5)
color?: string;
};
arrow?: boolean | { pointAtCenter: boolean };
rootClassName?: string;
placement?: PlacementType;
prefixCls?: string;
renderPanel?: (props: TourStepProps, current: number) => ReactNode;
gap?: Gap;
animated?: boolean | { placeholder: boolean };
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions;
zIndex?: number;
getPopupContainer?: TriggerProps['getPopupContainer'];
builtinPlacements?: TriggerProps['builtinPlacements'] | ((config?: { arrowPointAtCenter?: boolean }) => TriggerProps['builtinPlacements']);
disabledInteraction?: boolean;
}
block: 'center',
inline: 'center',
};

export type { TourProps };

const Tour: React.FC<TourProps> = props => {
const {
Expand All @@ -79,6 +48,7 @@ const Tour: React.FC<TourProps> = props => {
scrollIntoViewOptions = defaultScrollIntoViewOptions,
zIndex = 1001,
closeIcon,
closable,
builtinPlacements,
disabledInteraction,
...restProps
Expand Down Expand Up @@ -115,12 +85,21 @@ const Tour: React.FC<TourProps> = props => {
arrow: stepArrow,
className: stepClassName,
mask: stepMask,
scrollIntoViewOptions: stepScrollIntoViewOptions = defaultScrollIntoViewOptions,
scrollIntoViewOptions:
stepScrollIntoViewOptions = defaultScrollIntoViewOptions,
closeIcon: stepCloseIcon,
closable: stepClosable,
} = steps[mergedCurrent] || {};

const mergedClosable = useClosable(
prefixCls,
stepClosable,
stepCloseIcon,
closable,
closeIcon,
);

const mergedMask = mergedOpen && (stepMask ?? mask);
const mergedCloseIcon = stepCloseIcon ?? closeIcon;
const mergedScrollIntoViewOptions =
stepScrollIntoViewOptions ?? scrollIntoViewOptions;
const [posInfo, targetElement] = useTarget(
Expand Down Expand Up @@ -152,10 +131,12 @@ const Tour: React.FC<TourProps> = props => {

const mergedBuiltinPlacements = useMemo(() => {
if (builtinPlacements) {
return typeof builtinPlacements === 'function' ? builtinPlacements({arrowPointAtCenter}) : builtinPlacements;
return typeof builtinPlacements === 'function'
? builtinPlacements({ arrowPointAtCenter })
: builtinPlacements;
}
return getPlacements(arrowPointAtCenter);
}, [builtinPlacements, arrowPointAtCenter])
}, [builtinPlacements, arrowPointAtCenter]);

// ========================= Render =========================
// Skip if not init yet
Expand Down Expand Up @@ -187,8 +168,8 @@ const Tour: React.FC<TourProps> = props => {
handleClose();
onFinish?.();
}}
closeIcon={mergedCloseIcon}
{...steps[mergedCurrent]}
closable={mergedClosable}
/>
);

Expand Down
22 changes: 13 additions & 9 deletions src/TourStep/DefaultPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import * as React from 'react';
import type { TourStepProps } from '.';
import type { TourStepProps } from '../interface';
import classNames from 'classnames';
import pickAttrs from 'rc-util/lib/pickAttrs';

export default function DefaultPanel(props: TourStepProps) {
export type DefaultPanelProps = Exclude<TourStepProps, "closable"> & {
closable: Exclude<TourStepProps["closable"], boolean>;
};

export default function DefaultPanel(props: DefaultPanelProps) {
const {
prefixCls,
current,
Expand All @@ -14,13 +19,11 @@ export default function DefaultPanel(props: TourStepProps) {
onNext,
onFinish,
className,
closeIcon,
closable,
} = props;

const mergedClosable = closeIcon !== false && closeIcon !== null;
const mergedCloseIcon = (closeIcon !== undefined && closeIcon !== true) ? closeIcon : (
<span className={`${prefixCls}-close-x`}>&times;</span>
)
const ariaProps = pickAttrs(closable || {}, true);
const closeIcon = closable?.closeIcon;
const mergedClosable = !!closable;

return (
<div className={classNames(`${prefixCls}-content`, className)}>
Expand All @@ -30,9 +33,10 @@ export default function DefaultPanel(props: TourStepProps) {
type="button"
onClick={onClose}
aria-label="Close"
{...ariaProps}
className={`${prefixCls}-close`}
>
{mergedCloseIcon}
{closeIcon}
</button>
)}
<div className={`${prefixCls}-header`}>
Expand Down
38 changes: 7 additions & 31 deletions src/TourStep/index.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,13 @@
import * as React from 'react';
import type { ReactNode, CSSProperties } from 'react';
import type { PlacementType } from '../placements';
import DefaultPanel from './DefaultPanel';
import DefaultPanel, { DefaultPanelProps } from './DefaultPanel';
import type { TourStepProps, TourStepInfo } from '../interface';

export interface TourStepInfo {
arrow?: boolean | { pointAtCenter: boolean };
target?: HTMLElement | (() => HTMLElement) | null | (() => null);
title: ReactNode;
description?: ReactNode;
placement?: PlacementType;
mask?: boolean | {
style?: React.CSSProperties;
// to fill mask color, e.g. rgba(80,0,0,0.5)
color?: string;
};
className?: string;
style?: CSSProperties;
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions;
closeIcon?: ReactNode
}

export interface TourStepProps extends TourStepInfo {
prefixCls?: string;
total?: number;
current?: number;
onClose?: () => void;
onFinish?: () => void;
renderPanel?: (step: TourStepProps, current: number) => ReactNode;
onPrev?: () => void;
onNext?: () => void;
}
export type {
TourStepProps,
TourStepInfo,
};

const TourStep = (props: TourStepProps) => {
const TourStep = (props: DefaultPanelProps) => {
const { current, renderPanel } = props;

return (
Expand Down
89 changes: 89 additions & 0 deletions src/hooks/useClosable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useMemo } from 'react';
import type { TourProps } from '../interface';
import type { TourStepInfo } from '../TourStep';

type ClosableConfig = Exclude<TourStepInfo['closable'], boolean> | null;

function isConfigObj(
closable: TourStepInfo['closable'],
): closable is Exclude<TourStepInfo['closable'], boolean> {
return closable !== null && typeof closable === 'object';
}

function getClosableConfig(
prefixCls: string,
closable: TourStepInfo['closable'],
closeIcon: TourStepInfo['closeIcon'],
preset: true,
): ClosableConfig;
function getClosableConfig(
prefixCls: string,
closable: TourStepInfo['closable'],
closeIcon: TourStepInfo['closeIcon'],
preset: false,
): ClosableConfig | 'empty';
/**
* Convert `closable` to ClosableConfig.
* When `preset` is true, will auto fill ClosableConfig with default value.
*/
function getClosableConfig(
prefixCls: string,
closable: TourStepInfo['closable'],
closeIcon: TourStepInfo['closeIcon'],
preset: boolean,
): ClosableConfig | 'empty' {
if (
closable === false ||
(closeIcon === false && (!isConfigObj(closable) || !closable.closeIcon))
) {
return null;
}

const defaultIcon = <span className={`${prefixCls}-close-x`}>&times;</span>;
const mergedCloseIcon =
(typeof closeIcon !== 'boolean' && closeIcon) || defaultIcon;

if (isConfigObj(closable)) {
return {
...closable,
closeIcon: closable.closeIcon || mergedCloseIcon,
};
}

// When StepClosable no need auto fill, but RootClosable need this.
return preset || closable || closeIcon
? {
closeIcon: mergedCloseIcon,
}
: 'empty';
}

export function useClosable(
prefixCls: string,
stepClosable: TourStepInfo['closable'],
stepCloseIcon: TourStepInfo['closeIcon'],
closable: TourProps['closable'],
closeIcon: TourProps['closeIcon'],
) {
return useMemo(() => {
const stepClosableConfig = getClosableConfig(
prefixCls,
stepClosable,
stepCloseIcon,
false,
);

const rootClosableConfig = getClosableConfig(
prefixCls,
closable,
closeIcon,
true,
);

if (stepClosableConfig !== 'empty') {
return stepClosableConfig;
}

return rootClosableConfig;
}, [closable, closeIcon, prefixCls, stepClosable, stepCloseIcon]);
}
3 changes: 1 addition & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Tour from './Tour';
export type { TourProps } from './Tour';
export type { TourStepInfo, TourStepProps } from './TourStep';
export type { TourProps, TourStepInfo, TourStepProps } from './interface';

export default Tour;
Loading

0 comments on commit dc93652

Please sign in to comment.