diff --git a/.circleci/config.yml b/.circleci/config.yml index 6ba450000215..db56528cdef5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,15 +7,17 @@ version: 2.1 jobs: test-argos-ci: docker: - - image: cimg/node:16.20.0-browsers + - image: cimg/node:21.0-browsers + environment: + NODE_OPTIONS: --openssl-legacy-provider steps: - checkout - run: name: Install node_modules - command: npm i + command: yarn - run: name: Build dist file - command: npm run dist + command: npm run dist:esbuild - run: name: Run image screenshot tests command: npm run test-image diff --git a/.dumi/hooks/use.ts b/.dumi/hooks/use.ts new file mode 100644 index 000000000000..2502dcd1fa4e --- /dev/null +++ b/.dumi/hooks/use.ts @@ -0,0 +1,30 @@ +function use(promise: PromiseLike): T { + const internal: PromiseLike & { + status?: 'pending' | 'fulfilled' | 'rejected'; + value?: T; + reason?: any; + } = promise; + if (internal.status === 'fulfilled') { + return internal.value as T; + } + if (internal.status === 'rejected') { + throw internal.reason; + } else if (internal.status === 'pending') { + throw internal; + } else { + internal.status = 'pending'; + internal.then( + (result) => { + internal.status = 'fulfilled'; + internal.value = result; + }, + (reason) => { + internal.status = 'rejected'; + internal.reason = reason; + }, + ); + throw internal; + } +} + +export default use; diff --git a/.dumi/hooks/useDark.tsx b/.dumi/hooks/useDark.tsx new file mode 100644 index 000000000000..6b31dee12c40 --- /dev/null +++ b/.dumi/hooks/useDark.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export const DarkContext = React.createContext(false); + +export default function useDark() { + return React.useContext(DarkContext); +} diff --git a/.dumi/hooks/useFetch/cache.ts b/.dumi/hooks/useFetch/cache.ts new file mode 100644 index 000000000000..d6c6e791ef76 --- /dev/null +++ b/.dumi/hooks/useFetch/cache.ts @@ -0,0 +1,21 @@ +export default class FetchCache { + private cache: Map> = new Map(); + + get(key: string) { + return this.cache.get(key); + } + + set(key: string, value: PromiseLike) { + this.cache.set(key, value); + } + + promise(key: string, promiseFn: () => PromiseLike): PromiseLike { + const cached = this.get(key); + if (cached) { + return cached; + } + const promise = promiseFn(); + this.set(key, promise); + return promise; + } +} diff --git a/.dumi/hooks/useFetch/index.ts b/.dumi/hooks/useFetch/index.ts new file mode 100644 index 000000000000..99aa763ab3c3 --- /dev/null +++ b/.dumi/hooks/useFetch/index.ts @@ -0,0 +1,20 @@ +import fetch from 'cross-fetch'; +import use from '../use'; +import FetchCache from './cache'; + +const cache = new FetchCache(); + +const useFetch = (options: string | { request: () => PromiseLike; key: string }) => { + let request; + let key; + if (typeof options === 'string') { + request = () => fetch(options).then((res) => res.json()); + key = options; + } else { + request = options.request; + key = options.key; + } + return use(cache.promise(key, request)); +}; + +export default useFetch; diff --git a/.dumi/hooks/useLayoutState.ts b/.dumi/hooks/useLayoutState.ts index e69486fcc61f..45b3bec5183a 100644 --- a/.dumi/hooks/useLayoutState.ts +++ b/.dumi/hooks/useLayoutState.ts @@ -1,6 +1,6 @@ import { startTransition, useState } from 'react'; -const useLayoutState = ( +const useLayoutState: typeof useState = ( ...args: Parameters> ): ReturnType> => { const [state, setState] = useState(...args); diff --git a/.dumi/hooks/useLocale.ts b/.dumi/hooks/useLocale.ts index 9bc0dc4d4840..8ad296953196 100644 --- a/.dumi/hooks/useLocale.ts +++ b/.dumi/hooks/useLocale.ts @@ -5,10 +5,12 @@ export interface LocaleMap { en: Record; } -export default function useLocale( +function useLocale( localeMap?: LocaleMap, ): [Record, 'cn' | 'en'] { const { id } = useDumiLocale(); - const localeType = id === 'zh-CN' ? 'cn' : 'en'; - return [localeMap?.[localeType], localeType]; + const localeType = id === 'zh-CN' ? ('cn' as const) : ('en' as const); + return [localeMap?.[localeType]!, localeType]; } + +export default useLocale; diff --git a/.dumi/hooks/useMenu.tsx b/.dumi/hooks/useMenu.tsx index e6575504dc67..89bc9e16402e 100644 --- a/.dumi/hooks/useMenu.tsx +++ b/.dumi/hooks/useMenu.tsx @@ -1,22 +1,21 @@ +import React, { useMemo } from 'react'; import type { MenuProps } from 'antd'; -import { Tag, theme } from 'antd'; +import { Tag, version } from 'antd'; import { useFullSidebarData, useSidebarData } from 'dumi'; -import type { ReactNode } from 'react'; -import React, { useMemo } from 'react'; + import Link from '../theme/common/Link'; import useLocation from './useLocation'; -export type UseMenuOptions = { - before?: ReactNode; - after?: ReactNode; -}; +export interface UseMenuOptions { + before?: React.ReactNode; + after?: React.ReactNode; +} const useMenu = (options: UseMenuOptions = {}): [MenuProps['items'], string] => { const fullData = useFullSidebarData(); const { pathname, search } = useLocation(); const sidebarData = useSidebarData(); const { before, after } = options; - const { token } = theme.useToken(); const menuItems = useMemo(() => { const sidebarItems = [...(sidebarData ?? [])]; @@ -33,7 +32,7 @@ const useMenu = (options: UseMenuOptions = {}): [MenuProps['items'], string] => key.startsWith('/changelog'), )?.[1]; if (changelogData) { - sidebarItems.push(...changelogData); + sidebarItems.splice(1, 0, changelogData[0]); } } if (pathname.startsWith('/changelog')) { @@ -41,10 +40,23 @@ const useMenu = (options: UseMenuOptions = {}): [MenuProps['items'], string] => key.startsWith('/docs/react'), )?.[1]; if (reactDocData) { - sidebarItems.unshift(...reactDocData); + sidebarItems.unshift(reactDocData[0]); + sidebarItems.push(...reactDocData.slice(1)); } } + const getItemTag = (tag: string, show = true) => + tag && + show && ( + + {tag.replace('VERSION', version)} + + ); + return ( sidebarItems?.reduce>((result, group) => { if (group?.title) { @@ -53,7 +65,7 @@ const useMenu = (options: UseMenuOptions = {}): [MenuProps['items'], string] => const childrenGroup = group.children.reduce< Record[number]['children']> >((childrenResult, child) => { - const type = (child.frontmatter as any).type ?? 'default'; + const type = child.frontmatter?.type ?? 'default'; if (!childrenResult[type]) { childrenResult[type] = []; } @@ -104,17 +116,16 @@ const useMenu = (options: UseMenuOptions = {}): [MenuProps['items'], string] => key: group?.title, children: group.children?.map((item) => ({ label: ( - + {before} {item?.title} - {(item.frontmatter as any).subtitle} + {item.frontmatter?.subtitle} - {(item.frontmatter as any).tag && ( - - {(item.frontmatter as any).tag} - - )} + {getItemTag(item.frontmatter?.tag, !before && !after)} {after} ), @@ -126,15 +137,19 @@ const useMenu = (options: UseMenuOptions = {}): [MenuProps['items'], string] => const list = group.children || []; // 如果有 date 字段,我们就对其进行排序 if (list.every((info) => info?.frontmatter?.date)) { - list.sort((a, b) => (a.frontmatter.date > b.frontmatter.date ? -1 : 1)); + list.sort((a, b) => (a.frontmatter?.date > b.frontmatter?.date ? -1 : 1)); } result.push( ...list.map((item) => ({ label: ( - + {before} {item?.title} + {getItemTag((item.frontmatter as any).tag, !before && !after)} {after} ), @@ -145,7 +160,7 @@ const useMenu = (options: UseMenuOptions = {}): [MenuProps['items'], string] => return result; }, []) ?? [] ); - }, [sidebarData, fullData, pathname, search]); + }, [sidebarData, fullData, pathname, search, options]); return [menuItems, pathname]; }; diff --git a/.dumi/hooks/useSiteToken.ts b/.dumi/hooks/useSiteToken.ts deleted file mode 100644 index 425114ea96aa..000000000000 --- a/.dumi/hooks/useSiteToken.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { theme } from 'antd'; -import { useContext } from 'react'; -import { ConfigContext } from 'antd/es/config-provider'; - -const { useToken } = theme; - -const useSiteToken = () => { - const result = useToken(); - const { getPrefixCls, iconPrefixCls } = useContext(ConfigContext); - const rootPrefixCls = getPrefixCls(); - const { token } = result; - const siteMarkdownCodeBg = token.colorFillTertiary; - - return { - ...result, - token: { - ...token, - headerHeight: 64, - menuItemBorder: 2, - mobileMaxWidth: 767.99, - siteMarkdownCodeBg, - antCls: `.${rootPrefixCls}`, - iconCls: `.${iconPrefixCls}`, - /** 56 */ - marginFarXS: (token.marginXXL / 6) * 7, - /** 80 */ - marginFarSM: (token.marginXXL / 3) * 5, - /** 96 */ - marginFar: token.marginXXL * 2, - codeFamily: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace`, - }, - }; -}; - -export default useSiteToken; diff --git a/.dumi/hooks/useThemeAnimation.ts b/.dumi/hooks/useThemeAnimation.ts new file mode 100644 index 000000000000..34b72e780960 --- /dev/null +++ b/.dumi/hooks/useThemeAnimation.ts @@ -0,0 +1,127 @@ +import { useEffect, useRef } from 'react'; +import { removeCSS, updateCSS } from 'rc-util/lib/Dom/dynamicCSS'; + +import theme from '../../components/theme'; + +const viewTransitionStyle = ` +::view-transition-old(root), +::view-transition-new(root) { + animation: none; + mix-blend-mode: normal; +} + +.dark::view-transition-old(root) { + z-index: 1; +} + +.dark::view-transition-new(root) { + z-index: 999; +} + +::view-transition-old(root) { + z-index: 999; +} + +::view-transition-new(root) { + z-index: 1; +} +`; + +const useThemeAnimation = () => { + const { + token: { colorBgElevated }, + } = theme.useToken(); + + const animateRef = useRef<{ colorBgElevated: string }>({ colorBgElevated }); + + const startAnimationTheme = (clipPath: string[], isDark: boolean) => { + updateCSS( + ` + * { + transition: none !important; + } + `, + 'disable-transition', + ); + + document.documentElement + .animate( + { + clipPath: isDark ? [...clipPath].reverse() : clipPath, + }, + { + duration: 500, + easing: 'ease-in', + pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)', + }, + ) + .addEventListener('finish', () => { + removeCSS('disable-transition'); + }); + }; + + const toggleAnimationTheme = ( + event: React.MouseEvent, + isDark: boolean, + ) => { + // @ts-ignore + if (!(event && typeof document.startViewTransition === 'function')) { + return; + } + const x = event.clientX; + const y = event.clientY; + const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y)); + updateCSS( + ` + [data-prefers-color='dark'] { + color-scheme: light !important; + } + + [data-prefers-color='light'] { + color-scheme: dark !important; + } + `, + 'color-scheme', + ); + document + // @ts-ignore + .startViewTransition(async () => { + // wait for theme change end + while (colorBgElevated === animateRef.current.colorBgElevated) { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 1000 / 60); + }); + } + const root = document.documentElement; + root.classList.remove(isDark ? 'dark' : 'light'); + root.classList.add(isDark ? 'light' : 'dark'); + }) + .ready.then(() => { + const clipPath = [ + `circle(0px at ${x}px ${y}px)`, + `circle(${endRadius}px at ${x}px ${y}px)`, + ]; + removeCSS('color-scheme'); + startAnimationTheme(clipPath, isDark); + }); + }; + + // inject transition style + useEffect(() => { + // @ts-ignore + if (typeof document.startViewTransition === 'function') { + updateCSS(viewTransitionStyle, 'view-transition-style'); + } + }, []); + + useEffect(() => { + if (colorBgElevated !== animateRef.current.colorBgElevated) { + animateRef.current.colorBgElevated = colorBgElevated; + } + }, [colorBgElevated]); + + return toggleAnimationTheme; +}; + +export default useThemeAnimation; diff --git a/.dumi/mirror-modal.js b/.dumi/mirror-modal.js new file mode 100644 index 000000000000..3128af996d90 --- /dev/null +++ b/.dumi/mirror-modal.js @@ -0,0 +1,173 @@ +(function createMirrorModal() { + if ( + (navigator.languages.includes('zh') || navigator.languages.includes('zh-CN')) && + /-cn\/?$/.test(window.location.pathname) && + !['ant-design.gitee.io', 'ant-design.antgroup.com'].includes(window.location.hostname) && + !window.location.host.includes('surge') + ) { + const ANTD_DOT_NOT_SHOW_MIRROR_MODAL = 'ANT_DESIGN_DO_NOT_OPEN_MIRROR_MODAL'; + + const lastShowTime = window.localStorage.getItem(ANTD_DOT_NOT_SHOW_MIRROR_MODAL); + if ( + lastShowTime && + lastShowTime !== 'true' && + Date.now() - new Date(lastShowTime).getTime() < 7 * 24 * 60 * 60 * 1000 + ) { + return; + } + + const style = document.createElement('style'); + style.innerHTML = ` + @keyframes mirror-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes mirror-zoom-in { + from { + transform: scale(0.8); + } + to { + transform: scale(1); + } + } + + .mirror-modal-mask { + position: fixed; + inset: 0; + height: '100vh'; + width: '100vw'; + background: rgba(0, 0, 0, 0.3); + z-index: 9999; + animation: mirror-fade-in 0.3s forwards; + } + + .mirror-modal-dialog { + position: fixed; + inset: 0; + top: 120px; + margin-left: auto; + margin-right: auto; + width: 400px; + height: 120px; + display: flex; + align-items: center; + flex-direction: column; + border-radius: 8px; + border: 1px solid #eee; + background: #fff; + padding: 20px 24px; + box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05); + animation: mirror-zoom-in 0.3s forwards; + } + + .mirror-modal-title { + font-size: 16px; + font-weight: 500; + align-self: flex-start; + margin-bottom: 8px; + } + + .mirror-modal-content { + font-size: 14px; + align-self: flex-start; + margin-bottom: 16px; + } + + .mirror-modal-btns { + align-self: flex-end; + margin-top: auto; + display: flex; + align-items: center; + } + + .mirror-modal-btn { + border-radius: 6px; + cursor: pointer; + height: 32px; + box-sizing: border-box; + font-size: 14px; + padding: 4px 16px; + display: inline-flex; + align-items: center; + text-decoration: none; + transition: all 0.2s; + } + + .mirror-modal-confirm-btn { + background: #1677ff; + color: #fff; + } + + .mirror-modal-confirm-btn:hover { + background: #4096ff; + } + + .mirror-modal-confirm-btn:active { + background: #0958d9; + } + + .mirror-modal-cancel-btn { + border: 1px solid #eee; + color: #000; + margin-right: 8px; + } + + .mirror-modal-cancel-btn:hover { + border-color: #4096ff; + color: #4096ff + } + + .mirror-modal-cancel-btn:active { + border-color: #0958d9; + color: #0958d9; + } + `; + document.head.append(style); + + const modal = document.createElement('div'); + modal.className = 'mirror-modal-mask'; + + const dialog = document.createElement('div'); + dialog.className = 'mirror-modal-dialog'; + modal.append(dialog); + + const title = document.createElement('div'); + title.className = 'mirror-modal-title'; + title.innerText = '提示'; + dialog.append(title); + + const content = document.createElement('div'); + content.className = 'mirror-modal-content'; + content.innerText = '国内用户推荐访问国内镜像以获得极速体验~'; + dialog.append(content); + + const btnWrapper = document.createElement('div'); + btnWrapper.className = 'mirror-modal-btns'; + dialog.append(btnWrapper); + + const cancelBtn = document.createElement('a'); + cancelBtn.className = 'mirror-modal-cancel-btn mirror-modal-btn'; + cancelBtn.innerText = '7 天内不再显示'; + btnWrapper.append(cancelBtn); + cancelBtn.addEventListener('click', () => { + window.localStorage.setItem(ANTD_DOT_NOT_SHOW_MIRROR_MODAL, new Date().toISOString()); + document.body.removeChild(modal); + document.head.removeChild(style); + document.body.style.overflow = ''; + }); + + const confirmBtn = document.createElement('a'); + confirmBtn.className = 'mirror-modal-confirm-btn mirror-modal-btn'; + confirmBtn.href = window.location.href.replace(window.location.host, 'ant-design.antgroup.com'); + confirmBtn.innerText = '🚀 立刻前往'; + btnWrapper.append(confirmBtn); + + document.body.append(modal); + document.body.style.overflow = 'hidden'; + } +})(); diff --git a/.dumi/pages/404/index.tsx b/.dumi/pages/404/index.tsx index 17f4312afa44..502df93dc827 100644 --- a/.dumi/pages/404/index.tsx +++ b/.dumi/pages/404/index.tsx @@ -1,7 +1,7 @@ import { HomeOutlined } from '@ant-design/icons'; -import { Button, Result } from 'antd'; import { Link, useLocation } from 'dumi'; import React, { useEffect } from 'react'; +import { Button, Result } from 'antd'; import * as utils from '../../theme/utils'; export interface NotFoundProps { diff --git a/.dumi/pages/index/components/Banner.tsx b/.dumi/pages/index/components/Banner.tsx index 781a09da4506..1a8554fe18d8 100644 --- a/.dumi/pages/index/components/Banner.tsx +++ b/.dumi/pages/index/components/Banner.tsx @@ -1,9 +1,9 @@ -import { css } from '@emotion/react'; -import { Button, Space, Typography } from 'antd'; +import { createStyles, css, useTheme } from 'antd-style'; import { Link, useLocation } from 'dumi'; import * as React from 'react'; +import classNames from 'classnames'; +import { Button, Space, Typography } from 'antd'; import useLocale from '../../../hooks/useLocale'; -import useSiteToken from '../../../hooks/useSiteToken'; import SiteContext from '../../../theme/slots/SiteContext'; import * as utils from '../../../theme/utils'; import { GroupMask } from './Group'; @@ -23,10 +23,8 @@ const locales = { }; const useStyle = () => { - const { token } = useSiteToken(); const { isMobile } = React.useContext(SiteContext); - - return { + return createStyles(({ token }) => ({ titleBase: css` h1& { font-family: AliPuHui, ${token.fontFamily}; @@ -48,7 +46,7 @@ const useStyle = () => { font-size: 68px; } `, - }; + }))(); }; export interface BannerProps { @@ -58,8 +56,8 @@ export interface BannerProps { export default function Banner({ children }: BannerProps) { const [locale] = useLocale(locales); const { pathname, search } = useLocation(); - const { token } = useSiteToken(); - const styles = useStyle(); + const token = useTheme(); + const { styles } = useStyle(); const { isMobile } = React.useContext(SiteContext); const isZhCN = utils.isZhCN(pathname); @@ -146,7 +144,7 @@ export default function Banner({ children }: BannerProps) { alt="bg" /> - + Ant Design 5.0 { - const { token } = useSiteToken(); - const { carousel } = useCarouselStyle(); +const useStyle = createStyles(({ token }) => { + const { carousel } = getCarouselStyle(); return { itemBase: css` @@ -47,17 +48,17 @@ const useStyle = () => { `, carousel, }; -}; +}); interface RecommendItemProps { extra: Extra; index: number; icons: Icon[]; - itemCss: SerializedStyles; + className?: string; } -const RecommendItem = ({ extra, index, icons, itemCss }: RecommendItemProps) => { - const style = useStyle(); - const { token } = useSiteToken(); +const RecommendItem = ({ extra, index, icons, className }: RecommendItemProps) => { + const token = useTheme(); + const { styles } = useStyle(); if (!extra) { return ; @@ -69,7 +70,7 @@ const RecommendItem = ({ extra, index, icons, itemCss }: RecommendItemProps) => key={extra?.title} href={extra.href} target="_blank" - css={[style.itemBase, itemCss]} + className={classNames(styles.itemBase, className)} rel="noreferrer" > {extra?.title} @@ -84,39 +85,61 @@ const RecommendItem = ({ extra, index, icons, itemCss }: RecommendItemProps) => ); }; -export interface BannerRecommendsProps { - extras?: Extra[]; - icons?: Icon[]; -} +export const BannerRecommendsFallback: FC = () => { + const { isMobile } = useContext(SiteContext); + const { styles } = useStyle(); + + const list = Array(3).fill(1); + + return isMobile ? ( + + {list.map((extra, index) => ( +
+ +
+ ))} +
+ ) : ( +
+ {list.map((_, index) => ( + + ))} +
+ ); +}; -export default function BannerRecommends({ extras = [], icons = [] }: BannerRecommendsProps) { - const styles = useStyle(); +export default function BannerRecommends() { + const { styles } = useStyle(); + const [, lang] = useLocale(); const { isMobile } = React.useContext(SiteContext); + const data = useSiteData(); + const extras = data?.extras?.[lang]; + const icons = data?.icons; const first3 = extras.length === 0 ? Array(3).fill(null) : extras.slice(0, 3); return (
{isMobile ? ( - + {first3.map((extra, index) => (
))}
) : ( -
+
{first3.map((extra, index) => ( ))} diff --git a/.dumi/pages/index/components/ComponentsList.tsx b/.dumi/pages/index/components/ComponentsList.tsx index 9dbea90ab63b..a528d1c4ce4a 100644 --- a/.dumi/pages/index/components/ComponentsList.tsx +++ b/.dumi/pages/index/components/ComponentsList.tsx @@ -1,24 +1,26 @@ /* eslint-disable react/jsx-pascal-case */ import React, { useContext } from 'react'; +import { CustomerServiceOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons'; import { - Space, - Typography, - Tour, - Tag, - DatePicker, Alert, - Modal, + Carousel, + DatePicker, FloatButton, + Modal, Progress, - Carousel, + Space, + Tag, + Tour, + Typography, } from 'antd'; +import { createStyles, css, useTheme } from 'antd-style'; +import classNames from 'classnames'; import dayjs from 'dayjs'; -import { CustomerServiceOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons'; -import { css } from '@emotion/react'; -import useSiteToken from '../../../hooks/useSiteToken'; + +import useDark from '../../../hooks/useDark'; import useLocale from '../../../hooks/useLocale'; import SiteContext from '../../../theme/slots/SiteContext'; -import { useCarouselStyle } from './util'; +import { getCarouselStyle } from './util'; const SAMPLE_CONTENT_EN = 'Ant Design 5.0 use CSS-in-JS technology to provide dynamic & mix theme ability. And which use component level CSS-in-JS solution get your application a better performance.'; @@ -56,39 +58,85 @@ const locales = { }; const useStyle = () => { - const { token } = useSiteToken(); - const { carousel } = useCarouselStyle(); + const isRootDark = useDark(); - return { - card: css` - border-radius: ${token.borderRadius}px; - background: #f5f8ff; - padding: ${token.paddingXL}px; - flex: none; - overflow: hidden; - position: relative; - display: flex; - flex-direction: column; - align-items: stretch; + return createStyles(({ token }) => { + const { carousel } = getCarouselStyle(); - > * { + return { + card: css` + border-radius: ${token.borderRadius}px; + border: 1px solid ${isRootDark ? token.colorBorder : 'transparent'}; + background: ${isRootDark ? token.colorBgContainer : '#f5f8ff'}; + padding: ${token.paddingXL}px; flex: none; - } - `, - cardCircle: css` - position: absolute; - width: 120px; - height: 120px; - background: #1677ff; - border-radius: 50%; - filter: blur(40px); - opacity: 0.1; - `, - mobileCard: css` - height: 395px; - `, - carousel, - }; + overflow: hidden; + position: relative; + display: flex; + flex-direction: column; + align-items: stretch; + + > * { + flex: none; + } + `, + cardCircle: css` + position: absolute; + width: 120px; + height: 120px; + background: #1677ff; + border-radius: 50%; + filter: blur(40px); + opacity: 0.1; + `, + mobileCard: css` + height: 395px; + `, + carousel, + }; + })(); +}; + +const ComponentItem: React.FC = ({ title, node, type, index }) => { + const tagColor = type === 'new' ? 'processing' : 'warning'; + const [locale] = useLocale(locales); + const tagText = type === 'new' ? locale.new : locale.update; + const { styles } = useStyle(); + const { isMobile } = useContext(SiteContext); + const token = useTheme(); + + return ( +
+ {/* Decorator */} +
+ + {/* Title */} + + + {title} + + {tagText} + + +
+ {node} +
+
+ ); }; interface ComponentItemProps { @@ -99,8 +147,8 @@ interface ComponentItemProps { } export default function ComponentsList() { - const { token } = useSiteToken(); - const styles = useStyle(); + const token = useTheme(); + const { styles } = useStyle(); const [locale] = useLocale(locales); const { isMobile } = useContext(SiteContext); @@ -234,47 +282,9 @@ export default function ComponentsList() { [isMobile], ); - const ComponentItem = ({ title, node, type, index }: ComponentItemProps) => { - const tagColor = type === 'new' ? 'processing' : 'warning'; - const tagText = type === 'new' ? locale.new : locale.update; - - return ( -
- {/* Decorator */} -
- - {/* Title */} - - - {title} - - {tagText} - - -
- {node} -
-
- ); - }; - return isMobile ? (
- + {COMPONENTS.map(({ title, node, type }, index) => ( ))} diff --git a/.dumi/pages/index/components/DesignFramework.tsx b/.dumi/pages/index/components/DesignFramework.tsx index 9e044bd5310a..1f744b343329 100644 --- a/.dumi/pages/index/components/DesignFramework.tsx +++ b/.dumi/pages/index/components/DesignFramework.tsx @@ -1,11 +1,12 @@ -import { Col, Row, Typography } from 'antd'; import React, { useContext } from 'react'; -import { css } from '@emotion/react'; +import { Col, Row, Typography } from 'antd'; +import { createStyles, useTheme } from 'antd-style'; import { Link, useLocation } from 'dumi'; + +import useDark from '../../../hooks/useDark'; import useLocale from '../../../hooks/useLocale'; -import useSiteToken from '../../../hooks/useSiteToken'; -import * as utils from '../../../theme/utils'; import SiteContext from '../../../theme/slots/SiteContext'; +import * as utils from '../../../theme/utils'; const SECONDARY_LIST = [ { @@ -62,14 +63,16 @@ const locales = { }; const useStyle = () => { - const { token } = useSiteToken(); + const isRootDark = useDark(); - return { + return createStyles(({ token, css }) => ({ card: css` padding: ${token.paddingSM}px; border-radius: ${token.borderRadius * 2}px; - background: #fff; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), + background: ${isRootDark ? 'rgba(0,0,0,0.45)' : token.colorBgElevated}; + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.03), + 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px rgba(0, 0, 0, 0.02); img { @@ -83,20 +86,20 @@ const useStyle = () => { display: block; border-radius: ${token.borderRadius * 2}px; padding: ${token.paddingMD}px ${token.paddingLG}px; - background: rgba(0, 0, 0, 0.02); - border: 1px solid rgba(0, 0, 0, 0.06); + background: ${isRootDark ? 'rgba(0,0,0,0.25)' : 'rgba(0, 0, 0, 0.02)'}; + border: 1px solid ${isRootDark ? 'rgba(255,255,255, 0.45)' : 'rgba(0, 0, 0, 0.06)'}; img { height: 48px; } `, - }; + }))(); }; export default function DesignFramework() { const [locale] = useLocale(locales); - const { token } = useSiteToken(); - const style = useStyle(); + const token = useTheme(); + const { styles } = useStyle(); const { pathname, search } = useLocation(); const isZhCN = utils.isZhCN(pathname); const { isMobile } = useContext(SiteContext); @@ -129,7 +132,7 @@ export default function DesignFramework() { return ( -
+
{title} - + {title} ; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; } -export function GroupMask({ children, style, disabled }: GroupMaskProps) { +export const GroupMask: React.FC = (props) => { + const { children, style, disabled, onMouseMove, onMouseEnter, onMouseLeave } = props; const additionalStyle: React.CSSProperties = disabled ? {} : { position: 'relative', - background: `rgba(255,255,255,0.1)`, - backdropFilter: `blur(25px)`, zIndex: 1, }; return (
{children}
); -} +}; export interface GroupProps { id?: string; @@ -48,9 +50,9 @@ export interface GroupProps { decoration?: React.ReactNode; } -export default function Group(props: GroupProps) { +const Group: React.FC = (props) => { const { id, title, titleColor, description, children, decoration, background, collapse } = props; - const { token } = useSiteToken(); + const token = useTheme(); const { isMobile } = useContext(SiteContext); const marginStyle: React.CSSProperties = collapse @@ -79,8 +81,8 @@ export default function Group(props: GroupProps) {
{description} @@ -107,11 +109,13 @@ export default function Group(props: GroupProps) { {childNode}
); -} +}; + +export default Group; diff --git a/.dumi/pages/index/components/PreviewBanner/ComponentsBlock.tsx b/.dumi/pages/index/components/PreviewBanner/ComponentsBlock.tsx new file mode 100644 index 000000000000..42dc50702d02 --- /dev/null +++ b/.dumi/pages/index/components/PreviewBanner/ComponentsBlock.tsx @@ -0,0 +1,259 @@ +import React from 'react'; +import { AntDesignOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons'; +import { + Alert, + Button, + Checkbox, + ColorPicker, + Dropdown, + Input, + message, + Modal, + Progress, + Select, + Slider, + Steps, + Switch, + Tooltip, +} from 'antd'; +import { createStyles } from 'antd-style'; +import classNames from 'classnames'; + +import useLocale from '../../../../hooks/useLocale'; + +const { _InternalPanelDoNotUseOrYouWillBeFired: ModalPanel } = Modal; +const { _InternalPanelDoNotUseOrYouWillBeFired: InternalTooltip } = Tooltip; +const { _InternalPanelDoNotUseOrYouWillBeFired: InternalMessage } = message; + +const locales = { + cn: { + range: '设置范围', + text: 'Ant Design 5.0 使用 CSS-in-JS 技术以提供动态与混合主题的能力。与此同时,我们使用组件级别的 CSS-in-JS 解决方案,让你的应用获得更好的性能。', + infoText: '信息内容展示', + dropdown: '下拉菜单', + finished: '已完成', + inProgress: '进行中', + waiting: '等待中', + option: '选项', + apple: '苹果', + banana: '香蕉', + orange: '橘子', + watermelon: '西瓜', + primary: '主要按钮', + danger: '危险按钮', + default: '默认按钮', + dashed: '虚线按钮', + icon: '图标按钮', + hello: '你好,Ant Design!', + release: 'Ant Design 5.0 正式发布!', + }, + en: { + range: 'Set Range', + text: 'Ant Design 5.0 use CSS-in-JS technology to provide dynamic & mix theme ability. And which use component level CSS-in-JS solution get your application a better performance.', + infoText: 'Info Text', + dropdown: 'Dropdown', + finished: 'Finished', + inProgress: 'In Progress', + waiting: 'Waiting', + option: 'Option', + apple: 'Apple', + banana: 'Banana', + orange: 'Orange', + watermelon: 'Watermelon', + primary: 'Primary', + danger: 'Danger', + default: 'Default', + dashed: 'Dashed', + icon: 'Icon', + hello: 'Hello, Ant Design!', + release: 'Ant Design 5.0 is released!', + }, +}; + +const useStyle = createStyles(({ token, css }) => { + const gap = token.padding; + + return { + holder: css` + width: 500px; + display: flex; + flex-direction: column; + row-gap: ${gap}px; + opacity: 0.65; + `, + + flex: css` + display: flex; + flex-wrap: nowrap; + column-gap: ${gap}px; + `, + ptg_20: css` + flex: 0 1 20%; + `, + ptg_none: css` + flex: none; + `, + block: css` + background-color: ${token.colorBgContainer}; + padding: ${token.paddingXS}px ${token.paddingSM}px; + border-radius: ${token.borderRadius}px; + border: 1px solid ${token.colorBorder}; + `, + noMargin: css` + margin: 0; + `, + }; +}); + +export interface ComponentsBlockProps { + className?: string; + style?: React.CSSProperties; +} + +const ComponentsBlock: React.FC = (props) => { + const { className, style } = props; + + const [locale] = useLocale(locales); + const { styles } = useStyle(); + + return ( +
+ + {locale.text} + + + + + {/* Line */} +
+ +
+ ({ + key: `opt${index}`, + label: `${locale.option} ${index}`, + })), + }} + > + {locale.dropdown} + +
+ + +
+ + + + + + + {/* Line */} +
+ 100°C, + }, + }} + defaultValue={[26, 37]} + /> +
+ + {/* Line */} +
+ + + + + +
+ + {/* Line */} +
+
+ } + unCheckedChildren={} + /> + + +
+
+ +
+ +
+ + + + +
+ ); +}; + +export default ComponentsBlock; diff --git a/.dumi/pages/index/components/PreviewBanner/index.tsx b/.dumi/pages/index/components/PreviewBanner/index.tsx new file mode 100644 index 000000000000..e66f5bd1f0dd --- /dev/null +++ b/.dumi/pages/index/components/PreviewBanner/index.tsx @@ -0,0 +1,161 @@ +import React, { Suspense } from 'react'; +import { Button, ConfigProvider, Space, Typography } from 'antd'; +import { createStyles, useTheme } from 'antd-style'; +import { Link, useLocation } from 'dumi'; + +import useLocale from '../../../../hooks/useLocale'; +import SiteContext from '../../../../theme/slots/SiteContext'; +import * as utils from '../../../../theme/utils'; +import { GroupMask } from '../Group'; +import useMouseTransform from './useMouseTransform'; + +const ComponentsBlock = React.lazy(() => import('./ComponentsBlock')); + +const locales = { + cn: { + slogan: '助力设计开发者「更灵活」地搭建出「更美」的产品,让用户「快乐工作」~', + start: '开始使用', + designLanguage: '设计语言', + }, + en: { + slogan: + 'Help designers/developers building beautiful products more flexible and working with happiness', + start: 'Getting Started', + designLanguage: 'Design Language', + }, +}; + +const useStyle = () => { + const { direction } = React.useContext(ConfigProvider.ConfigContext); + const isRTL = direction === 'rtl'; + + return createStyles(({ token, css, cx }) => { + const textShadow = `0 0 3px ${token.colorBgContainer}`; + + const mask = cx(css` + position: absolute; + inset: 0; + backdrop-filter: blur(4px); + transition: all 1s ease; + `); + + return { + holder: css` + height: 520px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + perspective: 800px; + /* fix safari bug by removing blur style */ + transform: translateZ(1000px); + row-gap: ${token.marginXL}px; + + &:hover .${mask} { + backdrop-filter: none; + } + `, + + mask, + + typography: css` + text-align: center; + position: relative; + z-index: 1; + padding-inline: ${token.paddingXL}px; + text-shadow: ${new Array(5) + .fill(null) + .map(() => textShadow) + .join(', ')}; + + h1 { + font-family: AliPuHui, ${token.fontFamily} !important; + font-weight: 900 !important; + font-size: ${token.fontSizeHeading2 * 2}px !important; + line-height: ${token.lineHeightHeading2} !important; + } + + p { + font-size: ${token.fontSizeLG}px !important; + font-weight: normal !important; + margin-bottom: 0; + } + `, + + block: css` + position: absolute; + inset-inline-end: 0; + top: -38px; + transform: ${isRTL ? 'rotate3d(24, 83, -45, 57deg)' : 'rotate3d(24, -83, 45, 57deg)'}; + `, + + child: css` + position: relative; + z-index: 1; + `, + }; + })(); +}; + +export interface PreviewBannerProps { + children?: React.ReactNode; +} + +const PreviewBanner: React.FC = (props) => { + const { children } = props; + + const [locale] = useLocale(locales); + const { styles } = useStyle(); + const { isMobile } = React.useContext(SiteContext); + const token = useTheme(); + const { pathname, search } = useLocation(); + const isZhCN = utils.isZhCN(pathname); + + const [componentsBlockStyle, mouseEvents] = useMouseTransform(); + + return ( + + {/* Image Left Top */} + bg + {/* Image Right Top */} + bg + +
+ {/* Mobile not show the component preview */} + + {!isMobile && } + +
+ + +

Ant Design 5.0

+

{locale.slogan}

+
+ + + + + + + + + +
{children}
+
+ + ); +}; + +export default PreviewBanner; diff --git a/.dumi/pages/index/components/PreviewBanner/useMouseTransform.tsx b/.dumi/pages/index/components/PreviewBanner/useMouseTransform.tsx new file mode 100644 index 000000000000..a6985067bb6a --- /dev/null +++ b/.dumi/pages/index/components/PreviewBanner/useMouseTransform.tsx @@ -0,0 +1,73 @@ +import React, { startTransition } from 'react'; +import { ConfigProvider } from 'antd'; + +const getTransformRotateStyle = ( + event: React.MouseEvent, + currentTarget: EventTarget & HTMLDivElement, + multiple: number, + isRTL: boolean, +): string => { + const box = currentTarget?.getBoundingClientRect(); + const calcX = -(event.clientY - box.y - box.height / 2) / multiple; + const calcY = (event.clientX - box.x - box.width / 2) / multiple; + return isRTL + ? `rotate3d(${24 + calcX}, ${83 + calcY}, -45, 57deg)` + : `rotate3d(${24 + calcX}, ${-83 + calcY}, 45, 57deg)`; +}; + +const useMouseTransform = ({ transitionDuration = 500, multiple = 36 } = {}) => { + const [componentsBlockStyle, setComponentsBlockStyle] = React.useState({}); + + const { direction } = React.useContext(ConfigProvider.ConfigContext); + + const isRTL = direction === 'rtl'; + + const onMouseMove: React.MouseEventHandler = (event) => { + const { currentTarget } = event; + startTransition(() => { + setComponentsBlockStyle((style) => ({ + ...style, + transform: getTransformRotateStyle(event, currentTarget, multiple, isRTL), + })); + }); + }; + + const onMouseEnter: React.MouseEventHandler = () => { + startTransition(() => { + setComponentsBlockStyle((style) => ({ + ...style, + transition: `transform ${transitionDuration / 1000}s`, + })); + }); + + setTimeout(() => { + startTransition(() => { + setComponentsBlockStyle((style) => ({ + ...style, + transition: '', + })); + }); + }, transitionDuration); + }; + + const onMouseLeave: React.MouseEventHandler = () => { + startTransition(() => { + setComponentsBlockStyle((style) => ({ + ...style, + transition: `transform ${transitionDuration / 1000}s`, + transform: '', + })); + }); + }; + + return [ + componentsBlockStyle, + { + onMouseMove, + onMouseEnter, + onMouseLeave, + }, + ] as const; +}; + +export default useMouseTransform; diff --git a/.dumi/pages/index/components/RecommendsOld.tsx b/.dumi/pages/index/components/RecommendsOld.tsx index 0a04a5559ad5..5e6c00fa47ac 100644 --- a/.dumi/pages/index/components/RecommendsOld.tsx +++ b/.dumi/pages/index/components/RecommendsOld.tsx @@ -1,14 +1,10 @@ import * as React from 'react'; +import { createStyles, css, useTheme } from 'antd-style'; import { Row, Col, Typography } from 'antd'; -import { css } from '@emotion/react'; import type { Recommendation } from './util'; -import useSiteToken from '../../../hooks/useSiteToken'; -const useStyle = () => { - const { token } = useSiteToken(); - - return { - card: css` +const useStyle = createStyles(({ token }) => ({ + card: css` height: 300px; background-size: 100% 100%; background-position: center; @@ -70,16 +66,15 @@ const useStyle = () => { } } `, - }; -}; +})); export interface RecommendsProps { recommendations?: Recommendation[]; } export default function Recommends({ recommendations = [] }: RecommendsProps) { - const { token } = useSiteToken(); - const style = useStyle(); + const token = useTheme(); + const { styles } = useStyle(); return ( @@ -89,7 +84,7 @@ export default function Recommends({ recommendations = [] }: RecommendsProps) { return ( {data ? ( -
+
{data?.title} {data.description} diff --git a/.dumi/pages/index/components/Theme/BackgroundImage.tsx b/.dumi/pages/index/components/Theme/BackgroundImage.tsx index 8881ec72e957..077f34dc2899 100644 --- a/.dumi/pages/index/components/Theme/BackgroundImage.tsx +++ b/.dumi/pages/index/components/Theme/BackgroundImage.tsx @@ -1,6 +1,8 @@ -import { css } from '@emotion/react'; -import React, { useMemo } from 'react'; -import useSiteToken from '../../../../hooks/useSiteToken'; +import React, { useMemo, useState } from 'react'; +import { createStyles, css } from 'antd-style'; +import classNames from 'classnames'; +import { CSSMotionList } from 'rc-motion'; + import { COLOR_IMAGES, getClosetColor } from './colorUtil'; export interface BackgroundImageProps { @@ -8,39 +10,75 @@ export interface BackgroundImageProps { isLight?: boolean; } -const useStyle = () => { - const { token } = useSiteToken(); - return { - image: css` - transition: all ${token.motionDurationSlow}; - position: absolute; - left: 0; - top: 0; - height: 100%; - width: 100%; - object-fit: cover; - object-position: right top; - `, - }; -}; +const useStyle = createStyles(({ token }) => ({ + image: css` + transition: all ${token.motionDurationSlow}; + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 100%; + object-fit: cover; + object-position: right top; + `, +})); + +const onShow = () => ({ + opacity: 1, +}); +const onHide = () => ({ + opacity: 0, +}); const BackgroundImage: React.FC = ({ colorPrimary, isLight }) => { const activeColor = useMemo(() => getClosetColor(colorPrimary), [colorPrimary]); + const { styles } = useStyle(); + + const [keyList, setKeyList] = useState([]); - const { image } = useStyle(); + React.useLayoutEffect(() => { + setKeyList([activeColor as string]); + }, [activeColor]); return ( - <> - {COLOR_IMAGES.filter(({ url }) => url).map(({ color, url }) => ( - - ))} - + + {({ key: color, className, style }) => { + const cls = classNames(styles.image, className); + const entity = COLOR_IMAGES.find((ent) => ent.color === color); + + if (!entity || !entity.url) { + return null as unknown as React.ReactElement; + } + + const { opacity } = style || {}; + + return ( + + + + + + ); + }} + ); }; diff --git a/.dumi/pages/index/components/Theme/ColorPicker.tsx b/.dumi/pages/index/components/Theme/ColorPicker.tsx index c9c6655b85a8..464566be4759 100644 --- a/.dumi/pages/index/components/Theme/ColorPicker.tsx +++ b/.dumi/pages/index/components/Theme/ColorPicker.tsx @@ -1,43 +1,48 @@ -import { css } from '@emotion/react'; +import React, { useEffect, useState } from 'react'; import { ColorPicker, Input, Space } from 'antd'; -import type { Color, ColorPickerProps } from 'antd/es/color-picker'; +import { createStyles } from 'antd-style'; +import type { Color } from 'antd/es/color-picker'; import { generateColor } from 'antd/es/color-picker/util'; -import type { FC } from 'react'; -import React, { useEffect, useState } from 'react'; -import useSiteToken from '../../../../hooks/useSiteToken'; +import classNames from 'classnames'; + import { PRESET_COLORS } from './colorUtil'; -const useStyle = () => { - const { token } = useSiteToken(); - - return { - color: css` - width: ${token.controlHeightLG / 2}px; - height: ${token.controlHeightLG / 2}px; - border-radius: 100%; - cursor: pointer; - transition: all ${token.motionDurationFast}; - display: inline-block; - - & > input[type="radio"] { - width: 0; - height: 0; - opacity: 0; - } - - &:focus-within { - // need ? - } - `, - - colorActive: css` - box-shadow: 0 0 0 1px ${token.colorBgContainer}, - 0 0 0 ${token.controlOutlineWidth * 2 + 1}px ${token.colorPrimary}; - `, - }; -}; +const useStyle = createStyles(({ token, css }) => ({ + color: css` + width: ${token.controlHeightLG / 2}px; + height: ${token.controlHeightLG / 2}px; + border-radius: 100%; + cursor: pointer; + transition: all ${token.motionDurationFast}; + display: inline-block; + + & > input[type='radio'] { + width: 0; + height: 0; + opacity: 0; + } + + &:focus-within { + // need ? + } + `, + + colorActive: css` + box-shadow: + 0 0 0 1px ${token.colorBgContainer}, + 0 0 0 ${token.controlOutlineWidth * 2 + 1}px ${token.colorPrimary}; + `, +})); + +export interface ColorPickerProps { + id?: string; + children?: React.ReactNode; + value?: string | Color; + onChange?: (value?: Color | string) => void; +} -const DebouncedColorPicker: FC = ({ value: color, onChange, children }) => { +const DebouncedColorPicker: React.FC = (props) => { + const { value: color, children, onChange } = props; const [value, setValue] = useState(color); useEffect(() => { @@ -55,40 +60,24 @@ const DebouncedColorPicker: FC = ({ value: color, onChange, ch {children} ); }; -export interface RadiusPickerProps { - value?: string | Color; - onChange?: (value: string) => void; -} - -export default function ThemeColorPicker({ value, onChange }: RadiusPickerProps) { - const style = useStyle(); +const ThemeColorPicker: React.FC = ({ value, onChange, id }) => { + const { styles } = useStyle(); const matchColors = React.useMemo(() => { - const valueStr = generateColor(value).toRgbString(); + const valueStr = generateColor(value || '').toRgbString(); let existActive = false; - const colors = PRESET_COLORS.map((color) => { const colorStr = generateColor(color).toRgbString(); const active = colorStr === valueStr; existActive = existActive || active; - - return { - color, - active, - picker: false, - }; + return { color, active, picker: false }; }); return [ @@ -104,11 +93,10 @@ export default function ThemeColorPicker({ value, onChange }: RadiusPickerProps) return ( { - onChange?.(event.target.value); - }} + value={typeof value === 'string' ? value : value?.toHexString()} + onChange={(event) => onChange?.(event.target.value)} style={{ width: 120 }} + id={id} /> @@ -117,10 +105,8 @@ export default function ThemeColorPicker({ value, onChange }: RadiusPickerProps) // eslint-disable-next-line jsx-a11y/label-has-associated-control ); -} +}; + +export default ThemeColorPicker; diff --git a/.dumi/pages/index/components/Theme/MobileCarousel.tsx b/.dumi/pages/index/components/Theme/MobileCarousel.tsx index 12fdeae6d42d..6833309cbc83 100644 --- a/.dumi/pages/index/components/Theme/MobileCarousel.tsx +++ b/.dumi/pages/index/components/Theme/MobileCarousel.tsx @@ -1,12 +1,11 @@ import * as React from 'react'; import { useState } from 'react'; -import { css } from '@emotion/react'; +import { createStyles, css, useTheme } from 'antd-style'; import { Typography, Carousel } from 'antd'; -import { useCarouselStyle } from '../util'; -import useSiteToken from '../../../../hooks/useSiteToken'; +import { getCarouselStyle } from '../util'; -const useStyle = () => { - const { carousel } = useCarouselStyle(); +const useStyle = createStyles(() => { + const { carousel } = getCarouselStyle(); return { carousel, container: css` @@ -20,7 +19,7 @@ const useStyle = () => { text-align: center; `, }; -}; +}); const mobileImageConfigList = [ { @@ -77,14 +76,14 @@ export interface MobileCarouselProps { } export default function MobileCarousel(props: MobileCarouselProps) { - const styles = useStyle(); + const { styles } = useStyle(); const { id, title, description } = props; - const { token } = useSiteToken(); + const token = useTheme(); const [currentSlider, setCurrentSlider] = useState(0); return ( -
-
+
+
- + {mobileImageConfigList.map((item, index) => (
diff --git a/.dumi/pages/index/components/Theme/RadiusPicker.tsx b/.dumi/pages/index/components/Theme/RadiusPicker.tsx index 589938130f06..ee7f03e574c7 100644 --- a/.dumi/pages/index/components/Theme/RadiusPicker.tsx +++ b/.dumi/pages/index/components/Theme/RadiusPicker.tsx @@ -1,12 +1,13 @@ -import { InputNumber, Space, Slider } from 'antd'; import React from 'react'; +import { InputNumber, Slider, Space } from 'antd'; export interface RadiusPickerProps { + id?: string; value?: number; onChange?: (value: number | null) => void; } -export default function RadiusPicker({ value, onChange }: RadiusPickerProps) { +export default function RadiusPicker({ value, onChange, id }: RadiusPickerProps) { return ( `${val}px`} parser={(str) => (str ? parseFloat(str) : (str as any))} + id={id} /> { - const { token } = useSiteToken(); - - return { - themeCard: css` - border-radius: ${token.borderRadius}px; - cursor: pointer; - transition: all ${token.motionDurationSlow}; - overflow: hidden; - display: inline-block; - - & > input[type="radio"] { - width: 0; - height: 0; - opacity: 0; - } - - img { - vertical-align: top; - box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), - 0 9px 28px 8px rgba(0, 0, 0, 0.05); - } - - &:focus-within, - &:hover { - transform: scale(1.04); - } - `, - - themeCardActive: css` - box-shadow: 0 0 0 1px ${token.colorBgContainer}, - 0 0 0 ${token.controlOutlineWidth * 2 + 1}px ${token.colorPrimary}; - - &, - &:hover:not(:focus-within) { - transform: scale(1); - } - `, - }; -}; +const useStyle = createStyles(({ token, css }) => ({ + themeCard: css` + border-radius: ${token.borderRadius}px; + cursor: pointer; + transition: all ${token.motionDurationSlow}; + overflow: hidden; + display: inline-block; + + & > input[type='radio'] { + width: 0; + height: 0; + opacity: 0; + position: absolute; + } + + img { + vertical-align: top; + box-shadow: + 0 3px 6px -4px rgba(0, 0, 0, 0.12), + 0 6px 16px 0 rgba(0, 0, 0, 0.08), + 0 9px 28px 8px rgba(0, 0, 0, 0.05); + } + + &:focus-within, + &:hover { + transform: scale(1.04); + } + `, + + themeCardActive: css` + box-shadow: + 0 0 0 1px ${token.colorBgContainer}, + 0 0 0 ${token.controlOutlineWidth * 2 + 1}px ${token.colorPrimary}; + + &, + &:hover:not(:focus-within) { + transform: scale(1); + } + `, +})); export interface ThemePickerProps { + id?: string; value?: string; onChange?: (value: string) => void; } -export default function ThemePicker({ value, onChange }: ThemePickerProps) { - const { token } = useSiteToken(); - const style = useStyle(); +export default function ThemePicker(props: ThemePickerProps) { + const { value, onChange, id } = props; + + const token = useTheme(); + const { styles } = useStyle(); const [locale] = useLocale(locales); return ( - {Object.keys(THEMES).map((theme) => { + {Object.keys(THEMES).map((theme, index) => { const url = THEMES[theme as THEME]; return ( {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} {locale[theme as keyof typeof locale]} diff --git a/.dumi/pages/index/components/Theme/colorUtil.ts b/.dumi/pages/index/components/Theme/colorUtil.ts index 0c3ebbccc008..de913e83f0ac 100644 --- a/.dumi/pages/index/components/Theme/colorUtil.ts +++ b/.dumi/pages/index/components/Theme/colorUtil.ts @@ -1,3 +1,4 @@ +import type { Color } from 'antd/es/color-picker'; import { generateColor } from 'antd/es/color-picker/util'; export const DEFAULT_COLOR = '#1677FF'; @@ -8,41 +9,50 @@ export const COLOR_IMAGES = [ color: DEFAULT_COLOR, // url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*QEAoSL8uVi4AAAAAAAAAAAAAARQnAQ', url: null, + webp: null, }, { color: '#5A54F9', - url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*MtVDSKukKj8AAAAAAAAAAAAAARQnAQ', + url: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*H8nRT7_q0EwAAAAAAAAAAAAADrJ8AQ/original', + webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*H8nRT7_q0EwAAAAAAAAAAAAADrJ8AQ/fmt.webp', }, { color: '#9E339F', url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*FMluR4vJhaQAAAAAAAAAAAAAARQnAQ', + webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*CMCMTKV51tIAAAAAAAAAAAAADrJ8AQ/fmt.webp', }, { color: PINK_COLOR, url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*DGZXS4YOGp0AAAAAAAAAAAAAARQnAQ', + webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*s5OdR6wZZIkAAAAAAAAAAAAADrJ8AQ/fmt.webp', }, { color: '#E0282E', url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*w6xcR7MriwEAAAAAAAAAAAAAARQnAQ', + webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*HE_4Qp_XfQQAAAAAAAAAAAAADrJ8AQ/fmt.webp', }, { color: '#F4801A', url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*VWFOTbEyU9wAAAAAAAAAAAAAARQnAQ', + webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*xTG2QbottAQAAAAAAAAAAAAADrJ8AQ/fmt.webp', }, { color: '#F2BD27', url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*1yydQLzw5nYAAAAAAAAAAAAAARQnAQ', + webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*hbPfSbF-xPIAAAAAAAAAAAAADrJ8AQ/fmt.webp', }, { color: '#00B96B', url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*XpGeRoZKGycAAAAAAAAAAAAAARQnAQ', + webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*iM6CQ496P3oAAAAAAAAAAAAADrJ8AQ/fmt.webp', }, ] as const; export const PRESET_COLORS = COLOR_IMAGES.map(({ color }) => color); const DISTANCE = 33; -export function getClosetColor(colorPrimary?: string | null) { + +export function getClosetColor(colorPrimary?: Color | string | null) { if (!colorPrimary) { return null; } diff --git a/.dumi/pages/index/components/Theme/index.tsx b/.dumi/pages/index/components/Theme/index.tsx index 1fdd88bafb6d..e528a1ad398e 100644 --- a/.dumi/pages/index/components/Theme/index.tsx +++ b/.dumi/pages/index/components/Theme/index.tsx @@ -1,10 +1,12 @@ +import * as React from 'react'; +import { defaultAlgorithm, defaultTheme } from '@ant-design/compatible'; import { BellOutlined, FolderOutlined, HomeOutlined, QuestionCircleOutlined, } from '@ant-design/icons'; -import { css } from '@emotion/react'; +import { TinyColor } from '@ctrl/tinycolor'; import type { MenuProps } from 'antd'; import { Breadcrumb, @@ -16,28 +18,33 @@ import { Menu, Radio, Space, - Typography, theme, + Typography, } from 'antd'; +import { createStyles, css, useTheme } from 'antd-style'; import type { Color } from 'antd/es/color-picker'; import { generateColor } from 'antd/es/color-picker/util'; -import * as React from 'react'; +import classNames from 'classnames'; +import { useLocation } from 'dumi'; + +import useDark from '../../../../hooks/useDark'; import useLocale from '../../../../hooks/useLocale'; -import useSiteToken from '../../../../hooks/useSiteToken'; +import Link from '../../../../theme/common/Link'; import SiteContext from '../../../../theme/slots/SiteContext'; +import * as utils from '../../../../theme/utils'; import Group from '../Group'; -import { useCarouselStyle } from '../util'; +import { getCarouselStyle } from '../util'; import BackgroundImage from './BackgroundImage'; import ColorPicker from './ColorPicker'; +import { DEFAULT_COLOR, getAvatarURL, getClosetColor, PINK_COLOR } from './colorUtil'; import MobileCarousel from './MobileCarousel'; import RadiusPicker from './RadiusPicker'; import type { THEME } from './ThemePicker'; import ThemePicker from './ThemePicker'; -import { DEFAULT_COLOR, PINK_COLOR, getAvatarURL, getClosetColor } from './colorUtil'; const { Header, Content, Sider } = Layout; -const TokenChecker = () => { +const TokenChecker: React.FC = () => { if (process.env.NODE_ENV !== 'production') { console.log('Demo Token:', theme.useToken()); } @@ -83,35 +90,44 @@ const locales = { }; // ============================= Style ============================= -const useStyle = () => { - const { token } = useSiteToken(); - const { carousel } = useCarouselStyle(); +const useStyle = createStyles(({ token, cx }) => { + const { carousel } = getCarouselStyle(); + + const demo = css` + overflow: hidden; + background: rgba(240, 242, 245, 0.25); + backdrop-filter: blur(50px); + box-shadow: 0 2px 10px 2px rgba(0, 0, 0, 0.1); + transition: all ${token.motionDurationSlow}; + `; return { - demo: css` - overflow: hidden; - background: rgba(240, 242, 245, 0.25); - backdrop-filter: blur(50px); - box-shadow: 0 2px 10px 2px rgba(0, 0, 0, 0.1); - transition: all ${token.motionDurationSlow}; - `, + demo, otherDemo: css` - backdrop-filter: blur(10px); - background: rgba(247, 247, 247, 0.5); + &.${cx(demo)} { + backdrop-filter: blur(10px); + background: rgba(247, 247, 247, 0.5); + } `, darkDemo: css` - background: #000; + &.${cx(demo)} { + background: #000; + } `, larkDemo: css` - // background: #f7f7f7; - background: rgba(240, 242, 245, 0.65); + &.${cx(demo)} { + // background: #f7f7f7; + background: rgba(240, 242, 245, 0.65); + } `, comicDemo: css` - // background: #ffe4e6; - background: rgba(240, 242, 245, 0.65); + &.${cx(demo)} { + // background: #ffe4e6; + background: rgba(240, 242, 245, 0.65); + } `, menu: css` @@ -168,12 +184,6 @@ const useStyle = () => { } `, - logoImgPureColor: css` - img { - transform: translate3d(-30px, 0, 0); - } - `, - transBg: css` background: transparent !important; `, @@ -184,10 +194,10 @@ const useStyle = () => { `, carousel, }; -}; +}); // ========================== Menu Config ========================== -const subMenuItems: MenuProps['items'] = [ +const subMenuItems = [ { key: `Design Values`, label: `Design Values`, @@ -236,6 +246,10 @@ function getTitleColor(colorPrimary: string | Color, isLight?: boolean) { case '#F2BD27': return undefined; + case '#5A54F9': + case '#E0282E': + return '#FFF'; + default: return color.toHsb().b < 0.7 ? '#FFF' : undefined; } @@ -268,17 +282,46 @@ const ThemesInfo: Record> = { colorPrimary: PINK_COLOR, borderRadius: 16, }, + v4: { + ...defaultTheme.token, + }, }; +const normalize = (value: number) => value / 255; + +function rgbToColorMatrix(color: string) { + const rgb = new TinyColor(color).toRgb(); + const { r, g, b } = rgb; + + const invertValue = normalize(r) * 100; + const sepiaValue = 100; + const saturateValue = Math.max(normalize(r), normalize(g), normalize(b)) * 10000; + const hueRotateValue = + ((Math.atan2( + Math.sqrt(3) * (normalize(g) - normalize(b)), + 2 * normalize(r) - normalize(g) - normalize(b), + ) * + 180) / + Math.PI + + 360) % + 360; + + return `invert(${invertValue}%) sepia(${sepiaValue}%) saturate(${saturateValue}%) hue-rotate(${hueRotateValue}deg)`; +} + export default function Theme() { - const style = useStyle(); - const { token } = useSiteToken(); - const [locale] = useLocale(locales); + const { styles } = useStyle(); + const token = useTheme(); + const [locale, lang] = useLocale(locales); + const isZhCN = lang === 'cn'; + const { search } = useLocation(); const [themeData, setThemeData] = React.useState(ThemeDefault); const onThemeChange = (_: Partial, nextThemeData: ThemeData) => { - setThemeData(nextThemeData); + React.startTransition(() => { + setThemeData({ ...ThemesInfo[nextThemeData.themeType], ...nextThemeData }); + }); }; const { compact, themeType, colorPrimary, ...themeToken } = themeData; @@ -298,8 +341,12 @@ export default function Theme() { algorithms.push(theme.compactAlgorithm); } + if (themeType === 'v4') { + algorithms.push(defaultAlgorithm); + } + return algorithms; - }, [isLight, compact]); + }, [isLight, compact, themeType]); // ================================ Themes ================================ React.useEffect(() => { @@ -307,12 +354,18 @@ export default function Theme() { ...ThemeDefault, themeType, ...ThemesInfo[themeType], - } as any; + }; setThemeData(mergedData); form.setFieldsValue(mergedData); }, [themeType]); + const isRootDark = useDark(); + + React.useEffect(() => { + onThemeChange({}, { ...themeData, themeType: isRootDark ? 'dark' : 'default' }); + }, [isRootDark]); + // ================================ Tokens ================================ const closestColor = getClosetColor(colorPrimaryValue); @@ -348,25 +401,11 @@ export default function Theme() { theme={{ token: { ...themeToken, - ...(isLight - ? {} - : { - // colorBgContainer: '#474C56', - // colorBorderSecondary: 'rgba(255,255,255,0.06)', - }), colorPrimary: colorPrimaryValue, }, hashed: true, algorithm: algorithmFn, components: { - Slider: { - // 1677FF - }, - Card: isLight - ? {} - : { - // colorBgContainer: '#474C56', - }, Layout: isLight ? { colorBgHeader: 'transparent', @@ -377,9 +416,9 @@ export default function Theme() { }, Menu: isLight ? { - colorItemBg: 'transparent', - colorSubItemBg: 'transparent', - colorActiveBarWidth: 0, + itemBg: 'transparent', + subMenuItemBg: 'transparent', + activeBarBorderWidth: 0, } : { // colorItemBg: 'transparent', @@ -387,30 +426,33 @@ export default function Theme() { // colorItemBgActive: 'rgba(255,255,255,0.2)', // colorItemBgSelected: 'rgba(255,255,255,0.2)', }, + ...(themeType === 'v4' ? defaultTheme.components : {}), }, }} >
- -
+ +
{/* Logo */} -
-
+
+
@@ -418,11 +460,11 @@ export default function Theme() {

Ant Design 5.0

- +
- - + + - - - - - - }>Design - Themes - + + }, + { title: 'Design', menu: { items: subMenuItems } }, + { title: 'Themes' }, + ]} + /> {locale.customizeTheme} - - + + + + + + } > @@ -466,9 +520,9 @@ export default function Theme() { form={form} initialValues={themeData} onValuesChange={onThemeChange} - labelCol={{ span: 4 }} - wrapperCol={{ span: 20 }} - css={style.form} + labelCol={{ span: 3 }} + wrapperCol={{ span: 21 }} + className={styles.form} > @@ -480,11 +534,20 @@ export default function Theme() { - - - {locale.default} - {locale.compact} - + + @@ -499,13 +562,13 @@ export default function Theme() { const posStyle: React.CSSProperties = { position: 'absolute', }; - const leftTopImageStyle = { + const leftTopImageStyle: React.CSSProperties = { left: '50%', transform: 'translate3d(-900px, 0, 0)', top: -100, height: 500, }; - const rightBottomImageStyle = { + const rightBottomImageStyle: React.CSSProperties = { right: '50%', transform: 'translate3d(750px, 0, 0)', bottom: -100, diff --git a/.dumi/pages/index/components/util.ts b/.dumi/pages/index/components/util.ts index 67921644f1bc..295ff4bee7aa 100644 --- a/.dumi/pages/index/components/util.ts +++ b/.dumi/pages/index/components/util.ts @@ -1,5 +1,5 @@ -import * as React from 'react'; -import { css } from '@emotion/react'; +import { css } from 'antd-style'; +import useFetch from '../../../hooks/useFetch'; export interface Author { avatar: string; @@ -80,26 +80,11 @@ export function preLoad(list: string[]) { } } -export function useSiteData(): [Partial, boolean] { - const [data, setData] = React.useState>({}); - const [loading, setLoading] = React.useState(false); - - React.useEffect(() => { - if (Object.keys(data ?? {}).length === 0 && typeof fetch !== 'undefined') { - setLoading(true); - fetch(`https://render.alipay.com/p/h5data/antd4-config_website-h5data.json`) - .then((res) => res.json()) - .then((result) => { - setData(result); - setLoading(false); - }); - } - }, []); - - return [data, loading]; +export function useSiteData(): Partial { + return useFetch('https://render.alipay.com/p/h5data/antd4-config_website-h5data.json'); } -export const useCarouselStyle = () => ({ +export const getCarouselStyle = () => ({ carousel: css` .slick-dots.slick-dots-bottom { bottom: -22px; diff --git a/.dumi/pages/index/index.tsx b/.dumi/pages/index/index.tsx index e23bc5421108..775df42f83be 100644 --- a/.dumi/pages/index/index.tsx +++ b/.dumi/pages/index/index.tsx @@ -1,24 +1,25 @@ -import { css } from '@emotion/react'; -import { ConfigProvider } from 'antd'; -import { useLocale as useDumiLocale } from 'dumi'; -import React from 'react'; +import React, { Suspense } from 'react'; +import { ConfigProvider, theme } from 'antd'; +import { createStyles, css } from 'antd-style'; + +import useDark from '../../hooks/useDark'; import useLocale from '../../hooks/useLocale'; -import Banner from './components/Banner'; -import BannerRecommends from './components/BannerRecommends'; -import ComponentsList from './components/ComponentsList'; -import DesignFramework from './components/DesignFramework'; +// import BannerRecommends, { BannerRecommendsFallback } from './components/BannerRecommends'; +import PreviewBanner from './components/PreviewBanner'; import Group from './components/Group'; -import Theme from './components/Theme'; -import { useSiteData } from './components/util'; -const useStyle = () => ({ +const ComponentsList = React.lazy(() => import('./components/ComponentsList')); +const DesignFramework = React.lazy(() => import('./components/DesignFramework')); +const Theme = React.lazy(() => import('./components/Theme')); + +const useStyle = createStyles(() => ({ image: css` position: absolute; left: 0; top: -50px; height: 160px; `, -}); +})); const locales = { cn: { @@ -37,45 +38,64 @@ const locales = { const Homepage: React.FC = () => { const [locale] = useLocale(locales); - const { id: localeId } = useDumiLocale(); - const localeStr = localeId === 'zh-CN' ? 'cn' : 'en'; - const { image } = useStyle(); - const [siteData] = useSiteData(); + const { styles } = useStyle(); + const { token } = theme.useToken(); + + const isRootDark = useDark(); return ( - -
- - - -
- - +
+ + {/* 文档很久没更新了,先藏起来 */} + {/* }> + + */} + + +
+ {/* 定制主题 */} + + + + + + + {/* 组件列表 */} + + - - - } - > + + + + {/* 设计语言 */} + + } + > + - -
-
- + +
+
+
); }; diff --git a/.dumi/pages/theme-editor/index.tsx b/.dumi/pages/theme-editor/index.tsx index 5d11f141ba3e..33e4f3f437c9 100644 --- a/.dumi/pages/theme-editor/index.tsx +++ b/.dumi/pages/theme-editor/index.tsx @@ -1,17 +1,11 @@ -import { css } from '@emotion/react'; -import { Button, ConfigProvider, Modal, Spin, Typography, message } from 'antd'; -import { ThemeEditor, enUS, zhCN } from 'antd-token-previewer'; -import type { ThemeConfig } from 'antd/es/config-provider/context'; +import { enUS, zhCN } from 'antd-token-previewer'; import { Helmet } from 'dumi'; -import React, { Suspense, useCallback, useEffect, useState } from 'react'; -import type { JSONContent, TextContent } from 'vanilla-jsoneditor'; +import React, { Suspense, useEffect } from 'react'; +import type { ThemeConfig } from 'antd/es/config-provider/context'; +import { Button, message, Skeleton } from 'antd'; import useLocale from '../../hooks/useLocale'; -const JSONEditor = React.lazy(() => import('../../theme/common/JSONEditor')); - -function isObject(target: any) { - return Object.prototype.toString.call(target) === '[object Object]'; -} +const ThemeEditor = React.lazy(() => import('antd-token-previewer/lib/ThemeEditor')); const locales = { cn: { @@ -38,17 +32,6 @@ const locales = { }, }; -const useStyle = () => ({ - header: css({ - display: 'flex', - height: 56, - alignItems: 'center', - padding: '0 24px', - justifyContent: 'space-between', - borderBottom: '1px solid #F0F0F0', - }), -}); - const ANT_DESIGN_V5_THEME_EDITOR_THEME = 'ant-design-v5-theme-editor-theme'; const CustomTheme = () => { @@ -57,81 +40,19 @@ const CustomTheme = () => { const [theme, setTheme] = React.useState({}); - const [editModelOpen, setEditModelOpen] = useState(false); - const [editThemeFormatRight, setEditThemeFormatRight] = useState(true); - const [themeConfigContent, setThemeConfigContent] = useState({ - text: '{}', - json: undefined, - }); - useEffect(() => { const storedConfig = localStorage.getItem(ANT_DESIGN_V5_THEME_EDITOR_THEME); if (storedConfig) { const themeConfig = JSON.parse(storedConfig); - const originThemeConfig = { - json: themeConfig, - text: undefined, - }; - setThemeConfigContent(originThemeConfig); setTheme(themeConfig); } }, []); - const styles = useStyle(); - const handleSave = () => { localStorage.setItem(ANT_DESIGN_V5_THEME_EDITOR_THEME, JSON.stringify(theme)); messageApi.success(locale.saveSuccessfully); }; - const handleEditConfig = () => { - setEditModelOpen(true); - }; - - const editModelClose = useCallback(() => { - setEditModelOpen(false); - }, [themeConfigContent]); - - const handleEditConfigChange = (newcontent, preContent, status) => { - setThemeConfigContent(newcontent); - setEditThemeFormatRight(!status.contentErrors); - }; - - const editSave = useCallback(() => { - const contentFormatError = !editThemeFormatRight; - - if (contentFormatError) { - message.error(locale.editJsonContentTypeError); - return; - } - const themeConfig = themeConfigContent.text - ? JSON.parse(themeConfigContent.text) - : themeConfigContent.json; - if (!isObject(themeConfig)) { - message.error(locale.editJsonContentTypeError); - return; - } - setTheme(themeConfig); - editModelClose(); - messageApi.success(locale.editSuccessfully); - }, [themeConfigContent, editThemeFormatRight]); - - const handleExport = () => { - const file = new File([JSON.stringify(theme, null, 2)], `Ant Design Theme.json`, { - type: 'text/json; charset=utf-8;', - }); - const tmpLink = document.createElement('a'); - const objectUrl = URL.createObjectURL(file); - - tmpLink.href = objectUrl; - tmpLink.download = file.name; - document.body.appendChild(tmpLink); - tmpLink.click(); - - document.body.removeChild(tmpLink); - URL.revokeObjectURL(objectUrl); - }; - return (
@@ -139,54 +60,23 @@ const CustomTheme = () => { {contextHolder} - -
- - {locale.title} - -
- - - -
- } - > - - - - - - -
-
+ }> { setTheme(newTheme.config); }} locale={lang === 'cn' ? zhCN : enUS} + actions={ + + } /> -
+
); }; diff --git a/.dumi/preset/.gitkeep b/.dumi/preset/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/.dumi/rehypeAntd.ts b/.dumi/rehypeAntd.ts index 9db6f6571716..8f10bf4d8485 100644 --- a/.dumi/rehypeAntd.ts +++ b/.dumi/rehypeAntd.ts @@ -8,7 +8,7 @@ function rehypeAntd(): UnifiedTransformer { return (tree, vFile) => { const { filename } = vFile.data.frontmatter as any; - unistUtilVisit.visit(tree, 'element', (node) => { + unistUtilVisit.visit(tree, 'element', (node, i, parent) => { if (node.tagName === 'DumiDemoGrid') { // replace DumiDemoGrid to DemoWrapper, to implement demo toolbar node.tagName = 'DemoWrapper'; @@ -62,8 +62,39 @@ function rehypeAntd(): UnifiedTransformer { (node.properties.className as string[]).push('component-api-table'); } else if (node.type === 'element' && (node.tagName === 'Link' || node.tagName === 'a')) { const { tagName } = node; - node.properties.sourceType = tagName; + node.properties!.sourceType = tagName; node.tagName = 'LocaleLink'; + } else if (node.type === 'element' && node.tagName === 'video') { + node.tagName = 'VideoPlayer'; + } else if (node.tagName === 'SourceCode') { + const { lang } = node.properties!; + + if (typeof lang === 'string' && lang.startsWith('sandpack')) { + const code = (node.children[0] as any).value as string; + const configRegx = /^const sandpackConfig = ([\S\s]*?});/; + const [configString] = code.match(configRegx) || []; + // eslint-disable-next-line no-eval + const config = configString && eval(`(${configString.replace(configRegx, '$1')})`); + Object.keys(config || {}).forEach((key) => { + if (typeof config[key] === 'object') { + config[key] = JSON.stringify(config[key]); + } + }); + + parent!.children.splice(i!, 1, { + type: 'element', + tagName: 'Sandpack', + properties: { + ...config, + }, + children: [ + { + type: 'text', + value: code.replace(configRegx, '').trim(), + }, + ], + }); + } } }); }; diff --git a/.dumi/remarkAntd.ts b/.dumi/remarkAntd.ts index 85970e2c01ec..84dc32df3e60 100644 --- a/.dumi/remarkAntd.ts +++ b/.dumi/remarkAntd.ts @@ -1,12 +1,15 @@ import { unistUtilVisit } from 'dumi'; +import type { UnifiedTransformer } from 'dumi'; -export default function remarkMeta() { +function remarkMeta(): UnifiedTransformer { return (tree, vFile) => { // read frontmatter unistUtilVisit.visit(tree, 'yaml', (node) => { if (!/(^|[\n\r])description:/.test(node.value)) { - vFile.data.frontmatter.__autoDescription = true; + (vFile.data.frontmatter as any).__autoDescription = true; } }); }; } + +export default remarkMeta; diff --git a/.dumi/theme/SiteThemeProvider.tsx b/.dumi/theme/SiteThemeProvider.tsx index da43b7aac49d..99847195058d 100644 --- a/.dumi/theme/SiteThemeProvider.tsx +++ b/.dumi/theme/SiteThemeProvider.tsx @@ -1,26 +1,51 @@ -import { ConfigProvider, theme as antdTheme } from 'antd'; +import React, { useContext } from 'react'; +import { theme as antdTheme, ConfigProvider } from 'antd'; +import type { ThemeConfig } from 'antd'; import type { ThemeProviderProps } from 'antd-style'; import { ThemeProvider } from 'antd-style'; -import type { FC } from 'react'; -import React, { useContext } from 'react'; +import SiteContext from './slots/SiteContext'; + +interface NewToken { + bannerHeight: number; + headerHeight: number; + menuItemBorder: number; + mobileMaxWidth: number; + siteMarkdownCodeBg: string; + antCls: string; + iconCls: string; + marginFarXS: number; + marginFarSM: number; + marginFar: number; + codeFamily: string; + contentMarginTop: number; + anchorTop: number; +} -const SiteThemeProvider: FC = ({ children, theme, ...rest }) => { +// 通过给 antd-style 扩展 CustomToken 对象类型定义,可以为 useTheme 中增加相应的 token 对象 +declare module 'antd-style' { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface CustomToken extends NewToken {} +} + +const headerHeight = 64; +const bannerHeight = 38; + +const SiteThemeProvider: React.FC> = ({ children, theme, ...rest }) => { const { getPrefixCls, iconPrefixCls } = useContext(ConfigProvider.ConfigContext); const rootPrefixCls = getPrefixCls(); const { token } = antdTheme.useToken(); - + const { bannerVisible } = useContext(SiteContext); React.useEffect(() => { - ConfigProvider.config({ - theme, - }); + ConfigProvider.config({ theme: theme as ThemeConfig }); }, [theme]); return ( - {...rest} theme={theme} customToken={{ - headerHeight: 64, + headerHeight, + bannerHeight, menuItemBorder: 2, mobileMaxWidth: 767.99, siteMarkdownCodeBg: token.colorFillTertiary, @@ -33,6 +58,8 @@ const SiteThemeProvider: FC = ({ children, theme, ...rest }) /** 96 */ marginFar: token.marginXXL * 2, codeFamily: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace`, + contentMarginTop: 40, + anchorTop: headerHeight + token.margin + (bannerVisible ? bannerHeight : 0), }} > {children} diff --git a/.dumi/theme/antd.js b/.dumi/theme/antd.js deleted file mode 100644 index b35b5b28907c..000000000000 --- a/.dumi/theme/antd.js +++ /dev/null @@ -1,4 +0,0 @@ -// Need import for the additional core style -// exports.styleCore = require('../components/style/reset.css'); - -module.exports = require('../../components'); diff --git a/.dumi/theme/builtins/Alert/index.tsx b/.dumi/theme/builtins/Alert/index.tsx index c206aff27db1..b00ec1f63016 100644 --- a/.dumi/theme/builtins/Alert/index.tsx +++ b/.dumi/theme/builtins/Alert/index.tsx @@ -1,6 +1,6 @@ +import React from 'react'; import type { AlertProps } from 'antd'; import { Alert } from 'antd'; -import React from 'react'; const MdAlert: React.FC = ({ style, ...props }) => ( diff --git a/.dumi/theme/builtins/ColorChunk/index.tsx b/.dumi/theme/builtins/ColorChunk/index.tsx index 5ec1bdd2e384..598abb34b2b1 100644 --- a/.dumi/theme/builtins/ColorChunk/index.tsx +++ b/.dumi/theme/builtins/ColorChunk/index.tsx @@ -1,47 +1,42 @@ -import * as React from 'react'; import { TinyColor, type ColorInput } from '@ctrl/tinycolor'; -import { css } from '@emotion/react'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { createStyles } from 'antd-style'; +import * as React from 'react'; interface ColorChunkProps { children?: React.ReactNode; - color?: ColorInput; + value?: ColorInput; } -const useStyle = () => { - const { token } = useSiteToken(); - - return { - codeSpan: css` +const useStyle = createStyles(({ token, css }) => ({ + codeSpan: css` padding: 0.2em 0.4em; font-size: 0.9em; background: ${token.siteMarkdownCodeBg}; border-radius: ${token.borderRadius}px; font-family: monospace; `, - dot: css` + dot: css` display: inline-block; width: 6px; height: 6px; - border-radius: ${token.borderRadiusSM}px; + border-radius: 50%; margin-inline-end: 4px; border: 1px solid ${token.colorSplit}; `, - }; -}; +})); const ColorChunk: React.FC = (props) => { - const styles = useStyle(); - const { color, children } = props; + const { styles } = useStyle(); + const { value, children } = props; const dotColor = React.useMemo(() => { - const _color = new TinyColor(color).toHex8String(); + const _color = new TinyColor(value).toHex8String(); return _color.endsWith('ff') ? _color.slice(0, -2) : _color; - }, [color]); + }, [value]); return ( - - + + {children ?? dotColor} ); diff --git a/.dumi/theme/builtins/ComponentOverview/index.tsx b/.dumi/theme/builtins/ComponentOverview/index.tsx index 19b5b5ed289e..827ee71168dd 100644 --- a/.dumi/theme/builtins/ComponentOverview/index.tsx +++ b/.dumi/theme/builtins/ComponentOverview/index.tsx @@ -1,62 +1,65 @@ import React, { memo, useContext, useMemo, useRef, useState } from 'react'; import type { CSSProperties } from 'react'; -import { Link, useIntl, useSidebarData, useLocation } from 'dumi'; -import { css } from '@emotion/react'; -import debounce from 'lodash/debounce'; -import { Card, Col, Divider, Input, Row, Space, Tag, Typography, Affix } from 'antd'; import { SearchOutlined } from '@ant-design/icons'; +import { Affix, Card, Col, Divider, Input, Row, Space, Tag, Typography } from 'antd'; +import { createStyles, useTheme } from 'antd-style'; +import { useIntl, useLocation, useSidebarData } from 'dumi'; +import debounce from 'lodash/debounce'; +import scrollIntoView from 'scroll-into-view-if-needed'; + +import Link from '../../common/Link'; +import SiteContext from '../../slots/SiteContext'; import type { Component } from './ProComponentsList'; import proComponentsList from './ProComponentsList'; -import useSiteToken from '../../../hooks/useSiteToken'; -import SiteContext from '../../slots/SiteContext'; -const useStyle = () => { - const { token } = useSiteToken(); - return { - componentsOverviewGroupTitle: css` - margin-bottom: 24px !important; - `, - componentsOverviewTitle: css` - overflow: hidden; - color: ${token.colorTextHeading}; - text-overflow: ellipsis; - `, - componentsOverviewImg: css` - display: flex; - align-items: center; - justify-content: center; - height: 152px; - `, - componentsOverviewCard: css` - cursor: pointer; - transition: all 0.5s; - &:hover { - box-shadow: 0 6px 16px -8px #00000014, 0 9px 28px #0000000d, 0 12px 48px 16px #00000008; - } - `, - componentsOverviewAffix: css` - display: flex; - transition: all 0.3s; - justify-content: space-between; - `, - componentsOverviewSearch: css` - padding: 0; - .anticon-search { - color: ${token.colorTextDisabled}; - } - `, - componentsOverviewContent: css` - &:empty:after { - display: block; - padding: 16px 0 40px; - color: ${token.colorTextDisabled}; - text-align: center; - border-bottom: 1px solid ${token.colorSplit}; - content: 'Not Found'; - } - `, - }; -}; +const useStyle = createStyles(({ token, css }) => ({ + componentsOverviewGroupTitle: css` + margin-bottom: 24px !important; + `, + componentsOverviewTitle: css` + overflow: hidden; + color: ${token.colorTextHeading}; + text-overflow: ellipsis; + `, + componentsOverviewImg: css` + display: flex; + align-items: center; + justify-content: center; + height: 152px; + `, + componentsOverviewCard: css` + cursor: pointer; + transition: all 0.5s; + &:hover { + box-shadow: + 0 6px 16px -8px #00000014, + 0 9px 28px #0000000d, + 0 12px 48px 16px #00000008; + } + `, + componentsOverviewAffix: css` + display: flex; + transition: all 0.3s; + justify-content: space-between; + `, + componentsOverviewSearch: css` + padding: 0; + box-shadow: none !important; + .anticon-search { + color: ${token.colorTextDisabled}; + } + `, + componentsOverviewContent: css` + &:empty:after { + display: block; + padding: 16px 0 40px; + color: ${token.colorTextDisabled}; + text-align: center; + border-bottom: 1px solid ${token.colorSplit}; + content: 'Not Found'; + } + `, +})); const onClickCard = (pathname: string) => { if (window.gtag) { @@ -79,14 +82,14 @@ const reportSearch = debounce<(value: string) => void>((value) => { const { Title } = Typography; const Overview: React.FC = () => { - const style = useStyle(); + const { styles } = useStyle(); const { theme } = useContext(SiteContext); const data = useSidebarData(); const [searchBarAffixed, setSearchBarAffixed] = useState(false); - const { token } = useSiteToken(); - const { borderRadius, colorBgContainer, fontSizeXL } = token; + const token = useTheme(); + const { borderRadius, colorBgContainer, fontSizeXL, anchorTop } = token; const affixedStyle: CSSProperties = { boxShadow: 'rgba(50, 50, 93, 0.25) 0 6px 12px -2px, rgba(0, 0, 0, 0.3) 0 3px 7px -3px', @@ -102,7 +105,7 @@ const Overview: React.FC = () => { const [search, setSearch] = useState(() => { const params = new URLSearchParams(urlSearch); if (params.has('s')) { - return params.get('s'); + return params.get('s') || ''; } return ''; }); @@ -111,7 +114,7 @@ const Overview: React.FC = () => { const onKeyDown: React.KeyboardEventHandler = (event) => { if (event.keyCode === 13 && search.trim().length) { - sectionRef.current?.querySelector('.components-overview-card')?.click(); + sectionRef.current?.querySelector(`.${styles.componentsOverviewCard}`)?.click(); } }; @@ -120,12 +123,12 @@ const Overview: React.FC = () => { data .filter((item) => item?.title) .map<{ title: string; children: Component[] }>((item) => ({ - title: item?.title, + title: item?.title || '', children: item.children.map((child) => ({ - title: child.frontmatter?.title, - subtitle: child.frontmatter.subtitle, - cover: child.frontmatter.cover, - coverDark: child.frontmatter.coverDark, + title: child.frontmatter?.title || '', + subtitle: child.frontmatter?.subtitle, + cover: child.frontmatter?.cover, + coverDark: child.frontmatter?.coverDark, link: child.link, })), })) @@ -143,16 +146,29 @@ const Overview: React.FC = () => { return (
- -
+ setSearchBarAffixed(!!affixed)}> +
{ setSearch(e.target.value); reportSearch(e.target.value); + if (sectionRef.current && searchBarAffixed) { + scrollIntoView(sectionRef.current, { + scrollMode: 'if-needed', + block: 'start', + behavior: (actions) => + actions.forEach(({ el, top }) => { + el.scrollTop = top - 64; + }), + }); + } }} onKeyDown={onKeyDown} bordered={false} @@ -162,7 +178,7 @@ const Overview: React.FC = () => {
-
+
{groups .filter((i) => i?.title) .map((group) => { @@ -174,7 +190,7 @@ const Overview: React.FC = () => { ); return components?.length ? (
- + <Title level={2} className={styles.componentsOverviewGroupTitle}> <Space align="center"> <span style={{ fontSize: 24 }}>{group?.title}</span> <Tag style={{ display: 'block' }}>{components.length}</Tag> @@ -190,27 +206,25 @@ const Overview: React.FC = () => { url += urlSearch; } - /** Link 不能跳转到外链 */ - const ComponentLink = isExternalLink ? 'a' : Link; - return ( <Col xs={24} sm={12} lg={8} xl={6} key={component?.title}> - <ComponentLink to={url} href={url} onClick={() => onClickCard(url)}> + <Link to={url}> <Card + onClick={() => onClickCard(url)} bodyStyle={{ backgroundRepeat: 'no-repeat', backgroundPosition: 'bottom right', backgroundImage: `url(${component?.tag || ''})`, }} size="small" - css={style.componentsOverviewCard} + className={styles.componentsOverviewCard} title={ - <div css={style.componentsOverviewTitle}> + <div className={styles.componentsOverviewTitle}> {component?.title} {component.subtitle} </div> } > - <div css={style.componentsOverviewImg}> + <div className={styles.componentsOverviewImg}> <img src={ theme.includes('dark') && component.coverDark @@ -221,7 +235,7 @@ const Overview: React.FC = () => { /> </div> </Card> - </ComponentLink> + </Link> </Col> ); })} diff --git a/.dumi/theme/builtins/ComponentTokenTable/index.tsx b/.dumi/theme/builtins/ComponentTokenTable/index.tsx index cb184fd1f256..7d64eb2d5aed 100644 --- a/.dumi/theme/builtins/ComponentTokenTable/index.tsx +++ b/.dumi/theme/builtins/ComponentTokenTable/index.tsx @@ -1,12 +1,11 @@ -import { RightOutlined } from '@ant-design/icons'; -import { css } from '@emotion/react'; -import { ConfigProvider, Table } from 'antd'; +import { RightOutlined, LinkOutlined, QuestionCircleOutlined } from '@ant-design/icons'; +import { createStyles, css, useTheme } from 'antd-style'; import { getDesignToken } from 'antd-token-previewer'; +import React, { useMemo, useState } from 'react'; import tokenMeta from 'antd/es/version/token-meta.json'; import tokenData from 'antd/es/version/token.json'; -import React, { useMemo, useState } from 'react'; +import { ConfigProvider, Table, Popover, Typography } from 'antd'; import useLocale from '../../../hooks/useLocale'; -import useSiteToken from '../../../hooks/useSiteToken'; import { useColumns } from '../TokenTable'; const defaultToken = getDesignToken(); @@ -17,16 +16,26 @@ const locales = { description: '描述', type: '类型', value: '默认值', + componentToken: '组件 Token', + globalToken: '全局 Token', + help: '如何定制?', + customizeTokenLink: '/docs/react/customize-theme-cn#修改主题变量', + customizeComponentTokenLink: '/docs/react/customize-theme-cn#修改组件变量', }, en: { token: 'Token Name', description: 'Description', type: 'Type', value: 'Default Value', + componentToken: 'Component Token', + globalToken: 'Global Token', + help: 'How to use?', + customizeTokenLink: '/docs/react/customize-theme#customize-design-token', + customizeComponentTokenLink: 'docs/react/customize-theme#customize-component-token', }, }; -const useStyle = () => ({ +const useStyle = createStyles(() => ({ tableTitle: css` cursor: pointer; position: relative; @@ -42,44 +51,69 @@ const useStyle = () => ({ transition: all 0.3s; } `, -}); + help: css` + margin-left: 8px; + font-size: 12px; + font-weight: normal; + color: #999; + a { + color: #999; + } + `, +})); interface SubTokenTableProps { defaultOpen?: boolean; title: string; + helpText: React.ReactNode; + helpLink: string; tokens: string[]; + component?: string; } -const SubTokenTable: React.FC<SubTokenTableProps> = ({ defaultOpen, tokens, title }) => { +const SubTokenTable: React.FC<SubTokenTableProps> = ({ + defaultOpen, + tokens, + title, + helpText, + helpLink, + component, +}) => { const [, lang] = useLocale(locales); - const { token } = useSiteToken(); + const token = useTheme(); const columns = useColumns(); const [open, setOpen] = useState<boolean>(defaultOpen || process.env.NODE_ENV !== 'production'); - const { tableTitle, arrowIcon } = useStyle(); + const { styles } = useStyle(); if (!tokens.length) { return null; } const data = tokens - .sort((token1, token2) => { - const hasColor1 = token1.toLowerCase().includes('color'); - const hasColor2 = token2.toLowerCase().includes('color'); - - if (hasColor1 && !hasColor2) { - return -1; - } - - if (!hasColor1 && hasColor2) { - return 1; - } - - return token1 < token2 ? -1 : 1; - }) + .sort( + component + ? undefined + : (token1, token2) => { + const hasColor1 = token1.toLowerCase().includes('color'); + const hasColor2 = token2.toLowerCase().includes('color'); + + if (hasColor1 && !hasColor2) { + return -1; + } + + if (!hasColor1 && hasColor2) { + return 1; + } + + return token1 < token2 ? -1 : 1; + }, + ) .map((name) => { - const meta = tokenMeta[name]; + const meta = component + ? tokenMeta.components[component].find((item) => item.token === name) + : tokenMeta.global[name]; if (!meta) { return null; @@ -89,16 +123,58 @@ const SubTokenTable: React.FC<SubTokenTableProps> = ({ defaultOpen, tokens, titl name, desc: lang === 'cn' ? meta.desc : meta.descEn, type: meta.type, - value: defaultToken[name], + value: component ? tokenData[component].component[name] : defaultToken[name], }; }) .filter(Boolean); + const code = component + ? `<ConfigProvider + theme={{ + components: { + ${component}: { + /* here is your component tokens */ + }, + }, + }} +> + ... +</ConfigProvider>` + : `<ConfigProvider + theme={{ + token: { + /* here is your global tokens */ + }, + }} +> + ... +</ConfigProvider>`; + return ( - <div> - <div css={tableTitle} onClick={() => setOpen(!open)}> - <RightOutlined css={arrowIcon} rotate={open ? 90 : 0} /> - <h3>{title}</h3> + <> + <div className={styles.tableTitle} onClick={() => setOpen(!open)}> + <RightOutlined className={styles.arrowIcon} rotate={open ? 90 : 0} /> + <h3> + {title} + <Popover + title={null} + popupStyle={{ width: 400 }} + content={ + <Typography> + <pre style={{ fontSize: 12 }}>{code}</pre> + <a href={helpLink} target="_blank" rel="noreferrer"> + <LinkOutlined style={{ marginRight: 4 }} /> + {helpText} + </a> + </Typography> + } + > + <span className={styles.help}> + <QuestionCircleOutlined style={{ marginRight: 3 }} /> + {helpText} + </span> + </Popover> + </h3> </div> {open && ( <ConfigProvider theme={{ token: { borderRadius: 0 } }}> @@ -113,7 +189,7 @@ const SubTokenTable: React.FC<SubTokenTableProps> = ({ defaultOpen, tokens, titl /> </ConfigProvider> )} - </div> + </> ); }; @@ -122,28 +198,41 @@ export interface ComponentTokenTableProps { } const ComponentTokenTable: React.FC<ComponentTokenTableProps> = ({ component }) => { + const [locale] = useLocale(locales); const [mergedGlobalTokens] = useMemo(() => { const globalTokenSet = new Set<string>(); - let componentTokens: Record<string, string> = {}; component.split(',').forEach((comp) => { - const { global: globalTokens = [], component: singleComponentTokens = [] } = - tokenData[comp] || {}; + const { global: globalTokens = [] } = tokenData[comp] || {}; globalTokens.forEach((token: string) => { globalTokenSet.add(token); }); - - componentTokens = { - ...componentTokens, - ...singleComponentTokens, - }; }); - return [Array.from(globalTokenSet), componentTokens] as const; + return [Array.from(globalTokenSet)] as const; }, [component]); - return <SubTokenTable title="Global Token" tokens={mergedGlobalTokens} />; + return ( + <> + {tokenMeta.components[component] && ( + <SubTokenTable + title={locale.componentToken} + helpText={locale.help} + helpLink={locale.customizeTokenLink} + tokens={tokenMeta.components[component].map((item) => item.token)} + component={component} + defaultOpen + /> + )} + <SubTokenTable + title={locale.globalToken} + helpText={locale.help} + helpLink={locale.customizeComponentTokenLink} + tokens={mergedGlobalTokens} + /> + </> + ); }; export default React.memo(ComponentTokenTable); diff --git a/.dumi/theme/builtins/DemoWrapper/index.tsx b/.dumi/theme/builtins/DemoWrapper/index.tsx index 5261aad89bf6..6cceeb3c5174 100644 --- a/.dumi/theme/builtins/DemoWrapper/index.tsx +++ b/.dumi/theme/builtins/DemoWrapper/index.tsx @@ -1,8 +1,8 @@ import React, { useContext } from 'react'; import { DumiDemoGrid, FormattedMessage } from 'dumi'; -import { Tooltip } from 'antd'; import { BugFilled, BugOutlined, CodeFilled, CodeOutlined } from '@ant-design/icons'; import classNames from 'classnames'; +import { Tooltip } from 'antd'; import DemoContext from '../../slots/DemoContext'; import useLayoutState from '../../../hooks/useLayoutState'; @@ -75,8 +75,7 @@ const DemoWrapper: typeof DumiDemoGrid = ({ items }) => { )} </Tooltip> </span> - {/* FIXME: find a new way instead of `key` to trigger re-render */} - <DumiDemoGrid items={demos} key={`${expandAll}${showDebug}`} /> + <DumiDemoGrid items={demos} /> </div> ); }; diff --git a/.dumi/theme/builtins/IconSearch/Category.tsx b/.dumi/theme/builtins/IconSearch/Category.tsx index f697fa01a6ec..1cbbdf4c57c6 100644 --- a/.dumi/theme/builtins/IconSearch/Category.tsx +++ b/.dumi/theme/builtins/IconSearch/Category.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { message } from 'antd'; import { useIntl } from 'dumi'; +import { message } from 'antd'; import CopyableIcon from './CopyableIcon'; import type { ThemeType } from './index'; import type { CategoriesKeys } from './fields'; @@ -16,7 +16,7 @@ const Category: React.FC<CategoryProps> = (props) => { const { icons, title, newIcons, theme } = props; const intl = useIntl(); const [justCopied, setJustCopied] = React.useState<string | null>(null); - const copyId = React.useRef<NodeJS.Timeout | null>(null); + const copyId = React.useRef<ReturnType<typeof setTimeout> | null>(null); const onCopied = React.useCallback((type: string, text: string) => { message.success( <span> diff --git a/.dumi/theme/builtins/IconSearch/CopyableIcon.tsx b/.dumi/theme/builtins/IconSearch/CopyableIcon.tsx index 4563192391c2..8b5b720fc1c0 100644 --- a/.dumi/theme/builtins/IconSearch/CopyableIcon.tsx +++ b/.dumi/theme/builtins/IconSearch/CopyableIcon.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; -import { Badge, message } from 'antd'; import classNames from 'classnames'; import * as AntdIcons from '@ant-design/icons'; +import { Badge, message } from 'antd'; import type { ThemeType } from './index'; const allIcons: { diff --git a/.dumi/theme/builtins/IconSearch/IconSearch.tsx b/.dumi/theme/builtins/IconSearch/IconSearch.tsx new file mode 100644 index 000000000000..2994df36ea38 --- /dev/null +++ b/.dumi/theme/builtins/IconSearch/IconSearch.tsx @@ -0,0 +1,150 @@ +import type { CSSProperties } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; +import Icon, * as AntdIcons from '@ant-design/icons'; +import type { IntlShape } from 'react-intl'; +import { createStyles, useTheme } from 'antd-style'; +import { useIntl } from 'dumi'; +import debounce from 'lodash/debounce'; +import type { SegmentedProps } from 'antd'; +import { Affix, Empty, Input, Segmented } from 'antd'; +import Category from './Category'; +import { FilledIcon, OutlinedIcon, TwoToneIcon } from './themeIcons'; +import type { CategoriesKeys } from './fields'; +import { categories } from './fields'; + +export enum ThemeType { + Filled = 'Filled', + Outlined = 'Outlined', + TwoTone = 'TwoTone', +} + +const allIcons: { [key: string]: any } = AntdIcons; + +const useStyle = createStyles(({ css }) => ({ + iconSearchAffix: css` + display: flex; + transition: all 0.3s; + justify-content: space-between; + `, +})); + +const options = (intl: IntlShape): SegmentedProps['options'] => [ + { + value: ThemeType.Outlined, + icon: <Icon component={OutlinedIcon} />, + label: intl.formatMessage({ id: 'app.docs.components.icon.outlined' }), + }, + { + value: ThemeType.Filled, + icon: <Icon component={FilledIcon} />, + label: intl.formatMessage({ id: 'app.docs.components.icon.filled' }), + }, + { + value: ThemeType.TwoTone, + icon: <Icon component={TwoToneIcon} />, + label: intl.formatMessage({ id: 'app.docs.components.icon.two-tone' }), + }, +]; + +interface IconSearchState { + theme: ThemeType; + searchKey: string; +} + +const IconSearch: React.FC = () => { + const intl = useIntl(); + const { styles } = useStyle(); + const [displayState, setDisplayState] = useState<IconSearchState>({ + searchKey: '', + theme: ThemeType.Outlined, + }); + const token = useTheme(); + + const newIconNames: string[] = []; + + const handleSearchIcon = debounce((e: React.ChangeEvent<HTMLInputElement>) => { + setDisplayState((prevState) => ({ ...prevState, searchKey: e.target.value })); + }, 300); + + const handleChangeTheme = useCallback((value) => { + setDisplayState((prevState) => ({ ...prevState, theme: value as ThemeType })); + }, []); + + const renderCategories = useMemo<React.ReactNode | React.ReactNode[]>(() => { + const { searchKey = '', theme } = displayState; + + const categoriesResult = Object.keys(categories) + .map((key) => { + let iconList = categories[key as CategoriesKeys]; + if (searchKey) { + const matchKey = searchKey + // eslint-disable-next-line prefer-regex-literals + .replace(new RegExp(`^<([a-zA-Z]*)\\s/>$`, 'gi'), (_, name) => name) + .replace(/(Filled|Outlined|TwoTone)$/, '') + .toLowerCase(); + iconList = iconList.filter((iconName) => iconName.toLowerCase().includes(matchKey)); + } + + const ignore = [ + 'CopyrightCircle', // same as Copyright + 'DollarCircle', // same as Dollar + ]; + iconList = iconList.filter((icon) => !ignore.includes(icon)); + + return { + category: key, + icons: iconList + .map((iconName) => iconName + theme) + .filter((iconName) => allIcons[iconName]), + }; + }) + .filter(({ icons }) => !!icons.length) + .map(({ category, icons }) => ( + <Category + key={category} + title={category as CategoriesKeys} + theme={theme} + icons={icons} + newIcons={newIconNames} + /> + )); + return categoriesResult.length ? categoriesResult : <Empty style={{ margin: '2em 0' }} />; + }, [displayState.searchKey, displayState.theme]); + + const [searchBarAffixed, setSearchBarAffixed] = useState<boolean>(false); + const { borderRadius, colorBgContainer, anchorTop } = token; + + const affixedStyle: CSSProperties = { + boxShadow: 'rgba(50, 50, 93, 0.25) 0 6px 12px -2px, rgba(0, 0, 0, 0.3) 0 3px 7px -3px', + padding: 8, + margin: -8, + borderRadius, + backgroundColor: colorBgContainer, + }; + + return ( + <div className="markdown"> + <Affix offsetTop={anchorTop} onChange={setSearchBarAffixed}> + <div className={styles.iconSearchAffix} style={searchBarAffixed ? affixedStyle : {}}> + <Segmented + size="large" + value={displayState.theme} + options={options(intl)} + onChange={handleChangeTheme} + /> + <Input.Search + placeholder={intl.formatMessage({ id: 'app.docs.components.icon.search.placeholder' })} + style={{ flex: 1, marginInlineStart: 16 }} + allowClear + autoFocus + size="large" + onChange={handleSearchIcon} + /> + </div> + </Affix> + {renderCategories} + </div> + ); +}; + +export default IconSearch; diff --git a/.dumi/theme/builtins/IconSearch/fields.ts b/.dumi/theme/builtins/IconSearch/fields.ts index e86cd406393b..f171a94065bf 100644 --- a/.dumi/theme/builtins/IconSearch/fields.ts +++ b/.dumi/theme/builtins/IconSearch/fields.ts @@ -165,9 +165,11 @@ const logo = [ 'Weibo', 'Twitter', 'Wechat', + 'WhatsApp', 'Youtube', 'AlipayCircle', 'Taobao', + 'Dingtalk', 'Skype', 'Qq', 'MediumWorkmark', diff --git a/.dumi/theme/builtins/IconSearch/index.tsx b/.dumi/theme/builtins/IconSearch/index.tsx index 2f32af9dd276..7ad79fce79a3 100644 --- a/.dumi/theme/builtins/IconSearch/index.tsx +++ b/.dumi/theme/builtins/IconSearch/index.tsx @@ -1,151 +1,65 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import type { CSSProperties } from 'react'; -import Icon, * as AntdIcons from '@ant-design/icons'; -import type { SegmentedProps } from 'antd'; -import type { IntlShape } from 'react-intl'; -import { Segmented, Input, Empty, Affix } from 'antd'; -import { css } from '@emotion/react'; -import { useIntl } from 'dumi'; -import debounce from 'lodash/debounce'; -import Category from './Category'; -import { FilledIcon, OutlinedIcon, TwoToneIcon } from './themeIcons'; -import type { CategoriesKeys } from './fields'; -import { categories } from './fields'; -import useSiteToken from '../../../hooks/useSiteToken'; +import React, { Suspense } from 'react'; +import { createStyles } from 'antd-style'; +import { Skeleton } from 'antd'; -export enum ThemeType { - Filled = 'Filled', - Outlined = 'Outlined', - TwoTone = 'TwoTone', -} +const IconSearch = React.lazy(() => import('./IconSearch')); -const allIcons: { [key: string]: any } = AntdIcons; - -const useStyle = () => ({ - iconSearchAffix: css` +const useStyle = createStyles(({ css }) => ({ + searchWrapper: css` + display: flex; + gap: 16px; + > *:first-child { + flex: 0 0 328px; + } + > *:last-child { + flex: 1; + } + `, + fallbackWrapper: css` display: flex; - transition: all 0.3s; + flex-wrap: wrap; justify-content: space-between; + > * { + flex: 0 0 15%; + margin: 3px 0; + } `, -}); - -const options = (intl: IntlShape): SegmentedProps['options'] => [ - { - value: ThemeType.Outlined, - icon: <Icon component={OutlinedIcon} />, - label: intl.formatMessage({ id: 'app.docs.components.icon.outlined' }), - }, - { - value: ThemeType.Filled, - icon: <Icon component={FilledIcon} />, - label: intl.formatMessage({ id: 'app.docs.components.icon.filled' }), - }, - { - value: ThemeType.TwoTone, - icon: <Icon component={TwoToneIcon} />, - label: intl.formatMessage({ id: 'app.docs.components.icon.two-tone' }), - }, -]; - -interface IconSearchState { - theme: ThemeType; - searchKey: string; -} - -const IconSearch: React.FC = () => { - const intl = useIntl(); - const { iconSearchAffix } = useStyle(); - const [displayState, setDisplayState] = useState<IconSearchState>({ - searchKey: '', - theme: ThemeType.Outlined, - }); - - const newIconNames: string[] = []; + skeletonWrapper: css` + text-align: center; - const handleSearchIcon = debounce((e: React.ChangeEvent<HTMLInputElement>) => { - setDisplayState((prevState) => ({ ...prevState, searchKey: e.target.value })); - }, 300); - - const handleChangeTheme = useCallback((value) => { - setDisplayState((prevState) => ({ ...prevState, theme: value as ThemeType })); - }, []); - - const renderCategories = useMemo<React.ReactNode | React.ReactNode[]>(() => { - const { searchKey = '', theme } = displayState; - - const categoriesResult = Object.keys(categories) - .map((key) => { - let iconList = categories[key as CategoriesKeys]; - if (searchKey) { - const matchKey = searchKey - // eslint-disable-next-line prefer-regex-literals - .replace(new RegExp(`^<([a-zA-Z]*)\\s/>$`, 'gi'), (_, name) => name) - .replace(/(Filled|Outlined|TwoTone)$/, '') - .toLowerCase(); - iconList = iconList.filter((iconName) => iconName.toLowerCase().includes(matchKey)); - } - - const ignore = [ - 'CopyrightCircle', // same as Copyright - 'DollarCircle', // same as Dollar - ]; - iconList = iconList.filter((icon) => !ignore.includes(icon)); - - return { - category: key, - icons: iconList - .map((iconName) => iconName + theme) - .filter((iconName) => allIcons[iconName]), - }; - }) - .filter(({ icons }) => !!icons.length) - .map(({ category, icons }) => ( - <Category - key={category} - title={category as CategoriesKeys} - theme={theme} - icons={icons} - newIcons={newIconNames} - /> - )); - return categoriesResult.length ? categoriesResult : <Empty style={{ margin: '2em 0' }} />; - }, [displayState.searchKey, displayState.theme]); - - const [searchBarAffixed, setSearchBarAffixed] = useState<boolean>(false); - const { token } = useSiteToken(); - const { borderRadius, colorBgContainer } = token; + > * { + width: 100% !important; + } + `, +})); - const affixedStyle: CSSProperties = { - boxShadow: 'rgba(50, 50, 93, 0.25) 0 6px 12px -2px, rgba(0, 0, 0, 0.3) 0 3px 7px -3px', - padding: 8, - margin: -8, - borderRadius, - backgroundColor: colorBgContainer, - }; +const IconSearchFallback = () => { + const { styles } = useStyle(); return ( - <div className='markdown'> - <Affix offsetTop={24} onChange={setSearchBarAffixed}> - <div css={iconSearchAffix} style={searchBarAffixed ? affixedStyle : {}}> - <Segmented - size='large' - value={displayState.theme} - options={options(intl)} - onChange={handleChangeTheme} - /> - <Input.Search - placeholder={intl.formatMessage({ id: 'app.docs.components.icon.search.placeholder' })} - style={{ flex: 1, marginInlineStart: 16 }} - allowClear - autoFocus - size='large' - onChange={handleSearchIcon} - /> - </div> - </Affix> - {renderCategories} - </div> + <> + <div className={styles.searchWrapper}> + <Skeleton.Button active style={{ width: '100%', height: 40 }} /> + <Skeleton.Input active style={{ width: '100%', height: 40 }} /> + </div> + <Skeleton.Button active style={{ margin: '28px 0 10px', width: 100 }} /> + <div className={styles.fallbackWrapper}> + {Array(24) + .fill(1) + .map((_, index) => ( + <div key={index} className={styles.skeletonWrapper}> + <Skeleton.Node active style={{ height: 110, width: '100%' }}> + {' '} + </Skeleton.Node> + </div> + ))} + </div> + </> ); }; -export default IconSearch; +export default () => ( + <Suspense fallback={<IconSearchFallback />}> + <IconSearch /> + </Suspense> +); diff --git a/.dumi/theme/builtins/ImagePreview/index.tsx b/.dumi/theme/builtins/ImagePreview/index.tsx index 7ea701a6895b..a364c1844ebf 100644 --- a/.dumi/theme/builtins/ImagePreview/index.tsx +++ b/.dumi/theme/builtins/ImagePreview/index.tsx @@ -1,10 +1,13 @@ import React from 'react'; import classNames from 'classnames'; -import { Image } from 'antd'; import toArray from 'rc-util/lib/Children/toArray'; +import { Image } from 'antd'; interface ImagePreviewProps { children: React.ReactNode[]; + className?: string; + /** Do not show padding & background */ + pure?: boolean; } function isGood(className: string): boolean { @@ -26,9 +29,8 @@ function isGoodBadImg(imgMeta: any): boolean { function isCompareImg(imgMeta: any): boolean { return isGoodBadImg(imgMeta) || imgMeta.inline; } - const ImagePreview: React.FC<ImagePreviewProps> = (props) => { - const { children } = props; + const { children, className: rootClassName, pure } = props; const imgs = toArray(children).filter((ele) => ele.type === 'img'); const imgsMeta = imgs.map((img) => { @@ -67,21 +69,32 @@ const ImagePreview: React.FC<ImagePreviewProps> = (props) => { : {}; const hasCarousel = imgs.length > 1 && !comparable; - const previewClassName = classNames({ - 'preview-image-boxes': true, - clearfix: true, + + const previewClassName = classNames(rootClassName, 'clearfix', 'preview-image-boxes', { 'preview-image-boxes-compare': comparable, 'preview-image-boxes-with-carousel': hasCarousel, }); + // ===================== Render ===================== + const imgWrapperCls = 'preview-image-wrapper'; + return ( <div className={previewClassName}> + {!imgs.length && ( + <div + className={imgWrapperCls} + style={pure ? { background: 'transparent', padding: 0 } : {}} + > + {children} + </div> + )} + {imagesList.map((_, index) => { if (!comparable && index !== 0) { return null; } const coverMeta = imgsMeta[index]; - const imageWrapperClassName = classNames('preview-image-wrapper', { + const imageWrapperClassName = classNames(imgWrapperCls, { good: coverMeta.isGood, bad: coverMeta.isBad, }); diff --git a/.dumi/theme/builtins/InlinePopover/index.tsx b/.dumi/theme/builtins/InlinePopover/index.tsx index d2327e8ff4c0..2621eb0d2d45 100644 --- a/.dumi/theme/builtins/InlinePopover/index.tsx +++ b/.dumi/theme/builtins/InlinePopover/index.tsx @@ -1,6 +1,6 @@ import { PictureOutlined } from '@ant-design/icons'; -import { Image, Tooltip, Typography } from 'antd'; import React from 'react'; +import { Image, Tooltip, Typography } from 'antd'; import useLocale from '../../../hooks/useLocale'; const locales = { @@ -17,9 +17,8 @@ export interface InlinePopoverProps { } // 鼠标悬浮弹出 Popover 组件,用于帮助用户更快看到一些属性对应的预览效果 -const InlinePopover: React.FC = (props: InlinePopoverProps) => { +const InlinePopover: React.FC<InlinePopoverProps> = (props) => { const { previewURL } = props; - const [locale] = useLocale(locales); const [visible, setVisible] = React.useState(false); diff --git a/.dumi/theme/builtins/InstallDependencies/index.tsx b/.dumi/theme/builtins/InstallDependencies/index.tsx new file mode 100644 index 000000000000..fd61fa091b4a --- /dev/null +++ b/.dumi/theme/builtins/InstallDependencies/index.tsx @@ -0,0 +1,62 @@ +import SourceCode from 'dumi/theme-default/builtins/SourceCode'; +import React from 'react'; +import type { TabsProps } from 'antd'; +import { Tabs } from 'antd'; +import NpmLogo from './npm'; +import PnpmLogo from './pnpm'; +import YarnLogo from './yarn'; + +interface InstallProps { + npm?: string; + yarn?: string; + pnpm?: string; +} + +const npmLabel = ( + <span className="snippet-label"> + <NpmLogo /> + npm + </span> +); + +const pnpmLabel = ( + <span className="snippet-label"> + <PnpmLogo /> + pnpm + </span> +); + +const yarnLabel = ( + <span className="snippet-label"> + <YarnLogo /> + yarn + </span> +); + +const InstallDependencies: React.FC<InstallProps> = (props) => { + const { npm, yarn, pnpm } = props; + const items = React.useMemo<TabsProps['items']>( + () => + [ + { + key: 'npm', + children: npm ? <SourceCode lang="bash">{npm}</SourceCode> : null, + label: npmLabel, + }, + { + key: 'yarn', + children: yarn ? <SourceCode lang="bash">{yarn}</SourceCode> : null, + label: yarnLabel, + }, + { + key: 'pnpm', + children: pnpm ? <SourceCode lang="bash">{pnpm}</SourceCode> : null, + label: pnpmLabel, + }, + ].filter((item) => item.children), + [npm, yarn, pnpm], + ); + return <Tabs className="antd-site-snippet" defaultActiveKey="npm" items={items} />; +}; + +export default InstallDependencies; diff --git a/.dumi/theme/builtins/InstallDependencies/npm.tsx b/.dumi/theme/builtins/InstallDependencies/npm.tsx new file mode 100644 index 000000000000..ffd67556885c --- /dev/null +++ b/.dumi/theme/builtins/InstallDependencies/npm.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +interface IconProps { + className?: string; + style?: React.CSSProperties; +} + +const NpmIcon: React.FC<IconProps> = (props) => { + const { className, style } = props; + return ( + <svg + className={className} + style={style} + fill="#E53E3E" + focusable="false" + height="1em" + stroke="#E53E3E" + strokeWidth="0" + viewBox="0 0 16 16" + width="1em" + > + <path d="M0 0v16h16v-16h-16zM13 13h-2v-8h-3v8h-5v-10h10v10z" /> + </svg> + ); +}; + +export default NpmIcon; diff --git a/.dumi/theme/builtins/InstallDependencies/pnpm.tsx b/.dumi/theme/builtins/InstallDependencies/pnpm.tsx new file mode 100644 index 000000000000..1be5a1ce3a70 --- /dev/null +++ b/.dumi/theme/builtins/InstallDependencies/pnpm.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +interface IconProps { + className?: string; + style?: React.CSSProperties; +} + +const PnpmIcon: React.FC<IconProps> = (props) => { + const { className, style } = props; + return ( + <svg + className={className} + style={style} + aria-hidden="true" + fill="#F69220" + focusable="false" + height="1em" + role="img" + stroke="#F69220" + strokeWidth="0" + viewBox="0 0 24 24" + width="1em" + > + <path d="M0 0v7.5h7.5V0zm8.25 0v7.5h7.498V0zm8.25 0v7.5H24V0zM8.25 8.25v7.5h7.498v-7.5zm8.25 0v7.5H24v-7.5zM0 16.5V24h7.5v-7.5zm8.25 0V24h7.498v-7.5zm8.25 0V24H24v-7.5z" /> + </svg> + ); +}; + +export default PnpmIcon; diff --git a/.dumi/theme/builtins/InstallDependencies/yarn.tsx b/.dumi/theme/builtins/InstallDependencies/yarn.tsx new file mode 100644 index 000000000000..c79ac4eee773 --- /dev/null +++ b/.dumi/theme/builtins/InstallDependencies/yarn.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +interface IconProps { + className?: string; + style?: React.CSSProperties; +} + +const YarnIcon: React.FC<IconProps> = (props) => { + const { className, style } = props; + return ( + <svg + className={className} + style={style} + aria-hidden="true" + fill="#2C8EBB" + focusable="false" + height="1em" + stroke="#2C8EBB" + strokeWidth="0" + viewBox="0 0 496 512" + width="1em" + > + <path d="M393.9 345.2c-39 9.3-48.4 32.1-104 47.4 0 0-2.7 4-10.4 5.8-13.4 3.3-63.9 6-68.5 6.1-12.4.1-19.9-3.2-22-8.2-6.4-15.3 9.2-22 9.2-22-8.1-5-9-9.9-9.8-8.1-2.4 5.8-3.6 20.1-10.1 26.5-8.8 8.9-25.5 5.9-35.3.8-10.8-5.7.8-19.2.8-19.2s-5.8 3.4-10.5-3.6c-6-9.3-17.1-37.3 11.5-62-1.3-10.1-4.6-53.7 40.6-85.6 0 0-20.6-22.8-12.9-43.3 5-13.4 7-13.3 8.6-13.9 5.7-2.2 11.3-4.6 15.4-9.1 20.6-22.2 46.8-18 46.8-18s12.4-37.8 23.9-30.4c3.5 2.3 16.3 30.6 16.3 30.6s13.6-7.9 15.1-5c8.2 16 9.2 46.5 5.6 65.1-6.1 30.6-21.4 47.1-27.6 57.5-1.4 2.4 16.5 10 27.8 41.3 10.4 28.6 1.1 52.7 2.8 55.3.8 1.4 13.7.8 36.4-13.2 12.8-7.9 28.1-16.9 45.4-17 16.7-.5 17.6 19.2 4.9 22.2zM496 256c0 136.9-111.1 248-248 248S0 392.9 0 256 111.1 8 248 8s248 111.1 248 248zm-79.3 75.2c-1.7-13.6-13.2-23-28-22.8-22 .3-40.5 11.7-52.8 19.2-4.8 3-8.9 5.2-12.4 6.8 3.1-44.5-22.5-73.1-28.7-79.4 7.8-11.3 18.4-27.8 23.4-53.2 4.3-21.7 3-55.5-6.9-74.5-1.6-3.1-7.4-11.2-21-7.4-9.7-20-13-22.1-15.6-23.8-1.1-.7-23.6-16.4-41.4 28-12.2.9-31.3 5.3-47.5 22.8-2 2.2-5.9 3.8-10.1 5.4h.1c-8.4 3-12.3 9.9-16.9 22.3-6.5 17.4.2 34.6 6.8 45.7-17.8 15.9-37 39.8-35.7 82.5-34 36-11.8 73-5.6 79.6-1.6 11.1 3.7 19.4 12 23.8 12.6 6.7 30.3 9.6 43.9 2.8 4.9 5.2 13.8 10.1 30 10.1 6.8 0 58-2.9 72.6-6.5 6.8-1.6 11.5-4.5 14.6-7.1 9.8-3.1 36.8-12.3 62.2-28.7 18-11.7 24.2-14.2 37.6-17.4 12.9-3.2 21-15.1 19.4-28.2z" /> + </svg> + ); +}; + +export default YarnIcon; diff --git a/.dumi/theme/builtins/Previewer/CodePreviewer.tsx b/.dumi/theme/builtins/Previewer/CodePreviewer.tsx index 298fd8bc71d5..4f4bb6c3b1ac 100644 --- a/.dumi/theme/builtins/Previewer/CodePreviewer.tsx +++ b/.dumi/theme/builtins/Previewer/CodePreviewer.tsx @@ -1,21 +1,14 @@ -import { - CheckOutlined, - LinkOutlined, - SnippetsOutlined, - ThunderboltOutlined, -} from '@ant-design/icons'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import { LinkOutlined, ThunderboltOutlined, UpOutlined } from '@ant-design/icons'; import type { Project } from '@stackblitz/sdk'; import stackblitzSdk from '@stackblitz/sdk'; import { Alert, Badge, Space, Tooltip } from 'antd'; +import { createStyles, css } from 'antd-style'; import classNames from 'classnames'; -import { FormattedMessage, useSiteData } from 'dumi'; -import toReactElement from 'jsonml-to-react-element'; -import JsonML from 'jsonml.js/lib/utils'; +import { FormattedMessage, useSiteData, LiveContext } from 'dumi'; import LZString from 'lz-string'; -import Prism from 'prismjs'; -import React, { useContext, useEffect, useRef, useState } from 'react'; -import CopyToClipboard from 'react-copy-to-clipboard'; -import type { AntdPreviewerProps } from '.'; + +import type { AntdPreviewerProps } from './Previewer'; import useLocation from '../../../hooks/useLocation'; import BrowserFrame from '../../common/BrowserFrame'; import ClientOnly from '../../common/ClientOnly'; @@ -28,31 +21,10 @@ import RiddleIcon from '../../common/RiddleIcon'; import type { SiteContextProps } from '../../slots/SiteContext'; import SiteContext from '../../slots/SiteContext'; import { ping } from '../../utils'; +import LiveDemo from 'dumi/theme-default/slots/LiveDemo'; const { ErrorBoundary } = Alert; -function toReactComponent(jsonML: any) { - return toReactElement(jsonML, [ - [ - (node: any) => JsonML.isElement(node) && JsonML.getTagName(node) === 'pre', - (node: any, index: any) => { - // ref: https://github.com/benjycui/bisheng/blob/master/packages/bisheng/src/bisheng-plugin-highlight/lib/browser.js#L7 - const attr = JsonML.getAttributes(node); - return React.createElement( - 'pre', - { - key: index, - className: `language-${attr.lang}`, - }, - React.createElement('code', { - dangerouslySetInnerHTML: { __html: attr.highlighted }, - }), - ); - }, - ], - ]); -} - function compress(string: string): string { return LZString.compressToBase64(string) .replace(/\+/g, '-') // Convert '+' to '-' @@ -88,6 +60,31 @@ function useShowRiddleButton() { return showRiddleButton; } +const useStyle = createStyles(({ token }) => { + const { borderRadius } = token; + return { + codeHideBtn: css` + width: 100%; + height: 40px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 0 0 ${borderRadius}px ${borderRadius}px; + border-top: 1px solid ${token.colorSplit}; + color: ${token.colorTextSecondary}; + transition: all 0.2s ease-in-out; + background-color: ${token.colorBgElevated}; + cursor: pointer; + &:hover { + color: ${token.colorPrimary}; + } + span { + margin-right: ${token.marginXXS}px; + } + `, + }; +}); + const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => { const { asset, @@ -111,6 +108,10 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => { const { pkg } = useSiteData(); const location = useLocation(); + const { enabled: liveEnabled } = useContext(LiveContext); + + const { styles } = useStyle(); + const entryCode = asset.dependencies['index.tsx'].value; const showRiddleButton = useShowRiddleButton(); @@ -120,8 +121,6 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => { const riddleIconRef = useRef<HTMLFormElement>(null); const codepenIconRef = useRef<HTMLFormElement>(null); const [codeExpand, setCodeExpand] = useState<boolean>(false); - const [copyTooltipOpen, setCopyTooltipOpen] = useState<boolean>(false); - const [copied, setCopied] = useState<boolean>(false); const [codeType, setCodeType] = useState<string>('tsx'); const { theme } = useContext<SiteContextProps>(SiteContext); @@ -130,13 +129,6 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => { const [showOnlineUrl, setShowOnlineUrl] = useState<boolean>(false); - const highlightedCodes = { - jsx: Prism.highlight(jsx, Prism.languages.javascript, 'jsx'), - tsx: Prism.highlight(entryCode, Prism.languages.javascript, 'jsx'), - }; - - const highlightedStyle = style ? Prism.highlight(style, Prism.languages.css, 'css') : ''; - useEffect(() => { const regexp = /preview-(\d+)-ant-design/; // matching PR preview addresses setShowOnlineUrl( @@ -149,18 +141,6 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => { track({ type: 'expand', demo }); }; - const handleCodeCopied = (demo: string) => { - setCopied(true); - track({ type: 'copy', demo }); - }; - - const onCopyTooltipOpenChange = (open: boolean) => { - setCopyTooltipOpen(open); - if (open) { - setCopied(false); - } - }; - useEffect(() => { if (asset.id === hash.slice(1)) { anchorRef.current?.click(); @@ -194,7 +174,6 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => { }); const localizedTitle = title; - const introChildren = <div dangerouslySetInnerHTML={{ __html: description }} />; const highlightClass = classNames('highlight-wrapper', { 'highlight-wrapper-expand': codeExpand, }); @@ -204,7 +183,7 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => { <html lang="en"> <head> <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="viewport" content="width=device-width"> <meta name="theme-color" content="#000000"> </head> <body> @@ -228,15 +207,13 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => { const suffix = codeType === 'tsx' ? 'tsx' : 'js'; - const dependencies: Record<PropertyKey, string> = jsx.split('\n').reduce( + const dependencies = (jsx as string).split('\n').reduce<Record<PropertyKey, string>>( (acc, line) => { const matches = line.match(/import .+? from '(.+)';$/); - if (matches && matches[1] && !line.includes('antd')) { + if (matches?.[1]) { const paths = matches[1].split('/'); - if (paths.length) { - const dep = paths[0].startsWith('@') ? `${paths[0]}/${paths[1]}` : paths[0]; - acc[dep] = 'latest'; - } + const dep = paths[0].startsWith('@') ? `${paths[0]}/${paths[1]}` : paths[0]; + acc[dep] ??= pkgDependencyList[dep] ?? 'latest'; } return acc; }, @@ -389,10 +366,13 @@ createRoot(document.getElementById('container')).render(<Demo />); const codeBox: React.ReactNode = ( <section className={codeBoxClass} id={asset.id}> <section className="code-box-demo" style={codeBoxDemoStyle}> - <ErrorBoundary> - <React.StrictMode>{liveDemo.current}</React.StrictMode> - </ErrorBoundary> - {style ? <style dangerouslySetInnerHTML={{ __html: style }} /> : null} + {!liveEnabled ? ( + <ErrorBoundary> + <React.StrictMode>{liveDemo.current}</React.StrictMode> + </ErrorBoundary> + ) : ( + <LiveDemo /> + )} </section> <section className="code-box-meta markdown"> <div className="code-box-title"> @@ -403,7 +383,9 @@ createRoot(document.getElementById('container')).render(<Demo />); </Tooltip> <EditButton title={<FormattedMessage id="app.content.edit-demo" />} filename={filename} /> </div> - <div className="code-box-description">{introChildren}</div> + {description && ( + <div className="code-box-description" dangerouslySetInnerHTML={{ __html: description }} /> + )} <Space wrap size="middle" className="code-box-actions"> {showOnlineUrl && ( <Tooltip title={<FormattedMessage id="app.demo.online" />}> @@ -413,7 +395,7 @@ createRoot(document.getElementById('container')).render(<Demo />); rel="noreferrer" href={docsOnlineUrl} > - <LinkOutlined className="code-box-online" /> + <LinkOutlined aria-label="open in new tab" className="code-box-online" /> </a> </Tooltip> )} @@ -486,23 +468,17 @@ createRoot(document.getElementById('container')).render(<Demo />); <ThunderboltOutlined className="code-box-stackblitz" /> </span> </Tooltip> - <CopyToClipboard text={entryCode} onCopy={() => handleCodeCopied(asset.id)}> - <Tooltip - open={copyTooltipOpen as boolean} - onOpenChange={onCopyTooltipOpenChange} - title={<FormattedMessage id={`app.demo.${copied ? 'copied' : 'copy'}`} />} - > - {React.createElement(copied && copyTooltipOpen ? CheckOutlined : SnippetsOutlined, { - className: 'code-box-code-copy code-box-code-action', - })} - </Tooltip> - </CopyToClipboard> <Tooltip title={<FormattedMessage id="app.demo.separate" />}> - <a className="code-box-code-action" target="_blank" rel="noreferrer" href={demoUrl}> + <a + className="code-box-code-action" + aria-label="open in new tab" + target="_blank" + rel="noreferrer" + href={demoUrl} + > <ExternalLinkIcon className="code-box-separate" /> </a> </Tooltip> - <Tooltip title={<FormattedMessage id={`app.demo.code.${codeExpand ? 'hide' : 'show'}`} />} > @@ -531,26 +507,50 @@ createRoot(document.getElementById('container')).render(<Demo />); </Tooltip> </Space> </section> - <section className={highlightClass} key="code"> - <CodePreview - codes={highlightedCodes} - toReactComponent={toReactComponent} - onCodeTypeChange={(type) => setCodeType(type)} - /> - {highlightedStyle ? ( - <div key="style" className="highlight"> - <pre> - <code className="css" dangerouslySetInnerHTML={{ __html: highlightedStyle }} /> - </pre> + {codeExpand && ( + <section className={highlightClass} key="code"> + <CodePreview + sourceCode={entryCode} + jsxCode={jsx} + styleCode={style} + onCodeTypeChange={setCodeType} + /> + <div + tabIndex={0} + role="button" + className={styles.codeHideBtn} + onClick={() => setCodeExpand(false)} + > + <UpOutlined /> + <FormattedMessage id="app.demo.code.hide.simplify" /> </div> - ) : null} - </section> + </section> + )} </section> ); + useEffect(() => { + // In Safari, if style tag be inserted into non-head tag, + // it will affect the rendering ability of the browser, + // resulting in some response delays like following issue: + // https://github.com/ant-design/ant-design/issues/39995 + // So we insert style tag into head tag. + if (!style) { + return; + } + const styleTag = document.createElement('style') as HTMLStyleElement; + styleTag.type = 'text/css'; + styleTag.innerHTML = style; + (styleTag as any)['data-demo-url'] = demoUrl; + document.head.appendChild(styleTag); + return () => { + document.head.removeChild(styleTag); + }; + }, [style, demoUrl]); + if (version) { return ( - <Badge.Ribbon text={version} color={version.includes('<') ? 'red' : null}> + <Badge.Ribbon text={version} color={version.includes('<') ? 'red' : undefined}> {codeBox} </Badge.Ribbon> ); diff --git a/.dumi/theme/builtins/Previewer/DesignPreviewer.tsx b/.dumi/theme/builtins/Previewer/DesignPreviewer.tsx index 22ca70535d5e..d3b591aebaee 100644 --- a/.dumi/theme/builtins/Previewer/DesignPreviewer.tsx +++ b/.dumi/theme/builtins/Previewer/DesignPreviewer.tsx @@ -1,13 +1,13 @@ import type { FC } from 'react'; import React, { useRef } from 'react'; -import { createStyles, css } from 'antd-style'; +import { createStyles } from 'antd-style'; import { CheckOutlined, SketchOutlined } from '@ant-design/icons'; import { nodeToGroup } from 'html2sketch'; import copy from 'copy-to-clipboard'; import { App } from 'antd'; -import type { AntdPreviewerProps } from '.'; +import type { AntdPreviewerProps } from './Previewer'; -const useStyle = createStyles(({ token }) => ({ +const useStyle = createStyles(({ token, css }) => ({ wrapper: css` border: 1px solid ${token.colorBorderSecondary}; border-radius: ${token.borderRadius}px; diff --git a/.dumi/theme/builtins/Previewer/Previewer.tsx b/.dumi/theme/builtins/Previewer/Previewer.tsx new file mode 100644 index 000000000000..6a1ea91b9dbc --- /dev/null +++ b/.dumi/theme/builtins/Previewer/Previewer.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import type { IPreviewerProps } from 'dumi'; +import { LiveProvider, useTabMeta } from 'dumi'; + +import CodePreviewer from './CodePreviewer'; +import DesignPreviewer from './DesignPreviewer'; + +export interface AntdPreviewerProps extends IPreviewerProps { + originDebug?: IPreviewerProps['debug']; +} + +const Previewer: React.FC<AntdPreviewerProps> = (props) => { + const tab = useTabMeta(); + + if (tab?.frontmatter.title === 'Design') { + return <DesignPreviewer {...props} />; + } + + const codePreviewer = <CodePreviewer {...props} />; + + if (props.live === false || props.iframe) { + return codePreviewer; + } + + return ( + <LiveProvider + initialCode={ + Object.entries(props.asset.dependencies).filter(([, { type }]) => type === 'FILE')[0][1] + .value + } + demoId={props.asset.id} + > + {codePreviewer} + </LiveProvider> + ); +}; + +export default Previewer; diff --git a/.dumi/theme/builtins/Previewer/index.tsx b/.dumi/theme/builtins/Previewer/index.tsx index b3785af9526a..e36c556112f8 100644 --- a/.dumi/theme/builtins/Previewer/index.tsx +++ b/.dumi/theme/builtins/Previewer/index.tsx @@ -1,22 +1,40 @@ -import type { FC } from 'react'; -import React from 'react'; +import React, { Suspense } from 'react'; import type { IPreviewerProps } from 'dumi'; -import { useTabMeta } from 'dumi'; -import CodePreviewer from './CodePreviewer'; -import DesignPreviewer from './DesignPreviewer'; +import { Skeleton, Alert } from 'antd'; +import { createStyles } from 'antd-style'; -export interface AntdPreviewerProps extends IPreviewerProps { - originDebug?: IPreviewerProps['debug']; -} +const { ErrorBoundary } = Alert; -const Previewer: FC<AntdPreviewerProps> = ({ ...props }) => { - const tab = useTabMeta(); +const Previewer = React.lazy(() => import('./Previewer')); - if (tab?.frontmatter.title === 'Design') { - return <DesignPreviewer {...props} />; - } +const useStyle = createStyles(({ css }) => ({ + skeletonWrapper: css` + width: 100% !important; + height: 500px; + margin-bottom: 16px; + `, +})); - return <CodePreviewer {...props} />; +export default (props: IPreviewerProps) => { + const { styles } = useStyle(); + return ( + <ErrorBoundary> + <Suspense + fallback={ + <Skeleton.Node + active + className={styles.skeletonWrapper} + style={{ + width: '100%', + height: '100%', + }} + > + {' '} + </Skeleton.Node> + } + > + <Previewer {...props} /> + </Suspense> + </ErrorBoundary> + ); }; - -export default Previewer; diff --git a/.dumi/theme/builtins/ResourceArticles/index.tsx b/.dumi/theme/builtins/ResourceArticles/index.tsx index 4fafbcf46a0f..b6fbf635e9e2 100644 --- a/.dumi/theme/builtins/ResourceArticles/index.tsx +++ b/.dumi/theme/builtins/ResourceArticles/index.tsx @@ -1,15 +1,15 @@ /* eslint-disable react/no-array-index-key */ import * as React from 'react'; +import { Suspense } from 'react'; import dayjs from 'dayjs'; -import { FormattedMessage, useIntl } from 'dumi'; -import { Tabs, Skeleton, Avatar, Divider, Empty } from 'antd'; -import { css } from '@emotion/react'; -import { useSiteData } from '../../../pages/index/components/util'; +import { FormattedMessage } from 'dumi'; +import { createStyles } from 'antd-style'; +import { Avatar, Divider, Empty, Skeleton, Tabs } from 'antd'; import type { Article, Authors } from '../../../pages/index/components/util'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { useSiteData } from '../../../pages/index/components/util'; +import useLocale from '../../../hooks/useLocale'; -const useStyle = () => { - const { token } = useSiteToken(); +const useStyle = createStyles(({ token, css }) => { const { antCls } = token; return { @@ -51,14 +51,16 @@ const useStyle = () => { padding: 0; font-size: 14px; list-style: none; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - ${antCls}-avatar > img { max-width: unset; } `, }; -}; +}); interface ArticleListProps { name: React.ReactNode; @@ -67,11 +69,11 @@ interface ArticleListProps { } const ArticleList: React.FC<ArticleListProps> = ({ name, data = [], authors = [] }) => { - const { articleList } = useStyle(); + const { styles } = useStyle(); return ( <td> <h4>{name}</h4> - <ul css={articleList}> + <ul className={styles.articleList}> {data.length === 0 ? ( <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> ) : ( @@ -95,17 +97,15 @@ const ArticleList: React.FC<ArticleListProps> = ({ name, data = [], authors = [] ); }; -export default () => { - const { locale } = useIntl(); - const isZhCN = locale === 'zh-CN'; - const [{ articles = { cn: [], en: [] }, authors = [] }, loading] = useSiteData(); - - const styles = useStyle(); +const Articles: React.FC = () => { + const [, lang] = useLocale(); + const isZhCN = lang === 'cn'; + const { articles = { cn: [], en: [] }, authors = [] } = useSiteData(); // ========================== Data ========================== const mergedData = React.useMemo(() => { const yearData: Record<number | string, Record<string, Article[]>> = {}; - articles[isZhCN ? 'cn' : 'en']?.forEach((article) => { + articles[lang]?.forEach((article) => { const year = dayjs(article.date).year(); yearData[year] = yearData[year] || {}; yearData[year][article.type] = [...(yearData[year][article.type] || []), article]; @@ -113,42 +113,47 @@ export default () => { return yearData; }, [articles]); - // ========================= Render ========================= - let content: React.ReactNode; + const yearList = Object.keys(mergedData).sort((a, b) => Number(b) - Number(a)); - if (loading) { - content = <Skeleton active />; - } else { - const yearList = Object.keys(mergedData).sort((a, b) => Number(b) - Number(a)); - content = yearList.length ? ( - <Tabs> - {yearList.map((year) => ( - <Tabs.TabPane tab={`${year}${isZhCN ? ' 年' : ''}`} key={year}> - <table> - <tbody> - <tr> - <ArticleList - name={<FormattedMessage id="app.docs.resource.design" />} - data={mergedData[year].design} - authors={authors} - /> - <ArticleList - name={<FormattedMessage id="app.docs.resource.develop" />} - data={mergedData[year].develop} - authors={authors} - /> - </tr> - </tbody> - </table> - </Tabs.TabPane> - ))} - </Tabs> - ) : null; + if (yearList.length === 0) { + return null; } return ( - <div id="articles" css={styles.articles}> - {content} + <Tabs + items={yearList.map((year) => ({ + key: year, + label: `${year}${isZhCN ? ' 年' : ''}`, + children: ( + <table> + <tbody> + <tr> + <ArticleList + name={<FormattedMessage id="app.docs.resource.design" />} + data={mergedData[year].design} + authors={authors} + /> + <ArticleList + name={<FormattedMessage id="app.docs.resource.develop" />} + data={mergedData[year].develop} + authors={authors} + /> + </tr> + </tbody> + </table> + ), + }))} + /> + ); +}; + +export default () => { + const { styles } = useStyle(); + return ( + <div id="articles" className={styles.articles}> + <Suspense fallback={<Skeleton active />}> + <Articles /> + </Suspense> </div> ); }; diff --git a/.dumi/theme/builtins/ResourceCards/index.tsx b/.dumi/theme/builtins/ResourceCards/index.tsx index 429a1582d435..f8494a6ce48a 100644 --- a/.dumi/theme/builtins/ResourceCards/index.tsx +++ b/.dumi/theme/builtins/ResourceCards/index.tsx @@ -1,12 +1,10 @@ import React from 'react'; -import { Col, Row, Tooltip } from 'antd'; -import { css } from '@emotion/react'; +import { createStyles } from 'antd-style'; import { ExclamationCircleOutlined } from '@ant-design/icons'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { Col, Row, Tooltip } from 'antd'; import useLocale from '../../../hooks/useLocale'; -const useStyle = () => { - const { token } = useSiteToken(); +const useStyle = createStyles(({ token, css }) => { const { boxShadowSecondary } = token; return { @@ -60,7 +58,7 @@ const useStyle = () => { line-height: 22px; `, }; -}; +}); export type Resource = { title: string; @@ -88,7 +86,7 @@ export type ResourceCardProps = { }; const ResourceCard: React.FC<ResourceCardProps> = ({ resource }) => { - const styles = useStyle(); + const { styles } = useStyle(); const [locale] = useLocale(locales); const { title: titleStr, description, cover, src, official } = resource; @@ -104,25 +102,25 @@ const ResourceCard: React.FC<ResourceCardProps> = ({ resource }) => { return ( <Col xs={24} sm={12} md={8} lg={6} style={{ padding: 12 }}> - <a css={styles.card} target="_blank" href={src} rel="noreferrer"> + <a className={styles.card} target="_blank" href={src} rel="noreferrer"> <img - css={styles.image} + className={styles.image} src={cover} alt={title} style={coverColor ? { backgroundColor: coverColor } : {}} /> {official ? ( - <div css={styles.badge}>{locale.official}</div> + <div className={styles.badge}>{locale.official}</div> ) : ( <Tooltip title={locale.thirdPartDesc}> - <div css={styles.badge}> + <div className={styles.badge}> <ExclamationCircleOutlined /> {locale.thirdPart} </div> </Tooltip> )} - <p css={styles?.title}>{title}</p> - <p css={styles.description}>{description}</p> + <p className={styles?.title}>{title}</p> + <p className={styles.description}>{description}</p> </a> </Col> ); diff --git a/.dumi/theme/builtins/Sandpack/Sandpack.ts b/.dumi/theme/builtins/Sandpack/Sandpack.ts new file mode 100644 index 000000000000..8e8b767f0fc1 --- /dev/null +++ b/.dumi/theme/builtins/Sandpack/Sandpack.ts @@ -0,0 +1,3 @@ +import { Sandpack } from '@codesandbox/sandpack-react'; + +export default Sandpack; diff --git a/.dumi/theme/builtins/Sandpack/index.tsx b/.dumi/theme/builtins/Sandpack/index.tsx new file mode 100644 index 000000000000..cc38bbc75192 --- /dev/null +++ b/.dumi/theme/builtins/Sandpack/index.tsx @@ -0,0 +1,107 @@ +import type { FC, ReactNode } from 'react'; +import React, { Suspense } from 'react'; +import { useSearchParams } from 'dumi'; +import { createStyles } from 'antd-style'; +import { Skeleton } from 'antd'; + +const OriginSandpack = React.lazy(() => import('./Sandpack')); + +const indexContent = `import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './app'; +import './index.css'; + +const root = createRoot(document.getElementById("root")); +root.render(<App />); +`; + +const useStyle = createStyles(({ token, css }) => ({ + fallback: css` + width: 100%; + > * { + width: 100% !important; + border-radius: 8px; + } + `, + placeholder: css` + color: ${token.colorTextDescription}; + font-size: 16px; + `, +})); + +const SandpackFallback = () => { + const { styles } = useStyle(); + + return ( + <div className={styles.fallback}> + <Skeleton.Node active style={{ height: 500, width: '100%' }}> + <span className={styles.placeholder}>Loading Demo...</span> + </Skeleton.Node> + </div> + ); +}; + +type SandpackProps = { + children?: ReactNode; + dark?: boolean; + autorun?: boolean; + dependencies?: string; +}; + +const Sandpack: FC<SandpackProps> = ({ + children, + dark, + dependencies: extraDeps, + autorun = false, +}) => { + const [searchParams] = useSearchParams(); + const dependencies = extraDeps && JSON.parse(extraDeps); + + const setup = { + dependencies: { + react: '^18.0.0', + 'react-dom': '^18.0.0', + antd: '^5.0.0', + ...dependencies, + }, + devDependencies: { + '@types/react': '^18.0.0', + '@types/react-dom': '^18.0.0', + typescript: '^5', + }, + entry: 'index.tsx', + }; + + const options = { + activeFile: 'app.tsx' as never, + visibleFiles: ['index.tsx', 'app.tsx', 'package.json', 'index.css'] as any, + showLineNumbers: true, + editorHeight: '500px', + autorun, + }; + + return ( + <Suspense fallback={<SandpackFallback />}> + <OriginSandpack + theme={searchParams.getAll('theme').includes('dark') ? 'dark' : undefined} + customSetup={setup} + options={options} + files={{ + 'index.tsx': indexContent, + 'index.css': `html, body { + padding: 0; + margin: 0; + background: ${dark ? '#000' : '#fff'}; +} + +#root { + padding: 24px; +}`, + 'app.tsx': children, + }} + /> + </Suspense> + ); +}; + +export default Sandpack; diff --git a/.dumi/theme/builtins/TokenCompare/index.tsx b/.dumi/theme/builtins/TokenCompare/index.tsx new file mode 100644 index 000000000000..9ae37389f0b0 --- /dev/null +++ b/.dumi/theme/builtins/TokenCompare/index.tsx @@ -0,0 +1,119 @@ +// 用于 color.md 中的颜色对比 +import React from 'react'; +import classNames from 'classnames'; +import { TinyColor } from '@ctrl/tinycolor'; +import { createStyles } from 'antd-style'; +import tokenMeta from 'antd/es/version/token-meta.json'; +import { Space, theme } from 'antd'; +import useLocale from '../../../hooks/useLocale'; + +const useStyle = createStyles(({ token, css }) => { + const height = token.controlHeightLG; + const dotSize = height / 5; + + return { + container: css` + background: #fff; + border-radius: ${token.borderRadiusLG}px; + overflow: hidden; + `, + + row: css` + display: flex; + align-items: center; + `, + + col: css` + flex: 1 1 33%; + height: ${height}px; + display: flex; + align-items: center; + justify-content: center; + color: rgba(0,0,0,0.88); + border-right: 1px solid rgba(0, 0, 0, 0.1); + `, + + colDark: css` + background: #000; + color: #fff; + `, + + dot: css` + border-radius: 100%; + width: ${dotSize}px; + height: ${dotSize}px; + background: #000; + box-shadow: 0 0 0 1px rgba(150, 150, 150, 0.25); + `, + + dotColor: css` + width: ${token.fontSize * 6}px; + white-space: nowrap; + `, + }; +}); + +function color2Rgba(color: string) { + return `#${new TinyColor(color).toHex8().toUpperCase()}`; +} + +interface ColorCircleProps { + color?: string; +} + +function ColorCircle({ color }: ColorCircleProps) { + const { styles } = useStyle(); + + return ( + <Space size={4}> + <div className={styles.dot} style={{ background: color }} /> + <div className={styles.dotColor}>{color}</div> + </Space> + ); +} + +export interface TokenCompareProps { + tokenNames?: string; +} + +export default function TokenCompare(props: TokenCompareProps) { + const { tokenNames = '' } = props; + const [, lang] = useLocale({}); + const { styles } = useStyle(); + + const tokenList = React.useMemo(() => { + const list = tokenNames.split('|'); + + const lightTokens = theme.getDesignToken(); + const darkTokens = theme.getDesignToken({ + algorithm: theme.darkAlgorithm, + }); + + return list.map((tokenName) => { + const meta = tokenMeta.global[tokenName]; + const name = lang === 'cn' ? meta.name : meta.nameEn; + + return { + name: name.replace('颜色', '').replace('色', '').replace('Color', '').trim(), + light: color2Rgba(lightTokens[tokenName]), + dark: color2Rgba(darkTokens[tokenName]), + }; + }); + }, [tokenNames]); + + return ( + <div className={styles.container}> + {tokenList.map((data) => ( + <div key={data.name} className={styles.row}> + <div className={styles.col}>{data.name}</div> + <div className={styles.col}> + <ColorCircle color={data.light} /> + </div> + <div className={classNames(styles.col, styles.colDark)}> + <ColorCircle color={data.dark} /> + </div> + </div> + ))} + </div> + ); +} diff --git a/.dumi/theme/builtins/TokenTable/index.tsx b/.dumi/theme/builtins/TokenTable/index.tsx index 3c2b17d68572..6cb8fdfdf1c7 100644 --- a/.dumi/theme/builtins/TokenTable/index.tsx +++ b/.dumi/theme/builtins/TokenTable/index.tsx @@ -1,13 +1,11 @@ import type { FC } from 'react'; import * as React from 'react'; -/* eslint import/no-unresolved: 0 */ -import { css } from '@emotion/react'; -import type { TableProps } from 'antd'; -import { Table } from 'antd'; +import { createStyles } from 'antd-style'; import { getDesignToken } from 'antd-token-previewer'; import tokenMeta from 'antd/es/version/token-meta.json'; +import type { TableProps } from 'antd'; +import { Table } from 'antd'; import useLocale from '../../../hooks/useLocale'; -import useSiteToken from '../../../hooks/useSiteToken'; import ColorChunk from '../ColorChunk'; type TokenTableProps = { @@ -39,11 +37,8 @@ const locales = { }, }; -const useStyle = () => { - const { token } = useSiteToken(); - - return { - codeSpan: css` +const useStyle = createStyles(({ token, css }) => ({ + codeSpan: css` margin: 0 1px; padding: 0.2em 0.4em; font-size: 0.9em; @@ -52,12 +47,11 @@ const useStyle = () => { border-radius: 3px; font-family: monospace; `, - }; -}; +})); export function useColumns(): Exclude<TableProps<TokenData>['columns'], undefined> { const [locale] = useLocale(locales); - const styles = useStyle(); + const { styles } = useStyle(); return [ { @@ -74,7 +68,7 @@ export function useColumns(): Exclude<TableProps<TokenData>['columns'], undefine title: locale.type, key: 'type', dataIndex: 'type', - render: (_, record) => <span css={styles.codeSpan}>{record.type}</span>, + render: (_, record) => <span className={styles.codeSpan}>{record.type}</span>, }, { title: locale.value, @@ -84,7 +78,7 @@ export function useColumns(): Exclude<TableProps<TokenData>['columns'], undefine typeof record.value === 'string' && (record.value.startsWith('#') || record.value.startsWith('rgb')); if (isColor) { - return <ColorChunk color={record.value}>{record.value}</ColorChunk>; + return <ColorChunk value={record.value}>{record.value}</ColorChunk>; } return typeof record.value !== 'string' ? JSON.stringify(record.value) : record.value; }, @@ -98,7 +92,7 @@ const TokenTable: FC<TokenTableProps> = ({ type }) => { const data = React.useMemo<TokenData[]>( () => - Object.entries(tokenMeta) + Object.entries(tokenMeta.global) .filter(([, meta]) => meta.source === type) .map(([token, meta]) => ({ name: token, diff --git a/.dumi/theme/builtins/VideoPlayer/index.tsx b/.dumi/theme/builtins/VideoPlayer/index.tsx new file mode 100644 index 000000000000..02ef59424644 --- /dev/null +++ b/.dumi/theme/builtins/VideoPlayer/index.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { createStyles, css } from 'antd-style'; +import classNames from 'classnames'; +import { PlayCircleFilled, PauseCircleFilled } from '@ant-design/icons'; + +const useStyles = createStyles(({ cx, token }) => { + const play = css` + position: absolute; + right: ${token.paddingLG}px; + bottom: ${token.paddingLG}px; + font-size: 64px; + display: flex; + align-items: center; + justify-content: center; + color: rgba(0, 0, 0, 0.65); + opacity: 0; + transition: opacity ${token.motionDurationSlow}; + `; + + return { + container: css` + position: relative; + `, + + holder: css` + position: relative; + cursor: pointer; + + &:hover { + .${cx(play)} { + opacity: 1; + } + } + `, + + video: css` + width: 100%; + `, + + play, + }; +}); + +export default function VideoPlayer({ + className, + ...restProps +}: React.HtmlHTMLAttributes<HTMLVideoElement>) { + const { styles } = useStyles(); + const videoRef = React.useRef<HTMLVideoElement>(null); + const [playing, setPlaying] = React.useState(false); + + React.useEffect(() => { + if (playing) { + videoRef.current?.play(); + } else { + videoRef.current?.pause(); + } + }, [playing]); + + return ( + <div + className={classNames(styles.container, className)} + tabIndex={0} + role="button" + title="play or pause" + onClick={() => { + setPlaying(!playing); + }} + > + <div className={classNames(styles.holder)}> + <video ref={videoRef} className={styles.video} muted loop {...restProps} /> + + <div className={styles.play}>{playing ? <PauseCircleFilled /> : <PlayCircleFilled />}</div> + </div> + </div> + ); +} diff --git a/.dumi/theme/common/BehaviorMap/BehaviorMap.tsx b/.dumi/theme/common/BehaviorMap/BehaviorMap.tsx new file mode 100644 index 000000000000..5dd49559ff27 --- /dev/null +++ b/.dumi/theme/common/BehaviorMap/BehaviorMap.tsx @@ -0,0 +1,333 @@ +import G6 from '@antv/g6'; +import { createStyles, css } from 'antd-style'; +import { useRouteMeta } from 'dumi'; +import React, { useEffect, useRef } from 'react'; + +G6.registerNode('behavior-start-node', { + draw: (cfg, group) => { + const textWidth = G6.Util.getTextSize(cfg!.label, 16)[0]; + const size = [textWidth + 20 * 2, 48]; + const keyShape = group!.addShape('rect', { + name: 'start-node', + attrs: { + width: size[0], + height: size[1], + y: -size[1] / 2, + radius: 8, + fill: '#fff', + }, + }); + group!.addShape('text', { + attrs: { + text: `${cfg!.label}`, + fill: 'rgba(0, 0, 0, 0.88)', + fontSize: 16, + fontWeight: 500, + x: 20, + textBaseline: 'middle', + }, + name: 'start-node-text', + }); + return keyShape; + }, + getAnchorPoints() { + return [ + [0, 0.5], + [1, 0.5], + ]; + }, +}); + +G6.registerNode( + 'behavior-sub-node', + { + draw: (cfg, group) => { + const textWidth = G6.Util.getTextSize(cfg!.label, 14)[0]; + const padding = 16; + const size = [textWidth + 16 * 2 + (cfg!.targetType ? 12 : 0) + (cfg!.link ? 20 : 0), 40]; + const keyShape = group!.addShape('rect', { + name: 'sub-node', + attrs: { + width: size[0], + height: size[1], + y: -size[1] / 2, + radius: 8, + fill: '#fff', + cursor: 'pointer', + }, + }); + group!.addShape('text', { + attrs: { + text: `${cfg!.label}`, + x: cfg!.targetType ? 12 + 16 : padding, + fill: 'rgba(0, 0, 0, 0.88)', + fontSize: 14, + textBaseline: 'middle', + cursor: 'pointer', + }, + name: 'sub-node-text', + }); + if (cfg!.targetType) { + group!.addShape('rect', { + name: 'sub-node-type', + attrs: { + width: 8, + height: 8, + radius: 4, + y: -4, + x: 12, + fill: cfg!.targetType === 'mvp' ? '#1677ff' : '#A0A0A0', + cursor: 'pointer', + }, + }); + } + if (cfg!.children) { + const { length } = cfg!.children as any; + group!.addShape('rect', { + name: 'sub-node-children-length', + attrs: { + width: 20, + height: 20, + radius: 10, + y: -10, + x: size[0] - 4, + fill: '#404040', + cursor: 'pointer', + }, + }); + group!.addShape('text', { + name: 'sub-node-children-length-text', + attrs: { + text: `${length}`, + x: size[0] + 6 - G6.Util.getTextSize(`${length}`, 12)[0] / 2, + textBaseline: 'middle', + fill: '#fff', + fontSize: 12, + cursor: 'pointer', + }, + }); + } + if (cfg!.link) { + group!.addShape('dom', { + attrs: { + width: 16, + height: 16, + x: size[0] - 12 - 16, + y: -8, + cursor: 'pointer', + // DOM's html + html: ` + <div style="width: 16px; height: 16px;"> + <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="DatePicker" transform="translate(-890.000000, -441.000000)" fill-rule="nonzero"> + <g id="编组-30" transform="translate(288.000000, 354.000000)"> + <g id="编组-7备份-7" transform="translate(522.000000, 79.000000)"> + <g id="right-circle-outlinedd" transform="translate(80.000000, 8.000000)"> + <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="16" height="16"></rect> + <path d="M10.4171875,7.8984375 L6.5734375,5.1171875 C6.490625,5.0578125 6.375,5.115625 6.375,5.21875 L6.375,5.9515625 C6.375,6.1109375 6.4515625,6.2625 6.58125,6.35625 L8.853125,8 L6.58125,9.64375 C6.4515625,9.7375 6.375,9.8875 6.375,10.0484375 L6.375,10.78125 C6.375,10.8828125 6.490625,10.9421875 6.5734375,10.8828125 L10.4171875,8.1015625 C10.4859375,8.0515625 10.4859375,7.9484375 10.4171875,7.8984375 Z" id="路径" fill="#BFBFBF"></path> + <path d="M8,1 C4.134375,1 1,4.134375 1,8 C1,11.865625 4.134375,15 8,15 C11.865625,15 15,11.865625 15,8 C15,4.134375 11.865625,1 8,1 Z M8,13.8125 C4.790625,13.8125 2.1875,11.209375 2.1875,8 C2.1875,4.790625 4.790625,2.1875 8,2.1875 C11.209375,2.1875 13.8125,4.790625 13.8125,8 C13.8125,11.209375 11.209375,13.8125 8,13.8125 Z" id="形状" fill="#BFBFBF"></path> + </g> + </g> + </g> + </g> + </g> + </svg> + </div> + `, + }, + // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性 + name: 'sub-node-link', + }); + } + return keyShape; + }, + getAnchorPoints() { + return [ + [0, 0.5], + [1, 0.5], + ]; + }, + options: { + stateStyles: { + hover: { + stroke: '#1677ff', + 'sub-node-link': { + html: ` + <div style="width: 16px; height: 16px;"> + <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="DatePicker" transform="translate(-890.000000, -441.000000)" fill-rule="nonzero"> + <g id="编组-30" transform="translate(288.000000, 354.000000)"> + <g id="编组-7备份-7" transform="translate(522.000000, 79.000000)"> + <g id="right-circle-outlinedd" transform="translate(80.000000, 8.000000)"> + <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="16" height="16"></rect> + <path d="M10.4171875,7.8984375 L6.5734375,5.1171875 C6.490625,5.0578125 6.375,5.115625 6.375,5.21875 L6.375,5.9515625 C6.375,6.1109375 6.4515625,6.2625 6.58125,6.35625 L8.853125,8 L6.58125,9.64375 C6.4515625,9.7375 6.375,9.8875 6.375,10.0484375 L6.375,10.78125 C6.375,10.8828125 6.490625,10.9421875 6.5734375,10.8828125 L10.4171875,8.1015625 C10.4859375,8.0515625 10.4859375,7.9484375 10.4171875,7.8984375 Z" id="路径" fill="#1677ff"></path> + <path d="M8,1 C4.134375,1 1,4.134375 1,8 C1,11.865625 4.134375,15 8,15 C11.865625,15 15,11.865625 15,8 C15,4.134375 11.865625,1 8,1 Z M8,13.8125 C4.790625,13.8125 2.1875,11.209375 2.1875,8 C2.1875,4.790625 4.790625,2.1875 8,2.1875 C11.209375,2.1875 13.8125,4.790625 13.8125,8 C13.8125,11.209375 11.209375,13.8125 8,13.8125 Z" id="形状" fill="#1677ff"></path> + </g> + </g> + </g> + </g> + </g> + </svg> + </div> + `, + }, + }, + }, + }, + }, + 'rect', +); + +const dataTransform = (data: BehaviorMapItem) => { + const changeData = (d: any, level = 0) => { + const clonedData: any = { + ...d, + }; + switch (level) { + case 0: + clonedData.type = 'behavior-start-node'; + break; + case 1: + clonedData.type = 'behavior-sub-node'; + clonedData.collapsed = true; + break; + default: + clonedData.type = 'behavior-sub-node'; + break; + } + + if (d.children) { + clonedData.children = d.children.map((child: any) => changeData(child, level + 1)); + } + return clonedData; + }; + return changeData(data); +}; + +type BehaviorMapItem = { + id: string; + label: string; + targetType?: 'mvp' | 'extension'; + children?: BehaviorMapItem[]; + link?: string; +}; + +const useStyle = createStyles(() => ({ + container: css` + width: 100%; + height: 600px; + background-color: #f5f5f5; + border: 1px solid #e8e8e8; + border-radius: 8px; + overflow: hidden; + position: relative; + `, + title: css` + position: absolute; + top: 20px; + left: 20px; + font-size: 16px; + `, + tips: css` + display: flex; + position: absolute; + bottom: 20px; + right: 20px; + `, + mvp: css` + margin-right: 20px; + display: flex; + align-items: center; + &::before { + display: block; + width: 8px; + height: 8px; + margin-right: 8px; + background-color: #1677ff; + border-radius: 50%; + content: ''; + } + `, + extension: css` + display: flex; + align-items: center; + &::before { + display: block; + width: 8px; + height: 8px; + margin-right: 8px; + background-color: #a0a0a0; + border-radius: 50%; + content: ''; + } + `, +})); + +export type BehaviorMapProps = { + data: BehaviorMapItem; +}; + +const BehaviorMap: React.FC<BehaviorMapProps> = ({ data }) => { + const ref = useRef<HTMLDivElement>(null); + const { styles } = useStyle(); + const meta = useRouteMeta(); + + useEffect(() => { + const graph = new G6.TreeGraph({ + container: ref.current!, + width: ref.current!.scrollWidth, + height: ref.current!.scrollHeight, + renderer: 'svg', + modes: { + default: ['collapse-expand', 'drag-canvas'], + }, + defaultEdge: { + type: 'cubic-horizontal', + style: { + lineWidth: 1, + stroke: '#BFBFBF', + }, + }, + layout: { + type: 'mindmap', + direction: 'LR', + getHeight: () => 48, + getWidth: (node: any) => G6.Util.getTextSize(node.label, 16)[0] + 20 * 2, + getVGap: () => 10, + getHGap: () => 60, + getSide: (node: any) => node.data.direction, + }, + }); + + graph.on('node:mouseenter', (e) => { + graph.setItemState(e.item!, 'hover', true); + }); + graph.on('node:mouseleave', (e) => { + graph.setItemState(e.item!, 'hover', false); + }); + graph.on('node:click', (e) => { + const { link } = e.item!.getModel(); + if (link) { + window.location.hash = link as string; + } + }); + + graph.data(dataTransform(data)); + graph.render(); + graph.fitCenter(); + }, []); + + return ( + <div ref={ref} className={styles.container}> + <div className={styles.title}>{`${meta.frontmatter.title} 行为模式地图`}</div> + <div className={styles.tips}> + <div className={styles.mvp}>MVP 行为目的</div> + <div className={styles.extension}>拓展行为目的</div> + </div> + </div> + ); +}; + +export default BehaviorMap; diff --git a/.dumi/theme/common/BehaviorMap/index.tsx b/.dumi/theme/common/BehaviorMap/index.tsx index 5dd49559ff27..4868d8987a08 100644 --- a/.dumi/theme/common/BehaviorMap/index.tsx +++ b/.dumi/theme/common/BehaviorMap/index.tsx @@ -1,333 +1,41 @@ -import G6 from '@antv/g6'; -import { createStyles, css } from 'antd-style'; -import { useRouteMeta } from 'dumi'; -import React, { useEffect, useRef } from 'react'; +import type { FC } from 'react'; +import React, { Suspense } from 'react'; +import { createStyles } from 'antd-style'; +import { Skeleton } from 'antd'; +import type { BehaviorMapProps } from './BehaviorMap'; -G6.registerNode('behavior-start-node', { - draw: (cfg, group) => { - const textWidth = G6.Util.getTextSize(cfg!.label, 16)[0]; - const size = [textWidth + 20 * 2, 48]; - const keyShape = group!.addShape('rect', { - name: 'start-node', - attrs: { - width: size[0], - height: size[1], - y: -size[1] / 2, - radius: 8, - fill: '#fff', - }, - }); - group!.addShape('text', { - attrs: { - text: `${cfg!.label}`, - fill: 'rgba(0, 0, 0, 0.88)', - fontSize: 16, - fontWeight: 500, - x: 20, - textBaseline: 'middle', - }, - name: 'start-node-text', - }); - return keyShape; - }, - getAnchorPoints() { - return [ - [0, 0.5], - [1, 0.5], - ]; - }, -}); +const InternalBehaviorMap = React.lazy(() => import('./BehaviorMap')); -G6.registerNode( - 'behavior-sub-node', - { - draw: (cfg, group) => { - const textWidth = G6.Util.getTextSize(cfg!.label, 14)[0]; - const padding = 16; - const size = [textWidth + 16 * 2 + (cfg!.targetType ? 12 : 0) + (cfg!.link ? 20 : 0), 40]; - const keyShape = group!.addShape('rect', { - name: 'sub-node', - attrs: { - width: size[0], - height: size[1], - y: -size[1] / 2, - radius: 8, - fill: '#fff', - cursor: 'pointer', - }, - }); - group!.addShape('text', { - attrs: { - text: `${cfg!.label}`, - x: cfg!.targetType ? 12 + 16 : padding, - fill: 'rgba(0, 0, 0, 0.88)', - fontSize: 14, - textBaseline: 'middle', - cursor: 'pointer', - }, - name: 'sub-node-text', - }); - if (cfg!.targetType) { - group!.addShape('rect', { - name: 'sub-node-type', - attrs: { - width: 8, - height: 8, - radius: 4, - y: -4, - x: 12, - fill: cfg!.targetType === 'mvp' ? '#1677ff' : '#A0A0A0', - cursor: 'pointer', - }, - }); - } - if (cfg!.children) { - const { length } = cfg!.children as any; - group!.addShape('rect', { - name: 'sub-node-children-length', - attrs: { - width: 20, - height: 20, - radius: 10, - y: -10, - x: size[0] - 4, - fill: '#404040', - cursor: 'pointer', - }, - }); - group!.addShape('text', { - name: 'sub-node-children-length-text', - attrs: { - text: `${length}`, - x: size[0] + 6 - G6.Util.getTextSize(`${length}`, 12)[0] / 2, - textBaseline: 'middle', - fill: '#fff', - fontSize: 12, - cursor: 'pointer', - }, - }); - } - if (cfg!.link) { - group!.addShape('dom', { - attrs: { - width: 16, - height: 16, - x: size[0] - 12 - 16, - y: -8, - cursor: 'pointer', - // DOM's html - html: ` - <div style="width: 16px; height: 16px;"> - <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="DatePicker" transform="translate(-890.000000, -441.000000)" fill-rule="nonzero"> - <g id="编组-30" transform="translate(288.000000, 354.000000)"> - <g id="编组-7备份-7" transform="translate(522.000000, 79.000000)"> - <g id="right-circle-outlinedd" transform="translate(80.000000, 8.000000)"> - <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="16" height="16"></rect> - <path d="M10.4171875,7.8984375 L6.5734375,5.1171875 C6.490625,5.0578125 6.375,5.115625 6.375,5.21875 L6.375,5.9515625 C6.375,6.1109375 6.4515625,6.2625 6.58125,6.35625 L8.853125,8 L6.58125,9.64375 C6.4515625,9.7375 6.375,9.8875 6.375,10.0484375 L6.375,10.78125 C6.375,10.8828125 6.490625,10.9421875 6.5734375,10.8828125 L10.4171875,8.1015625 C10.4859375,8.0515625 10.4859375,7.9484375 10.4171875,7.8984375 Z" id="路径" fill="#BFBFBF"></path> - <path d="M8,1 C4.134375,1 1,4.134375 1,8 C1,11.865625 4.134375,15 8,15 C11.865625,15 15,11.865625 15,8 C15,4.134375 11.865625,1 8,1 Z M8,13.8125 C4.790625,13.8125 2.1875,11.209375 2.1875,8 C2.1875,4.790625 4.790625,2.1875 8,2.1875 C11.209375,2.1875 13.8125,4.790625 13.8125,8 C13.8125,11.209375 11.209375,13.8125 8,13.8125 Z" id="形状" fill="#BFBFBF"></path> - </g> - </g> - </g> - </g> - </g> - </svg> - </div> - `, - }, - // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性 - name: 'sub-node-link', - }); - } - return keyShape; - }, - getAnchorPoints() { - return [ - [0, 0.5], - [1, 0.5], - ]; - }, - options: { - stateStyles: { - hover: { - stroke: '#1677ff', - 'sub-node-link': { - html: ` - <div style="width: 16px; height: 16px;"> - <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="DatePicker" transform="translate(-890.000000, -441.000000)" fill-rule="nonzero"> - <g id="编组-30" transform="translate(288.000000, 354.000000)"> - <g id="编组-7备份-7" transform="translate(522.000000, 79.000000)"> - <g id="right-circle-outlinedd" transform="translate(80.000000, 8.000000)"> - <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="16" height="16"></rect> - <path d="M10.4171875,7.8984375 L6.5734375,5.1171875 C6.490625,5.0578125 6.375,5.115625 6.375,5.21875 L6.375,5.9515625 C6.375,6.1109375 6.4515625,6.2625 6.58125,6.35625 L8.853125,8 L6.58125,9.64375 C6.4515625,9.7375 6.375,9.8875 6.375,10.0484375 L6.375,10.78125 C6.375,10.8828125 6.490625,10.9421875 6.5734375,10.8828125 L10.4171875,8.1015625 C10.4859375,8.0515625 10.4859375,7.9484375 10.4171875,7.8984375 Z" id="路径" fill="#1677ff"></path> - <path d="M8,1 C4.134375,1 1,4.134375 1,8 C1,11.865625 4.134375,15 8,15 C11.865625,15 15,11.865625 15,8 C15,4.134375 11.865625,1 8,1 Z M8,13.8125 C4.790625,13.8125 2.1875,11.209375 2.1875,8 C2.1875,4.790625 4.790625,2.1875 8,2.1875 C11.209375,2.1875 13.8125,4.790625 13.8125,8 C13.8125,11.209375 11.209375,13.8125 8,13.8125 Z" id="形状" fill="#1677ff"></path> - </g> - </g> - </g> - </g> - </g> - </svg> - </div> - `, - }, - }, - }, - }, - }, - 'rect', -); - -const dataTransform = (data: BehaviorMapItem) => { - const changeData = (d: any, level = 0) => { - const clonedData: any = { - ...d, - }; - switch (level) { - case 0: - clonedData.type = 'behavior-start-node'; - break; - case 1: - clonedData.type = 'behavior-sub-node'; - clonedData.collapsed = true; - break; - default: - clonedData.type = 'behavior-sub-node'; - break; - } - - if (d.children) { - clonedData.children = d.children.map((child: any) => changeData(child, level + 1)); - } - return clonedData; - }; - return changeData(data); -}; - -type BehaviorMapItem = { - id: string; - label: string; - targetType?: 'mvp' | 'extension'; - children?: BehaviorMapItem[]; - link?: string; -}; - -const useStyle = createStyles(() => ({ - container: css` +const useStyle = createStyles(({ token, css }) => ({ + fallback: css` width: 100%; - height: 600px; - background-color: #f5f5f5; - border: 1px solid #e8e8e8; - border-radius: 8px; - overflow: hidden; - position: relative; - `, - title: css` - position: absolute; - top: 20px; - left: 20px; - font-size: 16px; - `, - tips: css` - display: flex; - position: absolute; - bottom: 20px; - right: 20px; - `, - mvp: css` - margin-right: 20px; - display: flex; - align-items: center; - &::before { - display: block; - width: 8px; - height: 8px; - margin-right: 8px; - background-color: #1677ff; - border-radius: 50%; - content: ''; + > * { + width: 100%; + border-radius: 8px; } `, - extension: css` - display: flex; - align-items: center; - &::before { - display: block; - width: 8px; - height: 8px; - margin-right: 8px; - background-color: #a0a0a0; - border-radius: 50%; - content: ''; - } + placeholder: css` + color: ${token.colorTextDescription}; + font-size: 16px; `, })); -export type BehaviorMapProps = { - data: BehaviorMapItem; -}; - -const BehaviorMap: React.FC<BehaviorMapProps> = ({ data }) => { - const ref = useRef<HTMLDivElement>(null); +const BehaviorMapFallback = () => { const { styles } = useStyle(); - const meta = useRouteMeta(); - - useEffect(() => { - const graph = new G6.TreeGraph({ - container: ref.current!, - width: ref.current!.scrollWidth, - height: ref.current!.scrollHeight, - renderer: 'svg', - modes: { - default: ['collapse-expand', 'drag-canvas'], - }, - defaultEdge: { - type: 'cubic-horizontal', - style: { - lineWidth: 1, - stroke: '#BFBFBF', - }, - }, - layout: { - type: 'mindmap', - direction: 'LR', - getHeight: () => 48, - getWidth: (node: any) => G6.Util.getTextSize(node.label, 16)[0] + 20 * 2, - getVGap: () => 10, - getHGap: () => 60, - getSide: (node: any) => node.data.direction, - }, - }); - - graph.on('node:mouseenter', (e) => { - graph.setItemState(e.item!, 'hover', true); - }); - graph.on('node:mouseleave', (e) => { - graph.setItemState(e.item!, 'hover', false); - }); - graph.on('node:click', (e) => { - const { link } = e.item!.getModel(); - if (link) { - window.location.hash = link as string; - } - }); - - graph.data(dataTransform(data)); - graph.render(); - graph.fitCenter(); - }, []); return ( - <div ref={ref} className={styles.container}> - <div className={styles.title}>{`${meta.frontmatter.title} 行为模式地图`}</div> - <div className={styles.tips}> - <div className={styles.mvp}>MVP 行为目的</div> - <div className={styles.extension}>拓展行为目的</div> - </div> + <div className={styles.fallback}> + <Skeleton.Node active style={{ height: 600, width: '100%' }}> + <span className={styles.placeholder}>正在载入行为模式地图...</span> + </Skeleton.Node> </div> ); }; +const BehaviorMap: FC<BehaviorMapProps> = (props) => ( + <Suspense fallback={<BehaviorMapFallback />}> + <InternalBehaviorMap {...props} /> + </Suspense> +); + export default BehaviorMap; diff --git a/.dumi/theme/common/CodePreview.tsx b/.dumi/theme/common/CodePreview.tsx index 492b05849788..164a554018f3 100644 --- a/.dumi/theme/common/CodePreview.tsx +++ b/.dumi/theme/common/CodePreview.tsx @@ -1,39 +1,161 @@ -import React from 'react'; -import { Tabs } from 'antd'; +import { Button, Tabs, Typography } from 'antd'; +import { createStyles } from 'antd-style'; +import { LiveContext } from 'dumi'; +import toReactElement from 'jsonml-to-react-element'; +import JsonML from 'jsonml.js/lib/utils'; +import Prism from 'prismjs'; +import React, { useContext, useEffect, useMemo } from 'react'; +import LiveCode from './LiveCode'; + +const useStyle = createStyles(({ token, css }) => { + const { colorIcon, colorBgTextHover, antCls } = token; + + return { + code: css` + position: relative; + margin-top: -16px; + `, + + copyButton: css` + color: ${colorIcon}; + position: absolute; + top: 16px; + inset-inline-end: 16px; + width: 32px; + text-align: center; + background: ${colorBgTextHover}; + padding: 0; + `, + + copyIcon: css` + ${antCls}-typography-copy { + margin-inline-start: 0; + } + ${antCls}-typography-copy:not(${antCls}-typography-copy-success) { + color: ${colorIcon}; + + &:hover { + color: ${colorIcon}; + } + } + `, + }; +}); const LANGS = { tsx: 'TypeScript', jsx: 'JavaScript', + style: 'CSS', }; interface CodePreviewProps { - codes?: Record<PropertyKey, string>; - toReactComponent?: (node: any) => React.ReactNode; + sourceCode?: string; + jsxCode?: string; + styleCode?: string; onCodeTypeChange?: (activeKey: string) => void; } -const CodePreview: React.FC<CodePreviewProps> = ({ toReactComponent, codes, onCodeTypeChange }) => { - const langList = Object.keys(codes).sort().reverse(); +function toReactComponent(jsonML: any[]) { + return toReactElement(jsonML, [ + [ + (node: any) => JsonML.isElement(node) && JsonML.getTagName(node) === 'pre', + (node: any, index: number) => { + const attr = JsonML.getAttributes(node); + return ( + <pre key={index} className={`language-${attr.lang}`}> + <code dangerouslySetInnerHTML={{ __html: attr.highlighted }} /> + </pre> + ); + }, + ], + ]); +} - let content: React.ReactNode; +const CodePreview: React.FC<CodePreviewProps> = ({ + sourceCode = '', + jsxCode = '', + styleCode = '', + onCodeTypeChange, +}) => { + // 避免 Tabs 数量不稳定的闪动问题 + const initialCodes = {} as Record<'tsx' | 'jsx' | 'style', string>; + if (sourceCode) { + initialCodes.tsx = ''; + } + if (jsxCode) { + initialCodes.jsx = ''; + } + if (styleCode) { + initialCodes.style = ''; + } + const [highlightedCodes, setHighlightedCodes] = React.useState(initialCodes); + const sourceCodes = { + tsx: sourceCode, + jsx: jsxCode, + style: styleCode, + } as Record<'tsx' | 'jsx' | 'style', string>; + useEffect(() => { + const codes = { + tsx: Prism.highlight(sourceCode, Prism.languages.javascript, 'jsx'), + jsx: Prism.highlight(jsxCode, Prism.languages.javascript, 'jsx'), + style: Prism.highlight(styleCode, Prism.languages.css, 'css'), + }; + // 去掉空的代码类型 + Object.keys(codes).forEach((key: keyof typeof codes) => { + if (!codes[key]) { + delete codes[key]; + } + }); + setHighlightedCodes(codes); + }, [jsxCode, sourceCode, styleCode]); + + const langList = Object.keys(highlightedCodes); + + const { styles } = useStyle(); + + const { enabled: liveEnabled } = useContext(LiveContext); + + const items = useMemo( + () => + langList.map((lang: keyof typeof LANGS) => ({ + label: LANGS[lang], + key: lang, + children: ( + <div className={styles.code}> + {lang === 'tsx' && liveEnabled ? ( + <LiveCode /> + ) : ( + toReactComponent(['pre', { lang, highlighted: highlightedCodes[lang] }]) + )} + <Button type="text" className={styles.copyButton}> + <Typography.Text className={styles.copyIcon} copyable={{ text: sourceCodes[lang] }} /> + </Button> + </div> + ), + })), + [JSON.stringify(highlightedCodes)], + ); + + if (!langList.length) { + return null; + } if (langList.length === 1) { - content = toReactComponent(['pre', { lang: langList[0], highlighted: codes[langList[0]] }]); - } else { - content = ( - <Tabs - centered - onChange={onCodeTypeChange} - items={langList.map((lang) => ({ - label: LANGS[lang], - key: lang, - children: toReactComponent(['pre', { lang, highlighted: codes[lang] }]), - }))} - /> + return liveEnabled ? ( + <LiveCode /> + ) : ( + toReactComponent([ + 'pre', + { + lang: langList[0], + highlighted: highlightedCodes[langList[0] as keyof typeof LANGS], + className: 'highlight', + }, + ]) ); } - return <div className="highlight">{content}</div>; + return <Tabs centered className="highlight" onChange={onCodeTypeChange} items={items} />; }; export default CodePreview; diff --git a/.dumi/theme/common/Color/ColorPaletteTool.tsx b/.dumi/theme/common/Color/ColorPaletteTool.tsx index 25d55032b357..2c2de6d3f9ef 100644 --- a/.dumi/theme/common/Color/ColorPaletteTool.tsx +++ b/.dumi/theme/common/Color/ColorPaletteTool.tsx @@ -1,28 +1,46 @@ -import { ColorPicker } from 'antd'; -import type { Color } from 'antd/es/color-picker'; import { FormattedMessage } from 'dumi'; import React, { useMemo, useState } from 'react'; +import { ColorPicker } from 'antd'; +import type { Color } from 'antd/es/color-picker'; import ColorPatterns from './ColorPatterns'; +import useLocale from '../../../hooks/useLocale'; const primaryMinSaturation = 70; // 主色推荐最小饱和度 const primaryMinBrightness = 70; // 主色推荐最小亮度 +const locales = { + cn: { + saturation: (s: string) => `饱和度建议不低于${primaryMinSaturation}(现在${s})`, + brightness: (b: string) => `亮度建议不低于${primaryMinBrightness}(现在${b})`, + }, + en: { + saturation: (s: string) => + `Saturation is recommended not to be lower than ${primaryMinSaturation}(currently${s})`, + brightness: (b: string) => + `Brightness is recommended not to be lower than ${primaryMinBrightness}(currently${b})`, + }, +}; + const ColorPaletteTool: React.FC = () => { const [primaryColor, setPrimaryColor] = useState<string>('#1890ff'); const [primaryColorInstance, setPrimaryColorInstance] = useState<Color>(null); + + const [locale] = useLocale(locales); + const handleChangeColor = (color: Color, hex: string) => { setPrimaryColor(hex); setPrimaryColorInstance(color); }; + const colorValidation = useMemo<React.ReactNode>(() => { let text = ''; if (primaryColorInstance) { - const { s, b } = primaryColorInstance.toHsb(); + const { s, b } = primaryColorInstance.toHsb() || {}; if (s * 100 < primaryMinSaturation) { - text += ` 饱和度建议不低于${primaryMinSaturation}(现在 ${(s * 100).toFixed(2)})`; + text += locale.saturation((s * 100).toFixed(2)); } if (b * 100 < primaryMinBrightness) { - text += ` 亮度建议不低于${primaryMinBrightness}(现在 ${(b * 100).toFixed(2)})`; + text += locale.brightness((s * 100).toFixed(2)); } } return <span className="color-palette-picker-validation">{text.trim()}</span>; @@ -32,7 +50,9 @@ const ColorPaletteTool: React.FC = () => { <div className="color-palette-pick"> <FormattedMessage id="app.docs.color.pick-primary" /> </div> - <div className="main-color">{ColorPatterns({ color: primaryColor })}</div> + <div className="main-color"> + <ColorPatterns color={primaryColor} /> + </div> <div className="color-palette-picker"> <span style={{ display: 'inline-block', verticalAlign: 'middle' }}> <ColorPicker value={primaryColor} onChange={handleChangeColor} /> diff --git a/.dumi/theme/common/Color/ColorPaletteToolDark.tsx b/.dumi/theme/common/Color/ColorPaletteToolDark.tsx index cdeb72503325..59400cfae121 100644 --- a/.dumi/theme/common/Color/ColorPaletteToolDark.tsx +++ b/.dumi/theme/common/Color/ColorPaletteToolDark.tsx @@ -1,15 +1,31 @@ -import { Col, ColorPicker, Row } from 'antd'; import { FormattedMessage } from 'dumi'; import React, { useMemo, useState } from 'react'; +import { Col, ColorPicker, Row } from 'antd'; import ColorPatterns from './ColorPatterns'; +import useLocale from '../../../hooks/useLocale'; const primaryMinSaturation = 70; // 主色推荐最小饱和度 const primaryMinBrightness = 70; // 主色推荐最小亮度 +const locales = { + cn: { + saturation: (s: string) => `饱和度建议不低于${primaryMinSaturation}(现在${s})`, + brightness: (b: string) => `亮度建议不低于${primaryMinBrightness}(现在${b})`, + }, + en: { + saturation: (s: string) => + `Saturation is recommended not to be lower than ${primaryMinSaturation}(currently${s})`, + brightness: (b: string) => + `Brightness is recommended not to be lower than ${primaryMinBrightness}(currently${b})`, + }, +}; + const ColorPaletteTool: React.FC = () => { const [primaryColor, setPrimaryColor] = useState<string>('#1890ff'); const [backgroundColor, setBackgroundColor] = useState<string>('#141414'); - const [primaryColorInstance, setPrimaryColorInstance] = useState(null); + const [primaryColorInstance, setPrimaryColorInstance] = useState<Color>(null); + + const [locale] = useLocale(locales); const handleChangeColor = (color: Color, hex: string) => { setPrimaryColor(hex); @@ -23,12 +39,12 @@ const ColorPaletteTool: React.FC = () => { const colorValidation = useMemo<React.ReactNode>(() => { let text = ''; if (primaryColorInstance) { - const { s, b } = primaryColorInstance.toHsb(); + const { s, b } = primaryColorInstance.toHsb() || {}; if (s * 100 < primaryMinSaturation) { - text += ` 饱和度建议不低于${primaryMinSaturation}(现在 ${(s * 100).toFixed(2)})`; + text += locale.saturation((s * 100).toFixed(2)); } if (b * 100 < primaryMinBrightness) { - text += ` 亮度建议不低于${primaryMinBrightness}(现在 ${(b * 100).toFixed(2)})`; + text += locale.brightness((s * 100).toFixed(2)); } } return ( @@ -41,7 +57,7 @@ const ColorPaletteTool: React.FC = () => { return ( <div className="color-palette-horizontal color-palette-horizontal-dark"> <div className="main-color"> - {ColorPatterns({ color: primaryColor, dark: true, backgroundColor })} + <ColorPatterns color={primaryColor} backgroundColor={backgroundColor} dark /> </div> <div className="color-palette-picker"> <Row> diff --git a/.dumi/theme/common/Color/ColorPalettes.tsx b/.dumi/theme/common/Color/ColorPalettes.tsx index d689535050e5..2c4bd1729cc2 100644 --- a/.dumi/theme/common/Color/ColorPalettes.tsx +++ b/.dumi/theme/common/Color/ColorPalettes.tsx @@ -1,4 +1,4 @@ -import classnames from 'classnames'; +import classNames from 'classnames'; import React from 'react'; import Palette from './Palette'; @@ -80,7 +80,7 @@ const colors = [ const ColorPalettes: React.FC<{ dark?: boolean }> = (props) => { const { dark } = props; return ( - <div className={classnames('color-palettes', { 'color-palettes-dark': dark })}> + <div className={classNames('color-palettes', { 'color-palettes-dark': dark })}> {colors.map((color) => ( <Palette key={color.name} color={color} dark={dark} showTitle /> ))} diff --git a/.dumi/theme/common/Color/ColorPatterns.tsx b/.dumi/theme/common/Color/ColorPatterns.tsx index d40d88a0c997..080b5651cea9 100644 --- a/.dumi/theme/common/Color/ColorPatterns.tsx +++ b/.dumi/theme/common/Color/ColorPatterns.tsx @@ -9,11 +9,15 @@ interface ColorPatternsProps { backgroundColor?: string; } -const ColorPatterns = ({ color, dark, backgroundColor }: ColorPatternsProps) => { +const ColorPatterns: React.FC<ColorPatternsProps> = ({ color, dark, backgroundColor }) => { const colors = generate(color, dark ? { theme: 'dark', backgroundColor } : {}); - return uniq(colors).map((colorString, i) => ( - <ColorBlock color={colorString} index={i + 1} dark={dark} key={colorString} /> - )); + return ( + <> + {uniq(colors).map((colorString, i) => ( + <ColorBlock color={colorString} index={i + 1} dark={dark} key={colorString} /> + ))} + </> + ); }; export default ColorPatterns; diff --git a/.dumi/theme/common/Color/ColorStyle.tsx b/.dumi/theme/common/Color/ColorStyle.tsx index 9de6c08de0a2..218cde0f46fc 100644 --- a/.dumi/theme/common/Color/ColorStyle.tsx +++ b/.dumi/theme/common/Color/ColorStyle.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { Global, css } from '@emotion/react'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { useTheme } from 'antd-style'; -const gray = { +const gray: { [key: number]: string } = { 1: '#fff', 2: '#fafafa', 3: '#f5f5f5', @@ -19,7 +19,7 @@ const gray = { }; const ColorStyle = () => { - const { token } = useSiteToken(); + const token = useTheme(); const makePalette = (color: string, index: number = 1): string => { if (index <= 10) { @@ -37,7 +37,7 @@ ${makePalette(color, index + 1)} if (index <= 13) { return ` .palette-gray-${index} { - background: ${(gray as any)[index]}; + background: ${gray[index]}; } ${makeGrayPalette(index + 1)} `; diff --git a/.dumi/theme/common/Color/Palette.tsx b/.dumi/theme/common/Color/Palette.tsx index 41423ca92cd9..0b7d999ceb00 100644 --- a/.dumi/theme/common/Color/Palette.tsx +++ b/.dumi/theme/common/Color/Palette.tsx @@ -1,7 +1,7 @@ import { presetDarkPalettes } from '@ant-design/colors'; -import { message } from 'antd'; import React, { useEffect } from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; +import { message } from 'antd'; const rgbToHex = (rgbString: string): string => { const rgb = rgbString.match(/\d+/g); diff --git a/.dumi/theme/common/ComponentChangelog/ComponentChangelog.tsx b/.dumi/theme/common/ComponentChangelog/ComponentChangelog.tsx new file mode 100644 index 000000000000..57dc1602e50d --- /dev/null +++ b/.dumi/theme/common/ComponentChangelog/ComponentChangelog.tsx @@ -0,0 +1,194 @@ +/* eslint-disable global-require */ +import React, { useMemo } from 'react'; +import { HistoryOutlined } from '@ant-design/icons'; +import { Button, Drawer, Timeline, Typography } from 'antd'; +import { createStyles } from 'antd-style'; + +import useFetch from '../../../hooks/useFetch'; +import useLocale from '../../../hooks/useLocale'; +import Link from '../Link'; + +const useStyle = createStyles(({ token, css }) => ({ + history: css` + position: absolute; + top: 0; + inset-inline-end: 0; + `, + + li: css` + // white-space: pre; + `, + + ref: css` + margin-left: ${token.marginXS}px; + `, +})); + +export interface ComponentChangelogProps { + pathname: string; +} + +const locales = { + cn: { + full: '完整更新日志', + changelog: '更新日志', + loading: '加载中...', + empty: '暂无更新', + }, + en: { + full: 'Full Changelog', + changelog: 'Changelog', + loading: 'loading...', + empty: 'Nothing update', + }, +}; + +function ParseChangelog(props: { changelog: string; refs: string[]; styles: any }) { + const { changelog = '', refs = [], styles } = props; + + const parsedChangelog = React.useMemo(() => { + const nodes: React.ReactNode[] = []; + + let isQuota = false; + let lastStr = ''; + + for (let i = 0; i < changelog.length; i += 1) { + const char = changelog[i]; + + if (char !== '`') { + lastStr += char; + } else { + let node: React.ReactNode = lastStr; + if (isQuota) { + node = <code>{node}</code>; + } + + nodes.push(node); + lastStr = ''; + isQuota = !isQuota; + } + } + + nodes.push(lastStr); + + return nodes; + }, [changelog]); + + return ( + <> + {/* Changelog */} + <span>{parsedChangelog}</span> + + {/* Refs */} + {refs?.map((ref) => ( + <a className={styles.ref} key={ref} href={ref} target="_blank" rel="noreferrer"> + #{ref.match(/^.*\/(\d+)$/)?.[1]} + </a> + ))} + </> + ); +} + +type ChangelogInfo = { + version: string; + changelog: string; + refs: string[]; +}; + +function useChangelog(componentPath: string, lang: 'cn' | 'en'): ChangelogInfo[] { + const data: any = useFetch( + lang === 'cn' + ? { + key: 'component-changelog-cn', + request: () => import('../../../preset/components-changelog-cn.json'), + } + : { + key: 'component-changelog-en', + request: () => import('../../../preset/components-changelog-en.json'), + }, + ); + + return useMemo(() => { + const component = componentPath.replace(/-/g, ''); + + const componentName = Object.keys(data).find( + (name) => name.toLowerCase() === component.toLowerCase(), + ); + + return data[componentName!]; + }, [data, componentPath]); +} + +export default function ComponentChangelog(props: ComponentChangelogProps) { + const { pathname = '' } = props; + const [locale, lang] = useLocale(locales); + const [show, setShow] = React.useState(false); + + const { styles } = useStyle(); + + const componentPath = pathname.match(/\/components\/([^/]+)/)?.[1] || ''; + + const list = useChangelog(componentPath, lang); + + const timelineItems = React.useMemo(() => { + const changelogMap: Record<string, ChangelogInfo[]> = {}; + + list?.forEach((info) => { + changelogMap[info.version] = changelogMap[info.version] || []; + changelogMap[info.version].push(info); + }); + + return Object.keys(changelogMap).map((version) => { + const changelogList = changelogMap[version]; + + return { + children: ( + <Typography> + <h4>{version}</h4> + <ul> + {changelogList.map((info, index) => ( + <li key={index} className={styles.li}> + <ParseChangelog {...info} styles={styles} /> + </li> + ))} + </ul> + </Typography> + ), + }; + }); + }, [list]); + + if (!list || !list.length) { + return null; + } + + return ( + <> + <Button + className={styles.history} + icon={<HistoryOutlined />} + onClick={() => { + setShow(true); + }} + > + {locale.changelog} + </Button> + <Drawer + title={locale.changelog} + extra={ + <Link style={{ fontSize: 14 }} to={`/changelog${lang === 'cn' ? '-cn' : ''}`}> + {locale.full} + </Link> + } + open={show} + width="40vw" + onClose={() => { + setShow(false); + }} + destroyOnClose + > + <Timeline items={timelineItems} /> + </Drawer> + </> + ); +} diff --git a/.dumi/theme/common/ComponentChangelog/index.tsx b/.dumi/theme/common/ComponentChangelog/index.tsx new file mode 100644 index 000000000000..cbaae2977db2 --- /dev/null +++ b/.dumi/theme/common/ComponentChangelog/index.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import type { ComponentChangelogProps } from './ComponentChangelog'; +import ComponentChangelog from './ComponentChangelog'; + +export default (props: ComponentChangelogProps) => ( + <React.Suspense fallback={null}> + <ComponentChangelog {...props} /> + </React.Suspense> +); diff --git a/.dumi/theme/common/DirectionIcon.tsx b/.dumi/theme/common/DirectionIcon.tsx new file mode 100644 index 000000000000..07f0aa6ac01c --- /dev/null +++ b/.dumi/theme/common/DirectionIcon.tsx @@ -0,0 +1,18 @@ +import Icon from '@ant-design/icons'; +import React from 'react'; +import type { DirectionType } from 'antd/es/config-provider'; + +const ltrD = + 'M448 64l512 0 0 128-128 0 0 768-128 0 0-768-128 0 0 768-128 0 0-448c-123.712 0-224-100.288-224-224s100.288-224 224-224zM64 448l256 224-256 224z'; +const rtlD = + 'M256 64l512 0 0 128-128 0 0 768-128 0 0-768-128 0 0 768-128 0 0-448c-123.712 0-224-100.288-224-224s100.288-224 224-224zM960 896l-256-224 256-224z'; + +const DirectionIcon: React.FC<{ direction: DirectionType; className?: string }> = (props) => ( + <Icon {...props}> + <svg viewBox="0 0 1024 1024" fill="currentColor"> + <path d={props.direction === 'ltr' ? ltrD : rtlD} /> + </svg> + </Icon> +); + +export default DirectionIcon; diff --git a/.dumi/theme/common/EditButton.tsx b/.dumi/theme/common/EditButton.tsx index c695890d23c0..64b8423d51e3 100644 --- a/.dumi/theme/common/EditButton.tsx +++ b/.dumi/theme/common/EditButton.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import { Tooltip } from 'antd'; import { EditOutlined } from '@ant-design/icons'; -import { css } from '@emotion/react'; -import useSiteToken from '../../hooks/useSiteToken'; +import { createStyles } from 'antd-style'; +import { Tooltip } from 'antd'; const branchUrl = 'https://github.com/ant-design/ant-design/edit/master/'; @@ -11,9 +10,7 @@ export interface EditButtonProps { filename?: string; } -const useStyle = () => { - const { token } = useSiteToken(); - +const useStyle = createStyles(({ token, css }) => { const { colorIcon, colorText, iconCls } = token; return { @@ -39,15 +36,15 @@ const useStyle = () => { } `, }; -}; +}); export default function EditButton({ title, filename }: EditButtonProps) { - const styles = useStyle(); + const { styles } = useStyle(); return ( <Tooltip title={title}> <a - css={styles.editButton} + className={styles.editButton} href={`${branchUrl}${filename}`} target="_blank" rel="noopener noreferrer" diff --git a/.dumi/theme/common/GlobalStyles.tsx b/.dumi/theme/common/GlobalStyles.tsx index 8c4212878eb5..d08758442811 100644 --- a/.dumi/theme/common/GlobalStyles.tsx +++ b/.dumi/theme/common/GlobalStyles.tsx @@ -15,6 +15,7 @@ import { Responsive, SearchBar, } from './styles'; +import InlineCard from './styles/InlineCard'; const GlobalStyles = () => ( <> @@ -29,6 +30,7 @@ const GlobalStyles = () => ( <Responsive /> <NProgress /> <PreviewImage /> + <InlineCard /> <ColorStyle /> <HeadingAnchor /> <SearchBar /> diff --git a/.dumi/theme/common/Link.tsx b/.dumi/theme/common/Link.tsx index 68b7bfe3f002..a7dac2436f02 100644 --- a/.dumi/theme/common/Link.tsx +++ b/.dumi/theme/common/Link.tsx @@ -1,28 +1,54 @@ -import type { MouseEvent } from 'react'; -import React, { forwardRef, startTransition } from 'react'; -import { useNavigate } from 'dumi'; +import type { MouseEvent, MouseEventHandler } from 'react'; +import React, { forwardRef, useLayoutEffect, useMemo, useTransition } from 'react'; +import { useLocation, useNavigate } from 'dumi'; +import nprogress from 'nprogress'; -export type LinkProps = { - to?: string; +export interface LinkProps { + to?: string | { pathname?: string; search?: string; hash?: string }; children?: React.ReactNode; + style?: React.CSSProperties; className?: string; -}; + onClick?: MouseEventHandler; +} const Link = forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => { const { to, children, ...rest } = props; + const [isPending, startTransition] = useTransition(); const navigate = useNavigate(); + const { pathname } = useLocation(); + + const href = useMemo(() => { + if (typeof to === 'object') { + return `${to.pathname || pathname}${to.search || ''}${to.hash || ''}`; + } + return to; + }, [to]); const handleClick = (e: MouseEvent<HTMLAnchorElement>) => { - if (!to.startsWith('http')) { - e.preventDefault(); - startTransition(() => { - navigate(to); - }); + props.onClick?.(e); + if (!href?.startsWith('http')) { + // Should support open in new tab + if (!e.metaKey && !e.ctrlKey && !e.shiftKey) { + e.preventDefault(); + startTransition(() => { + if (href) { + navigate(href); + } + }); + } } }; + useLayoutEffect(() => { + if (isPending) { + nprogress.start(); + } else { + nprogress.done(); + } + }, [isPending]); + return ( - <a ref={ref} href={to} onClick={handleClick} {...rest}> + <a ref={ref} onClick={handleClick} {...rest} href={href}> {children} </a> ); diff --git a/.dumi/theme/common/LiveCode.tsx b/.dumi/theme/common/LiveCode.tsx new file mode 100644 index 000000000000..9f80e95c6e04 --- /dev/null +++ b/.dumi/theme/common/LiveCode.tsx @@ -0,0 +1,85 @@ +import type { FC } from 'react'; +import React, { useEffect, useState } from 'react'; +import { createStyles } from 'antd-style'; +import LiveEditor from '../slots/LiveEditor'; +import LiveError from '../slots/LiveError'; +import { EditFilled } from '@ant-design/icons'; +import { Tooltip } from 'antd'; +import useLocale from '../../hooks/useLocale'; + +const useStyle = createStyles(({ token, css }) => { + const { colorPrimaryBorder, colorIcon, colorPrimary } = token; + + return { + editor: css` + .npm__react-simple-code-editor__textarea { + outline: none; + + &:hover { + border: 1px solid ${colorPrimaryBorder} !important; + } + + &:focus { + border: 1px solid ${colorPrimary} !important; + } + } + `, + + editableIcon: css` + position: absolute; + height: 32px; + width: 32px; + display: flex; + align-items: center; + justify-content: center; + top: 16px; + inset-inline-end: 56px; + color: ${colorIcon}; + `, + }; +}); + +const locales = { + cn: { + demoEditable: '编辑 Demo 可实时预览', + }, + en: { + demoEditable: 'Edit demo with real-time preview', + }, +}; + +const HIDE_LIVE_DEMO_TIP = 'hide-live-demo-tip'; + +const LiveCode: FC = () => { + const [open, setOpen] = useState(false); + const { styles } = useStyle(); + const [locale] = useLocale(locales); + + useEffect(() => { + const shouldOpen = !localStorage.getItem(HIDE_LIVE_DEMO_TIP); + if (shouldOpen) { + setOpen(true); + } + }, []); + + const handleOpenChange = (newOpen: boolean) => { + setOpen(newOpen); + if (!newOpen) { + localStorage.setItem(HIDE_LIVE_DEMO_TIP, 'true'); + } + }; + + return ( + <> + <div className={styles.editor}> + <LiveEditor /> + <LiveError /> + </div> + <Tooltip title={locale.demoEditable} open={open} onOpenChange={handleOpenChange}> + <EditFilled className={styles.editableIcon} /> + </Tooltip> + </> + ); +}; + +export default LiveCode; diff --git a/.dumi/theme/common/Loading.tsx b/.dumi/theme/common/Loading.tsx index fa29eb9e5a10..912bd7cb3796 100644 --- a/.dumi/theme/common/Loading.tsx +++ b/.dumi/theme/common/Loading.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Skeleton, Space, Spin } from 'antd'; import { useLocation } from 'dumi'; +import { Skeleton, Space, Spin } from 'antd'; const Loading: React.FC = () => { const { pathname } = useLocation(); diff --git a/.dumi/theme/common/PrevAndNext.tsx b/.dumi/theme/common/PrevAndNext.tsx index 01ed50f062af..826db93beb81 100644 --- a/.dumi/theme/common/PrevAndNext.tsx +++ b/.dumi/theme/common/PrevAndNext.tsx @@ -1,13 +1,15 @@ -import type { ReactElement } from 'react'; -import React, { useMemo } from 'react'; -import type { MenuProps } from 'antd'; -import type { MenuItemType } from 'antd/es/menu/hooks/useItems'; import { LeftOutlined, RightOutlined } from '@ant-design/icons'; -import { createStyles, css } from 'antd-style'; +import { createStyles } from 'antd-style'; +import type { ReactElement } from 'react'; +import React, { useMemo, useContext } from 'react'; import classNames from 'classnames'; +import type { MenuItemType } from 'antd/es/menu/hooks/useItems'; +import type { MenuProps } from 'antd'; import useMenu from '../../hooks/useMenu'; +import SiteContext from '../slots/SiteContext'; +import type { SiteContextProps } from '../slots/SiteContext'; -const useStyle = createStyles(({ token }) => { +const useStyle = createStyles(({ token, css }) => { const { colorSplit, iconCls, fontSizeIcon } = token; return { @@ -27,6 +29,7 @@ const useStyle = createStyles(({ token }) => { text-decoration: none; ${iconCls} { + color: #999; font-size: ${fontSizeIcon}px; transition: all 0.3s; } @@ -37,6 +40,9 @@ const useStyle = createStyles(({ token }) => { `, prevNav: css` text-align: start; + display: flex; + justify-content: flex-start; + align-items: center; .footer-nav-icon-after { display: none; @@ -44,19 +50,22 @@ const useStyle = createStyles(({ token }) => { .footer-nav-icon-before { position: relative; - margin-inline-end: 1em; - vertical-align: middle; line-height: 0; - right: 0; - transition: right 0.3s; + vertical-align: middle; + transition: inset-inline-end 0.3s; + margin-inline-end: 1em; + inset-inline-end: 0; } &:hover .footer-nav-icon-before { - right: 0.2em; + inset-inline-end: 0.2em; } `, nextNav: css` text-align: end; + display: flex; + justify-content: flex-end; + align-items: center; .footer-nav-icon-before { display: none; @@ -64,16 +73,16 @@ const useStyle = createStyles(({ token }) => { .footer-nav-icon-after { position: relative; - margin-inline-start: 1em; margin-bottom: 1px; - vertical-align: middle; line-height: 0; - left: 0; - transition: left 0.3s; + vertical-align: middle; + transition: inset-inline-start 0.3s; + margin-inline-start: 1em; + inset-inline-start: 0; } &:hover .footer-nav-icon-after { - left: 0.2em; + inset-inline-start: 0.2em; } `, }; @@ -94,13 +103,17 @@ const flattenMenu = (menuItems: MenuProps['items']): MenuProps['items'] | null = return null; }; -const PrevAndNext: React.FC = () => { +const PrevAndNext: React.FC<{ rtl?: boolean }> = ({ rtl }) => { const { styles } = useStyle(); + const beforeProps = { className: 'footer-nav-icon-before' }; + const afterProps = { className: 'footer-nav-icon-after' }; + + const before = rtl ? <RightOutlined {...beforeProps} /> : <LeftOutlined {...beforeProps} />; + const after = rtl ? <LeftOutlined {...afterProps} /> : <RightOutlined {...afterProps} />; + + const [menuItems, selectedKey] = useMenu({ before, after }); - const [menuItems, selectedKey] = useMenu({ - before: <LeftOutlined className="footer-nav-icon-before" />, - after: <RightOutlined className="footer-nav-icon-after" />, - }); + const { isMobile } = useContext<SiteContextProps>(SiteContext); const [prev, next] = useMemo(() => { const flatMenu = flattenMenu(menuItems); @@ -119,15 +132,19 @@ const PrevAndNext: React.FC = () => { ]; }, [menuItems, selectedKey]); + if (isMobile) { + return null; + } + return ( <section className={styles.prevNextNav}> {prev && React.cloneElement(prev.label as ReactElement, { - className: classNames(styles.pageNav, styles.prevNav), + className: classNames(styles.pageNav, styles.prevNav, prev.className), })} {next && React.cloneElement(next.label as ReactElement, { - className: classNames(styles.pageNav, styles.nextNav), + className: classNames(styles.pageNav, styles.nextNav, next.className), })} </section> ); diff --git a/.dumi/theme/common/ThemeSwitch/index.tsx b/.dumi/theme/common/ThemeSwitch/index.tsx index 69895928f851..05caa5d536bd 100644 --- a/.dumi/theme/common/ThemeSwitch/index.tsx +++ b/.dumi/theme/common/ThemeSwitch/index.tsx @@ -1,28 +1,41 @@ -import { BgColorsOutlined } from '@ant-design/icons'; +import React from 'react'; +import { BgColorsOutlined, SmileOutlined } from '@ant-design/icons'; import { FloatButton } from 'antd'; -import { CompactTheme, DarkTheme, Motion } from 'antd-token-previewer/es/icons'; +import { useTheme } from 'antd-style'; +import { CompactTheme, DarkTheme } from 'antd-token-previewer/es/icons'; +// import { Motion } from 'antd-token-previewer/es/icons'; import { FormattedMessage, Link, useLocation } from 'dumi'; -import React from 'react'; -import useSiteToken from '../../../hooks/useSiteToken'; + +import useThemeAnimation from '../../../hooks/useThemeAnimation'; import { getLocalizedPathname, isZhCN } from '../../utils'; import ThemeIcon from './ThemeIcon'; -export type ThemeName = 'light' | 'dark' | 'compact' | 'motion-off'; +export type ThemeName = 'light' | 'dark' | 'compact' | 'motion-off' | 'happy-work'; -export type ThemeSwitchProps = { +export interface ThemeSwitchProps { value?: ThemeName[]; onChange: (value: ThemeName[]) => void; -}; +} -const ThemeSwitch: React.FC<ThemeSwitchProps> = (props: ThemeSwitchProps) => { +const ThemeSwitch: React.FC<ThemeSwitchProps> = (props) => { const { value = ['light'], onChange } = props; - const { token } = useSiteToken(); + const token = useTheme(); const { pathname, search } = useLocation(); - const isMotionOff = value.includes('motion-off'); + // const isMotionOff = value.includes('motion-off'); + const isHappyWork = value.includes('happy-work'); + const isDark = value.includes('dark'); + + const toggleAnimationTheme = useThemeAnimation(); return ( - <FloatButton.Group trigger="click" icon={<ThemeIcon />}> + <FloatButton.Group + trigger="click" + icon={<ThemeIcon />} + aria-label="Theme Switcher" + badge={{ dot: true }} + style={{ zIndex: 1010 }} + > <Link to={getLocalizedPathname('/theme-editor', isZhCN(pathname), search)} style={{ display: 'block', marginBottom: token.margin }} @@ -34,9 +47,12 @@ const ThemeSwitch: React.FC<ThemeSwitchProps> = (props: ThemeSwitchProps) => { </Link> <FloatButton icon={<DarkTheme />} - type={value.includes('dark') ? 'primary' : 'default'} - onClick={() => { - if (value.includes('dark')) { + type={isDark ? 'primary' : 'default'} + onClick={(e) => { + // Toggle animation when switch theme + toggleAnimationTheme(e, isDark); + + if (isDark) { onChange(value.filter((theme) => theme !== 'dark')); } else { onChange([...value, 'dark']); @@ -57,18 +73,19 @@ const ThemeSwitch: React.FC<ThemeSwitchProps> = (props: ThemeSwitchProps) => { tooltip={<FormattedMessage id="app.theme.switch.compact" />} /> <FloatButton - icon={<Motion />} - type={!isMotionOff ? 'primary' : 'default'} + badge={{ dot: true }} + icon={<SmileOutlined />} + type={isHappyWork ? 'primary' : 'default'} onClick={() => { - if (isMotionOff) { - onChange(value.filter((theme) => theme !== 'motion-off')); + if (isHappyWork) { + onChange(value.filter((theme) => theme !== 'happy-work')); } else { - onChange([...value, 'motion-off']); + onChange([...value, 'happy-work']); } }} tooltip={ <FormattedMessage - id={isMotionOff ? 'app.theme.switch.motion.off' : 'app.theme.switch.motion.on'} + id={isHappyWork ? 'app.theme.switch.happy-work.off' : 'app.theme.switch.happy-work.on'} /> } /> diff --git a/.dumi/theme/common/styles/BrowserMockup.tsx b/.dumi/theme/common/styles/BrowserMockup.tsx index 1313b7620a5c..f0c5266bc84f 100644 --- a/.dumi/theme/common/styles/BrowserMockup.tsx +++ b/.dumi/theme/common/styles/BrowserMockup.tsx @@ -47,7 +47,7 @@ export default () => ( top: -1.6em; left: 5.5em; display: block; - width: ~'calc(100% - 6em)'; + width: calc(100% - 6em); height: 1.2em; background-color: white; border-radius: 2px; diff --git a/.dumi/theme/common/styles/Common.tsx b/.dumi/theme/common/styles/Common.tsx index a805422a238c..c2f9cf7f2b67 100644 --- a/.dumi/theme/common/styles/Common.tsx +++ b/.dumi/theme/common/styles/Common.tsx @@ -1,9 +1,13 @@ import { css, Global } from '@emotion/react'; import React from 'react'; +import { useTheme } from 'antd-style'; -export default () => ( - <Global - styles={css` +export default () => { + const { anchorTop } = useTheme(); + + return ( + <Global + styles={css` body, div, dl, @@ -55,6 +59,19 @@ export default () => ( vertical-align: middle; border-style: none; } + + [id] { + scroll-margin-top: ${anchorTop}px; + } + + [data-prefers-color='dark'] { + color-scheme: dark; + } + + [data-prefers-color='light'] { + color-scheme: light; + } `} - /> -); + /> + ); +}; diff --git a/.dumi/theme/common/styles/Demo.tsx b/.dumi/theme/common/styles/Demo.tsx index 1266a8709498..e29c4797ce6c 100644 --- a/.dumi/theme/common/styles/Demo.tsx +++ b/.dumi/theme/common/styles/Demo.tsx @@ -1,9 +1,9 @@ -import { css, Global } from '@emotion/react'; import React from 'react'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { css, Global } from '@emotion/react'; +import { useTheme } from 'antd-style'; const GlobalDemoStyles: React.FC = () => { - const { token } = useSiteToken(); + const token = useTheme(); const { antCls, iconCls } = token; @@ -60,36 +60,29 @@ const GlobalDemoStyles: React.FC = () => { &-expand-trigger { position: relative; - margin-left: 12px; color: #3b4357; font-size: 20px; cursor: pointer; opacity: 0.75; transition: all 0.3s; + margin-inline-start: 12px; &:hover { opacity: 1; } - - ${antCls}-row-rtl & { - margin-right: 8px; - margin-left: 0; - } } &-title { position: absolute; top: -14px; - margin-left: 16px; padding: 1px 8px; color: #777; background: ${token.colorBgContainer}; border-radius: ${token.borderRadius}px ${token.borderRadius}px 0 0; transition: background-color 0.4s; + margin-inline-start: 16px; ${antCls}-row-rtl & { - margin-right: 16px; - margin-left: 0; border-radius: ${token.borderRadius}px 0 0 ${token.borderRadius}px; } @@ -109,11 +102,11 @@ const GlobalDemoStyles: React.FC = () => { position: absolute; top: 7px; right: -16px; - padding-right: 6px; font-size: 12px; text-decoration: none; background: inherit; transform: scale(0.9); + padding-inline-end: 6px; ${iconCls} { color: ${token.colorTextSecondary}; @@ -127,9 +120,6 @@ const GlobalDemoStyles: React.FC = () => { ${antCls}-row${antCls}-row-rtl & { right: auto; left: -22px; - margin-right: 0; - padding-right: 8px; - padding-left: 6px; } } @@ -165,14 +155,9 @@ const GlobalDemoStyles: React.FC = () => { > p { width: 100%; margin: 0.5em 0; - padding-right: 25px; font-size: 12px; word-break: break-word; - - ${antCls}-row-rtl & { - padding-right: 0; - padding-left: 25px; - } + padding-inline-end: 25px; } } @@ -338,6 +323,8 @@ const GlobalDemoStyles: React.FC = () => { background: ${token.colorBgContainer}; border: none; box-shadow: unset; + padding: 12px 16px; + font-size: 13px; } } diff --git a/.dumi/theme/common/styles/Highlight.tsx b/.dumi/theme/common/styles/Highlight.tsx index e2a9481c4aff..2f43706dd396 100644 --- a/.dumi/theme/common/styles/Highlight.tsx +++ b/.dumi/theme/common/styles/Highlight.tsx @@ -1,9 +1,9 @@ import { css, Global } from '@emotion/react'; import React from 'react'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { useTheme } from 'antd-style'; export default () => { - const { token } = useSiteToken(); + const token = useTheme(); return ( <Global diff --git a/.dumi/theme/common/styles/Icon.tsx b/.dumi/theme/common/styles/Icon.tsx index 2259d0ac054e..05e159be72c0 100644 --- a/.dumi/theme/common/styles/Icon.tsx +++ b/.dumi/theme/common/styles/Icon.tsx @@ -1,9 +1,9 @@ import { css, Global } from '@emotion/react'; import React from 'react'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { useTheme } from 'antd-style'; export default () => { - const { token } = useSiteToken(); + const token = useTheme(); const { antCls, iconCls } = token; diff --git a/.dumi/theme/common/styles/IconPickSearcher.tsx b/.dumi/theme/common/styles/IconPickSearcher.tsx index 2fe9168f00e0..be378a8a6897 100644 --- a/.dumi/theme/common/styles/IconPickSearcher.tsx +++ b/.dumi/theme/common/styles/IconPickSearcher.tsx @@ -1,9 +1,9 @@ import { css, Global } from '@emotion/react'; import React from 'react'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { useTheme } from 'antd-style'; export default () => { - const { token } = useSiteToken(); + const token = useTheme(); const { iconCls } = token; diff --git a/.dumi/theme/common/styles/InlineCard.tsx b/.dumi/theme/common/styles/InlineCard.tsx new file mode 100644 index 000000000000..41080183b3ff --- /dev/null +++ b/.dumi/theme/common/styles/InlineCard.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { css, Global } from '@emotion/react'; + +export default () => ( + <Global + styles={css` + .design-inline-cards { + display: flex; + margin: 0 -20px; + } + .design-inline-cards > * { + flex: 10%; + margin: 0 20px; + } + .design-inline-cards img { + width: 100%; + max-width: 100%; + } + .design-inline-cards h4 { + margin-bottom: 0; + } + `} + /> +); diff --git a/.dumi/theme/common/styles/Markdown.tsx b/.dumi/theme/common/styles/Markdown.tsx index 3a3a5875e6eb..0eef2359a976 100644 --- a/.dumi/theme/common/styles/Markdown.tsx +++ b/.dumi/theme/common/styles/Markdown.tsx @@ -1,10 +1,10 @@ -import { css, Global } from '@emotion/react'; import React from 'react'; import { TinyColor } from '@ctrl/tinycolor'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { css, Global } from '@emotion/react'; +import { useTheme } from 'antd-style'; -export default () => { - const { token } = useSiteToken(); +const GlobalStyle: React.FC = () => { + const token = useTheme(); const { antCls } = token; @@ -28,9 +28,17 @@ export default () => { max-height: 100%; } + .markdown > a > img, + .markdown > img { + display: block; + margin: 0 auto; + } + .markdown p > img { - margin: 34px 0; + margin: 34px auto; box-shadow: 0 8px 20px rgba(143, 168, 191, 0.35); + max-width: 1024px; + display: block; } .markdown p > img.markdown-inline-image { @@ -180,6 +188,39 @@ export default () => { line-height: 2; } } + .pic-plus { + & > * { + display: inline-block !important; + vertical-align: middle; + } + span { + margin: 0 20px; + color: #aaa; + font-size: 30px; + } + } + .antd-site-snippet { + .ant-tabs-tab { + .snippet-label { + display: flex; + align-items: center; + justify-content: center; + svg { + margin-inline-end: 8px; + } + } + } + .dumi-default-source-code { + margin: 0 auto; + background-color: ${token.siteMarkdownCodeBg}; + border-radius: ${token.borderRadius}px; + > pre.prism-code { + padding: 12px 20px; + font-size: 13px; + line-height: 2; + } + } + } .markdown table td > a:not(:last-child) { margin-right: 0 !important; @@ -328,13 +369,12 @@ export default () => { &:first-child { width: 18%; min-width: 58px; - color: #595959; + color: ${token.colorText}; font-weight: 600; white-space: nowrap; } &:nth-child(2) { - width: 55%; min-width: 160px; } @@ -455,3 +495,5 @@ export default () => { /> ); }; + +export default GlobalStyle; diff --git a/.dumi/theme/common/styles/NProgress.tsx b/.dumi/theme/common/styles/NProgress.tsx index 7550779ca913..096108a8af86 100644 --- a/.dumi/theme/common/styles/NProgress.tsx +++ b/.dumi/theme/common/styles/NProgress.tsx @@ -1,9 +1,9 @@ import { css, Global } from '@emotion/react'; import React from 'react'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { useTheme } from 'antd-style'; export default () => { - const { token } = useSiteToken(); + const token = useTheme(); return ( <Global styles={css` diff --git a/.dumi/theme/common/styles/PreviewImage.tsx b/.dumi/theme/common/styles/PreviewImage.tsx index 08aff35caaf2..0cfb280f055f 100644 --- a/.dumi/theme/common/styles/PreviewImage.tsx +++ b/.dumi/theme/common/styles/PreviewImage.tsx @@ -1,9 +1,9 @@ import { css, Global } from '@emotion/react'; import React from 'react'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { useTheme } from 'antd-style'; export default () => { - const { token } = useSiteToken(); + const token = useTheme(); return ( <Global diff --git a/.dumi/theme/common/styles/Reset.tsx b/.dumi/theme/common/styles/Reset.tsx index 8b9198988995..de14849a7414 100644 --- a/.dumi/theme/common/styles/Reset.tsx +++ b/.dumi/theme/common/styles/Reset.tsx @@ -1,9 +1,9 @@ import { css, Global } from '@emotion/react'; import React from 'react'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { useTheme } from 'antd-style'; export default () => { - const { token } = useSiteToken(); + const token = useTheme(); return ( <Global @@ -53,7 +53,7 @@ export default () => { font-family: ${token.fontFamily}; line-height: ${token.lineHeight}; background: ${token.colorBgContainer}; - transition: background 1s cubic-bezier(0.075, 0.82, 0.165, 1); + transition: background-color 1s cubic-bezier(0.075, 0.82, 0.165, 1); } `} /> diff --git a/.dumi/theme/common/styles/Responsive.tsx b/.dumi/theme/common/styles/Responsive.tsx index 2b99062a578d..cf31359be328 100644 --- a/.dumi/theme/common/styles/Responsive.tsx +++ b/.dumi/theme/common/styles/Responsive.tsx @@ -1,16 +1,16 @@ import { css, Global } from '@emotion/react'; import React from 'react'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { useTheme } from 'antd-style'; export default () => { - const { token } = useSiteToken(); + const token = useTheme(); return ( <Global styles={css` .nav-phone-icon { position: absolute; - top: 25px; + bottom: 17px; right: 30px; z-index: 1; display: none; @@ -87,7 +87,7 @@ export default () => { } .prev-next-nav { - width: ~'calc(100% - 32px)'; + width: calc(100% - 32px); margin-left: 16px; .ant-row-rtl & { diff --git a/.dumi/theme/common/styles/SearchBar.tsx b/.dumi/theme/common/styles/SearchBar.tsx index ffbea9c6d21d..ee031c7a67bc 100644 --- a/.dumi/theme/common/styles/SearchBar.tsx +++ b/.dumi/theme/common/styles/SearchBar.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { css, Global } from '@emotion/react'; -import useSiteToken from '../../../hooks/useSiteToken'; +import { useTheme } from 'antd-style'; const THEME_PREFIX = 'dumi-default-'; export default () => { - const { token } = useSiteToken(); + const token = useTheme(); return ( <Global diff --git a/.dumi/theme/layouts/DocLayout/index.tsx b/.dumi/theme/layouts/DocLayout/index.tsx index a57f73c87303..8ddbf828c777 100644 --- a/.dumi/theme/layouts/DocLayout/index.tsx +++ b/.dumi/theme/layouts/DocLayout/index.tsx @@ -1,17 +1,17 @@ -import ConfigProvider from 'antd/es/config-provider'; -import zhCN from 'antd/es/locale/zh_CN'; import classNames from 'classnames'; import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn'; import { Helmet, useOutlet, useSiteData } from 'dumi'; import React, { useContext, useEffect, useLayoutEffect, useMemo, useRef } from 'react'; +import zhCN from 'antd/es/locale/zh_CN'; +import ConfigProvider from 'antd/es/config-provider'; import useLocale from '../../../hooks/useLocale'; import useLocation from '../../../hooks/useLocation'; import GlobalStyles from '../../common/GlobalStyles'; -import Footer from '../../slots/Footer'; import Header from '../../slots/Header'; import SiteContext from '../../slots/SiteContext'; import '../../static/style'; +import IndexLayout from '../IndexLayout'; import ResourceLayout from '../ResourceLayout'; import SidebarLayout from '../SidebarLayout'; @@ -32,7 +32,7 @@ const DocLayout: React.FC = () => { const location = useLocation(); const { pathname, search, hash } = location; const [locale, lang] = useLocale(locales); - const timerRef = useRef<NodeJS.Timeout | null>(null); + const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const { direction } = useContext(SiteContext); const { loading } = useSiteData(); @@ -60,13 +60,10 @@ const DocLayout: React.FC = () => { if (id) document.getElementById(decodeURIComponent(id))?.scrollIntoView(); }, [loading, hash]); - React.useEffect(() => { + useEffect(() => { if (typeof (window as any).ga !== 'undefined') { (window as any).ga('send', 'pageview', pathname + search); } - if (typeof (window as any)._hmt !== 'undefined') { - (window as any)._hmt.push(['_trackPageview', pathname + search]); - } }, [location]); const content = useMemo(() => { @@ -75,10 +72,9 @@ const DocLayout: React.FC = () => { ['/index'].some((path) => pathname.startsWith(path)) ) { return ( - <> + <IndexLayout title={locale.title} desc={locale.description}> {outlet} - <Footer /> - </> + </IndexLayout> ); } if (pathname.startsWith('/docs/resource')) { @@ -94,17 +90,14 @@ const DocLayout: React.FC = () => { <> <Helmet encodeSpecialCharacters={false}> <html - lang={lang} + lang={lang === 'cn' ? 'zh-CN' : lang} data-direction={direction} className={classNames({ rtl: direction === 'rtl' })} /> - <title>{locale?.title} - - = { [K in keyof T]: [K, T[K]] }[keyof T][]; type SiteState = Partial>; const RESPONSIVE_MOBILE = 768; +export const ANT_DESIGN_NOT_SHOW_BANNER = 'ANT_DESIGN_NOT_SHOW_BANNER'; -const styleCache = createCache(); -if (typeof global !== 'undefined') { - (global as any).styleCache = styleCache; -} +// const styleCache = createCache(); +// if (typeof global !== 'undefined') { +// (global as any).styleCache = styleCache; +// } const getAlgorithm = (themes: ThemeName[] = []) => - themes.map((theme) => { - if (theme === 'dark') { - return antdTheme.darkAlgorithm; - } - if (theme === 'compact') { - return antdTheme.compactAlgorithm; - } - return antdTheme.defaultAlgorithm; - }); + themes + .map((theme) => { + if (theme === 'dark') { + return antdTheme.darkAlgorithm; + } + if (theme === 'compact') { + return antdTheme.compactAlgorithm; + } + return null; + }) + .filter((item) => item) as typeof antdTheme.darkAlgorithm[]; const GlobalLayout: React.FC = () => { const outlet = useOutlet(); const { pathname } = useLocation(); const [searchParams, setSearchParams] = useSearchParams(); - const [{ theme = [], direction, isMobile }, setSiteState] = useLayoutState({ - isMobile: false, - direction: 'ltr', - theme: ['light', 'motion-off'], - }); + const [{ theme = [], direction, isMobile, bannerVisible = false }, setSiteState] = + useLayoutState({ + isMobile: false, + direction: 'ltr', + theme: [], + bannerVisible: false, + }); const updateSiteConfig = useCallback( (props: SiteState) => { @@ -69,6 +80,10 @@ const GlobalLayout: React.FC = () => { ...nextSearchParams, theme: value.filter((t) => t !== 'light'), }); + + document + .querySelector('html') + ?.setAttribute('data-prefers-color', value.includes('dark') ? 'dark' : 'light'); } }); @@ -86,8 +101,16 @@ const GlobalLayout: React.FC = () => { useEffect(() => { const _theme = searchParams.getAll('theme') as ThemeName[]; const _direction = searchParams.get('direction') as DirectionType; - - setSiteState({ theme: _theme, direction: _direction === 'rtl' ? 'rtl' : 'ltr' }); + const storedBannerVisibleLastTime = + localStorage && localStorage.getItem(ANT_DESIGN_NOT_SHOW_BANNER); + const storedBannerVisible = + storedBannerVisibleLastTime && dayjs().diff(dayjs(storedBannerVisibleLastTime), 'day') >= 1; + + setSiteState({ + theme: _theme, + direction: _direction === 'rtl' ? 'rtl' : 'ltr', + bannerVisible: storedBannerVisibleLastTime ? !!storedBannerVisible : true, + }); // Handle isMobile updateMobileMode(); @@ -103,36 +126,64 @@ const GlobalLayout: React.FC = () => { updateSiteConfig, theme: theme!, isMobile: isMobile!, + bannerVisible, }), - [isMobile, direction, updateSiteConfig, theme], + [isMobile, direction, updateSiteConfig, theme, bannerVisible], ); + const [styleCache] = React.useState(() => createCache()); + + useServerInsertedHTML(() => { + const styleText = extractStyle(styleCache, true); + return `, + }; + }); + return styles.filter(Boolean); +} + +export const getHash = (str: string, length = 8) => + createHash('md5').update(str).digest('hex').slice(0, length); + /** * extends dumi internal tech stack, for customize previewer props */ @@ -19,9 +44,11 @@ class AntdReactTechStack extends ReactTechStack { const codePath = opts.fileAbsPath!.replace(/\.\w+$/, '.tsx'); const code = fs.existsSync(codePath) ? fs.readFileSync(codePath, 'utf-8') : ''; - const pkgDependencyList = localPackage.dependencies; - props.pkgDependencyList = pkgDependencyList; + props.pkgDependencyList = { + ...localPackage.devDependencies, + ...localPackage.dependencies, + }; props.jsx = sylvanas.parseText(code); if (md) { @@ -40,10 +67,33 @@ class AntdReactTechStack extends ReactTechStack { } } -const resolve = (path: string): string => require.resolve(path); +const resolve = (p: string): string => require.resolve(p); const RoutesPlugin = (api: IApi) => { - const ssrCssFileName = `ssr-${Date.now()}.css`; + // const ssrCssFileName = `ssr-${Date.now()}.css`; + + const writeCSSFile = (key: string, hashKey: string, cssString: string) => { + const fileName = `style-${key}.${getHash(hashKey)}.css`; + + const filePath = path.join(api.paths.absOutputPath, fileName); + + if (!fs.existsSync(filePath)) { + api.logger.event(chalk.grey(`write to: ${filePath}`)); + fs.writeFileSync(filePath, cssString, 'utf8'); + } + + return fileName; + }; + + const addLinkStyle = (html: string, cssFile: string, prepend = false) => { + const prefix = api.userConfig.publicPath || api.config.publicPath; + + if (prepend) { + return html.replace('', ``); + } + + return html.replace('', ``); + }; api.registerTechStack(() => new AntdReactTechStack()); @@ -78,20 +128,56 @@ const RoutesPlugin = (api: IApi) => { files // exclude dynamic route path, to avoid deploy failed by `:id` directory .filter((f) => !f.path.includes(':')) - // FIXME: workaround to make emotion support react 18 pipeableStream - // ref: https://github.com/emotion-js/emotion/issues/2800#issuecomment-1221296308 .map((file) => { - let styles = ''; + let globalStyles = ''; + + // Debug for file content: uncomment this if need check raw out + // const tmpFileName = `_${file.path.replace(/\//g, '-')}`; + // const tmpFilePath = path.join(api.paths.absOutputPath, tmpFileName); + // fs.writeFileSync(tmpFilePath, file.content, 'utf8'); // extract all emotion style tags from body - file.content = file.content.replace(/ -## Design Token +## 主题变量(Design Token) diff --git a/components/flex/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/flex/__tests__/__snapshots__/demo-extend.test.ts.snap new file mode 100644 index 000000000000..3b3773654dd7 --- /dev/null +++ b/components/flex/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -0,0 +1,675 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders components/flex/demo/align.tsx extend context correctly 1`] = ` +
+

+ Select justify : +

+
+
+ + + + + + +
+
+

+ Select align : +

+
+
+ + + +
+
+
+ + + + +
+
+`; + +exports[`renders components/flex/demo/align.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/flex/demo/basic.tsx extend context correctly 1`] = ` +
+
+ + +
+
+
+
+
+
+
+
+`; + +exports[`renders components/flex/demo/basic.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/flex/demo/combination.tsx extend context correctly 1`] = ` +
+
+
+ avatar +
+

+ “antd is an enterprise-class UI design language and React UI library.” +

+ + + Get Start + + +
+
+
+
+`; + +exports[`renders components/flex/demo/combination.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/flex/demo/debug.tsx extend context correctly 1`] = ` +Array [ +
+
+
+
+
+
, +
+
+
+
+
+
, +] +`; + +exports[`renders components/flex/demo/debug.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/flex/demo/gap.tsx extend context correctly 1`] = ` +
+
+ + + + +
+
+ + + + +
+
+`; + +exports[`renders components/flex/demo/gap.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/flex/demo/wrap.tsx extend context correctly 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+`; + +exports[`renders components/flex/demo/wrap.tsx extend context correctly 2`] = `[]`; diff --git a/components/flex/__tests__/__snapshots__/demo.test.ts.snap b/components/flex/__tests__/__snapshots__/demo.test.ts.snap new file mode 100644 index 000000000000..3bd7aec00563 --- /dev/null +++ b/components/flex/__tests__/__snapshots__/demo.test.ts.snap @@ -0,0 +1,663 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders components/flex/demo/align.tsx correctly 1`] = ` +
+

+ Select justify : +

+
+
+ + + + + + +
+
+

+ Select align : +

+
+
+ + + +
+
+
+ + + + +
+
+`; + +exports[`renders components/flex/demo/basic.tsx correctly 1`] = ` +
+
+ + +
+
+
+
+
+
+
+
+`; + +exports[`renders components/flex/demo/combination.tsx correctly 1`] = ` +
+
+
+ avatar +
+

+ “antd is an enterprise-class UI design language and React UI library.” +

+ + + Get Start + + +
+
+
+
+`; + +exports[`renders components/flex/demo/debug.tsx correctly 1`] = ` +Array [ +
+
+
+
+
+
, +
+
+
+
+
+
, +] +`; + +exports[`renders components/flex/demo/gap.tsx correctly 1`] = ` +
+
+ + + + +
+
+ + + + +
+
+`; + +exports[`renders components/flex/demo/wrap.tsx correctly 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+`; diff --git a/components/flex/__tests__/__snapshots__/index.test.tsx.snap b/components/flex/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000000..6809df8adf8e --- /dev/null +++ b/components/flex/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Flex rtl render component should be rendered correctly in RTL direction 1`] = ` +
+
+ test1 +
+
+ test2 +
+
+`; diff --git a/components/qrcode/__tests__/demo-extend.test.ts b/components/flex/__tests__/demo-extend.test.ts similarity index 73% rename from components/qrcode/__tests__/demo-extend.test.ts rename to components/flex/__tests__/demo-extend.test.ts index 79dea1e4faf5..59f0f997b6d0 100644 --- a/components/qrcode/__tests__/demo-extend.test.ts +++ b/components/flex/__tests__/demo-extend.test.ts @@ -1,3 +1,3 @@ import { extendTest } from '../../../tests/shared/demoTest'; -extendTest('qrcode'); +extendTest('flex'); diff --git a/components/badge/__tests__/demo.test.ts b/components/flex/__tests__/demo.test.ts similarity index 74% rename from components/badge/__tests__/demo.test.ts rename to components/flex/__tests__/demo.test.ts index e1bdd02d5eee..9f18ce06a392 100644 --- a/components/badge/__tests__/demo.test.ts +++ b/components/flex/__tests__/demo.test.ts @@ -1,3 +1,3 @@ import demoTest from '../../../tests/shared/demoTest'; -demoTest('badge'); +demoTest('flex'); diff --git a/components/back-top/__tests__/image.test.ts b/components/flex/__tests__/image.test.ts similarity index 52% rename from components/back-top/__tests__/image.test.ts rename to components/flex/__tests__/image.test.ts index 2651ed880e1d..3f8a7a1d0b19 100644 --- a/components/back-top/__tests__/image.test.ts +++ b/components/flex/__tests__/image.test.ts @@ -1,5 +1,5 @@ import { imageDemoTest } from '../../../tests/shared/imageTest'; -describe('BackTop image', () => { - imageDemoTest('back-top'); +describe('flex image', () => { + imageDemoTest('flex'); }); diff --git a/components/flex/__tests__/index.test.tsx b/components/flex/__tests__/index.test.tsx new file mode 100644 index 000000000000..7ec72caa718a --- /dev/null +++ b/components/flex/__tests__/index.test.tsx @@ -0,0 +1,70 @@ +import React from 'react'; + +import Flex from '..'; +import mountTest from '../../../tests/shared/mountTest'; +import rtlTest from '../../../tests/shared/rtlTest'; +import { render } from '../../../tests/utils'; + +const FunCom = React.forwardRef((props, ref) => ( +
+ test FC +
+)); + +class ClassCom extends React.PureComponent<{ className?: string }> { + render() { + return
test Class
; + } +} + +describe('Flex', () => { + mountTest(() => ( + +
test1
+
test2
+
+ )); + rtlTest(() => ( + +
test1
+
test2
+
+ )); + it('Flex', () => { + const { container, rerender } = render(test); + expect(container.querySelector('.ant-flex')).toHaveStyle({ justifyContent: 'center' }); + rerender(test); + expect(container.querySelector('.ant-flex')).toHaveStyle({ flex: '0 1 auto' }); + rerender(test); + expect(container.querySelector('.ant-flex')).toHaveStyle({ gap: '100px' }); + }); + it('Component work', () => { + const testFcRef = React.createRef(); + const testClsRef = React.createRef(); + const { container, rerender } = render(test); + expect(container.querySelector('.ant-flex')?.tagName).toBe('DIV'); + rerender(test); + expect(container.querySelector('.ant-flex')?.tagName).toBe('SPAN'); + rerender( }>test); + expect(container.querySelector('.ant-flex')?.textContent).toBe('test FC'); + expect(testFcRef.current).toBeTruthy(); + rerender( }>test); + expect(container.querySelector('.ant-flex')?.textContent).toBe('test Class'); + expect(testClsRef.current).toBeTruthy(); + }); + + it('when vertical=true should stretch work', () => { + const { container, rerender } = render(test); + expect(container.querySelector('.ant-flex')).toHaveClass( + 'ant-flex-align-stretch', + ); + rerender( + + test + , + ); + expect(container.querySelector('.ant-flex')).toHaveClass( + 'ant-flex-align-center', + ); + }); +}); diff --git a/components/flex/demo/align.md b/components/flex/demo/align.md new file mode 100644 index 000000000000..b98b06ce0218 --- /dev/null +++ b/components/flex/demo/align.md @@ -0,0 +1,7 @@ +## zh-CN + +设置对齐方式。 + +## en-US + +Set align. diff --git a/components/flex/demo/align.tsx b/components/flex/demo/align.tsx new file mode 100644 index 000000000000..ef6923f4689c --- /dev/null +++ b/components/flex/demo/align.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button, Flex, Segmented } from 'antd'; +import type { FlexProps } from 'antd'; +import type { SegmentedProps } from 'antd/es/segmented'; + +const boxStyle: React.CSSProperties = { + width: '100%', + height: 120, + borderRadius: 6, + border: '1px solid #40a9ff', +}; + +const justifyOptions = [ + 'flex-start', + 'center', + 'flex-end', + 'space-between', + 'space-around', + 'space-evenly', +]; + +const alignOptions = ['flex-start', 'center', 'flex-end']; + +const App: React.FC = () => { + const [justify, setJustify] = React.useState(justifyOptions[0]); + const [alignItems, setAlignItems] = React.useState(alignOptions[0]); + return ( + +

Select justify :

+ +

Select align :

+ + + + + + + +
+ ); +}; + +export default App; diff --git a/components/flex/demo/basic.md b/components/flex/demo/basic.md new file mode 100644 index 000000000000..d8863c5bd73f --- /dev/null +++ b/components/flex/demo/basic.md @@ -0,0 +1,7 @@ +## zh-CN + +最简单的用法。 + +## en-US + +The basic usage. diff --git a/components/flex/demo/basic.tsx b/components/flex/demo/basic.tsx new file mode 100644 index 000000000000..fa6b183e6a6c --- /dev/null +++ b/components/flex/demo/basic.tsx @@ -0,0 +1,27 @@ +/* eslint-disable react/no-array-index-key */ +import React from 'react'; +import { Flex, Radio } from 'antd'; + +const baseStyle: React.CSSProperties = { + width: '25%', + height: 54, +}; + +const App: React.FC = () => { + const [value, setValue] = React.useState('horizontal'); + return ( + + setValue(e.target.value)}> + horizontal + vertical + + + {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} + + + ); +}; + +export default App; diff --git a/components/flex/demo/combination.md b/components/flex/demo/combination.md new file mode 100644 index 000000000000..cb5a86034632 --- /dev/null +++ b/components/flex/demo/combination.md @@ -0,0 +1,7 @@ +## zh-CN + +嵌套使用,可以实现更复杂的布局。 + +## en-US + +Nesting can achieve more complex layouts. diff --git a/components/flex/demo/combination.tsx b/components/flex/demo/combination.tsx new file mode 100644 index 000000000000..05cc57f09831 --- /dev/null +++ b/components/flex/demo/combination.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Button, Card, Flex, Typography } from 'antd'; + +const cardStyle: React.CSSProperties = { + width: 620, +}; + +const imgStyle: React.CSSProperties = { + display: 'block', + width: 273, +}; + +const App: React.FC = () => ( + + + avatar + + + “antd is an enterprise-class UI design language and React UI library.” + + + + + +); + +export default App; diff --git a/components/flex/demo/debug.md b/components/flex/demo/debug.md new file mode 100644 index 000000000000..22f1c2128928 --- /dev/null +++ b/components/flex/demo/debug.md @@ -0,0 +1,7 @@ +## zh-CN + +调试专用。 + +## en-US + +Use for debug. diff --git a/components/flex/demo/debug.tsx b/components/flex/demo/debug.tsx new file mode 100644 index 000000000000..50f504005ded --- /dev/null +++ b/components/flex/demo/debug.tsx @@ -0,0 +1,33 @@ +/* eslint-disable react/no-array-index-key */ +import React from 'react'; +import { Flex } from 'antd'; + +const App: React.FC = () => ( + <> + + {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} + + + {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} + + +); + +export default App; diff --git a/components/flex/demo/gap.md b/components/flex/demo/gap.md new file mode 100644 index 000000000000..3a8352de1637 --- /dev/null +++ b/components/flex/demo/gap.md @@ -0,0 +1,7 @@ +## zh-CN + +使用 `gap` 设置元素之间的间距,预设了 `small`、`middle`、`large` 三种尺寸,也可以自定义间距。 + +## en-US + +Set the `gap` between elements, which has three preset sizes: `small`, `middle`, `large`, You can also customize the gap size. diff --git a/components/flex/demo/gap.tsx b/components/flex/demo/gap.tsx new file mode 100644 index 000000000000..1d80f1f76ad8 --- /dev/null +++ b/components/flex/demo/gap.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Button, Flex, Radio, Slider } from 'antd'; +import type { SizeType } from 'antd/es/config-provider/SizeContext'; + +const App: React.FC = () => { + const [gapSize, setGapSize] = React.useState('small'); + const [customGapSize, setCustomGapSize] = React.useState(0); + return ( + + setGapSize(e.target.value)}> + {['small', 'middle', 'large', 'customize'].map((size) => ( + + {size} + + ))} + + {gapSize === 'customize' && } + + + + + + + + ); +}; + +export default App; diff --git a/components/flex/demo/wrap.md b/components/flex/demo/wrap.md new file mode 100644 index 000000000000..0a306a11c186 --- /dev/null +++ b/components/flex/demo/wrap.md @@ -0,0 +1,7 @@ +## zh-CN + +自动换行。 + +## en-US + +Auto wrap line. diff --git a/components/flex/demo/wrap.tsx b/components/flex/demo/wrap.tsx new file mode 100644 index 000000000000..13e9629c4962 --- /dev/null +++ b/components/flex/demo/wrap.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Button, Flex } from 'antd'; + +const Demo: React.FC = () => ( + + {Array.from({ length: 24 }, (_, i) => ( + + ))} + +); + +export default Demo; diff --git a/components/flex/index.en-US.md b/components/flex/index.en-US.md new file mode 100644 index 000000000000..933d48920258 --- /dev/null +++ b/components/flex/index.en-US.md @@ -0,0 +1,50 @@ +--- +category: Components +group: Layout +title: Flex +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*SMzgSJZE_AwAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*8yArQ43EGccAAAAAAAAAAAAADrJ8AQ/original +tag: New +--- + +Flex. Available since `5.10.0`. + +## When To Use + +- Good for setting spacing between elements. +- Suitable for setting various horizontal and vertical alignments. + +### Difference with Space component + +- Space is used to set the spacing between inline elements. It will add a wrapper element for each child element for inline alignment. Suitable for equidistant arrangement of multiple child elements in rows and columns. +- Flex is used to set the layout of block-level elements. It does not add a wrapper element. Suitable for layout of child elements in vertical or horizontal direction, and provides more flexibility and control. + +## Examples + + +Basic +align +gap +Wrap +combination +debug + +## API + +> This component is available since `antd@5.10.0`. The default behavior of Flex in horizontal mode is to align upward, In vertical mode, aligns the stretch, You can adjust this via properties. + +Common props ref:[Common props](/docs/react/common-props) + +| Property | Description | type | Default | Version | +| --- | --- | --- | --- | --- | +| vertical | Is direction of the flex vertical, use `flex-direction: column` | boolean | `false` | | +| wrap | Set whether the element is displayed in a single line or in multiple lines | reference [flex-wrap](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap) | nowrap | | +| justify | Sets the alignment of elements in the direction of the main axis | reference [justify-content](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content) | normal | | +| align | Sets the alignment of elements in the direction of the cross axis | reference [align-items](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items) | normal | | +| flex | flex CSS shorthand properties | reference [flex](https://developer.mozilla.org/en-US/docs/Web/CSS/flex) | normal | | +| gap | Sets the gap between grids | `small` \| `middle` \| `large` \| string \| number | - | | +| component | custom element type | React.ComponentType | `div` | | + +## Design Token + + diff --git a/components/flex/index.tsx b/components/flex/index.tsx new file mode 100644 index 000000000000..cebaca23e0a6 --- /dev/null +++ b/components/flex/index.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import classNames from 'classnames'; +import omit from 'rc-util/lib/omit'; + +import { isPresetSize } from '../_util/gapSize'; +import { ConfigContext } from '../config-provider'; +import type { ConfigConsumerProps } from '../config-provider'; +import type { FlexProps } from './interface'; +import useStyle from './style'; +import createFlexClassNames from './utils'; + +const Flex = React.forwardRef((props, ref) => { + const { + prefixCls: customizePrefixCls, + rootClassName, + className, + style, + flex, + gap, + children, + vertical = false, + component: Component = 'div', + ...othersProps + } = props; + + const { + flex: ctxFlex, + direction: ctxDirection, + getPrefixCls, + } = React.useContext(ConfigContext); + + const prefixCls = getPrefixCls('flex', customizePrefixCls); + + const [wrapSSR, hashId] = useStyle(prefixCls); + + const mergedVertical = vertical ?? ctxFlex?.vertical; + + const mergedCls = classNames( + className, + rootClassName, + ctxFlex?.className, + prefixCls, + hashId, + createFlexClassNames(prefixCls, props), + { + [`${prefixCls}-rtl`]: ctxDirection === 'rtl', + [`${prefixCls}-gap-${gap}`]: isPresetSize(gap), + [`${prefixCls}-vertical`]: mergedVertical, + }, + ); + + const mergedStyle: React.CSSProperties = { ...ctxFlex?.style, ...style }; + + if (flex) { + mergedStyle.flex = flex; + } + + if (gap && !isPresetSize(gap)) { + mergedStyle.gap = gap; + } + + return wrapSSR( + + {children} + , + ); +}); + +if (process.env.NODE_ENV !== 'production') { + Flex.displayName = 'Flex'; +} + +export default Flex; diff --git a/components/flex/index.zh-CN.md b/components/flex/index.zh-CN.md new file mode 100644 index 000000000000..965d8d63ba19 --- /dev/null +++ b/components/flex/index.zh-CN.md @@ -0,0 +1,51 @@ +--- +category: Components +subtitle: 弹性布局 +group: 布局 +title: Flex +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*SMzgSJZE_AwAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*8yArQ43EGccAAAAAAAAAAAAADrJ8AQ/original +tag: New +--- + +弹性布局。自 `5.10.0` 版本开始提供该组件。 + +## 何时使用 + +- 适合设置元素之间的间距。 +- 适合设置各种水平、垂直对齐方式。 + +### 与 Space 组件的区别 + +- Space 为内联元素提供间距,其本身会为每一个子元素添加包裹元素用于内联对齐。适用于行、列中多个子元素的等距排列。 +- Flex 为块级元素提供间距,其本身不会添加包裹元素。适用于垂直或水平方向上的子元素布局,并提供了更多的灵活性和控制能力。 + +## 代码演示 + + +基本布局 +对齐方式 +设置间隙 +自动换行 +组合使用 +调试专用 + +## API + +> 自 `antd@5.10.0` 版本开始提供该组件。Flex 组件默认行为在水平模式下,为向上对齐,在垂直模式下,为拉伸对齐,你可以通过属性进行调整。 + +通用属性参考:[通用属性](/docs/react/common-props) + +| 属性 | 说明 | 类型 | 默认值 | 版本 | +| --- | --- | --- | --- | --- | +| vertical | flex 主轴的方向是否垂直,使用 `flex-direction: column` | boolean | `false` | +| wrap | 设置元素单行显示还是多行显示 | 参考 [flex-wrap](https://developer.mozilla.org/zh-CN/docs/Web/CSS/flex-wrap) | nowrap | | +| justify | 设置元素在主轴方向上的对齐方式 | 参考 [justify-content](https://developer.mozilla.org/zh-CN/docs/Web/CSS/justify-content) | normal | | +| align | 设置元素在交叉轴方向上的对齐方式 | 参考 [align-items](https://developer.mozilla.org/zh-CN/docs/Web/CSS/align-items) | normal | | +| flex | flex CSS 简写属性 | 参考 [flex](https://developer.mozilla.org/zh-CN/docs/Web/CSS/flex) | normal | | +| gap | 设置网格之间的间隙 | `small` \| `middle` \| `large` \| string \| number | - | | +| component | 自定义元素类型 | React.ComponentType | `div` | | + +## Design Token + + diff --git a/components/flex/interface.ts b/components/flex/interface.ts new file mode 100644 index 000000000000..6e7f42151b72 --- /dev/null +++ b/components/flex/interface.ts @@ -0,0 +1,17 @@ +import type React from 'react'; + +import type { AnyObject, CustomComponent } from '../_util/type'; +import type { SizeType } from '../config-provider/SizeContext'; + +export interface FlexProps

extends React.HTMLAttributes { + prefixCls?: string; + rootClassName?: string; + vertical?: boolean; + wrap?: React.CSSProperties['flexWrap']; + justify?: React.CSSProperties['justifyContent']; + align?: React.CSSProperties['alignItems']; + flex?: React.CSSProperties['flex']; + gap?: React.CSSProperties['gap'] | SizeType; + children: React.ReactNode; + component?: CustomComponent

; +} diff --git a/components/flex/style/index.ts b/components/flex/style/index.ts new file mode 100644 index 000000000000..d90e646578eb --- /dev/null +++ b/components/flex/style/index.ts @@ -0,0 +1,111 @@ +import type { CSSInterpolation } from '@ant-design/cssinjs'; + +import type { FullToken, GenerateStyle } from '../../theme/internal'; +import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import { alignItemsValues, flexWrapValues, justifyContentValues } from '../utils'; + +/** Component only token. Which will handle additional calculation of alias token */ +export interface ComponentToken { + // Component token here +} + +export interface FlexToken extends FullToken<'Flex'> { + /** + * @nameZH 小间隙 + * @nameEN Small Gap + * @desc 控制元素的小间隙。 + * @descEN Control the small gap of the element. + */ + flexGapSM: number; + /** + * @nameZH 间隙 + * @nameEN Gap + * @desc 控制元素的间隙。 + * @descEN Control the gap of the element. + */ + flexGap: number; + /** + * @nameZH 大间隙 + * @nameEN Large Gap + * @desc 控制元素的大间隙。 + * @descEN Control the large gap of the element. + */ + flexGapLG: number; +} + +const genFlexStyle: GenerateStyle = (token) => { + const { componentCls } = token; + return { + [componentCls]: { + display: 'flex', + '&-vertical': { + flexDirection: 'column', + }, + '&-rtl': { + direction: 'rtl', + }, + '&:empty': { + display: 'none', + }, + }, + }; +}; + +const genFlexGapStyle: GenerateStyle = (token) => { + const { componentCls } = token; + return { + [componentCls]: { + '&-gap-small': { + gap: token.flexGapSM, + }, + '&-gap-middle': { + gap: token.flexGap, + }, + '&-gap-large': { + gap: token.flexGapLG, + }, + }, + }; +}; + +const genFlexWrapStyle: GenerateStyle = (token) => { + const { componentCls } = token; + const wrapStyle: CSSInterpolation = {}; + flexWrapValues.forEach((value) => { + wrapStyle[`${componentCls}-wrap-${value}`] = { flexWrap: value }; + }); + return wrapStyle; +}; + +const genAlignItemsStyle: GenerateStyle = (token) => { + const { componentCls } = token; + const alignStyle: CSSInterpolation = {}; + alignItemsValues.forEach((value) => { + alignStyle[`${componentCls}-align-${value}`] = { alignItems: value }; + }); + return alignStyle; +}; + +const genJustifyContentStyle: GenerateStyle = (token) => { + const { componentCls } = token; + const justifyStyle: CSSInterpolation = {}; + justifyContentValues.forEach((value) => { + justifyStyle[`${componentCls}-justify-${value}`] = { justifyContent: value }; + }); + return justifyStyle; +}; + +export default genComponentStyleHook<'Flex'>('Flex', (token) => { + const flexToken = mergeToken(token, { + flexGapSM: token.paddingXS, + flexGap: token.padding, + flexGapLG: token.paddingLG, + }); + return [ + genFlexStyle(flexToken), + genFlexGapStyle(flexToken), + genFlexWrapStyle(flexToken), + genAlignItemsStyle(flexToken), + genJustifyContentStyle(flexToken), + ]; +}); diff --git a/components/flex/utils.ts b/components/flex/utils.ts new file mode 100644 index 000000000000..1cd814e6979a --- /dev/null +++ b/components/flex/utils.ts @@ -0,0 +1,68 @@ +import classNames from 'classnames'; + +import type { FlexProps } from './interface'; + +export const flexWrapValues = ['wrap', 'nowrap', 'wrap-reverse'] as const; + +export const justifyContentValues = [ + 'flex-start', + 'flex-end', + 'start', + 'end', + 'center', + 'space-between', + 'space-around', + 'space-evenly', + 'stretch', + 'normal', + 'left', + 'right', +] as const; + +export const alignItemsValues = [ + 'center', + 'start', + 'end', + 'flex-start', + 'flex-end', + 'self-start', + 'self-end', + 'baseline', + 'normal', + 'stretch', +] as const; + +const genClsWrap = (prefixCls: string, props: FlexProps) => { + const wrapCls: Record = {}; + flexWrapValues.forEach((cssKey) => { + wrapCls[`${prefixCls}-wrap-${cssKey}`] = props.wrap === cssKey; + }); + return wrapCls; +}; + +const genClsAlign = (prefixCls: string, props: FlexProps) => { + const alignCls: Record = {}; + alignItemsValues.forEach((cssKey) => { + alignCls[`${prefixCls}-align-${cssKey}`] = props.align === cssKey; + }); + alignCls[`${prefixCls}-align-stretch`] = !props.align && !!props.vertical; + return alignCls; +}; + +const genClsJustify = (prefixCls: string, props: FlexProps) => { + const justifyCls: Record = {}; + justifyContentValues.forEach((cssKey) => { + justifyCls[`${prefixCls}-justify-${cssKey}`] = props.justify === cssKey; + }); + return justifyCls; +}; + +function createFlexClassNames(prefixCls: string, props: FlexProps) { + return classNames({ + ...genClsWrap(prefixCls, props), + ...genClsAlign(prefixCls, props), + ...genClsJustify(prefixCls, props), + }); +} + +export default createFlexClassNames; diff --git a/components/float-button/BackTop.tsx b/components/float-button/BackTop.tsx index 5213ee1abd22..ec4112905626 100644 --- a/components/float-button/BackTop.tsx +++ b/components/float-button/BackTop.tsx @@ -1,18 +1,20 @@ +import React, { useContext, useEffect, useState } from 'react'; import VerticalAlignTopOutlined from '@ant-design/icons/VerticalAlignTopOutlined'; import classNames from 'classnames'; import CSSMotion from 'rc-motion'; -import React, { memo, useContext, useEffect, useRef, useState } from 'react'; -import FloatButton, { floatButtonPrefixCls } from './FloatButton'; -import type { ConfigConsumerProps } from '../config-provider'; -import { ConfigContext } from '../config-provider'; +import { composeRef } from 'rc-util/lib/ref'; + import getScroll from '../_util/getScroll'; import scrollTo from '../_util/scrollTo'; import throttleByAnimationFrame from '../_util/throttleByAnimationFrame'; +import type { ConfigConsumerProps } from '../config-provider'; +import { ConfigContext } from '../config-provider'; import FloatButtonGroupContext from './context'; -import type { BackTopProps, FloatButtonProps, FloatButtonShape } from './interface'; +import FloatButton, { floatButtonPrefixCls } from './FloatButton'; +import type { BackTopProps, FloatButtonProps, FloatButtonRef, FloatButtonShape } from './interface'; import useStyle from './style'; -const BackTop: React.FC = (props) => { +const BackTop = React.forwardRef((props, ref) => { const { prefixCls: customizePrefixCls, className, @@ -28,10 +30,14 @@ const BackTop: React.FC = (props) => { const [visible, setVisible] = useState(visibilityHeight === 0); - const ref = useRef(null); + const internalRef = React.useRef(null); + + const mergedRef = composeRef(ref, internalRef); const getDefaultTarget = (): HTMLElement | Document | Window => - ref.current && ref.current.ownerDocument ? ref.current.ownerDocument : window; + internalRef.current && internalRef.current.ownerDocument + ? internalRef.current.ownerDocument + : window; const handleScroll = throttleByAnimationFrame( (e: React.UIEvent | { target: any }) => { @@ -72,7 +78,7 @@ const BackTop: React.FC = (props) => { {({ className: motionClassName }) => ( = (props) => { )} , ); -}; +}); if (process.env.NODE_ENV !== 'production') { BackTop.displayName = 'BackTop'; } -export default memo(BackTop); +export default BackTop; diff --git a/components/float-button/FloatButton.tsx b/components/float-button/FloatButton.tsx index 89c5bc6f1554..0d726e124575 100644 --- a/components/float-button/FloatButton.tsx +++ b/components/float-button/FloatButton.tsx @@ -1,28 +1,27 @@ +import React, { forwardRef, useContext, useMemo } from 'react'; import classNames from 'classnames'; import omit from 'rc-util/lib/omit'; -import React, { useContext, useMemo } from 'react'; -import warning from '../_util/warning'; + +import { devUseWarning } from '../_util/warning'; import Badge from '../badge'; import type { ConfigConsumerProps } from '../config-provider'; import { ConfigContext } from '../config-provider'; import Tooltip from '../tooltip'; -import Content from './FloatButtonContent'; import FloatButtonGroupContext from './context'; +import Content from './FloatButtonContent'; import type { CompoundedComponent, FloatButtonBadgeProps, FloatButtonContentProps, FloatButtonProps, + FloatButtonRef, FloatButtonShape, } from './interface'; import useStyle from './style'; export const floatButtonPrefixCls = 'float-btn'; -const FloatButton: React.ForwardRefRenderFunction< - HTMLAnchorElement | HTMLButtonElement, - FloatButtonProps -> = (props, ref) => { +const FloatButton = forwardRef((props, ref) => { const { prefixCls: customizePrefixCls, className, @@ -65,49 +64,49 @@ const FloatButton: React.ForwardRefRenderFunction< [prefixCls, description, icon, type], ); - const buttonNode: React.ReactNode = ( - - -

- -
- - + let buttonNode = ( +
+ +
); + if ('badge' in props) { + buttonNode = {buttonNode}; + } + + if ('tooltip' in props) { + buttonNode = ( + + {buttonNode} + + ); + } + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning('FloatButton'); + warning( !(shape === 'circle' && description), - 'FloatButton', + 'usage', 'supported only when `shape` is `square`. Due to narrow space for text, short sentence is recommended.', ); } return wrapSSR( props.href ? ( - } {...restProps} className={classString}> + {buttonNode} ) : ( - ), ); -}; +}) as CompoundedComponent; if (process.env.NODE_ENV !== 'production') { FloatButton.displayName = 'FloatButton'; } -const ForwardFloatButton = React.forwardRef< - HTMLAnchorElement | HTMLButtonElement, - FloatButtonProps ->(FloatButton) as CompoundedComponent; - -export default ForwardFloatButton; +export default FloatButton; diff --git a/components/float-button/FloatButtonGroup.tsx b/components/float-button/FloatButtonGroup.tsx index ba41255b2302..feec455fe3f8 100644 --- a/components/float-button/FloatButtonGroup.tsx +++ b/components/float-button/FloatButtonGroup.tsx @@ -1,14 +1,16 @@ -import React, { useRef, memo, useContext, useEffect, useCallback, useMemo } from 'react'; +import React, { memo, useCallback, useContext, useEffect } from 'react'; import CloseOutlined from '@ant-design/icons/CloseOutlined'; import FileTextOutlined from '@ant-design/icons/FileTextOutlined'; import classNames from 'classnames'; import CSSMotion from 'rc-motion'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; -import FloatButton, { floatButtonPrefixCls } from './FloatButton'; + +import { devUseWarning } from '../_util/warning'; import type { ConfigConsumerProps } from '../config-provider'; import { ConfigContext } from '../config-provider'; import { FloatButtonGroupProvider } from './context'; -import type { FloatButtonGroupProps } from './interface'; +import FloatButton, { floatButtonPrefixCls } from './FloatButton'; +import type { FloatButtonGroupProps, FloatButtonRef } from './interface'; import useStyle from './style'; const FloatButtonGroup: React.FC = (props) => { @@ -24,6 +26,8 @@ const FloatButtonGroup: React.FC = (props) => { trigger, children, onOpenChange, + open: customOpen, + ...floatButtonProps } = props; const { direction, getPrefixCls } = useContext(ConfigContext); @@ -39,12 +43,13 @@ const FloatButtonGroup: React.FC = (props) => { const wrapperCls = classNames(hashId, `${groupPrefixCls}-wrap`); - const [open, setOpen] = useMergedState(false, { value: props.open }); + const [open, setOpen] = useMergedState(false, { value: customOpen }); - const floatButtonGroupRef = useRef(null); - const floatButtonRef = useRef(null); + const floatButtonGroupRef = React.useRef(null); - const hoverAction = useMemo>(() => { + const floatButtonRef = React.useRef(null); + + const hoverAction = React.useMemo>(() => { const hoverTypeAction = { onMouseEnter() { setOpen(true); @@ -88,6 +93,17 @@ const FloatButtonGroup: React.FC = (props) => { } }, [trigger]); + // =================== Warning ===================== + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning('FloatButton.Group'); + + warning( + !('open' in props) || !!trigger, + 'usage', + '`open` need to be used together with `trigger`', + ); + } + return wrapSSR(
@@ -104,6 +120,8 @@ const FloatButtonGroup: React.FC = (props) => { shape={shape} icon={open ? closeIcon : icon} description={description} + aria-label={props['aria-label']} + {...floatButtonProps} /> ) : ( diff --git a/components/float-button/PurePanel.tsx b/components/float-button/PurePanel.tsx index 2732a06331fb..cba64c402421 100644 --- a/components/float-button/PurePanel.tsx +++ b/components/float-button/PurePanel.tsx @@ -1,11 +1,11 @@ /* eslint-disable react/no-array-index-key */ -import * as React from 'react'; import classNames from 'classnames'; +import * as React from 'react'; +import { ConfigContext } from '../config-provider'; +import BackTop from './BackTop'; import FloatButton, { floatButtonPrefixCls } from './FloatButton'; import FloatButtonGroup from './FloatButtonGroup'; -import BackTop from './BackTop'; -import type { FloatButtonProps, FloatButtonGroupProps } from './interface'; -import { ConfigContext } from '../config-provider'; +import type { FloatButtonGroupProps, FloatButtonProps } from './interface'; export interface PureFloatButtonProps extends Omit { backTop?: boolean; @@ -21,7 +21,8 @@ export interface PurePanelProps const PureFloatButton: React.FC = ({ backTop, ...props }) => backTop ? : ; -function PurePanel({ className, items, ...props }: PurePanelProps) { +/** @private Internal Component. Do not use in your production. */ +const PurePanel: React.FC = ({ className, items, ...props }) => { const { prefixCls: customizePrefixCls } = props; const { getPrefixCls } = React.useContext(ConfigContext); @@ -39,6 +40,6 @@ function PurePanel({ className, items, ...props }: PurePanelProps) { } return ; -} +}; -export default React.memo(PurePanel); +export default PurePanel; diff --git a/components/float-button/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/float-button/__tests__/__snapshots__/demo-extend.test.ts.snap index 3404593ff81b..68dcb4f51fcc 100644 --- a/components/float-button/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/float-button/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -2,7 +2,7 @@ exports[`renders components/float-button/demo/back-top.tsx extend context correctly 1`] = `
Scroll to bottom @@ -28,6 +28,8 @@ exports[`renders components/float-button/demo/back-top.tsx extend context correc
`; +exports[`renders components/float-button/demo/back-top.tsx extend context correctly 2`] = `[]`; + exports[`renders components/float-button/demo/badge.tsx extend context correctly 1`] = ` Array [ ,
-
- +
,
- + - 1 + + 1 + - - - 2 + + 2 + - + -
-
-
- -
, ] `; +exports[`renders components/float-button/demo/badge.tsx extend context correctly 2`] = `[]`; + exports[`renders components/float-button/demo/badge-debug.tsx extend context correctly 1`] = ` Array [
-
-
-
- -
, ] `; +exports[`renders components/float-button/demo/badge-debug.tsx extend context correctly 2`] = `[]`; + exports[`renders components/float-button/demo/basic.tsx extend context correctly 1`] = ` `; -exports[`renders components/float-button/demo/description.tsx extend context correctly 1`] = ` +exports[`renders components/float-button/demo/basic.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/float-button/demo/controlled.tsx extend context correctly 1`] = ` Array [ - + +
+ +
, + , +] +`; + +exports[`renders components/float-button/demo/controlled.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/float-button/demo/description.tsx extend context correctly 1`] = ` +Array [
+ , + , @@ -770,67 +773,48 @@ Array [ style="right: 164px;" type="button" > -
- -
- HELP -
+ + +
-
-
-
-
-
, ] `; +exports[`renders components/float-button/demo/description.tsx extend context correctly 2`] = `[]`; + exports[`renders components/float-button/demo/group.tsx extend context correctly 1`] = ` Array [
-
- + + + +
-
-
-
-
-
-
, -
- +
, +
+ + @@ -1194,63 +1052,44 @@ Array [ class="ant-float-btn ant-fade-appear ant-fade-appear-start ant-fade ant-float-btn-default ant-float-btn-square" type="button" > -
- + + +
-
-
-
-
-
, ] `; +exports[`renders components/float-button/demo/group.tsx extend context correctly 2`] = `[]`; + exports[`renders components/float-button/demo/group-menu.tsx extend context correctly 1`] = ` Array [
-
- + + +
-
-
-
-
-
, @@ -1323,63 +1141,44 @@ Array [ class="ant-float-btn ant-float-btn-primary ant-float-btn-circle" type="button" > -
- + + +
-
-
-
-
-
, ] `; +exports[`renders components/float-button/demo/group-menu.tsx extend context correctly 2`] = `[]`; + exports[`renders components/float-button/demo/render-panel.tsx extend context correctly 1`] = `
- +
+
+ + + +
+
+
+ + + +
+
- -
-
-
- -
- -
- -
-
-
- -
- -
- -
-
-
- -
- + +
-
- -
-
-
- -
- -
- -
-
-
- -
- -
- -
-
-
- -
- -
-
-
- - -
-
-
-`; - -exports[`renders components/float-button/demo/shape.tsx extend context correctly 1`] = ` -Array [ -
- -
-
-
- -
- , + +
+
+`; + +exports[`renders components/float-button/demo/render-panel.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/float-button/demo/shape.tsx extend context correctly 1`] = ` +Array [ , -] -`; - -exports[`renders components/float-button/demo/tooltip.tsx extend context correctly 1`] = ` -
- + , +] +`; + +exports[`renders components/float-button/demo/shape.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/float-button/demo/tooltip.tsx extend context correctly 1`] = ` +, , ] `; + +exports[`renders components/float-button/demo/type.tsx extend context correctly 2`] = `[]`; diff --git a/components/float-button/__tests__/__snapshots__/demo.test.ts.snap b/components/float-button/__tests__/__snapshots__/demo.test.ts.snap index 6ece9b8bd6f2..42dc22e59fa3 100644 --- a/components/float-button/__tests__/__snapshots__/demo.test.ts.snap +++ b/components/float-button/__tests__/__snapshots__/demo.test.ts.snap @@ -2,7 +2,7 @@ exports[`renders components/float-button/demo/back-top.tsx correctly 1`] = `
Scroll to bottom @@ -79,9 +79,9 @@ Array [ class="ant-float-btn-group ant-float-btn-group-circle ant-float-btn-group-circle-shadow" style="right:94px" > - + @@ -241,26 +245,28 @@ Array [ data-show="true" title="12" > - + - 1 + + 1 + - - - 2 + + 2 + - + @@ -307,36 +313,38 @@ Array [ data-show="true" title="123" > - + - 1 + + 1 + - - - 2 + + 2 + - - - 3 + + 3 + - + @@ -344,41 +352,37 @@ Array [ class="ant-float-btn ant-float-btn-default ant-float-btn-circle" type="button" > -
- + + +
-
+
, ] @@ -402,6 +406,7 @@ Array [ />
-
- + + +
-
+
`; -exports[`renders components/float-button/demo/description.tsx correctly 1`] = ` +exports[`renders components/float-button/demo/controlled.tsx correctly 1`] = ` Array [ -
- - , -
- - , -
+
+ +
, + , ] `; -exports[`renders components/float-button/demo/group.tsx correctly 1`] = ` +exports[`renders components/float-button/demo/description.tsx correctly 1`] = ` +Array [ + , + , + , +] +`; + +exports[`renders components/float-button/demo/group.tsx correctly 1`] = ` Array [
-
- + + + +
-
+
,
-
- + + + +
-
+
, ] @@ -932,41 +1046,37 @@ Array [ class="ant-float-btn ant-float-btn-primary ant-float-btn-circle" type="button" > -
- + + +
-
+
,
- +
+
+ + + +
+
+
+ +
, +] +`; + +exports[`renders components/float-button/demo/render-panel.tsx correctly 1`] = ` +
+ + + -
, -] -`; - -exports[`renders components/float-button/demo/render-panel.tsx correctly 1`] = ` -
-
+
+
+ +
-
- - -
- - -
- - + +
-
-
- - - - -
-
-
- -
-
-
- - -
-
-`; - -exports[`renders components/float-button/demo/shape.tsx correctly 1`] = ` -Array [ -
+
- - , + +
+
+`; + +exports[`renders components/float-button/demo/shape.tsx correctly 1`] = ` +Array [
, -] -`; - -exports[`renders components/float-button/demo/tooltip.tsx correctly 1`] = ` -
- + , +] +`; + +exports[`renders components/float-button/demo/tooltip.tsx correctly 1`] = ` + `; @@ -1587,88 +1642,80 @@ Array [ style="right:24px" type="button" > -
- + + + +
-
+
,
, ] `; diff --git a/components/float-button/__tests__/__snapshots__/group.test.tsx.snap b/components/float-button/__tests__/__snapshots__/group.test.tsx.snap index dd746d94a8a8..d7fafae07c7e 100644 --- a/components/float-button/__tests__/__snapshots__/group.test.tsx.snap +++ b/components/float-button/__tests__/__snapshots__/group.test.tsx.snap @@ -8,121 +8,109 @@ exports[`FloatButtonGroup should correct render 1`] = ` class="ant-float-btn ant-float-btn-default ant-float-btn-circle" type="button" > -
- + + +
-
+
`; diff --git a/components/float-button/__tests__/__snapshots__/index.test.tsx.snap b/components/float-button/__tests__/__snapshots__/index.test.tsx.snap index 4da826f05d5b..c39b4835284d 100644 --- a/components/float-button/__tests__/__snapshots__/index.test.tsx.snap +++ b/components/float-button/__tests__/__snapshots__/index.test.tsx.snap @@ -5,41 +5,37 @@ exports[`FloatButton rtl render component should be rendered correctly in RTL di class="ant-float-btn ant-float-btn-default ant-float-btn-circle ant-float-btn-rtl" type="button" > -
- + + +
-
+
`; @@ -48,40 +44,36 @@ exports[`FloatButton should correct render 1`] = ` class="ant-float-btn ant-float-btn-default ant-float-btn-circle" type="button" > -
- + + +
-
+
`; diff --git a/components/float-button/__tests__/back-top.test.tsx b/components/float-button/__tests__/back-top.test.tsx index 251ddd9dec3c..6dc737c83765 100644 --- a/components/float-button/__tests__/back-top.test.tsx +++ b/components/float-button/__tests__/back-top.test.tsx @@ -1,10 +1,12 @@ import React from 'react'; + import FloatButton from '..'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; import { fireEvent, render, waitFakeTimer } from '../../../tests/utils'; const { BackTop } = FloatButton; + describe('BackTop', () => { beforeEach(() => { jest.useFakeTimers(); @@ -51,4 +53,11 @@ describe('BackTop', () => { const { container } = render(); expect(container.querySelector('.ant-float-btn')?.style.color).toBe('red'); }); + + it('no error when BackTop work', () => { + const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + render(); + expect(errSpy).not.toHaveBeenCalled(); + errSpy.mockRestore(); + }); }); diff --git a/components/float-button/__tests__/group.test.tsx b/components/float-button/__tests__/group.test.tsx index 2458a146e9fe..3a62049b179d 100644 --- a/components/float-button/__tests__/group.test.tsx +++ b/components/float-button/__tests__/group.test.tsx @@ -84,4 +84,39 @@ describe('FloatButtonGroup', () => { fireEvent.click(container); expect(onOpenChange).toHaveBeenCalledTimes(2); }); + + it('warning if set `open` but not set `trigger`', () => { + const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + render( + + + + , + ); + + expect(warnSpy).not.toHaveBeenCalled(); + + render( + + + + , + ); + expect(warnSpy).toHaveBeenCalledWith( + 'Warning: [antd: FloatButton.Group] `open` need to be used together with `trigger`', + ); + warnSpy.mockRestore(); + }); + + it('menu should support badge', () => { + const { container } = render( + + + + , + ); + + expect(container.querySelector('.ant-badge')).toBeTruthy(); + }); }); diff --git a/components/float-button/__tests__/image.test.ts b/components/float-button/__tests__/image.test.ts index ef9b55aa6e9a..8f00fb5d62c3 100644 --- a/components/float-button/__tests__/image.test.ts +++ b/components/float-button/__tests__/image.test.ts @@ -1,5 +1,5 @@ import { imageDemoTest } from '../../../tests/shared/imageTest'; describe('float-button image', () => { - imageDemoTest('float-button'); + imageDemoTest('float-button', { splitTheme: true, onlyViewport: ['back-top.tsx'] }); }); diff --git a/components/float-button/demo/back-top.tsx b/components/float-button/demo/back-top.tsx index f359f93359c0..29b5958920ab 100644 --- a/components/float-button/demo/back-top.tsx +++ b/components/float-button/demo/back-top.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { FloatButton } from 'antd'; const App: React.FC = () => ( -
+
Scroll to bottom
Scroll to bottom
Scroll to bottom
diff --git a/components/float-button/demo/badge-debug.tsx b/components/float-button/demo/badge-debug.tsx index 8b17aa56c40a..354995069cea 100644 --- a/components/float-button/demo/badge-debug.tsx +++ b/components/float-button/demo/badge-debug.tsx @@ -1,6 +1,6 @@ +import React, { useState } from 'react'; import { ConfigProvider, FloatButton, Slider } from 'antd'; import type { AliasToken } from 'antd/es/theme/interface'; -import React, { useState } from 'react'; const App: React.FC = () => { const [radius, setRadius] = useState(0); diff --git a/components/float-button/demo/badge.tsx b/components/float-button/demo/badge.tsx index 63437d2746c1..00ae514332b9 100644 --- a/components/float-button/demo/badge.tsx +++ b/components/float-button/demo/badge.tsx @@ -1,12 +1,16 @@ import { QuestionCircleOutlined } from '@ant-design/icons'; -import { FloatButton } from 'antd'; import React from 'react'; +import { FloatButton } from 'antd'; const App: React.FC = () => ( <> - custom badge color
} badge={{ count: 5, color: 'blue' }} /> + custom badge color
} + badge={{ count: 5, color: 'blue' }} + /> diff --git a/components/float-button/demo/controlled.md b/components/float-button/demo/controlled.md new file mode 100644 index 000000000000..ac17a7d1639b --- /dev/null +++ b/components/float-button/demo/controlled.md @@ -0,0 +1,7 @@ +## zh-CN + +通过 `open` 设置组件为受控模式,需要配合 trigger 一起使用。 + +## en-US + +Set the component to controlled mode through `open`, which need to be used together with trigger. diff --git a/components/float-button/demo/controlled.tsx b/components/float-button/demo/controlled.tsx new file mode 100644 index 000000000000..90681b604bb5 --- /dev/null +++ b/components/float-button/demo/controlled.tsx @@ -0,0 +1,28 @@ +import { CommentOutlined, CustomerServiceOutlined } from '@ant-design/icons'; +import React, { useState } from 'react'; +import { FloatButton, Switch } from 'antd'; + +const App: React.FC = () => { + const [open, setOpen] = useState(true); + + const onChange = (checked: boolean) => { + setOpen(checked); + }; + + return ( + <> + } + > + + } /> + + + + ); +}; + +export default App; diff --git a/components/float-button/demo/description.tsx b/components/float-button/demo/description.tsx index 00b719b77137..a0f84740494b 100644 --- a/components/float-button/demo/description.tsx +++ b/components/float-button/demo/description.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { FloatButton } from 'antd'; import { FileTextOutlined } from '@ant-design/icons'; +import { FloatButton } from 'antd'; const App: React.FC = () => ( <> diff --git a/components/float-button/demo/group-menu.tsx b/components/float-button/demo/group-menu.tsx index 7954df99f899..1804b11595d7 100644 --- a/components/float-button/demo/group-menu.tsx +++ b/components/float-button/demo/group-menu.tsx @@ -1,6 +1,6 @@ +import { CommentOutlined, CustomerServiceOutlined } from '@ant-design/icons'; import React from 'react'; import { FloatButton } from 'antd'; -import { CustomerServiceOutlined, CommentOutlined } from '@ant-design/icons'; const App: React.FC = () => ( <> diff --git a/components/float-button/demo/group.tsx b/components/float-button/demo/group.tsx index 7b16979e4627..60c8cd53e860 100644 --- a/components/float-button/demo/group.tsx +++ b/components/float-button/demo/group.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { FloatButton } from 'antd'; import { QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons'; +import { FloatButton } from 'antd'; const App: React.FC = () => ( <> diff --git a/components/float-button/demo/render-panel.tsx b/components/float-button/demo/render-panel.tsx index 84edf89ff8ba..175f425f49e5 100644 --- a/components/float-button/demo/render-panel.tsx +++ b/components/float-button/demo/render-panel.tsx @@ -1,6 +1,6 @@ import { CustomerServiceOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons'; -import { FloatButton } from 'antd'; import React from 'react'; +import { FloatButton } from 'antd'; /** Test usage. Do not use in your production. */ const { _InternalPanelDoNotUseOrYouWillBeFired: InternalFloatButton } = FloatButton; diff --git a/components/float-button/demo/shape.tsx b/components/float-button/demo/shape.tsx index 1eaac27a94a0..6fc6f725fd0e 100644 --- a/components/float-button/demo/shape.tsx +++ b/components/float-button/demo/shape.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { FloatButton } from 'antd'; import { CustomerServiceOutlined } from '@ant-design/icons'; +import { FloatButton } from 'antd'; const App: React.FC = () => ( <> diff --git a/components/float-button/demo/type.tsx b/components/float-button/demo/type.tsx index 888c12e47078..58f7292944f1 100644 --- a/components/float-button/demo/type.tsx +++ b/components/float-button/demo/type.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { FloatButton } from 'antd'; import { QuestionCircleOutlined } from '@ant-design/icons'; +import { FloatButton } from 'antd'; const App: React.FC = () => ( <> diff --git a/components/float-button/index.en-US.md b/components/float-button/index.en-US.md index 9ac546a7bb60..9d4430477500 100644 --- a/components/float-button/index.en-US.md +++ b/components/float-button/index.en-US.md @@ -1,11 +1,12 @@ --- category: Components -group: Other +group: General title: FloatButton cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*HS-wTIIwu0kAAAAAAAAAAAAADrJ8AQ/original coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a0hwTY_rOSUAAAAAAAAAAAAADrJ8AQ/original demo: cols: 2 +tag: New --- FloatButton. Available since `5.0.0`. @@ -25,6 +26,7 @@ FloatButton. Available since `5.0.0`. FloatButton with tooltip FloatButton Group Menu mode +Controlled mode BackTop badge debug dot @@ -32,6 +34,8 @@ FloatButton. Available since `5.0.0`. ## API +Common props ref:[Common props](/docs/react/common-props) + > This component is available since `antd@5.0.0`. ### common API @@ -54,8 +58,8 @@ FloatButton. Available since `5.0.0`. | --- | --- | --- | --- | --- | | shape | Setting button shape of children | `circle` \| `square` | `circle` | | | trigger | Which action can trigger menu open/close | `click` \| `hover` | - | | -| open | Whether the menu is visible or not | boolean | - | | -| onOpenChange | Callback executed when active menu is changed | (open: boolean) => void | - | | +| open | Whether the menu is visible or not, use it with trigger | boolean | - | | +| onOpenChange | Callback executed when active menu is changed, use it with trigger | (open: boolean) => void | - | | ### FloatButton.BackTop diff --git a/components/float-button/index.ts b/components/float-button/index.ts index d3f6bdccb8a3..663dda19f0c8 100644 --- a/components/float-button/index.ts +++ b/components/float-button/index.ts @@ -1,6 +1,6 @@ +import BackTop from './BackTop'; import FloatButton from './FloatButton'; import FloatButtonGroup from './FloatButtonGroup'; -import BackTop from './BackTop'; import PurePanel from './PurePanel'; FloatButton.BackTop = BackTop; diff --git a/components/float-button/index.zh-CN.md b/components/float-button/index.zh-CN.md index dfa41a178d79..3b9efdc5e8d5 100644 --- a/components/float-button/index.zh-CN.md +++ b/components/float-button/index.zh-CN.md @@ -1,12 +1,13 @@ --- category: Components -group: 其他 +group: 通用 subtitle: 悬浮按钮 title: FloatButton cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*HS-wTIIwu0kAAAAAAAAAAAAADrJ8AQ/original coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a0hwTY_rOSUAAAAAAAAAAAAADrJ8AQ/original demo: cols: 2 +tag: New --- 悬浮按钮。自 `5.0.0` 版本开始提供该组件。 @@ -26,6 +27,7 @@ demo: 含有气泡卡片的悬浮按钮 浮动按钮组 菜单模式 +受控模式 回到顶部 徽标数 调试小圆点使用 @@ -33,6 +35,8 @@ demo: ## API +通用属性参考:[通用属性](/docs/react/common-props) + > 自 `antd@5.0.0` 版本开始提供该组件。 ### 共同的 API @@ -51,12 +55,12 @@ demo: ### FloatButton.Group -| 参数 | 说明 | 类型 | 默认值 | 版本 | -| ------------ | -------------------------------- | ----------------------- | -------- | ---- | -| shape | 设置包含的 FloatButton 按钮形状 | `circle` \| `square` | `circle` | | -| trigger | 触发方式(有触发方式为菜单模式) | `click` \| `hover` | - | | -| open | 受控展开 | boolean | - | | -| onOpenChange | 展开收起时的回调 | (open: boolean) => void | - | | +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| --- | --- | --- | --- | --- | +| shape | 设置包含的 FloatButton 按钮形状 | `circle` \| `square` | `circle` | | +| trigger | 触发方式(有触发方式为菜单模式) | `click` \| `hover` | - | | +| open | 受控展开,需配合 trigger 一起使用 | boolean | - | | +| onOpenChange | 展开收起时的回调,需配合 trigger 一起使用 | (open: boolean) => void | - | | ### FloatButton.BackTop @@ -67,6 +71,6 @@ demo: | visibilityHeight | 滚动高度达到此参数值才出现 BackTop | number | 400 | | | onClick | 点击按钮的回调函数 | () => void | - | | -## Design Token +## 主题变量(Design Token) diff --git a/components/float-button/interface.ts b/components/float-button/interface.ts index 18fa95766c54..42dcfd1ce6bd 100644 --- a/components/float-button/interface.ts +++ b/components/float-button/interface.ts @@ -1,10 +1,15 @@ import type React from 'react'; + import type { BadgeProps } from '../badge'; import type { TooltipProps } from '../tooltip'; import type BackTop from './BackTop'; import type Group from './FloatButtonGroup'; import type PurePanel from './PurePanel'; +export interface FloatButtonRef { + nativeElement: HTMLAnchorElement & HTMLButtonElement; +} + export type FloatButtonType = 'default' | 'primary'; export type FloatButtonShape = 'circle' | 'square'; @@ -27,6 +32,7 @@ export interface FloatButtonProps { target?: React.HTMLAttributeAnchorTarget; badge?: FloatButtonBadgeProps; onClick?: React.MouseEventHandler; + ['aria-label']?: React.HtmlHTMLAttributes['aria-label']; } export interface FloatButtonContentProps extends React.DOMAttributes { @@ -62,7 +68,7 @@ export interface BackTopProps extends Omit { } export type CompoundedComponent = React.ForwardRefExoticComponent< - FloatButtonProps & React.RefAttributes + FloatButtonProps & React.RefAttributes > & { Group: typeof Group; BackTop: typeof BackTop; diff --git a/components/float-button/style/index.ts b/components/float-button/style/index.ts index a0c031f5998c..16b8d468d290 100644 --- a/components/float-button/style/index.ts +++ b/components/float-button/style/index.ts @@ -220,9 +220,10 @@ const sharedFloatButtonStyle: GenerateStyle = (toke position: 'fixed', cursor: 'pointer', zIndex: 99, + // Do not remove the 'display: block' here. + // Deleting it will cause marginBottom to become ineffective. + // Ref: https://github.com/ant-design/ant-design/issues/44700 display: 'block', - justifyContent: 'center', - alignItems: 'center', width: floatButtonSize, height: floatButtonSize, insetInlineEnd: token.floatButtonInsetInlineEnd, diff --git a/components/form/ErrorList.tsx b/components/form/ErrorList.tsx index 8893e9375bdc..4ab2f8bc027c 100644 --- a/components/form/ErrorList.tsx +++ b/components/form/ErrorList.tsx @@ -1,4 +1,5 @@ import classNames from 'classnames'; +import type { CSSMotionProps } from 'rc-motion'; import CSSMotion, { CSSMotionList } from 'rc-motion'; import * as React from 'react'; import { useMemo } from 'react'; @@ -40,7 +41,7 @@ export interface ErrorListProps { onVisibleChanged?: (visible: boolean) => void; } -export default function ErrorList({ +const ErrorList: React.FC = ({ help, helpStatus, errors = EMPTY_LIST, @@ -48,14 +49,14 @@ export default function ErrorList({ className: rootClassName, fieldId, onVisibleChanged, -}: ErrorListProps) { +}) => { const { prefixCls } = React.useContext(FormItemPrefixContext); const baseClassName = `${prefixCls}-item-explain`; const [, hashId] = useStyle(prefixCls); - const collapseMotion = useMemo(() => initCollapseMotion(prefixCls), [prefixCls]); + const collapseMotion: CSSMotionProps = useMemo(() => initCollapseMotion(prefixCls), [prefixCls]); // We have to debounce here again since somewhere use ErrorList directly still need no shaking // ref: https://github.com/ant-design/ant-design/issues/36336 @@ -131,4 +132,6 @@ export default function ErrorList({ }} ); -} +}; + +export default ErrorList; diff --git a/components/form/Form.tsx b/components/form/Form.tsx index f52410f5a142..f0e3743e103b 100644 --- a/components/form/Form.tsx +++ b/components/form/Form.tsx @@ -12,13 +12,18 @@ import { SizeContextProvider } from '../config-provider/SizeContext'; import useSize from '../config-provider/hooks/useSize'; import type { ColProps } from '../grid/col'; import type { FormContextProps } from './context'; -import { FormContext, FormProvider, ValidateMessagesContext } from './context'; +import { FormContext, FormProvider } from './context'; import useForm, { type FormInstance } from './hooks/useForm'; import useFormWarning from './hooks/useFormWarning'; import type { FormLabelAlign } from './interface'; import useStyle from './style'; +import ValidateMessagesContext from './validateMessagesContext'; +import type { FeedbackIcons } from './FormItem'; -export type RequiredMark = boolean | 'optional'; +export type RequiredMark = + | boolean + | 'optional' + | ((labelNode: React.ReactNode, info: { required: boolean }) => React.ReactNode); export type FormLayout = 'horizontal' | 'inline' | 'vertical'; export interface FormProps extends Omit, 'form'> { @@ -31,6 +36,7 @@ export interface FormProps extends Omit, 'form labelCol?: ColProps; wrapperCol?: ColProps; form?: FormInstance; + feedbackIcons?: FeedbackIcons; size?: SizeType; disabled?: boolean; scrollToFirstError?: Options | boolean; @@ -62,6 +68,8 @@ const InternalForm: React.ForwardRefRenderFunction = (p requiredMark, onFinishFailed, name, + style, + feedbackIcons, ...restFormProps } = props; @@ -99,13 +107,14 @@ const InternalForm: React.ForwardRefRenderFunction = (p const formClassName = classNames( prefixCls, + `${prefixCls}-${layout}`, { - [`${prefixCls}-${layout}`]: true, [`${prefixCls}-hide-required-mark`]: mergedRequiredMark === false, [`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-${mergedSize}`]: mergedSize, }, hashId, + contextForm?.className, className, rootClassName, ); @@ -126,8 +135,19 @@ const InternalForm: React.ForwardRefRenderFunction = (p requiredMark: mergedRequiredMark, itemRef: __INTERNAL__.itemRef, form: wrapForm, + feedbackIcons, }), - [name, labelAlign, labelCol, wrapperCol, layout, mergedColon, mergedRequiredMark, wrapForm], + [ + name, + labelAlign, + labelCol, + wrapperCol, + layout, + mergedColon, + mergedRequiredMark, + wrapForm, + feedbackIcons, + ], ); React.useImperativeHandle(ref, () => wrapForm); @@ -173,6 +193,7 @@ const InternalForm: React.ForwardRefRenderFunction = (p name={name} onFinishFailed={onInternalFinishFailed} form={wrapForm} + style={{ ...contextForm?.style, ...style }} className={formClassName} /> @@ -182,10 +203,14 @@ const InternalForm: React.ForwardRefRenderFunction = (p ); }; -const Form = React.forwardRef(InternalForm) as ( +const Form = React.forwardRef(InternalForm) as (( props: React.PropsWithChildren> & { ref?: React.Ref> }, -) => React.ReactElement; +) => React.ReactElement) & { displayName?: string }; + +if (process.env.NODE_ENV !== 'production') { + Form.displayName = 'Form'; +} -export { useForm, List, type FormInstance, useWatch }; +export { List, useForm, useWatch, type FormInstance }; export default Form; diff --git a/components/form/FormItem/ItemHolder.tsx b/components/form/FormItem/ItemHolder.tsx index d338594325cb..928a0b490703 100644 --- a/components/form/FormItem/ItemHolder.tsx +++ b/components/form/FormItem/ItemHolder.tsx @@ -1,27 +1,19 @@ -import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled'; -import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled'; -import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled'; -import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; +import * as React from 'react'; import classNames from 'classnames'; import type { Meta } from 'rc-field-form/lib/interface'; -import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; import isVisible from 'rc-util/lib/Dom/isVisible'; +import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; import omit from 'rc-util/lib/omit'; -import * as React from 'react'; -import type { FormItemProps, ValidateStatus } from '.'; + +import type { FormItemProps } from '.'; import { Row } from '../../grid'; +import type { ReportMetaChange } from '../context'; +import { FormContext, NoStyleItemContext } from '../context'; import FormItemInput from '../FormItemInput'; import FormItemLabel from '../FormItemLabel'; -import type { FormItemStatusContextProps, ReportMetaChange } from '../context'; -import { FormContext, FormItemInputContext, NoStyleItemContext } from '../context'; import useDebounce from '../hooks/useDebounce'; - -const iconMap = { - success: CheckCircleFilled, - warning: ExclamationCircleFilled, - error: CloseCircleFilled, - validating: LoadingOutlined, -}; +import { getStatus } from '../util'; +import StatusProvider from './StatusProvider'; export interface ItemHolderProps extends FormItemProps { prefixCls: string; @@ -88,52 +80,14 @@ export default function ItemHolder(props: ItemHolderProps) { // ======================== Status ======================== const getValidateState = (isDebounce = false) => { - let status: ValidateStatus = ''; const _errors = isDebounce ? debounceErrors : meta.errors; const _warnings = isDebounce ? debounceWarnings : meta.warnings; - if (validateStatus !== undefined) { - status = validateStatus; - } else if (meta.validating) { - status = 'validating'; - } else if (_errors.length) { - status = 'error'; - } else if (_warnings.length) { - status = 'warning'; - } else if (meta.touched || (hasFeedback && meta.validated)) { - // success feedback should display when pass hasFeedback prop and current value is valid value - status = 'success'; - } - return status; + + return getStatus(_errors, _warnings, meta, '', !!hasFeedback, validateStatus); }; const mergedValidateStatus = getValidateState(); - const formItemStatusContext = React.useMemo(() => { - let feedbackIcon: React.ReactNode; - if (hasFeedback) { - const IconNode = mergedValidateStatus && iconMap[mergedValidateStatus]; - feedbackIcon = IconNode ? ( - - - - ) : null; - } - - return { - status: mergedValidateStatus, - errors, - warnings, - hasFeedback, - feedbackIcon, - isFormItemInput: true, - }; - }, [mergedValidateStatus, hasFeedback]); - // ======================== Render ======================== const itemClassName = classNames(itemPrefixCls, className, rootClassName, { [`${itemPrefixCls}-with-help`]: hasHelp || debounceErrors.length || debounceWarnings.length, @@ -181,6 +135,7 @@ export default function ItemHolder(props: ItemHolderProps) { 'validateTrigger', 'valuePropName', 'wrapperCol', + 'validateDebounce', ])} > {/* Label */} @@ -204,9 +159,17 @@ export default function ItemHolder(props: ItemHolderProps) { onErrorVisibleChanged={onErrorVisibleChanged} > - + {children} - + diff --git a/components/form/FormItem/StatusProvider.tsx b/components/form/FormItem/StatusProvider.tsx new file mode 100644 index 000000000000..ca9c79b413eb --- /dev/null +++ b/components/form/FormItem/StatusProvider.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; +import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled'; +import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled'; +import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled'; +import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; +import classNames from 'classnames'; +import type { Meta } from 'rc-field-form/lib/interface'; + +import type { FeedbackIcons, ValidateStatus } from '.'; +import { FormContext, FormItemInputContext, type FormItemStatusContextProps } from '../context'; +import { getStatus } from '../util'; + +const iconMap = { + success: CheckCircleFilled, + warning: ExclamationCircleFilled, + error: CloseCircleFilled, + validating: LoadingOutlined, +}; + +export interface StatusProviderProps { + children?: React.ReactNode; + validateStatus?: ValidateStatus; + prefixCls: string; + meta: Meta; + errors: React.ReactNode[]; + warnings: React.ReactNode[]; + hasFeedback?: boolean | { icons?: FeedbackIcons }; + noStyle?: boolean; +} + +export default function StatusProvider({ + children, + errors, + warnings, + hasFeedback, + validateStatus, + prefixCls, + meta, + noStyle, +}: StatusProviderProps) { + const itemPrefixCls = `${prefixCls}-item`; + const { feedbackIcons } = React.useContext(FormContext); + + const mergedValidateStatus = getStatus( + errors, + warnings, + meta, + null, + !!hasFeedback, + validateStatus, + ); + + const { + isFormItemInput: parentIsFormItemInput, + status: parentStatus, + hasFeedback: parentHasFeedback, + feedbackIcon: parentFeedbackIcon, + } = React.useContext(FormItemInputContext); + + // ====================== Context ======================= + const formItemStatusContext = React.useMemo(() => { + let feedbackIcon: React.ReactNode; + if (hasFeedback) { + const customIcons = (hasFeedback !== true && hasFeedback.icons) || feedbackIcons; + const customIconNode = + mergedValidateStatus && + customIcons?.({ status: mergedValidateStatus, errors, warnings })?.[mergedValidateStatus]; + const IconNode = mergedValidateStatus && iconMap[mergedValidateStatus]; + feedbackIcon = + customIconNode !== false && IconNode ? ( + + {customIconNode || } + + ) : null; + } + + const context: FormItemStatusContextProps = { + status: mergedValidateStatus || '', + errors, + warnings, + hasFeedback: !!hasFeedback, + feedbackIcon, + isFormItemInput: true, + }; + + // No style will follow parent context + if (noStyle) { + context.status = (mergedValidateStatus ?? parentStatus) || ''; + context.isFormItemInput = parentIsFormItemInput; + context.hasFeedback = !!(hasFeedback ?? parentHasFeedback); + context.feedbackIcon = hasFeedback !== undefined ? context.feedbackIcon : parentFeedbackIcon; + } + + return context; + }, [mergedValidateStatus, hasFeedback, noStyle, parentIsFormItemInput, parentStatus]); + + // ======================= Render ======================= + return ( + + {children} + + ); +} diff --git a/components/form/FormItem/index.tsx b/components/form/FormItem/index.tsx index 68127ed50545..3009518dfedd 100644 --- a/components/form/FormItem/index.tsx +++ b/components/form/FormItem/index.tsx @@ -1,25 +1,26 @@ +import * as React from 'react'; import classNames from 'classnames'; -import type { FormInstance } from 'rc-field-form'; import { Field, FieldContext, ListContext } from 'rc-field-form'; import type { FieldProps } from 'rc-field-form/lib/Field'; -import type { Meta, NamePath } from 'rc-field-form/lib/interface'; +import type { InternalNamePath, Meta } from 'rc-field-form/lib/interface'; import useState from 'rc-util/lib/hooks/useState'; import { supportRef } from 'rc-util/lib/ref'; -import * as React from 'react'; + import { cloneElement, isValidElement } from '../../_util/reactNode'; -import warning from '../../_util/warning'; +import { devUseWarning } from '../../_util/warning'; import { ConfigContext } from '../../config-provider'; +import { FormContext, NoStyleItemContext } from '../context'; +import type { FormInstance } from '../Form'; import type { FormItemInputProps } from '../FormItemInput'; import type { FormItemLabelProps, LabelTooltipType } from '../FormItemLabel'; -import { FormContext, NoStyleItemContext } from '../context'; +import useChildren from '../hooks/useChildren'; import useFormItemStatus from '../hooks/useFormItemStatus'; import useFrameState from '../hooks/useFrameState'; import useItemRef from '../hooks/useItemRef'; +import useStyle from '../style'; import { getFieldId, toArray } from '../util'; import ItemHolder from './ItemHolder'; - -import useChildren from '../hooks/useChildren'; -import useStyle from '../style'; +import StatusProvider from './StatusProvider'; const NAME_SPLIT = '__SPLIT__'; @@ -35,6 +36,12 @@ type RenderChildren = (form: FormInstance) => React.ReactN type RcFieldProps = Omit, 'children'>; type ChildrenType = RenderChildren | React.ReactNode; +export type FeedbackIcons = (itemStatus: { + status: ValidateStatus; + errors?: React.ReactNode[]; + warnings?: React.ReactNode[]; +}) => { [key in ValidateStatus]?: React.ReactNode }; + interface MemoInputProps { value: any; update: any; @@ -62,7 +69,7 @@ export interface FormItemProps rootClassName?: string; children?: ChildrenType; id?: string; - hasFeedback?: boolean; + hasFeedback?: boolean | { icons: FeedbackIcons }; validateStatus?: ValidateStatus; required?: boolean; hidden?: boolean; @@ -73,13 +80,6 @@ export interface FormItemProps fieldKey?: React.Key | React.Key[]; } -function hasValidName(name?: NamePath): Boolean { - if (name === null) { - warning(false, 'Form.Item', '`null` is passed as `name` property'); - } - return !(name === undefined || name === null); -} - function genEmptyMeta(): Meta { return { errors: [], @@ -121,17 +121,24 @@ function InternalFormItem(props: FormItemProps): React.Rea const mergedValidateTrigger = validateTrigger !== undefined ? validateTrigger : contextValidateTrigger; - const hasName = hasValidName(name); + const hasName = !(name === undefined || name === null); const prefixCls = getPrefixCls('form', customizePrefixCls); // Style const [wrapSSR, hashId] = useStyle(prefixCls); + // ========================= Warn ========================= + const warning = devUseWarning('Form.Item'); + + if (process.env.NODE_ENV !== 'production') { + warning(name !== null, 'usage', '`null` is passed as `name` property'); + } + // ========================= MISC ========================= // Get `noStyle` required info const listContext = React.useContext(ListContext); - const fieldKeyPathRef = React.useRef(); + const fieldKeyPathRef = React.useRef(); // ======================== Errors ======================== // >>>>> Collect sub field errors @@ -214,7 +221,19 @@ function InternalFormItem(props: FormItemProps): React.Rea isRequired?: boolean, ): React.ReactNode { if (noStyle && !hidden) { - return baseChildren; + return ( + + {baseChildren} + + ); } return ( @@ -258,7 +277,7 @@ function InternalFormItem(props: FormItemProps): React.Rea validateTrigger={mergedValidateTrigger} onMetaChange={onMetaChange} > - {(control, renderMeta, context) => { + {(control, renderMeta, context: FormInstance) => { const mergedName = toArray(name).length && renderMeta ? renderMeta.name : []; const fieldId = getFieldId(mergedName, formName); @@ -288,37 +307,37 @@ function InternalFormItem(props: FormItemProps): React.Rea warning( !(shouldUpdate && dependencies), - 'Form.Item', + 'usage', "`shouldUpdate` and `dependencies` shouldn't be used together. See https://u.ant.design/form-deps.", ); if (Array.isArray(mergedChildren) && hasName) { warning( false, - 'Form.Item', + 'usage', 'A `Form.Item` with a `name` prop must have a single child element. For information on how to render more complex form items, see https://u.ant.design/complex-form-item.', ); childNode = mergedChildren; } else if (isRenderProps && (!(shouldUpdate || dependencies) || hasName)) { warning( !!(shouldUpdate || dependencies), - 'Form.Item', + 'usage', 'A `Form.Item` with a render function must have either `shouldUpdate` or `dependencies`.', ); warning( !hasName, - 'Form.Item', + 'usage', 'A `Form.Item` with a render function cannot be a field, and thus cannot have a `name` prop.', ); } else if (dependencies && !isRenderProps && !hasName) { warning( false, - 'Form.Item', + 'usage', 'Must set `name` or use a render function when `dependencies` is set.', ); } else if (isValidElement(mergedChildren)) { warning( mergedChildren.props.defaultValue === undefined, - 'Form.Item', + 'usage', '`defaultValue` will not work on controlled Field. You should use `initialValues` of Form instead.', ); @@ -384,7 +403,7 @@ function InternalFormItem(props: FormItemProps): React.Rea } else { warning( !mergedName.length, - 'Form.Item', + 'usage', '`name` is only used for validate React element. If you are using Form.Item as layout display, please remove `name` instead.', ); childNode = mergedChildren as React.ReactNode; diff --git a/components/form/FormItemInput.tsx b/components/form/FormItemInput.tsx index c81a5ce4da97..741a86d92061 100644 --- a/components/form/FormItemInput.tsx +++ b/components/form/FormItemInput.tsx @@ -1,10 +1,12 @@ -import classNames from 'classnames'; import * as React from 'react'; +import classNames from 'classnames'; + import type { ColProps } from '../grid/col'; import Col from '../grid/col'; import { FormContext, FormItemPrefixContext } from './context'; import ErrorList from './ErrorList'; import type { ValidateStatus } from './FormItem'; +import FallbackCmp from './style/fallbackCmp'; interface FormItemInputMiscProps { prefixCls: string; @@ -63,13 +65,13 @@ const FormItemInput: React.FC = (pr delete subFormContext.labelCol; delete subFormContext.wrapperCol; - const inputDom = ( + const inputDom: React.ReactNode = (
{children}
); const formItemContext = React.useMemo(() => ({ prefixCls, status }), [prefixCls, status]); - const errorListDom = + const errorListDom: React.ReactNode = marginBottom !== null || errors.length || warnings.length ? (
@@ -95,13 +97,13 @@ const FormItemInput: React.FC = (pr // If extra = 0, && will goes wrong // 0&&error -> 0 - const extraDom = extra ? ( + const extraDom: React.ReactNode = extra ? (
{extra}
) : null; - const dom = + const dom: React.ReactNode = formItemRender && formItemRender.mark === 'pro_table_render' && formItemRender.render ? ( formItemRender.render(props, { input: inputDom, errorList: errorListDom, extra: extraDom }) ) : ( @@ -116,6 +118,7 @@ const FormItemInput: React.FC = (pr {dom} + ); }; diff --git a/components/form/FormItemLabel.tsx b/components/form/FormItemLabel.tsx index a8f4dace0edd..8d53fff2f317 100644 --- a/components/form/FormItemLabel.tsx +++ b/components/form/FormItemLabel.tsx @@ -84,7 +84,7 @@ const FormItemLabel: React.FC, ...restTooltipProps } = tooltipProps; - const tooltipNode = ( + const tooltipNode: React.ReactNode = ( {React.cloneElement(icon, { className: `${prefixCls}-item-tooltip`, title: '' })} @@ -114,7 +114,13 @@ const FormItemLabel: React.FC {labelChildren} @@ -127,7 +133,7 @@ const FormItemLabel: React.FC = ({ children, ...props }) => { - warning(!!props.name, 'Form.List', 'Miss `name` prop.'); + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning('Form.List'); + + warning( + typeof props.name === 'number' || + (Array.isArray(props.name) ? !!props.name.length : !!props.name), + 'usage', + 'Miss `name` prop.', + ); + } const { getPrefixCls } = React.useContext(ConfigContext); const prefixCls = getPrefixCls('form', customizePrefixCls); diff --git a/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap index e4cf6308e731..87167b41b368 100644 --- a/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -96,7 +96,6 @@ Array [ class="ant-select-selection-search" >
+
+
+
+
+
+ +`; + +exports[`renders components/form/demo/custom-feedback-icons.tsx extend context correctly 2`] = `[]`; + exports[`renders components/form/demo/customized-form-controls.tsx extend context correctly 1`] = `
`; -exports[`renders components/form/demo/disabled.tsx extend context correctly 1`] = ` -Array [ -
+
-
- -
+ Password + +
+
-
- -
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+

+ Only Update when + + password2 + + updated: +

+
+      {}
+    
+
+ +`; + +exports[`renders components/form/demo/dependencies.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/form/demo/disabled.tsx extend context correctly 1`] = ` +Array [ + , +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
@@ -5161,14 +5473,11 @@ Array [ viewBox="64 64 896 896" width="1em" > - - -### `scrollToFirstError` and `scrollToField` not working on custom form control? +### `scrollToFirstError` and `scrollToField` not working? + +1. use custom form control See similar issues: [#28370](https://github.com/ant-design/ant-design/issues/28370) [#27994](https://github.com/ant-design/ant-design/issues/27994) `scrollToFirstError` and `scrollToField` deps on `id` attribute passed to form control, please make sure that it hasn't been ignored in your custom form control. Check [codesandbox](https://codesandbox.io/s/antd-reproduction-template-forked-25nul?file=/index.js) for solution. +2. multiple forms on same page + +If there are multiple forms on the page, and there are duplicate same `name` form item, the form scroll probably may find the form item with the same name in another form. You need to set a different `name` for the `Form` component to distinguish it. + ### Continue, why not use `ref` to bind element? Form can not get real DOM node when customize component not support `ref`. It will get warning in React Strict Mode if wrap with Class Component and call `findDOMNode`. So we use `id` to locate element. @@ -601,3 +697,31 @@ Form can not get real DOM node when customize component not support `ref`. It wi ### `setFieldsValue` do not trigger `onFieldsChange` or `onValuesChange`? It's by design. Only user interactive can trigger the change event. This design is aim to avoid call `setFieldsValue` in change event which may makes loop calling. + +### Why Form.Item not update value when children is nest? + +Form.Item will inject `value` and `onChange` to children when render. Once your field component is wrapped, props will not pass to the correct node. Follow code will not work as expect: + +```jsx + +
+

I am a wrapped Input

+ +
+
+``` + +You can use HOC to solve this problem, don't forget passing props to form control component: + +```jsx +const MyInput = (props) => ( +
+

I am a wrapped Input

+ +
+); + + + +; +``` diff --git a/components/form/index.ts b/components/form/index.ts index 0db126297264..1438f9f4f99c 100644 --- a/components/form/index.ts +++ b/components/form/index.ts @@ -1,14 +1,14 @@ import type { Rule, RuleObject, RuleRender } from 'rc-field-form/lib/interface'; import warning from '../_util/warning'; -import { FormProvider } from './context'; import ErrorList, { type ErrorListProps } from './ErrorList'; -import InternalForm, { type FormInstance, type FormProps, useForm, useWatch } from './Form'; +import InternalForm, { useForm, useWatch, type FormInstance, type FormProps } from './Form'; import Item, { type FormItemProps } from './FormItem'; import List, { type FormListFieldData, type FormListOperation, type FormListProps, } from './FormList'; +import { FormProvider } from './context'; import useFormInstance from './hooks/useFormInstance'; type InternalFormType = typeof InternalForm; @@ -44,16 +44,16 @@ Form.create = () => { }; export type { + ErrorListProps, FormInstance, - FormProps, FormItemProps, - ErrorListProps, + FormListFieldData, + FormListOperation, + FormListProps, + FormProps, Rule, RuleObject, RuleRender, - FormListProps, - FormListFieldData, - FormListOperation, }; export default Form; diff --git a/components/form/index.zh-CN.md b/components/form/index.zh-CN.md index e5902c42e0c7..5006944af1fe 100644 --- a/components/form/index.zh-CN.md +++ b/components/form/index.zh-CN.md @@ -27,6 +27,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA 表单标签可换行 非阻塞校验 字段监听 Hooks +校验时机 仅校验 字段路径前缀 动态增减表单项 @@ -47,15 +48,19 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA 自行处理表单数据 自定义校验 动态校验规则 +校验与更新依赖 校验其他组件 Disabled Input Debug -Dep Debug 测试 label 省略 测试特殊 col 24 用法 引用字段 +Custom feedback icons +组件 Token ## API +通用属性参考:[通用属性](/docs/react/common-props) + ### Form | 参数 | 说明 | 类型 | 默认值 | 版本 | @@ -65,6 +70,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA | component | 设置 Form 渲染元素,为 `false` 则不创建 DOM 节点 | ComponentType \| false | form | | | fields | 通过状态管理(如 redux)控制表单字段,如非强需求不推荐使用。查看[示例](#components-form-demo-global-state) | [FieldData](#fielddata)\[] | - | | | form | 经 `Form.useForm()` 创建的 form 控制实例,不提供时会自动创建 | [FormInstance](#forminstance) | - | | +| feedbackIcons | 当 `Form.Item` 有 `hasFeedback` 属性时可以自定义图标 | [FeedbackIcons](#feedbackicons) | - | 5.9.0 | | initialValues | 表单默认值,只有初始化以及重置时生效 | object | - | | | labelAlign | label 标签的文本对齐方式 | `left` \| `right` | `right` | | | labelWrap | label 标签的文本换行方式 | boolean | false | 4.18.0 | @@ -72,7 +78,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA | layout | 表单布局 | `horizontal` \| `vertical` \| `inline` | `horizontal` | | | name | 表单名称,会作为表单字段 `id` 前缀使用 | string | - | | | preserve | 当字段被删除时保留字段值 | boolean | true | 4.4.0 | -| requiredMark | 必选样式,可以切换为必选或者可选展示样式。此为 Form 配置,Form.Item 无法单独配置 | boolean \| `optional` | true | 4.6.0 | +| requiredMark | 必选样式,可以切换为必选或者可选展示样式。此为 Form 配置,Form.Item 无法单独配置 | boolean \| `optional` \| ((label: ReactNode, info: { required: boolean }) => ReactNode) | true | `renderProps`: 5.9.0 | | scrollToFirstError | 提交失败自动滚动到第一个错误字段 | boolean \| [Options](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options) | false | | | size | 设置字段组件的尺寸(仅限 antd 组件) | `small` \| `middle` \| `large` | - | | | validateMessages | 验证提示模板,说明[见下](#validatemessages) | [ValidateMessages](https://github.com/ant-design/ant-design/blob/6234509d18bac1ac60fbb3f92a5b2c6a6361295a/components/locale/en_US.ts#L88-L134) | - | | @@ -120,7 +126,7 @@ const validateMessages = { | extra | 额外的提示信息,和 `help` 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 | ReactNode | - | | | getValueFromEvent | 设置如何将 event 的值转换成字段值 | (..args: any\[]) => any | - | | | getValueProps | 为子元素添加额外的属性 | (value: any) => any | - | 4.2.0 | -| hasFeedback | 配合 `validateStatus` 属性使用,展示校验状态图标,建议只配合 Input 组件使用 | boolean | false | | +| hasFeedback | 配合 `validateStatus` 属性使用,展示校验状态图标,建议只配合 Input 组件使用 此外,它还可以通过 Icons 属性获取反馈图标。 | boolean \| { icons: [FeedbackIcons](#feedbackicons) } | false | icons: 5.9.0 | | help | 提示信息,如不设置,则会根据校验规则自动生成 | ReactNode | - | | | hidden | 是否隐藏字段(依然会收集和校验字段) | boolean | false | 4.4.0 | | htmlFor | 设置子元素 label `htmlFor` 属性 | string | - | | @@ -131,7 +137,7 @@ const validateMessages = { | messageVariables | 默认验证字段的信息 | Record<string, string> | - | 4.7.0 | | name | 字段名,支持数组 | [NamePath](#namepath) | - | | | normalize | 组件获取值后进行转换,再放入 Form 中。不支持异步 | (value, prevValue, prevValues) => any | - | | -| noStyle | 为 `true` 时不带样式,作为纯字段控件使用 | boolean | false | | +| noStyle | 为 `true` 时不带样式,作为纯字段控件使用。当自身没有 `validateStatus` 而父元素存在有 `validateStatus` 的 Form.Item 会继承父元素的 `validateStatus` | boolean | false | | | preserve | 当字段被删除时保留字段值 | boolean | true | 4.4.0 | | required | 必填样式设置。如不设置,则会根据校验规则自动生成 | boolean | false | | | rules | 校验规则,设置字段的校验逻辑。点击[此处](#components-form-demo-basic)查看示例 | [Rule](#rule)\[] | - | | @@ -139,9 +145,10 @@ const validateMessages = { | tooltip | 配置提示信息 | ReactNode \| [TooltipProps & { icon: ReactNode }](/components/tooltip-cn#api) | - | 4.7.0 | | trigger | 设置收集字段值变更的时机。点击[此处](#components-form-demo-customized-form-controls)查看示例 | string | `onChange` | | | validateFirst | 当某一规则校验不通过时,是否停止剩下的规则的校验。设置 `parallel` 时会并行校验 | boolean \| `parallel` | false | `parallel`: 4.5.0 | +| validateDebounce | 设置防抖,延迟毫秒数后进行校验 | number | - | 5.9.0 | | validateStatus | 校验状态,如不设置,则会根据校验规则自动生成,可选:'success' 'warning' 'error' 'validating' | string | - | | | validateTrigger | 设置字段校验的时机 | string \| string\[] | `onChange` | | -| valuePropName | 子节点的值的属性,如 Switch 的是 'checked'。该属性为 `getValueProps` 的封装,自定义 `getValueProps` 后会失效 | string | `value` | | +| valuePropName | 子节点的值的属性,如 Switch、Checkbox 的是 `checked`。该属性为 `getValueProps` 的封装,自定义 `getValueProps` 后会失效 | string | `value` | | | wrapperCol | 需要为输入控件设置布局样式时,使用该属性,用法同 `labelCol`。你可以通过 Form 的 `wrapperCol` 进行统一设置,不会作用于嵌套 Item。当和 Form 同时设置时,以 Item 为准 | [object](/components/grid-cn#col) | - | | 被设置了 `name` 属性的 `Form.Item` 包装的控件,表单控件会自动添加 `value`(或 `valuePropName` 指定的其他属性) `onChange`(或 `trigger` 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果: @@ -152,17 +159,21 @@ const validateMessages = { ### dependencies -当字段间存在依赖关系时使用。如果一个字段设置了 `dependencies` 属性。那么它所依赖的字段更新时,该字段将自动触发更新与校验。一种常见的场景,就是注册用户表单的“密码”与“确认密码”字段。“确认密码”校验依赖于“密码”字段,设置 `dependencies` 后,“密码”字段更新会重新触发“校验密码”的校验逻辑。你可以参考[具体例子](#components-form-demo-register)。 +当字段间存在依赖关系时使用。如果一个字段设置了 `dependencies` 属性。那么它所依赖的字段更新时,该字段将自动触发更新与校验。一种常见的场景,就是注册用户表单的“密码”与“确认密码”字段。“确认密码”校验依赖于“密码”字段,设置 `dependencies` 后,“密码”字段更新会重新触发“校验密码”的校验逻辑。你可以参考[具体例子](#components-form-demo-dependencies)。 `dependencies` 不应和 `shouldUpdate` 一起使用,因为这可能带来更新逻辑的混乱。 -从 `4.5.0` 版本开始,`dependencies` 支持使用 render props 类型 children 的 `Form.Item`。 +### FeedbackIcons + +`({ status: ValidateStatus, errors: ReactNode, warnings: ReactNode }) => Record` ### shouldUpdate Form 通过增量更新方式,只更新被修改的字段相关组件以达到性能优化目的。大部分场景下,你只需要编写代码或者与 [`dependencies`](#dependencies) 属性配合校验即可。而在某些特定场景,例如修改某个字段值后出现新的字段选项、或者纯粹希望表单任意变化都对某一个区域进行渲染。你可以通过 `shouldUpdate` 修改 Form.Item 的更新逻辑。 -当 `shouldUpdate` 为 `true` 时,Form 的任意变化都会使该 Form.Item 重新渲染。这对于自定义渲染一些区域十分有帮助: +当 `shouldUpdate` 为 `true` 时,Form 的任意变化都会使该 Form.Item 重新渲染。这对于自定义渲染一些区域十分有帮助,要注意 Form.Item 里包裹的子组件必须由函数返回,否则 `shouldUpdate` 不会起作用: + +相关issue:[#34500](https://github.com/ant-design/ant-design/issues/34500) ```jsx @@ -221,7 +232,7 @@ Form 通过增量更新方式,只更新被修改的字段相关组件以达到 | --- | --- | --- | --- | --- | | children | 渲染函数 | (fields: Field\[], operation: { add, remove, move }, meta: { errors }) => React.ReactNode | - | | | initialValue | 设置子元素默认值,如果与 Form 的 `initialValues` 冲突则以 Form 为准 | any\[] | - | 4.9.0 | -| name | 字段名,支持数组 | [NamePath](#namepath) | - | | +| name | 字段名,支持数组。List 本身也是字段,因而 `getFieldsValue()` 默认会返回 List 下所有值,你可以通过[参数](#getfieldsvalue)改变这一行为 | [NamePath](#namepath) | - | | | rules | 校验规则,仅支持自定义规则。需要配合 [ErrorList](#formerrorlist) 一同使用。 | { validator, message }\[] | - | 4.7.0 | ```tsx @@ -285,7 +296,7 @@ Form.List 渲染表单相关操作函数。 | getFieldError | 获取对应字段名的错误信息 | (name: [NamePath](#namepath)) => string\[] | | | getFieldInstance | 获取对应字段实例 | (name: [NamePath](#namepath)) => any | 4.4.0 | | getFieldsError | 获取一组字段名对应的错误信息,返回为数组形式 | (nameList?: [NamePath](#namepath)\[]) => FieldError\[] | | -| getFieldsValue | 获取一组字段名对应的值,会按照对应结构返回。默认返回现存字段值,当调用 `getFieldsValue(true)` 时返回所有值 | (nameList?: [NamePath](#namepath)\[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any | | +| getFieldsValue | 获取一组字段名对应的值,会按照对应结构返回。默认返回现存字段值,当调用 `getFieldsValue(true)` 时返回所有值 | [GetFieldsValue](#getfieldsvalue) | | | getFieldValue | 获取对应字段名的值 | (name: [NamePath](#namepath)) => any | | | isFieldsTouched | 检查一组字段是否被用户操作过,`allTouched` 为 `true` 时检查是否所有字段都被操作过 | (nameList?: [NamePath](#namepath)\[], allTouched?: boolean) => boolean | | | isFieldTouched | 检查对应字段是否被用户操作过 | (name: [NamePath](#namepath)) => boolean | | @@ -293,12 +304,26 @@ Form.List 渲染表单相关操作函数。 | resetFields | 重置一组字段到 `initialValues` | (fields?: [NamePath](#namepath)\[]) => void | | | scrollToField | 滚动到对应字段位置 | (name: [NamePath](#namepath), options: [ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)) => void | | | setFields | 设置一组字段状态 | (fields: [FieldData](#fielddata)\[]) => void | | -| setFieldValue | 设置表单的值(该值将直接传入 form store 中。如果你不希望传入对象被修改,请克隆后传入) | (name: [NamePath](#namepath), value: any) => void | 4.22.0 | -| setFieldsValue | 设置表单的值(该值将直接传入 form store 中。如果你不希望传入对象被修改,请克隆后传入)。如果你只想修改 Form.List 中单项值,请通过 `setFieldValue` 进行指定 | (values) => void | | +| setFieldValue | 设置表单的值(该值将直接传入 form store 中并且**重置错误信息**。如果你不希望传入对象被修改,请克隆后传入) | (name: [NamePath](#namepath), value: any) => void | 4.22.0 | +| setFieldsValue | 设置表单的值(该值将直接传入 form store 中并且**重置错误信息**。如果你不希望传入对象被修改,请克隆后传入)。如果你只想修改 Form.List 中单项值,请通过 `setFieldValue` 进行指定 | (values) => void | | | submit | 提交表单,与点击 `submit` 按钮效果相同 | () => void | | -| validateFields | 触发表单验证 | (nameList?: [NamePath](#namepath)\[], { validateOnly?: boolean }) => Promise | `validateOnly`: 5.5.0 | +| validateFields | 触发表单验证,设置 `recursive` 时会递归校验所有包含的路径 | (nameList?: [NamePath](#namepath)\[], config?: [ValidateConfig](#validateFields)) => Promise | | + +#### validateFields + +```tsx +export interface ValidateConfig { + // 5.5.0 新增。仅校验内容而不会将错误信息展示到 UI 上。 + validateOnly?: boolean; + // 5.9.0 新增。对提供的 `nameList` 与其子路径进行递归校验。 + recursive?: boolean; + // 5.11.0 新增。校验 dirty 的字段(touched + validated)。 + // 使用 `dirty` 可以很方便的仅校验用户操作过和被校验过的字段。 + dirty?: boolean; +} +``` -#### validateFields 返回示例 +返回示例: ```jsx validateFields() @@ -445,6 +470,41 @@ Form 仅会对变更的 Field 进行刷新,从而避免完整的组件刷新 `string | number | (string | number)[]` +#### GetFieldsValue + +`getFieldsValue` 提供了多种重载方法: + +##### getFieldsValue(nameList?: true | [NamePath](#namepath)\[], filterFunc?: FilterFunc) + +当不提供 `nameList` 时,返回所有注册字段,这也包含 List 下所有的值(即便 List 下没有绑定 Item)。 + +当 `nameList` 为 `true` 时,返回 store 中所有的值,包含未注册字段。例如通过 `setFieldsValue` 设置了不存在的 Item 的值,也可以通过 `true` 全部获取。 + +当 `nameList` 为数组时,返回规定路径的值。需要注意的是,`nameList` 为嵌套数组。例如你需要某路径值应该如下: + +```tsx +// 单个路径 +form.getFieldsValue([['user', 'age']]); + +// 多个路径 +form.getFieldsValue([ + ['user', 'age'], + ['preset', 'account'], +]); +``` + +##### getFieldsValue({ strict?: boolean, filter?: FilterFunc }) + +`5.8.0` 新增接受配置参数。当 `strict` 为 `true` 时会仅匹配 Item 的值。例如 `{ list: [{ bamboo: 1, little: 2 }] }` 中,如果 List 仅绑定了 `bamboo` 字段,那么 `getFieldsValue({ strict: true })` 会只获得 `{ list: [{ bamboo: 1 }] }`。 + +#### FilterFunc + +用于过滤一些字段值,`meta` 会返回字段相关信息。例如可以用来获取仅被用户修改过的值等等。 + +```tsx +type FilterFunc = (meta: { touched: boolean; validating: boolean }) => boolean; +``` + #### FieldData | 名称 | 说明 | 类型 | @@ -489,12 +549,22 @@ type Rule = RuleConfig | ((form: FormInstance) => RuleConfig); | form | 指定 Form 实例 | FormInstance | 当前 context 中的 Form | 5.4.0 | | preserve | 是否监视没有对应的 `Form.Item` 的字段 | boolean | false | 5.4.0 | -## Design Token +## 主题变量(Design Token) ## FAQ +### Switch、Checkbox 为什么不能绑定数据? + +Form.Item 默认绑定值属性到 `value` 上,而 Switch、Checkbox 等组件的值属性为 `checked`。你可以通过 `valuePropName` 来修改绑定的值属性。 + +```tsx | pure + + + +``` + ### 自定义 validator 没有效果 这是由于你的 `validator` 有错误导致 `callback` 没有执行到。你可以选择通过 `async` 返回一个 promise 或者使用 `try...catch` 进行错误捕获: @@ -544,6 +614,26 @@ validator(rule, value, callback) => { 1. Form 的 `initialValues` 拥有最高优先级 2. Field 的 `initialValue` 次之 \*. 多个同 `name` Item 都设置 `initialValue` 时,则 Item 的 `initialValue` 不生效 +### 为什么 `getFieldsValue` 在初次渲染的时候拿不到值? + +`getFieldsValue` 默认返回收集的字段数据,而在初次渲染时 Form.Item 节点尚未渲染,因而无法收集到数据。你可以通过 `getFieldsValue(true)` 来获取所有字段数据。 + +### 为什么 `setFieldsValue` 设置字段为 `undefined` 时,有的组件不会重置为空? + +在 React 中,`value` 从确定值改为 `undefined` 表示从受控变为非受控,因而不会重置展示值(但是 Form 中的值确实已经改变)。你可以通过 HOC 改变这一逻辑: + +```jsx +const MyInput = ({ + // 强制保持受控逻辑 + value = '', + ...rest +}) => ; + + + +; +``` + ### 为什么字段设置 `rules` 后更改值 `onFieldsChange` 会触发三次? 字段除了本身的值变化外,校验也是其状态之一。因而在触发字段变化会经历以下几个阶段: @@ -587,12 +677,18 @@ React 中异步更新会导致受控组件交互行为异常。当用户交互 } -### 自定义表单控件 `scrollToFirstError` 和 `scrollToField` 失效? +### `scrollToFirstError` 和 `scrollToField` 失效? + +1. 使用了自定义表单控件 类似问题:[#28370](https://github.com/ant-design/ant-design/issues/28370) [#27994](https://github.com/ant-design/ant-design/issues/27994) 滚动依赖于表单控件元素上绑定的 `id` 字段,如果自定义控件没有将 `id` 赋到正确的元素上,这个功能将失效。你可以参考这个 [codesandbox](https://codesandbox.io/s/antd-reproduction-template-forked-25nul?file=/index.js)。 +2. 页面内有多个表单 + +页面内如果有多个表单,且存在表单项 `name` 重复,表单滚动定位可能会查找到另一个表单的同名表单项上。需要给表单 `Form` 组件设置不同的 `name` 以区分。 + ### 继上,为何不通过 `ref` 绑定元素? 当自定义组件不支持 `ref` 时,Form 无法获取子元素真实 DOM 节点,而通过包裹 Class Component 调用 `findDOMNode` 会在 React Strict Mode 下触发警告。因而我们使用 id 来进行元素定位。 @@ -601,6 +697,34 @@ React 中异步更新会导致受控组件交互行为异常。当用户交互 是的,change 事件仅当用户交互才会触发。该设计是为了防止在 change 事件中调用 `setFieldsValue` 导致的循环问题。如果仅仅需要组件内消费,可以通过 `useWatch` 或者 `Field.renderProps` 来实现。 +### 为什么 Form.Item 嵌套子组件后,不更新表单值? + +Form.Item 在渲染时会注入 `value` 与 `onChange` 事件给子元素,当你的字段组件被包裹时属性将无法传递。所以以下代码是不会生效的: + +```jsx + +
+

I am a wrapped Input

+ +
+
+``` + +你可以通过 HOC 自定义组件形式来解决这个问题: + +```jsx +const MyInput = (props) => ( +
+

I am a wrapped Input

+ +
+); + + + +; +``` + ### 有更多参考文档吗? - 你可以阅读[《antd v4 Form 使用心得》](https://zhuanlan.zhihu.com/p/375753910)获得一些使用帮助以及建议。 diff --git a/components/form/style/fallbackCmp.ts b/components/form/style/fallbackCmp.ts new file mode 100644 index 000000000000..cf3e11336573 --- /dev/null +++ b/components/form/style/fallbackCmp.ts @@ -0,0 +1,29 @@ +/** + * Fallback of IE. + * Safe to remove. + */ + +// Style as inline component +import { prepareToken, type FormToken } from '.'; +import { genSubStyleComponent, type GenerateStyle } from '../../theme/internal'; + +// ============================= Fallback ============================= +const genFallbackStyle: GenerateStyle = (token) => { + const { formItemCls } = token; + + return { + '@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none)': { + // Fallback for IE, safe to remove we not support it anymore + [`${formItemCls}-control`]: { + display: 'flex', + }, + }, + }; +}; + +// ============================== Export ============================== +export default genSubStyleComponent(['Form', 'item-item'], (token, { rootPrefixCls }) => { + const formToken = prepareToken(token, rootPrefixCls); + + return [genFallbackStyle(formToken)]; +}); diff --git a/components/form/style/index.ts b/components/form/style/index.ts index 7bc45763f836..9fae4ef00489 100644 --- a/components/form/style/index.ts +++ b/components/form/style/index.ts @@ -1,10 +1,61 @@ +import type { CSSProperties } from 'react'; import type { CSSObject } from '@ant-design/cssinjs'; + +import { resetComponent } from '../../style'; import { genCollapseMotion, zoomIn } from '../../style/motion'; import type { AliasToken, FullToken, GenerateStyle } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal'; -import { resetComponent } from '../../style'; +import type { GenStyleFn } from '../../theme/util/genComponentStyleHook'; import genFormValidateMotionStyle from './explain'; +export interface ComponentToken { + /** + * @desc 必填项标记颜色 + * @descEN Required mark color + */ + labelRequiredMarkColor: string; + /** + * @desc 标签颜色 + * @descEN Label color + */ + labelColor: string; + /** + * @desc 标签字体大小 + * @descEN Label font size + */ + labelFontSize: number; + /** + * @desc 标签高度 + * @descEN Label height + */ + labelHeight: number; + /** + * @desc 标签冒号前间距 + * @descEN Label colon margin-inline-start + */ + labelColonMarginInlineStart: number; + /** + * @desc 标签冒号后间距 + * @descEN Label colon margin-inline-end + */ + labelColonMarginInlineEnd: number; + /** + * @desc 表单项间距 + * @descEN Form item margin bottom + */ + itemMarginBottom: number; + /** + * @desc 垂直布局标签内边距 + * @descEN Vertical layout label padding + */ + verticalLabelPadding: CSSProperties['padding']; + /** + * @desc 垂直布局标签外边距 + * @descEN Vertical layout label margin + */ + verticalLabelMargin: CSSProperties['margin']; +} + export interface FormToken extends FullToken<'Form'> { formItemCls: string; rootPrefixCls: string; @@ -113,13 +164,25 @@ const genFormStyle: GenerateStyle = (token) => { }; const genFormItemStyle: GenerateStyle = (token) => { - const { formItemCls, iconCls, componentCls, rootPrefixCls } = token; + const { + formItemCls, + iconCls, + componentCls, + rootPrefixCls, + labelRequiredMarkColor, + labelColor, + labelFontSize, + labelHeight, + labelColonMarginInlineStart, + labelColonMarginInlineEnd, + itemMarginBottom, + } = token; return { [formItemCls]: { ...resetComponent(token), - marginBottom: token.marginLG, + marginBottom: itemMarginBottom, verticalAlign: 'top', '&-with-help': { @@ -148,7 +211,6 @@ const genFormItemStyle: GenerateStyle = (token) => { // = Label = // ============================================================== [`${formItemCls}-label`]: { - display: 'inline-block', flexGrow: 0, overflow: 'hidden', whiteSpace: 'nowrap', @@ -170,9 +232,9 @@ const genFormItemStyle: GenerateStyle = (token) => { display: 'inline-flex', alignItems: 'center', maxWidth: '100%', - height: token.controlHeight, - color: token.colorTextHeading, - fontSize: token.fontSize, + height: labelHeight, + color: labelColor, + fontSize: labelFontSize, [`> ${iconCls}`]: { fontSize: token.fontSize, @@ -183,7 +245,7 @@ const genFormItemStyle: GenerateStyle = (token) => { [`&${formItemCls}-required:not(${formItemCls}-required-mark-optional)::before`]: { display: 'inline-block', marginInlineEnd: token.marginXXS, - color: token.colorError, + color: labelRequiredMarkColor, fontSize: token.fontSize, fontFamily: 'SimSun, sans-serif', lineHeight: 1, @@ -217,12 +279,12 @@ const genFormItemStyle: GenerateStyle = (token) => { content: '":"', position: 'relative', marginBlock: 0, - marginInlineStart: token.marginXXS / 2, - marginInlineEnd: token.marginXS, + marginInlineStart: labelColonMarginInlineStart, + marginInlineEnd: labelColonMarginInlineEnd, }, [`&${formItemCls}-no-colon::after`]: { - content: '" "', + content: '"\\a0"', }, }, }, @@ -231,7 +293,7 @@ const genFormItemStyle: GenerateStyle = (token) => { // = Input = // ============================================================== [`${formItemCls}-control`]: { - display: 'flex', + ['--ant-display' as any]: 'flex', flexDirection: 'column', flexGrow: 1, @@ -322,7 +384,7 @@ const genFormItemStyle: GenerateStyle = (token) => { }; const genHorizontalStyle: GenerateStyle = (token) => { - const { componentCls, formItemCls, rootPrefixCls } = token; + const { componentCls, formItemCls } = token; return { [`${componentCls}-horizontal`]: { @@ -337,9 +399,14 @@ const genHorizontalStyle: GenerateStyle = (token) => { minWidth: 0, }, + // Do not change this to `ant-col-24`! `-24` match all the responsive rules // https://github.com/ant-design/ant-design/issues/32980 - [`${formItemCls}-label.${rootPrefixCls}-col-24 + ${formItemCls}-control`]: { - minWidth: 'unset', + // https://github.com/ant-design/ant-design/issues/34903 + // https://github.com/ant-design/ant-design/issues/44538 + [`${formItemCls}-label[class$='-24'], ${formItemCls}-label[class*='-24 ']`]: { + [`& + ${formItemCls}-control`]: { + minWidth: 'unset', + }, }, }, }; @@ -362,10 +429,6 @@ const genInlineStyle: GenerateStyle = (token) => { flexWrap: 'nowrap', }, - '&-with-help': { - marginBottom: token.marginLG, - }, - [`> ${formItemCls}-label, > ${formItemCls}-control`]: { display: 'inline-block', @@ -389,8 +452,8 @@ const genInlineStyle: GenerateStyle = (token) => { }; const makeVerticalLayoutLabel = (token: FormToken): CSSObject => ({ - margin: 0, - padding: `0 0 ${token.paddingXS}px`, + padding: token.verticalLabelPadding, + margin: token.verticalLabelMargin, whiteSpace: 'initial', textAlign: 'start', @@ -398,24 +461,30 @@ const makeVerticalLayoutLabel = (token: FormToken): CSSObject => ({ margin: 0, '&::after': { - display: 'none', + // https://github.com/ant-design/ant-design/issues/43538 + visibility: 'hidden', }, }, }); const makeVerticalLayout = (token: FormToken): CSSObject => { - const { componentCls, formItemCls } = token; + const { componentCls, formItemCls, rootPrefixCls } = token; return { [`${formItemCls} ${formItemCls}-label`]: makeVerticalLayoutLabel(token), - [componentCls]: { + // ref: https://github.com/ant-design/ant-design/issues/45122 + [`${componentCls}:not(${componentCls}-inline)`]: { [formItemCls]: { flexWrap: 'wrap', - [`${formItemCls}-label, - ${formItemCls}-control`]: { - flex: '0 0 100%', - maxWidth: '100%', + [`${formItemCls}-label, ${formItemCls}-control`]: { + // When developer pass `xs: { span }`, + // It should follow the `xs` screen config + // ref: https://github.com/ant-design/ant-design/issues/44386 + [`&:not([class*=" ${rootPrefixCls}-col-xs"])`]: { + flex: '0 0 100%', + maxWidth: '100%', + }, }, }, }, @@ -476,20 +545,48 @@ const genVerticalStyle: GenerateStyle = (token) => { }; // ============================== Export ============================== -export default genComponentStyleHook('Form', (token, { rootPrefixCls }) => { +export const prepareToken: ( + token: Parameters>[0], + rootPrefixCls: string, +) => FormToken = (token, rootPrefixCls) => { const formToken = mergeToken(token, { formItemCls: `${token.componentCls}-item`, rootPrefixCls, }); - return [ - genFormStyle(formToken), - genFormItemStyle(formToken), - genFormValidateMotionStyle(formToken), - genHorizontalStyle(formToken), - genInlineStyle(formToken), - genVerticalStyle(formToken), - genCollapseMotion(formToken), - zoomIn, - ]; -}); + return formToken; +}; + +export default genComponentStyleHook( + 'Form', + (token, { rootPrefixCls }) => { + const formToken = prepareToken(token, rootPrefixCls); + + return [ + genFormStyle(formToken), + genFormItemStyle(formToken), + genFormValidateMotionStyle(formToken), + genHorizontalStyle(formToken), + genInlineStyle(formToken), + genVerticalStyle(formToken), + genCollapseMotion(formToken), + zoomIn, + ]; + }, + (token) => ({ + labelRequiredMarkColor: token.colorError, + labelColor: token.colorTextHeading, + labelFontSize: token.fontSize, + labelHeight: token.controlHeight, + labelColonMarginInlineStart: token.marginXXS / 2, + labelColonMarginInlineEnd: token.marginXS, + itemMarginBottom: token.marginLG, + verticalLabelPadding: `0 0 ${token.paddingXS}px`, + verticalLabelMargin: 0, + }), + { + // Let From style before the Grid + // ref https://github.com/ant-design/ant-design/issues/44386 + order: -1000, + }, +); diff --git a/components/form/util.ts b/components/form/util.ts index 857d5c66dee9..1b18bd552cd9 100644 --- a/components/form/util.ts +++ b/components/form/util.ts @@ -1,3 +1,6 @@ +import type { Meta } from 'rc-field-form/lib/interface'; + +import type { ValidateStatus } from './FormItem'; import type { InternalNamePath } from './interface'; // form item name black list. in form ,you can use form.id get the form item element. @@ -28,3 +31,31 @@ export function getFieldId(namePath: InternalNamePath, formName?: string): strin return isIllegalName ? `${defaultItemNamePrefixCls}_${mergedId}` : mergedId; } + +/** + * Get merged status by meta or passed `validateStatus`. + */ +export function getStatus( + errors: React.ReactNode[], + warnings: React.ReactNode[], + meta: Meta, + defaultValidateStatus: ValidateStatus | DefaultValue, + hasFeedback?: boolean, + validateStatus?: ValidateStatus, +): ValidateStatus | DefaultValue { + let status = defaultValidateStatus; + + if (validateStatus !== undefined) { + status = validateStatus; + } else if (meta.validating) { + status = 'validating'; + } else if (errors.length) { + status = 'error'; + } else if (warnings.length) { + status = 'warning'; + } else if (meta.touched || (hasFeedback && meta.validated)) { + // success feedback should display when pass hasFeedback prop and current value is valid value + status = 'success'; + } + return status; +} diff --git a/components/form/validateMessagesContext.tsx b/components/form/validateMessagesContext.tsx new file mode 100644 index 000000000000..c95aba332634 --- /dev/null +++ b/components/form/validateMessagesContext.tsx @@ -0,0 +1,7 @@ +import type { ValidateMessages } from 'rc-field-form/lib/interface'; +import { createContext } from 'react'; + +// ZombieJ: We export single file here since +// ConfigProvider use this which will make loop deps +// to import whole `rc-field-form` +export default createContext(undefined); diff --git a/components/grid/RowContext.ts b/components/grid/RowContext.ts index 9f38971117cf..cd6b0dc86c7e 100644 --- a/components/grid/RowContext.ts +++ b/components/grid/RowContext.ts @@ -3,7 +3,6 @@ import { createContext } from 'react'; export interface RowContextState { gutter?: [number, number]; wrap?: boolean; - supportFlexGap?: boolean; } const RowContext = createContext({}); diff --git a/components/grid/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/grid/__tests__/__snapshots__/demo-extend.test.ts.snap index fdea708c61fc..8434637a6398 100644 --- a/components/grid/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/grid/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -71,6 +71,8 @@ Array [ ] `; +exports[`renders components/grid/demo/basic.tsx extend context correctly 2`] = `[]`; + exports[`renders components/grid/demo/flex.tsx extend context correctly 1`] = ` Array [
,
@@ -911,6 +925,7 @@ Array [
,
Column @@ -1224,7 +1241,7 @@ Array [
Column @@ -1232,7 +1249,7 @@ Array [
Column @@ -1240,7 +1257,7 @@ Array [
Column @@ -1248,7 +1265,7 @@ Array [
Column @@ -1256,7 +1273,7 @@ Array [
Column @@ -1264,7 +1281,7 @@ Array [
Column @@ -1272,7 +1289,7 @@ Array [
Column @@ -1282,11 +1299,11 @@ Array [ Another Row:,
Column @@ -1294,7 +1311,7 @@ Array [
Column @@ -1302,7 +1319,7 @@ Array [
Column @@ -1310,7 +1327,7 @@ Array [
Column @@ -1344,6 +1361,8 @@ Array [ ] `; +exports[`renders components/grid/demo/playground.tsx extend context correctly 2`] = `[]`; + exports[`renders components/grid/demo/responsive.tsx extend context correctly 1`] = `
`; +exports[`renders components/grid/demo/responsive.tsx extend context correctly 2`] = `[]`; + exports[`renders components/grid/demo/responsive-more.tsx extend context correctly 1`] = `
`; +exports[`renders components/grid/demo/responsive-more.tsx extend context correctly 2`] = `[]`; + exports[`renders components/grid/demo/sort.tsx extend context correctly 1`] = `
`; +exports[`renders components/grid/demo/sort.tsx extend context correctly 2`] = `[]`; + exports[`renders components/grid/demo/useBreakpoint.tsx extend context correctly 1`] = ` Array [ Current break point: , @@ -1415,3 +1440,5 @@ Array [ , ] `; + +exports[`renders components/grid/demo/useBreakpoint.tsx extend context correctly 2`] = `[]`; diff --git a/components/grid/__tests__/__snapshots__/demo.test.ts.snap b/components/grid/__tests__/__snapshots__/demo.test.ts.snap index 96849bd844c4..eca7cc84c598 100644 --- a/components/grid/__tests__/__snapshots__/demo.test.ts.snap +++ b/components/grid/__tests__/__snapshots__/demo.test.ts.snap @@ -735,11 +735,11 @@ Array [
,
,
Column @@ -1167,7 +1170,7 @@ Array [
Column @@ -1175,7 +1178,7 @@ Array [
Column @@ -1183,7 +1186,7 @@ Array [
Column @@ -1191,7 +1194,7 @@ Array [
Column @@ -1199,7 +1202,7 @@ Array [
Column @@ -1207,7 +1210,7 @@ Array [
Column @@ -1215,7 +1218,7 @@ Array [
Column @@ -1225,11 +1228,11 @@ Array [ Another Row:,
Column @@ -1237,7 +1240,7 @@ Array [
Column @@ -1245,7 +1248,7 @@ Array [
Column @@ -1253,7 +1256,7 @@ Array [
Column diff --git a/components/grid/__tests__/__snapshots__/index.test.tsx.snap b/components/grid/__tests__/__snapshots__/index.test.tsx.snap index 02d838380be0..3f2ef8ab55d0 100644 --- a/components/grid/__tests__/__snapshots__/index.test.tsx.snap +++ b/components/grid/__tests__/__snapshots__/index.test.tsx.snap @@ -45,6 +45,6 @@ exports[`Grid should render Row 1`] = ` exports[`Grid when typeof gutter is object array in large screen 1`] = `
`; diff --git a/components/grid/__tests__/demo.test.ts b/components/grid/__tests__/demo.test.ts index 740a1914f1ff..e6e6a473fd7b 100644 --- a/components/grid/__tests__/demo.test.ts +++ b/components/grid/__tests__/demo.test.ts @@ -2,4 +2,5 @@ import demoTest from '../../../tests/shared/demoTest'; demoTest('grid', { testRootProps: false, + nameCheckPathOnly: true, }); diff --git a/components/grid/__tests__/gap.test.tsx b/components/grid/__tests__/gap.test.tsx index e3194e26f702..8f394f789546 100644 --- a/components/grid/__tests__/gap.test.tsx +++ b/components/grid/__tests__/gap.test.tsx @@ -1,12 +1,12 @@ import React from 'react'; import ReactDOMServer from 'react-dom/server'; + import { Col, Row } from '..'; import { render, screen } from '../../../tests/utils'; jest.mock('../../_util/styleChecker', () => ({ canUseDocElement: () => true, isStyleSupport: () => true, - detectFlexGapSupported: () => true, })); describe('Grid.Gap', () => { diff --git a/components/grid/__tests__/index.test.tsx b/components/grid/__tests__/index.test.tsx index e514b29104b3..94f96fd55e84 100644 --- a/components/grid/__tests__/index.test.tsx +++ b/components/grid/__tests__/index.test.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { act } from 'react-dom/test-utils'; + import { Col, Row } from '..'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; @@ -96,10 +97,10 @@ describe('Grid', () => { ); expect(asFragment().firstChild).toMatchSnapshot(); - expect(container.querySelector('div')!.style.marginLeft).toEqual('-20px'); - expect(container.querySelector('div')!.style.marginRight).toEqual('-20px'); - expect(container.querySelector('div')!.style.marginTop).toEqual('-200px'); - expect(container.querySelector('div')!.style.marginBottom).toEqual('-200px'); + expect(container.querySelector('div')?.style.marginLeft).toBe('-20px'); + expect(container.querySelector('div')?.style.marginRight).toBe('-20px'); + expect(container.querySelector('div')?.style.marginTop).toBe(''); + expect(container.querySelector('div')?.style.marginBottom).toBe(''); }); it('renders wrapped Col correctly', () => { @@ -132,10 +133,10 @@ describe('Grid', () => { it('should work current when gutter is array', () => { const { container } = render(); - expect(container.querySelector('div')!.style.marginLeft).toEqual('-8px'); - expect(container.querySelector('div')!.style.marginRight).toEqual('-8px'); - expect(container.querySelector('div')!.style.marginTop).toEqual('-10px'); - expect(container.querySelector('div')!.style.marginBottom).toEqual('-10px'); + expect(container.querySelector('div')?.style.marginLeft).toBe('-8px'); + expect(container.querySelector('div')?.style.marginRight).toBe('-8px'); + expect(container.querySelector('div')?.style.marginTop).toBe(''); + expect(container.querySelector('div')?.style.marginBottom).toBe(''); }); // By jsdom mock, actual jsdom not implemented matchMedia diff --git a/components/grid/__tests__/server.test.tsx b/components/grid/__tests__/server.test.tsx index 643b921ca5a6..a223446e9fc8 100644 --- a/components/grid/__tests__/server.test.tsx +++ b/components/grid/__tests__/server.test.tsx @@ -1,4 +1,5 @@ import React from 'react'; + import { Col, Row } from '..'; import { render } from '../../../tests/utils'; @@ -12,18 +13,14 @@ describe('Grid.Server', () => { , ); - expect((container.querySelector('.ant-row') as HTMLElement)!.style.marginLeft).toEqual('-4px'); - expect((container.querySelector('.ant-row') as HTMLElement)!.style.marginRight).toEqual('-4px'); - expect((container.querySelector('.ant-row') as HTMLElement)!.style.marginTop).toEqual('-8px'); - expect((container.querySelector('.ant-row') as HTMLElement)!.style.marginBottom).toEqual( - '-8px', - ); + expect(container.querySelector('.ant-row')?.style.marginLeft).toBe('-4px'); + expect(container.querySelector('.ant-row')?.style.marginRight).toBe('-4px'); + expect(container.querySelector('.ant-row')?.style.marginTop).toBe(''); + expect(container.querySelector('.ant-row')?.style.marginBottom).toBe(''); - expect((container.querySelector('.ant-col') as HTMLElement)!.style.paddingLeft).toEqual('4px'); - expect((container.querySelector('.ant-col') as HTMLElement)!.style.paddingRight).toEqual('4px'); - expect((container.querySelector('.ant-col') as HTMLElement)!.style.paddingTop).toEqual('8px'); - expect((container.querySelector('.ant-col') as HTMLElement)!.style.paddingBottom).toEqual( - '8px', - ); + expect((container.querySelector('.ant-col') as HTMLElement)?.style.paddingLeft).toBe('4px'); + expect((container.querySelector('.ant-col') as HTMLElement)?.style.paddingRight).toBe('4px'); + expect((container.querySelector('.ant-col') as HTMLElement)?.style.paddingTop).toBe(''); + expect((container.querySelector('.ant-col') as HTMLElement)?.style.paddingBottom).toBe(''); }); }); diff --git a/components/grid/col.tsx b/components/grid/col.tsx index ead96e49133a..ac2218ab32c8 100644 --- a/components/grid/col.tsx +++ b/components/grid/col.tsx @@ -1,9 +1,10 @@ -import classNames from 'classnames'; import * as React from 'react'; +import classNames from 'classnames'; + +import type { LiteralUnion } from '../_util/type'; import { ConfigContext } from '../config-provider'; import RowContext from './RowContext'; import { useColStyle } from './style'; -import type { LiteralUnion } from '../_util/type'; // https://github.com/ant-design/ant-design/issues/14324 type ColSpanType = number | string; @@ -49,7 +50,7 @@ function parseFlex(flex: FlexType): string { const sizes = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] as const; const Col = React.forwardRef((props, ref) => { const { getPrefixCls, direction } = React.useContext(ConfigContext); - const { gutter, wrap, supportFlexGap } = React.useContext(RowContext); + const { gutter, wrap } = React.useContext(RowContext); const { prefixCls: customizePrefixCls, @@ -115,13 +116,6 @@ const Col = React.forwardRef((props, ref) => { mergedStyle.paddingRight = horizontalGutter; } - // Vertical gutter use padding when gap not support - if (gutter && gutter[1] > 0 && !supportFlexGap) { - const verticalGutter = gutter[1] / 2; - mergedStyle.paddingTop = verticalGutter; - mergedStyle.paddingBottom = verticalGutter; - } - if (flex) { mergedStyle.flex = parseFlex(flex); diff --git a/components/grid/hooks/useBreakpoint.tsx b/components/grid/hooks/useBreakpoint.tsx index 30ac4eb552cc..fde0d8715243 100644 --- a/components/grid/hooks/useBreakpoint.tsx +++ b/components/grid/hooks/useBreakpoint.tsx @@ -1,4 +1,5 @@ -import { useEffect, useRef } from 'react'; +import { useRef } from 'react'; +import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; import useForceUpdate from '../../_util/hooks/useForceUpdate'; import type { ScreenMap } from '../../_util/responsiveObserver'; import useResponsiveObserver from '../../_util/responsiveObserver'; @@ -8,7 +9,7 @@ function useBreakpoint(refreshOnChange: boolean = true): ScreenMap { const forceUpdate = useForceUpdate(); const responsiveObserver = useResponsiveObserver(); - useEffect(() => { + useLayoutEffect(() => { const token = responsiveObserver.subscribe((supportScreens) => { screensRef.current = supportScreens; if (refreshOnChange) { diff --git a/components/grid/index.en-US.md b/components/grid/index.en-US.md index 96fcc9c0e7b7..114f4a842649 100644 --- a/components/grid/index.en-US.md +++ b/components/grid/index.en-US.md @@ -51,6 +51,8 @@ Layout uses a 24 grid layout to define the width of each "box", but does not rig ## API +Common props ref:[Common props](/docs/react/common-props) + If the Ant Design grid layout component does not meet your needs, you can use the excellent layout components of the community: - [react-flexbox-grid](http://roylee0704.github.io/react-flexbox-grid/) diff --git a/components/grid/index.ts b/components/grid/index.ts index 70bac60e2e0b..c8310c63c7df 100644 --- a/components/grid/index.ts +++ b/components/grid/index.ts @@ -9,6 +9,6 @@ function useBreakpoint() { export type { ColProps, ColSize } from './col'; export type { RowProps } from './row'; -export { Row, Col }; +export { Col, Row }; export default { useBreakpoint }; diff --git a/components/grid/index.zh-CN.md b/components/grid/index.zh-CN.md index b89610b762b7..44fcace7b836 100644 --- a/components/grid/index.zh-CN.md +++ b/components/grid/index.zh-CN.md @@ -50,6 +50,8 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*DLUwQ4B2_zQAAA ## API +通用属性参考:[通用属性](/docs/react/common-props) + Ant Design 的布局组件若不能满足你的需求,你也可以直接使用社区的优秀布局组件: - [react-flexbox-grid](http://roylee0704.github.io/react-flexbox-grid/) @@ -85,6 +87,6 @@ Ant Design 的布局组件若不能满足你的需求,你也可以直接使用 响应式栅格的断点扩展自 [BootStrap 4 的规则](https://getbootstrap.com/docs/4.0/layout/overview/#responsive-breakpoints)(不包含链接里 `occasionally` 的部分)。 -## Design Token +## 主题变量(Design Token) diff --git a/components/grid/row.tsx b/components/grid/row.tsx index c4fea7486cfe..d89a4333e1a4 100644 --- a/components/grid/row.tsx +++ b/components/grid/row.tsx @@ -1,10 +1,11 @@ -import classNames from 'classnames'; import * as React from 'react'; -import { ConfigContext } from '../config-provider'; -import useFlexGapSupport from '../_util/hooks/useFlexGapSupport'; +import classNames from 'classnames'; + import type { Breakpoint, ScreenMap } from '../_util/responsiveObserver'; import useResponsiveObserver, { responsiveArray } from '../_util/responsiveObserver'; +import { ConfigContext } from '../config-provider'; import RowContext from './RowContext'; +import type { RowContextState } from './RowContext'; import { useRowStyle } from './style'; const RowAligns = ['top', 'middle', 'bottom', 'stretch'] as const; @@ -48,7 +49,9 @@ function useMergePropByScreen(oriProp: RowProps['align'] | RowProps['justify'], for (let i = 0; i < responsiveArray.length; i++) { const breakpoint: Breakpoint = responsiveArray[i]; // if do not match, do nothing - if (!screen[breakpoint]) continue; + if (!screen[breakpoint]) { + continue; + } const curVal = oriProp[breakpoint]; if (curVal !== undefined) { setProp(curVal); @@ -102,8 +105,6 @@ const Row = React.forwardRef((props, ref) => { const mergeJustify = useMergePropByScreen(justify, curScreens); - const supportFlexGap = useFlexGapSupport(); - const gutterRef = React.useRef(gutter); const responsiveObserver = useResponsiveObserver(); @@ -162,27 +163,21 @@ const Row = React.forwardRef((props, ref) => { // Add gutter related style const rowStyle: React.CSSProperties = {}; const horizontalGutter = gutters[0] != null && gutters[0] > 0 ? gutters[0] / -2 : undefined; - const verticalGutter = gutters[1] != null && gutters[1] > 0 ? gutters[1] / -2 : undefined; if (horizontalGutter) { rowStyle.marginLeft = horizontalGutter; rowStyle.marginRight = horizontalGutter; } - if (supportFlexGap) { - // Set gap direct if flex gap support - [, rowStyle.rowGap] = gutters; - } else if (verticalGutter) { - rowStyle.marginTop = verticalGutter; - rowStyle.marginBottom = verticalGutter; - } + [, rowStyle.rowGap] = gutters; // "gutters" is a new array in each rendering phase, it'll make 'React.useMemo' effectless. // So we deconstruct "gutters" variable here. const [gutterH, gutterV] = gutters; - const rowContext = React.useMemo( - () => ({ gutter: [gutterH, gutterV] as [number, number], wrap, supportFlexGap }), - [gutterH, gutterV, wrap, supportFlexGap], + + const rowContext = React.useMemo( + () => ({ gutter: [gutterH, gutterV] as [number, number], wrap }), + [gutterH, gutterV, wrap], ); return wrapSSR( diff --git a/components/grid/style/index.ts b/components/grid/style/index.ts index 1772f59e8f03..2ebac21eb302 100644 --- a/components/grid/style/index.ts +++ b/components/grid/style/index.ts @@ -1,4 +1,5 @@ import type { CSSObject } from '@ant-design/cssinjs'; + import type { FullToken, GenerateStyle } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal'; @@ -114,11 +115,21 @@ const genLoopGridColumnsStyle = (token: GridColToken, sizeCls: string): CSSObjec order: 0, }; } else { - gridColumnsStyle[`${componentCls}${sizeCls}-${i}`] = { - display: 'block', - flex: `0 0 ${(i / gridColumns) * 100}%`, - maxWidth: `${(i / gridColumns) * 100}%`, - }; + gridColumnsStyle[`${componentCls}${sizeCls}-${i}`] = [ + // https://github.com/ant-design/ant-design/issues/44456 + // Form set `display: flex` on Col which will override `display: block`. + // Let's get it from css variable to support override. + { + ['--ant-display' as any]: 'block', + // Fallback to display if variable not support + display: 'block', + }, + { + display: 'var(--ant-display)', + flex: `0 0 ${(i / gridColumns) * 100}%`, + maxWidth: `${(i / gridColumns) * 100}%`, + }, + ]; gridColumnsStyle[`${componentCls}${sizeCls}-push-${i}`] = { insetInlineStart: `${(i / gridColumns) * 100}%`, }; diff --git a/components/icon/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/icon/__tests__/__snapshots__/demo-extend.test.ts.snap index 197125b8f12c..7d7731c3a59c 100644 --- a/components/icon/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/icon/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -2,11 +2,10 @@ exports[`renders components/icon/demo/basic.tsx extend context correctly 1`] = `
`; +exports[`renders components/icon/demo/basic.tsx extend context correctly 2`] = `[]`; + exports[`renders components/icon/demo/custom.tsx extend context correctly 1`] = `
`; +exports[`renders components/icon/demo/custom.tsx extend context correctly 2`] = `[]`; + exports[`renders components/icon/demo/iconfont.tsx extend context correctly 1`] = `
`; +exports[`renders components/icon/demo/iconfont.tsx extend context correctly 2`] = `[]`; + exports[`renders components/icon/demo/scriptUrl.tsx extend context correctly 1`] = `
`; +exports[`renders components/icon/demo/scriptUrl.tsx extend context correctly 2`] = `[]`; + exports[`renders components/icon/demo/two-tone.tsx extend context correctly 1`] = `
`; + +exports[`renders components/icon/demo/two-tone.tsx extend context correctly 2`] = `[]`; diff --git a/components/icon/__tests__/__snapshots__/demo.test.ts.snap b/components/icon/__tests__/__snapshots__/demo.test.ts.snap index 3ab377e05df0..2a09acb9c081 100644 --- a/components/icon/__tests__/__snapshots__/demo.test.ts.snap +++ b/components/icon/__tests__/__snapshots__/demo.test.ts.snap @@ -2,11 +2,10 @@ exports[`renders components/icon/demo/basic.tsx correctly 1`] = `
## List of icons diff --git a/components/icon/index.ts b/components/icon/index.ts index 7cc36928c5f5..7bb12314c4cc 100755 --- a/components/icon/index.ts +++ b/components/icon/index.ts @@ -1,7 +1,11 @@ -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; const Icon: React.FC = () => { - warning(false, 'Icon', 'Empty Icon'); + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning('Icon'); + + warning(false, 'usage', 'Empty Icon'); + } return null; }; diff --git a/components/icon/index.zh-CN.md b/components/icon/index.zh-CN.md index 09d0f3cffc3e..f9274c2cc703 100644 --- a/components/icon/index.zh-CN.md +++ b/components/icon/index.zh-CN.md @@ -13,11 +13,9 @@ demo: ## 使用方法 -使用图标组件,你需要安装 `@ant-design/icons` 图标组件包: +使用图标组件,你需要安装 [@ant-design/icons](https://github.com/ant-design/ant-design-icons) 图标组件包: -```bash -npm install --save @ant-design/icons -``` + ## 设计师专属 @@ -105,8 +103,8 @@ getTwoToneColor(); // #eb2f96 ```jsx import React from 'react'; -import ReactDOM from 'react-dom/client'; import { createFromIconfontCN } from '@ant-design/icons'; +import ReactDOM from 'react-dom/client'; const MyIcon = createFromIconfontCN({ scriptUrl: '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js', // 在 iconfont.cn 上生成 @@ -154,9 +152,10 @@ module.exports = { ```jsx import React from 'react'; -import ReactDOM from 'react-dom/client'; import Icon from '@ant-design/icons'; import MessageSvg from 'path/to/message.svg'; // 你的 '*.svg' 文件路径 +import ReactDOM from 'react-dom/client'; + // in create-react-app: // import { ReactComponent as MessageSvg } from 'path/to/message.svg'; @@ -173,6 +172,6 @@ ReactDOM.createRoot(mountNode).render(); | style | 计算后的 `svg` 元素样式 | CSSProperties | - | | | width | `svg` 元素宽度 | string \| number | `1em` | | -## Design Token +## 主题变量(Design Token) diff --git a/components/image/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/image/__tests__/__snapshots__/demo-extend.test.ts.snap index be1651315ee8..54510148b156 100644 --- a/components/image/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/image/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -41,6 +41,8 @@ exports[`renders components/image/demo/basic.tsx extend context correctly 1`] =
`; +exports[`renders components/image/demo/basic.tsx extend context correctly 2`] = `[]`; + exports[`renders components/image/demo/component-token.tsx extend context correctly 1`] = ` Array [
@@ -255,6 +259,8 @@ Array [ ] `; +exports[`renders components/image/demo/controlled-preview.tsx extend context correctly 2`] = `[]`; + exports[`renders components/image/demo/fallback.tsx extend context correctly 1`] = `
`; +exports[`renders components/image/demo/fallback.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/image/demo/imageRender.tsx extend context correctly 1`] = ` +
+ +
+
+ + + + Preview +
+
+
+`; + +exports[`renders components/image/demo/imageRender.tsx extend context correctly 2`] = `[]`; + exports[`renders components/image/demo/placeholder.tsx extend context correctly 1`] = `
`; +exports[`renders components/image/demo/placeholder.tsx extend context correctly 2`] = `[]`; + exports[`renders components/image/demo/preview-group.tsx extend context correctly 1`] = ` Array [
- -
-
- - - - Preview -
-
-
, +
+ , -] +
+
`; +exports[`renders components/image/demo/preview-group-visible.tsx extend context correctly 2`] = `[]`; + exports[`renders components/image/demo/preview-mask.tsx extend context correctly 1`] = `
`; +exports[`renders components/image/demo/preview-mask.tsx extend context correctly 2`] = `[]`; + exports[`renders components/image/demo/previewSrc.tsx extend context correctly 1`] = `
`; + +exports[`renders components/image/demo/previewSrc.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/image/demo/toolbarRender.tsx extend context correctly 1`] = ` +
+ +
+
+ + + + Preview +
+
+
+`; + +exports[`renders components/image/demo/toolbarRender.tsx extend context correctly 2`] = `[]`; diff --git a/components/image/__tests__/__snapshots__/demo.test.ts.snap b/components/image/__tests__/__snapshots__/demo.test.ts.snap index 4fd5b5d821f4..77b11f3afe9b 100644 --- a/components/image/__tests__/__snapshots__/demo.test.ts.snap +++ b/components/image/__tests__/__snapshots__/demo.test.ts.snap @@ -299,13 +299,54 @@ exports[`renders components/image/demo/fallback.tsx correctly 1`] = `
`; +exports[`renders components/image/demo/imageRender.tsx correctly 1`] = ` +
+ +
+
+ + + + Preview +
+
+
+`; + exports[`renders components/image/demo/placeholder.tsx correctly 1`] = `
- -
-
- - - - Preview -
-
-
, +
+ , -] +
+
`; exports[`renders components/image/demo/preview-mask.tsx correctly 1`] = ` @@ -745,11 +672,10 @@ exports[`renders components/image/demo/preview-mask.tsx correctly 1`] = ` class="ant-image-mask customize-mask" >
`; + +exports[`renders components/image/demo/toolbarRender.tsx correctly 1`] = ` +
+ +
+
+ + + + Preview +
+
+
+`; diff --git a/components/image/__tests__/__snapshots__/index.test.tsx.snap b/components/image/__tests__/__snapshots__/index.test.tsx.snap index 2207aaeeda62..a2814231812f 100644 --- a/components/image/__tests__/__snapshots__/index.test.tsx.snap +++ b/components/image/__tests__/__snapshots__/index.test.tsx.snap @@ -43,189 +43,194 @@ exports[`Image Default Group preview props 1`] = `
-
    -
  • - 1 / 1 -
  • - -
  • + + + +
  • -
  • - +
+
- - - -
  • - +
  • +
    - - - -
  • -