mirror of
https://github.com/ant-design/ant-design.git
synced 2024-12-11 23:00:20 +08:00
180 lines
5.6 KiB
TypeScript
180 lines
5.6 KiB
TypeScript
import React from 'react';
|
|
import { Col, ConfigProvider, Flex, Row, Tag, theme, Typography } from 'antd';
|
|
import { createStyles, css } from 'antd-style';
|
|
import classnames from 'classnames';
|
|
|
|
const MARK_BORDER_SIZE = 2;
|
|
|
|
const useStyle = createStyles(({ token }, markPos: [number, number, number, number]) => ({
|
|
container: css`
|
|
position: relative;
|
|
`,
|
|
col: css`
|
|
border-right: 1px solid ${token.colorBorderSecondary};
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: ${token.paddingMD}px;
|
|
overflow: hidden;
|
|
`,
|
|
listWrap: css`
|
|
row-gap: ${token.paddingXS}px;
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
`,
|
|
listItem: css`
|
|
cursor: pointer;
|
|
padding-block: ${token.paddingXS}px;
|
|
padding-inline: ${token.paddingSM}px;
|
|
transition: background-color ${token.motionDurationFast} ease;
|
|
&:hover {
|
|
background-color: ${token.controlItemBgHover};
|
|
}
|
|
&:not(:first-of-type) {
|
|
border-top: 1px solid ${token.colorBorderSecondary};
|
|
}
|
|
`,
|
|
marker: css`
|
|
position: absolute;
|
|
border: ${MARK_BORDER_SIZE}px solid ${token.colorWarning};
|
|
box-sizing: border-box;
|
|
z-index: 999999;
|
|
box-shadow: 0 0 0 1px #fff;
|
|
pointer-events: none;
|
|
left: ${markPos[0] - MARK_BORDER_SIZE}px;
|
|
top: ${markPos[1] - MARK_BORDER_SIZE}px;
|
|
width: ${markPos[2] + MARK_BORDER_SIZE * 2}px;
|
|
height: ${markPos[3] + MARK_BORDER_SIZE * 2}px;
|
|
`,
|
|
markerActive: css`
|
|
opacity: 1;
|
|
`,
|
|
markerNotActive: css`
|
|
opacity: 0;
|
|
`,
|
|
markerMotion: css`
|
|
transition:
|
|
opacity ${token.motionDurationSlow} ease,
|
|
all ${token.motionDurationSlow} ease;
|
|
`,
|
|
markerNotMotion: css`
|
|
transition: opacity ${token.motionDurationSlow} ease;
|
|
`,
|
|
}));
|
|
|
|
export interface SemanticPreviewProps {
|
|
semantics: { name: string; desc: string; version?: string }[];
|
|
children: React.ReactElement;
|
|
height?: number;
|
|
}
|
|
|
|
const SemanticPreview: React.FC<SemanticPreviewProps> = (props) => {
|
|
const { semantics = [], children, height } = props;
|
|
const { token } = theme.useToken();
|
|
|
|
// ======================= Semantic =======================
|
|
const getMarkClassName = React.useCallback(
|
|
(semanticKey: string) => `semantic-mark-${semanticKey}`,
|
|
[],
|
|
);
|
|
|
|
const semanticClassNames = React.useMemo<Record<string, string>>(() => {
|
|
const classNames: Record<string, string> = {};
|
|
|
|
semantics.forEach((semantic) => {
|
|
classNames[semantic.name] = getMarkClassName(semantic.name);
|
|
});
|
|
|
|
return classNames;
|
|
}, [semantics]);
|
|
|
|
const cloneNode = React.cloneElement(children, {
|
|
classNames: semanticClassNames,
|
|
});
|
|
|
|
// ======================== Hover =========================
|
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
const timerRef = React.useRef<ReturnType<typeof setTimeout>>();
|
|
|
|
const [positionMotion, setPositionMotion] = React.useState<boolean>(false);
|
|
const [hoverSemantic, setHoverSemantic] = React.useState<string | null>(null);
|
|
const [markPos, setMarkPos] = React.useState<[number, number, number, number]>([0, 0, 0, 0]);
|
|
|
|
const { styles } = useStyle(markPos);
|
|
|
|
React.useEffect(() => {
|
|
if (hoverSemantic) {
|
|
const targetClassName = getMarkClassName(hoverSemantic);
|
|
const targetElement = containerRef.current?.querySelector<HTMLElement>(`.${targetClassName}`);
|
|
const containerRect = containerRef.current?.getBoundingClientRect();
|
|
const targetRect = targetElement?.getBoundingClientRect();
|
|
setMarkPos([
|
|
(targetRect?.left || 0) - (containerRect?.left || 0),
|
|
(targetRect?.top || 0) - (containerRect?.top || 0),
|
|
targetRect?.width || 0,
|
|
targetRect?.height || 0,
|
|
]);
|
|
timerRef.current = setTimeout(() => {
|
|
setPositionMotion(true);
|
|
}, 10);
|
|
} else {
|
|
timerRef.current = setTimeout(() => {
|
|
setPositionMotion(false);
|
|
}, 500);
|
|
}
|
|
return () => {
|
|
if (timerRef.current) {
|
|
clearTimeout(timerRef.current);
|
|
}
|
|
};
|
|
}, [hoverSemantic]);
|
|
|
|
// ======================== Render ========================
|
|
return (
|
|
<div className={classnames(styles.container)} ref={containerRef}>
|
|
<Row style={{ minHeight: height }}>
|
|
<Col span={16} className={classnames(styles.col)}>
|
|
<ConfigProvider theme={{ token: { motion: false } }}>{cloneNode}</ConfigProvider>
|
|
</Col>
|
|
<Col span={8}>
|
|
<ul className={classnames(styles.listWrap)}>
|
|
{semantics.map<React.ReactNode>((semantic) => (
|
|
<li
|
|
key={semantic.name}
|
|
className={classnames(styles.listItem)}
|
|
onMouseEnter={() => setHoverSemantic(semantic.name)}
|
|
onMouseLeave={() => setHoverSemantic(null)}
|
|
>
|
|
<Flex vertical gap="small">
|
|
<Flex gap="small" align="center">
|
|
<Typography.Title level={5} style={{ margin: 0 }}>
|
|
{semantic.name}
|
|
</Typography.Title>
|
|
{semantic.version && <Tag color="blue">{semantic.version}</Tag>}
|
|
</Flex>
|
|
<Typography.Paragraph style={{ margin: 0, fontSize: token.fontSizeSM }}>
|
|
{semantic.desc}
|
|
</Typography.Paragraph>
|
|
</Flex>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</Col>
|
|
</Row>
|
|
<div
|
|
className={classnames(
|
|
styles.marker,
|
|
hoverSemantic ? styles.markerActive : styles.markerNotActive,
|
|
positionMotion ? styles.markerMotion : styles.markerNotMotion,
|
|
)}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SemanticPreview;
|