Merge branch feature into next-merge-feature

This commit is contained in:
lijianan
2025-10-22 20:15:30 +08:00
39 changed files with 763 additions and 331 deletions

View File

@@ -0,0 +1,101 @@
import React, { useCallback, useEffect, useEffectEvent } from 'react';
const ANT_SYNC_STORAGE_EVENT_KEY = 'ANT_SYNC_STORAGE_EVENT_KEY';
const isFunction = (val: any): val is (...args: any[]) => any => {
return typeof val === 'function';
};
interface Options<T> {
defaultValue?: T;
serializer?: (value: T) => string;
deserializer?: (value: string) => T;
onError?: (error: unknown) => void;
}
const useLocalStorage = <T>(key: string, options: Options<T> = {}) => {
const storage = typeof window !== 'undefined' ? localStorage : null;
const {
defaultValue,
serializer = JSON.stringify,
deserializer = JSON.parse,
onError = console.error,
} = options;
const getStoredValue = () => {
try {
const rawData = storage?.getItem(key);
if (rawData) {
return deserializer(rawData);
}
} catch (e) {
onError(e);
}
return defaultValue;
};
const [state, setState] = React.useState<T>(getStoredValue);
useEffect(() => {
setState(getStoredValue());
}, [key]);
const updateState: React.Dispatch<React.SetStateAction<T>> = (value) => {
const currentState = isFunction(value) ? value(state) : value;
setState(currentState);
try {
let newValue: string | null;
const oldValue = storage?.getItem(key);
if (typeof currentState === 'undefined') {
newValue = null;
storage?.removeItem(key);
} else {
newValue = serializer(currentState);
storage?.setItem(key, newValue);
}
dispatchEvent(
new CustomEvent(ANT_SYNC_STORAGE_EVENT_KEY, {
detail: { key, newValue, oldValue, storageArea: storage },
}),
);
} catch (e) {
onError(e);
}
};
const shouldSync = (ev: StorageEvent) => {
return ev && ev.key === key && ev.storageArea === storage;
};
const onNativeStorage = useCallback(
(event: StorageEvent) => {
if (shouldSync(event)) {
setState(getStoredValue());
}
},
[key],
);
const onCustomStorage = useCallback(
(event: Event) => {
if (shouldSync(event as StorageEvent)) {
setState(getStoredValue());
}
},
[key],
);
useEffect(() => {
window?.addEventListener('storage', onNativeStorage);
window?.addEventListener(ANT_SYNC_STORAGE_EVENT_KEY, onCustomStorage);
return () => {
window?.removeEventListener('storage', onNativeStorage);
window?.removeEventListener(ANT_SYNC_STORAGE_EVENT_KEY, onCustomStorage);
};
}, [key, onNativeStorage, onCustomStorage]);
return [state, useEffectEvent(updateState)] as const;
};
export default useLocalStorage;

View File

@@ -31,15 +31,17 @@ const locales = {
const useStyle = createStyles(({ cssVar, css, cx }, siteConfig: SiteContextProps) => {
const textShadow = `0 0 4px ${cssVar.colorBgContainer}`;
const isDark = siteConfig.theme.includes('dark');
const mask = cx(css`
position: absolute;
inset: 0;
backdrop-filter: blur(2px);
opacity: 1;
background-color: ${isDark ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.2)'};
background-color: rgba(255, 255, 255, 0.2);
transition: all 1s ease;
pointer-events: none;
[data-prefers-color='dark'] & {
background-color: rgba(0, 0, 0, 0.2);
}
`);
const block = cx(css`

View File

@@ -60,7 +60,15 @@ export type Extras = {
export type Icons = Icon[];
export type HeadingBanner = {
[key in 'cn' | 'en']: {
title?: string;
href?: string;
};
};
export type SiteData = {
headingBanner: HeadingBanner;
articles: Articles;
authors: Authors;
recommendations: Recommendations;

View File

@@ -1,10 +1,11 @@
import React, { Suspense, useEffect } from 'react';
import React, { Suspense } from 'react';
import { App, Button, ConfigProvider, Skeleton } from 'antd';
import { enUS, zhCN } from 'antd-token-previewer';
import type { ThemeConfig } from 'antd/es/config-provider/context';
import { Helmet } from 'dumi';
import useLocale from '../../hooks/useLocale';
import useLocalStorage from '../../hooks/useLocalStorage';
const ThemeEditor = React.lazy(() => import('antd-token-previewer/lib/ThemeEditor'));
@@ -33,24 +34,18 @@ const locales = {
},
};
const ANT_DESIGN_V5_THEME_EDITOR_THEME = 'ant-design-v5-theme-editor-theme';
const ANT_THEME_EDITOR_THEME = 'ant-theme-editor-theme';
const CustomTheme: React.FC = () => {
const { message } = App.useApp();
const [locale, lang] = useLocale(locales);
const [theme, setTheme] = React.useState<ThemeConfig>({});
useEffect(() => {
const storedConfig = localStorage.getItem(ANT_DESIGN_V5_THEME_EDITOR_THEME);
if (storedConfig) {
const themeConfig = JSON.parse(storedConfig);
setTheme(themeConfig);
}
}, []);
const [themeConfig, setThemeConfig] = useLocalStorage<ThemeConfig>(ANT_THEME_EDITOR_THEME, {
defaultValue: {},
});
const handleSave = () => {
localStorage.setItem(ANT_DESIGN_V5_THEME_EDITOR_THEME, JSON.stringify(theme));
setThemeConfig(themeConfig);
message.success(locale.saveSuccessfully);
};
@@ -65,11 +60,9 @@ const CustomTheme: React.FC = () => {
<ThemeEditor
advanced
hideAdvancedSwitcher
theme={{ name: 'Custom Theme', key: 'test', config: theme }}
theme={{ name: 'Custom Theme', key: 'test', config: themeConfig }}
style={{ height: 'calc(100vh - 64px)' }}
onThemeChange={(newTheme) => {
setTheme(newTheme.config);
}}
onThemeChange={(newTheme) => setThemeConfig(newTheme.config)}
locale={lang === 'cn' ? zhCN : enUS}
actions={
<Button type="primary" onClick={handleSave}>

View File

@@ -20,7 +20,9 @@
const isEnabled = always || enabledCondition.every(Boolean);
if (!isEnabled) return;
if (!isEnabled) {
return;
}
const prefixCls = 'antd-mirror-notify';
const primaryColor = '#1677ff';

View File

@@ -12,18 +12,18 @@ import type { MenuProps } from 'antd';
import { CompactTheme, DarkTheme } from 'antd-token-previewer/es/icons';
import { FormattedMessage, useLocation } from 'dumi';
import useLocalStorage from '../../../hooks/useLocalStorage';
import useThemeAnimation from '../../../hooks/useThemeAnimation';
import type { SiteContextProps } from '../../slots/SiteContext';
import SiteContext from '../../slots/SiteContext';
import { getLocalizedPathname, isLocalStorageNameSupported, isZhCN } from '../../utils';
import { getLocalizedPathname, isZhCN } from '../../utils';
import Link from '../Link';
import PromptDrawer from './PromptDrawer';
import ThemeIcon from './ThemeIcon';
export type ThemeName = 'light' | 'dark' | 'auto' | 'compact' | 'motion-off' | 'happy-work';
// 主题持久化存储键名
const ANT_DESIGN_SITE_THEME = 'ant-design-site-theme';
export const ANT_DESIGN_SITE_THEME = 'ant-design-site-theme';
export interface ThemeSwitchProps {
value?: ThemeName[];
@@ -36,6 +36,10 @@ const ThemeSwitch: React.FC<ThemeSwitchProps> = () => {
const lastThemeKey = useRef<string>(theme.includes('dark') ? 'dark' : 'light');
const [isMarketDrawerOpen, setIsMarketDrawerOpen] = useState(false);
const [, setTheme] = useLocalStorage<ThemeName>(ANT_DESIGN_SITE_THEME, {
defaultValue: undefined,
});
const badge = <Badge color="blue" style={{ marginTop: -1 }} />;
// 主题选项配置
@@ -159,14 +163,9 @@ const ThemeSwitch: React.FC<ThemeSwitchProps> = () => {
const filteredTheme = theme.filter((t) => !['light', 'dark', 'auto'].includes(t));
const newTheme = [...filteredTheme, themeKey];
updateSiteConfig({
theme: newTheme,
});
updateSiteConfig({ theme: newTheme });
// 持久化到 localStorage
if (isLocalStorageNameSupported()) {
localStorage.setItem(ANT_DESIGN_SITE_THEME, themeKey);
}
setTheme(themeKey);
} else {
// 其他主题选项是开关式的
const hasTheme = theme.includes(themeKey);

View File

@@ -20,11 +20,11 @@ import type { ThemeName } from '../common/ThemeSwitch';
import SiteThemeProvider from '../SiteThemeProvider';
import type { SimpleComponentClassNames, SiteContextProps } from '../slots/SiteContext';
import SiteContext from '../slots/SiteContext';
import { isLocalStorageNameSupported } from '../utils';
type SiteState = Partial<Omit<SiteContextProps, 'updateSiteContext'>>;
const RESPONSIVE_MOBILE = 768;
export const ANT_DESIGN_NOT_SHOW_BANNER = 'ANT_DESIGN_NOT_SHOW_BANNER';
// 主题持久化存储键名
@@ -58,13 +58,12 @@ const getFinalTheme = (urlTheme: ThemeName[]): ThemeName[] => {
// 只认 light/dark
const baseTheme = urlTheme.filter((t) => !['light', 'dark', 'auto'].includes(t));
const urlColor = urlTheme.find((t) => t === 'light' || t === 'dark');
if (urlColor) return [...baseTheme, urlColor];
if (isLocalStorageNameSupported()) {
const stored = localStorage.getItem(ANT_DESIGN_SITE_THEME) as ThemeName;
if (stored && ['light', 'dark', 'auto'].includes(stored)) {
return [...baseTheme, stored];
}
if (urlColor) {
return [...baseTheme, urlColor];
}
const stored = localStorage.getItem(ANT_DESIGN_SITE_THEME) as ThemeName;
if (stored && ['light', 'dark', 'auto'].includes(stored)) {
return [...baseTheme, stored];
}
// 默认 auto
return [...baseTheme, 'auto'];

View File

@@ -8,6 +8,8 @@ import { useLocation, useSiteData } from 'dumi';
import DumiSearchBar from 'dumi/theme-default/slots/SearchBar';
import useLocale from '../../../hooks/useLocale';
import useLocalStorage from '../../../hooks/useLocalStorage';
import { useAntdSiteConfig } from '../../../pages/index/components/util';
import ThemeSwitch from '../../common/ThemeSwitch';
import DirectionIcon from '../../icons/DirectionIcon';
import { ANT_DESIGN_NOT_SHOW_BANNER } from '../../layouts/GlobalLayout';
@@ -22,20 +24,7 @@ import SwitchBtn from './SwitchBtn';
const RESPONSIVE_XS = 1120;
const RESPONSIVE_SM = 1200;
const locales = {
cn: {
message: '语雀征文 · 说说你和开源的故事,赢取 Ant Design 精美周边 🎁',
shortMessage: '语雀征文 · 说说你和开源的故事,赢取 Ant Design 精美周边 🎁',
more: '前往了解',
link: 'https://www.yuque.com/opensource2023',
},
en: {
message: '',
shortMessage: '',
more: '',
link: '',
},
};
export const ANT_LOCAL_TYPE_KEY = 'ANT_LOCAL_TYPE_KEY';
const useStyle = createStyles(({ cssVar, token, css }) => {
const searchIconColor = '#ced4d9';
@@ -162,11 +151,13 @@ interface HeaderState {
// ================================= Header =================================
const Header: React.FC = () => {
const [locale, lang] = useLocale(locales);
const [, lang] = useLocale();
const { data: siteData } = useAntdSiteConfig();
const { pkg } = useSiteData();
const themeConfig = getThemeConfig();
const [headerState, setHeaderState] = useState<HeaderState>({
menuVisible: false,
windowWidth: 1400,
@@ -179,24 +170,33 @@ const Header: React.FC = () => {
const { styles } = useStyle();
const [, setTopBannerDay] = useLocalStorage<string>(ANT_DESIGN_NOT_SHOW_BANNER, {
defaultValue: undefined,
});
const [, setLocalType] = useLocalStorage<string>(ANT_LOCAL_TYPE_KEY, {
defaultValue: undefined,
});
const handleHideMenu = useCallback(() => {
setHeaderState((prev) => ({ ...prev, menuVisible: false }));
}, []);
const onWindowResize = useCallback(() => {
setHeaderState((prev) => ({ ...prev, windowWidth: window.innerWidth }));
}, []);
const onMenuVisibleChange = useCallback((visible: boolean) => {
setHeaderState((prev) => ({ ...prev, menuVisible: visible }));
}, []);
const onDirectionChange = () => {
updateSiteConfig({ direction: direction !== 'rtl' ? 'rtl' : 'ltr' });
};
const onBannerClose = () => {
updateSiteConfig({ bannerVisible: false });
if (utils.isLocalStorageNameSupported()) {
localStorage.setItem(ANT_DESIGN_NOT_SHOW_BANNER, dayjs().toISOString());
}
setTopBannerDay(dayjs().toISOString());
};
useEffect(() => {
@@ -238,9 +238,8 @@ const Header: React.FC = () => {
const currentProtocol = `${window.location.protocol}//`;
const currentHref = window.location.href.slice(currentProtocol.length);
if (utils.isLocalStorageNameSupported()) {
localStorage.setItem('locale', utils.isZhCN(pathname) ? 'en-US' : 'zh-CN');
}
setLocalType(utils.isZhCN(pathname) ? 'en-US' : 'zh-CN');
window.location.href =
currentProtocol +
currentHref.replace(
@@ -272,6 +271,12 @@ const Header: React.FC = () => {
const isHome = ['', 'index', 'index-cn'].includes(pathname);
const isZhCN = lang === 'cn';
const isRTL = direction === 'rtl';
// Get banner data from site config
const bannerData = siteData?.headingBanner?.[lang as 'cn' | 'en'];
const bannerTitle = bannerData?.title || '';
const bannerHref = bannerData?.href || '';
let responsive: null | 'narrow' | 'crowded' = null;
if (windowWidth < RESPONSIVE_XS) {
responsive = 'crowded';
@@ -373,7 +378,7 @@ const Header: React.FC = () => {
<MenuOutlined className="nav-phone-icon" />
</Popover>
)}
{isZhCN && bannerVisible && (
{isZhCN && bannerVisible && bannerTitle && bannerHref && (
<ConfigProvider
theme={{
token: {
@@ -385,23 +390,25 @@ const Header: React.FC = () => {
<Alert
className={styles.banner}
title={
<>
<span>{isMobile ? locale.shortMessage : locale.message}</span>
<a
className={styles.link}
href={locale.link}
target="_blank"
rel="noreferrer"
onClick={() => {
window.gtag?.('event', '点击', {
event_category: 'top_banner',
event_label: locale.link,
});
}}
>
{locale.more}
</a>
</>
bannerTitle && bannerHref ? (
<>
<span>{bannerTitle}</span>
<a
className={styles.link}
href={bannerHref}
target="_blank"
rel="noreferrer"
onClick={() => {
window.gtag?.('event', '点击', {
event_category: 'top_banner',
event_label: bannerHref,
});
}}
>
</a>
</>
) : null
}
type="info"
banner

View File

@@ -1,6 +1,7 @@
import semver from 'semver';
import flatten from 'lodash/flatten';
import flattenDeep from 'lodash/flattenDeep';
import semver from 'semver';
import deprecatedVersions from '../../../BUG_VERSIONS.json';
import themeConfig from '../themeConfig';
@@ -157,19 +158,6 @@ export function getLocalizedPathname(
return { pathname: fullPath, search };
}
export function isLocalStorageNameSupported() {
const testKey = 'test';
const storage = window.localStorage;
try {
storage.setItem(testKey, '1');
storage.removeItem(testKey);
return true;
} catch (error) {
console.error('Your web browser does not support storing settings locally.', error);
return false;
}
}
export function loadScript(src: string) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
@@ -177,7 +165,7 @@ export function loadScript(src: string) {
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head!.appendChild(script);
document.head.appendChild(script);
});
}

View File

@@ -131,17 +131,6 @@ export default defineConfig({
headScripts: [
`
(function () {
function isLocalStorageNameSupported() {
const testKey = 'test';
const storage = window.localStorage;
try {
storage.setItem(testKey, '1');
storage.removeItem(testKey);
return true;
} catch (error) {
return false;
}
}
// 优先级提高到所有静态资源的前面,语言不对,加载其他静态资源没意义
const pathname = location.pathname;
@@ -171,7 +160,7 @@ export default defineConfig({
}
// 首页无视链接里面的语言设置 https://github.com/ant-design/ant-design/issues/4552
if (isLocalStorageNameSupported() && (pathname === '/' || pathname === '/index-cn')) {
if (pathname === '/' || pathname === '/index-cn') {
const lang =
(window.localStorage && localStorage.getItem('locale')) ||
((navigator.language || navigator.browserLanguage).toLowerCase() === 'zh-cn'

View File

@@ -39,7 +39,9 @@ jobs:
const now = new Date();
for (const issue of issues) {
if (issue.pull_request) continue;
if (issue.pull_request) {
continue;
}
const updatedAt = new Date(issue.updated_at);
const daysInactive = (now - updatedAt) / (1000 * 60 * 60 * 24);

View File

@@ -217,7 +217,9 @@ const Alert = React.forwardRef<AlertRef, AlertProps>((props, ref) => {
// closeable when closeText or closeIcon is assigned
const isClosable = React.useMemo<boolean>(() => {
if (typeof closable === 'object' && closable.closeIcon) return true;
if (typeof closable === 'object' && closable.closeIcon) {
return true;
}
if (closeText) {
return true;
}

View File

@@ -1,7 +1,7 @@
## zh-CN
友好的 [React 错误处理](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html) 包裹组件。
友好的 [React 错误处理](https://zh-hans.react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) 包裹组件。
## en-US
ErrorBoundary Component for making error handling easier in [React](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html).
ErrorBoundary Component for making error handling easier in [React](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary).

View File

@@ -65,8 +65,12 @@ const App: React.FC = () => {
};
const cellRender: CalendarProps<Dayjs>['cellRender'] = (current, info) => {
if (info.type === 'date') return dateCellRender(current);
if (info.type === 'month') return monthCellRender(current);
if (info.type === 'date') {
return dateCellRender(current);
}
if (info.type === 'month') {
return monthCellRender(current);
}
return info.originNode;
};

View File

@@ -13,8 +13,12 @@ const PickerWithType = ({
type: PickerType;
onChange: TimePickerProps['onChange'] | DatePickerProps['onChange'];
}) => {
if (type === 'time') return <TimePicker onChange={onChange} />;
if (type === 'date') return <DatePicker onChange={onChange} />;
if (type === 'time') {
return <TimePicker onChange={onChange} />;
}
if (type === 'date') {
return <DatePicker onChange={onChange} />;
}
return <DatePicker picker={type} onChange={onChange} />;
};

View File

@@ -2,10 +2,10 @@
通过 `Form.useForm` 对表单数据域进行交互。
> 注意 `useForm` 是 [React Hooks](https://reactjs.org/docs/hooks-intro.html) 的实现,只能用于函数组件。如果是在 Class Component 下,你也可以通过 `ref` 获取数据域https://codesandbox.io/p/sandbox/ngtjtm
> 注意 `useForm` 是 [React Hooks](https://zh-hans.react.dev/reference/react/hooks) 的实现,只能用于函数组件。如果是在 Class Component 下,你也可以通过 `ref` 获取数据域https://codesandbox.io/p/sandbox/ngtjtm
## en-US
Call form method with `Form.useForm`.
> Note that `useForm` is a [React Hooks](https://reactjs.org/docs/hooks-intro.html) that only works in functional component. You can also use `ref` to get the form instance in class component: https://codesandbox.io/p/sandbox-ngtjtm
> Note that `useForm` is a [React Hooks](https://react.dev/reference/react/hooks) that only works in functional component. You can also use `ref` to get the form instance in class component: https://codesandbox.io/p/sandbox-ngtjtm

View File

@@ -606,7 +606,7 @@ Components inside Form.Item with name property will turn into controlled mode, w
### Why can not call `ref` of Form at first time?
`ref` only receives the mounted instance. please ref React official doc: <https://reactjs.org/docs/refs-and-the-dom.html#accessing-refs>
`ref` only receives the mounted instance. please ref React official doc: <https://react.dev/learn/manipulating-the-dom-with-refs#when-react-attaches-the-refs>
### Why will `resetFields` re-mount component?

View File

@@ -605,7 +605,7 @@ Form.Item 默认绑定值属性到 `value` 上,而 Switch、Checkbox 等组件
### 为什么第一次调用 `ref` 的 Form 为空?
`ref` 仅在节点被加载时才会被赋值,请参考 React 官方文档:<https://reactjs.org/docs/refs-and-the-dom.html#accessing-refs>
`ref` 仅在节点被加载时才会被赋值,请参考 React 官方文档:<https://zh-hans.react.dev/learn/manipulating-the-dom-with-refs#when-react-attaches-the-refs>
### 为什么 `resetFields` 会重新 mount 组件?

View File

@@ -11,8 +11,9 @@ const formItemNameBlackList = ['parentNode'];
const defaultItemNamePrefixCls: string = 'form_item';
export function toArray<T>(candidate?: T | T[] | false): T[] {
if (candidate === undefined || candidate === false) return [];
if (candidate === undefined || candidate === false) {
return [];
}
return Array.isArray(candidate) ? candidate : [candidate];
}

View File

@@ -77,7 +77,7 @@ Common props ref[Common props](/docs/react/common-props)
> When `Input` is used in a `Form.Item` context, if the `Form.Item` has the `id` props defined then `value`, `defaultValue`, and `id` props of `Input` are automatically set.
The rest of the props of Input are exactly the same as the original [input](https://reactjs.org/docs/dom-elements.html#all-supported-html-attributes).
The rest of the props of Input are exactly the same as the original [input](https://react.dev/reference/react-dom/components/input).
#### CountConfig

View File

@@ -78,7 +78,7 @@ demo:
> 如果 `Input` 在 `Form.Item` 内,并且 `Form.Item` 设置了 `id` 属性,则 `value` `defaultValue` 和 `id` 属性会被自动设置。
Input 的其他属性和 React 自带的 [input](https://reactjs.org/docs/dom-elements.html#all-supported-html-attributes) 一致。
Input 的其他属性和 React 自带的 [input](https://zh-hans.react.dev/reference/react-dom/components/input) 一致。
#### CountConfig

View File

@@ -16,8 +16,9 @@ const App: React.FC = () => {
fetch(`https://api.github.com/search/users?q=${key}`)
.then((res) => res.json())
.then(({ items = [] }) => {
if (ref.current !== key) return;
if (ref.current !== key) {
return;
}
setLoading(false);
setUsers(items.slice(0, 10));
});

View File

@@ -171,103 +171,203 @@ exports[`renders components/segmented/demo/block.tsx extend context correctly 1`
exports[`renders components/segmented/demo/block.tsx extend context correctly 2`] = `[]`;
exports[`renders components/segmented/demo/componentToken.tsx extend context correctly 1`] = `
<div
aria-label="segmented control"
class="ant-segmented css-var-test-id"
role="radiogroup"
tabindex="0"
>
Array [
<div
class="ant-segmented-group"
aria-label="segmented control"
class="ant-segmented css-var-test-id"
role="radiogroup"
tabindex="0"
>
<label
class="ant-segmented-item ant-segmented-item-selected"
<div
class="ant-segmented-group"
>
<input
checked=""
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="true"
class="ant-segmented-item-label"
role="radio"
title="Daily"
<label
class="ant-segmented-item ant-segmented-item-selected"
>
Daily
</div>
</label>
<label
class="ant-segmented-item"
<input
checked=""
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="true"
class="ant-segmented-item-label"
role="radio"
title="Daily"
>
Daily
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Weekly"
>
Weekly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Monthly"
>
Monthly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Quarterly"
>
Quarterly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Yearly"
>
Yearly
</div>
</label>
</div>
</div>,
  ,
<div
aria-label="segmented control"
class="ant-segmented css-var-test-id"
role="radiogroup"
tabindex="0"
>
<div
class="ant-segmented-group"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Weekly"
<label
class="ant-segmented-item ant-segmented-item-selected"
>
Weekly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Monthly"
<input
checked=""
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="true"
class="ant-segmented-item-label"
role="radio"
title="Daily"
>
Daily
</div>
</label>
<label
class="ant-segmented-item"
>
Monthly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Quarterly"
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Weekly"
>
Weekly
</div>
</label>
<label
class="ant-segmented-item"
>
Quarterly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Yearly"
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Monthly"
>
Monthly
</div>
</label>
<label
class="ant-segmented-item"
>
Yearly
</div>
</label>
</div>
</div>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Quarterly"
>
Quarterly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Yearly"
>
Yearly
</div>
</label>
</div>
</div>,
]
`;
exports[`renders components/segmented/demo/componentToken.tsx extend context correctly 2`] = `[]`;

View File

@@ -167,103 +167,203 @@ exports[`renders components/segmented/demo/block.tsx correctly 1`] = `
`;
exports[`renders components/segmented/demo/componentToken.tsx correctly 1`] = `
<div
aria-label="segmented control"
class="ant-segmented css-var-test-id"
role="radiogroup"
tabindex="0"
>
Array [
<div
class="ant-segmented-group"
aria-label="segmented control"
class="ant-segmented css-var-test-id"
role="radiogroup"
tabindex="0"
>
<label
class="ant-segmented-item ant-segmented-item-selected"
<div
class="ant-segmented-group"
>
<input
checked=""
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="true"
class="ant-segmented-item-label"
role="radio"
title="Daily"
<label
class="ant-segmented-item ant-segmented-item-selected"
>
Daily
</div>
</label>
<label
class="ant-segmented-item"
<input
checked=""
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="true"
class="ant-segmented-item-label"
role="radio"
title="Daily"
>
Daily
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Weekly"
>
Weekly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Monthly"
>
Monthly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Quarterly"
>
Quarterly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Yearly"
>
Yearly
</div>
</label>
</div>
</div>,
  ,
<div
aria-label="segmented control"
class="ant-segmented css-var-test-id"
role="radiogroup"
tabindex="0"
>
<div
class="ant-segmented-group"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Weekly"
<label
class="ant-segmented-item ant-segmented-item-selected"
>
Weekly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Monthly"
<input
checked=""
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="true"
class="ant-segmented-item-label"
role="radio"
title="Daily"
>
Daily
</div>
</label>
<label
class="ant-segmented-item"
>
Monthly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Quarterly"
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Weekly"
>
Weekly
</div>
</label>
<label
class="ant-segmented-item"
>
Quarterly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Yearly"
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Monthly"
>
Monthly
</div>
</label>
<label
class="ant-segmented-item"
>
Yearly
</div>
</label>
</div>
</div>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Quarterly"
>
Quarterly
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
name="test-id"
type="radio"
/>
<div
aria-checked="false"
class="ant-segmented-item-label"
role="radio"
title="Yearly"
>
Yearly
</div>
</label>
</div>
</div>,
]
`;
exports[`renders components/segmented/demo/controlled.tsx correctly 1`] = `

View File

@@ -2,22 +2,41 @@ import React from 'react';
import { ConfigProvider, Segmented } from 'antd';
const Demo: React.FC = () => (
<ConfigProvider
theme={{
components: {
Segmented: {
itemColor: '#222',
itemHoverColor: '#333',
itemHoverBg: 'rgba(0, 0, 0, 0.06)',
itemSelectedBg: '#aaa',
itemActiveBg: '#ccc',
itemSelectedColor: '#fff',
<>
<ConfigProvider
theme={{
components: {
Segmented: {
itemColor: '#222',
itemHoverColor: '#333',
itemHoverBg: 'rgba(0, 0, 0, 0.06)',
itemSelectedBg: '#aaa',
itemActiveBg: '#ccc',
itemSelectedColor: '#fff',
},
},
},
}}
>
<Segmented options={['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']} />
</ConfigProvider>
}}
>
<Segmented options={['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']} />
</ConfigProvider>
&nbsp;&nbsp;
<ConfigProvider
theme={{
components: {
Segmented: {
itemColor: '#222',
itemHoverColor: '#333',
itemHoverBg: 'rgba(0, 0, 0, 0.06)',
itemSelectedBg: 'linear-gradient(225deg, #c200ff 0%, #00ffff 100%)',
itemActiveBg: '#ccc',
itemSelectedColor: '#fff',
},
},
}}
>
<Segmented options={['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']} />
</ConfigProvider>
</>
);
export default Demo;

View File

@@ -65,7 +65,7 @@ function getItemDisabledStyle(cls: string, token: SegmentedToken): CSSObject {
function getItemSelectedStyle(token: SegmentedToken): CSSObject {
return {
backgroundColor: token.itemSelectedBg,
background: token.itemSelectedBg,
boxShadow: token.boxShadowTertiary,
};
}

View File

@@ -136,7 +136,7 @@ const Compact: React.FC<SpaceCompactProps> = (props) => {
</CompactItem>
);
}),
[size, childNodes, compactItemContext],
[childNodes, compactItemContext, direction, mergedSize, prefixCls],
);
// =========================== Render ===========================

View File

@@ -161,7 +161,7 @@ const Statistic = React.forwardRef<StatisticRef, StatisticProps>((props, ref) =>
</div>
</div>
)}
<Skeleton paragraph={false} loading={loading} className={`${prefixCls}-skeleton`}>
<Skeleton paragraph={false} loading={loading} className={`${prefixCls}-skeleton`} active>
<div className={contentClassNames} style={{ ...valueStyle, ...mergedStyles.content }}>
{prefix && (
<span className={prefixClassNames} style={mergedStyles.prefix}>

View File

@@ -166,7 +166,7 @@ exports[`renders components/statistic/demo/basic.tsx extend context correctly 1`
</div>
</div>
<div
class="ant-skeleton ant-statistic-skeleton css-var-test-id"
class="ant-skeleton ant-skeleton-active ant-statistic-skeleton css-var-test-id"
>
<div
class="ant-skeleton-section"
@@ -437,7 +437,7 @@ exports[`renders components/statistic/demo/component-token.tsx extend context co
</div>
</div>
<div
class="ant-skeleton ant-statistic-skeleton css-var-test-id"
class="ant-skeleton ant-skeleton-active ant-statistic-skeleton css-var-test-id"
>
<div
class="ant-skeleton-section"

View File

@@ -160,7 +160,7 @@ exports[`renders components/statistic/demo/basic.tsx correctly 1`] = `
</div>
</div>
<div
class="ant-skeleton ant-statistic-skeleton css-var-test-id"
class="ant-skeleton ant-skeleton-active ant-statistic-skeleton css-var-test-id"
>
<div
class="ant-skeleton-section"
@@ -427,7 +427,7 @@ exports[`renders components/statistic/demo/component-token.tsx correctly 1`] = `
</div>
</div>
<div
class="ant-skeleton ant-statistic-skeleton css-var-test-id"
class="ant-skeleton ant-skeleton-active ant-statistic-skeleton css-var-test-id"
>
<div
class="ant-skeleton-section"

View File

@@ -39,7 +39,9 @@ const App: React.FC = () => {
};
const remove = (targetKey: TargetKey) => {
if (!items) return;
if (!items) {
return;
}
const targetIndex = items.findIndex((item) => item.key === targetKey);
const newItems = items.filter((item) => item.key !== targetKey);

View File

@@ -0,0 +1,104 @@
import * as React from 'react';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import classNames from 'classnames';
import type { TimelineProps } from './Timeline';
import TimelineItem from './TimelineItem';
import type { TimelineItemProps } from './TimelineItem';
const TimelineItemList: React.FC<TimelineProps & { hashId: string; direction?: string }> = ({
prefixCls,
className,
pending = false,
children,
items,
rootClassName,
reverse = false,
direction,
hashId,
pendingDot,
mode = '' as TimelineProps['mode'],
...restProps
}) => {
const getPositionCls = (position: string, idx: number) => {
if (mode === 'alternate') {
if (position === 'right') {
return `${prefixCls}-item-right`;
}
if (position === 'left') {
return `${prefixCls}-item-left`;
}
return idx % 2 === 0 ? `${prefixCls}-item-left` : `${prefixCls}-item-right`;
}
if (mode === 'left') {
return `${prefixCls}-item-left`;
}
if (mode === 'right') {
return `${prefixCls}-item-right`;
}
if (position === 'right') {
return `${prefixCls}-item-right`;
}
return '';
};
const mergedItems = [...(items || [])];
const pendingNode = typeof pending === 'boolean' ? null : pending;
if (pending) {
mergedItems.push({
pending: !!pending,
dot: pendingDot || <LoadingOutlined />,
children: pendingNode,
});
}
if (reverse) {
mergedItems.reverse();
}
const itemsCount = mergedItems.length;
const lastCls = `${prefixCls}-item-last`;
const itemsList = mergedItems
.filter((item: TimelineItemProps) => !!item)
.map((item: TimelineItemProps, idx: number) => {
const pendingClass = idx === itemsCount - 2 ? lastCls : '';
const readyClass = idx === itemsCount - 1 ? lastCls : '';
const { className: itemClassName, ...itemProps } = item;
return (
<TimelineItem
{...itemProps}
className={classNames([
itemClassName,
!reverse && !!pending ? pendingClass : readyClass,
getPositionCls(item?.position ?? '', idx),
])}
key={item?.key || idx}
/>
);
});
const hasLabelItem = mergedItems.some((item: TimelineItemProps) => !!item?.label);
const classString = classNames(
prefixCls,
{
[`${prefixCls}-pending`]: !!pending,
[`${prefixCls}-reverse`]: !!reverse,
[`${prefixCls}-${mode}`]: !!mode && !hasLabelItem,
[`${prefixCls}-label`]: hasLabelItem,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
rootClassName,
hashId,
);
return (
<ol {...restProps} className={classString}>
{itemsList}
</ol>
);
};
export default TimelineItemList;

View File

@@ -76,8 +76,9 @@ const Editable: React.FC<EditableProps> = (props) => {
const onKeyDown: React.KeyboardEventHandler<HTMLTextAreaElement> = ({ keyCode }) => {
// We don't record keyCode when IME is using
if (inComposition.current) return;
if (inComposition.current) {
return;
}
lastKeyCode.current = keyCode;
};

View File

@@ -73,7 +73,7 @@ The `defaultXxxx` (e.g. `defaultValue`) of `Input`/`Select`(etc...) only works o
## Why does modifying props in mutable way not trigger a component update?
antd use shallow compare of props to optimize performance. You should always pass the new object when updating the state. Please ref [React's document](https://reactjs.org/docs/thinking-in-react.html)
antd use shallow compare of props to optimize performance. You should always pass the new object when updating the state. Please ref [React's document](https://react.dev/learn/thinking-in-react)
## After I set the `value` of an `Input`/`Select`(etc.) component, the value cannot be changed by user's action.
@@ -181,7 +181,7 @@ Static methods like message/notification/Modal.confirm are not using the same re
## Why shouldn't I use component internal props or state with ref?
You should only access the API by official doc with ref. Directly access internal `props` or `state` is not recommended which will make your code strong coupling with current version. Any refactor will break your code like refactor with [Hooks](https://reactjs.org/docs/hooks-intro.html) version, delete or rename internal `props` or `state`, adjust internal node constructor, etc.
You should only access the API by official doc with ref. Directly access internal `props` or `state` is not recommended which will make your code strong coupling with current version. Any refactor will break your code like refactor with [Hooks](https://react.dev/reference/react/hooks) version, delete or rename internal `props` or `state`, adjust internal node constructor, etc.
<div id="why-open"></div>

View File

@@ -73,7 +73,7 @@ antd 在 minor 和 patch 版本迭代中会避免引入破坏性变更,遵从
## 为什么修改组件传入的对象或数组属性组件不会更新?
antd 内部会对 props 进行浅比较实现性能优化。当状态变更,你总是应该传递一个新的对象。具体请参考 [React 的文档](https://zh-hans.reactjs.org/docs/thinking-in-react.html)
antd 内部会对 props 进行浅比较实现性能优化。当状态变更,你总是应该传递一个新的对象。具体请参考 [React 的文档](https://zh-hans.react.dev/learn/thinking-in-react)
## 当我设置了 `Input`/`Select` 等的 `value` 时它就无法修改了。
@@ -204,7 +204,7 @@ message/notification/Modal.confirm 等静态方法不同于 `<Button />` 的渲
## 为什么我不应该通过 ref 访问组件内部的 props 和 state
你通过 ref 获得引用时只应该使用文档提供的方法。直接读取组件内部的 `props``state` 不是一个好的设计,这会使你的代码与组件版本强耦合。任何重构都可能会使你的代码无法工作,其中重构包括且不仅限于改造成 [Hooks](https://reactjs.org/docs/hooks-intro.html) 版本、移除 / 更名内部 `props``state`、调整内部 React 节点结构等等。
你通过 ref 获得引用时只应该使用文档提供的方法。直接读取组件内部的 `props``state` 不是一个好的设计,这会使你的代码与组件版本强耦合。任何重构都可能会使你的代码无法工作,其中重构包括且不仅限于改造成 [Hooks](https://zh-hans.react.dev/reference/react/hooks) 版本、移除 / 更名内部 `props``state`、调整内部 React 节点结构等等。
<div id="why-open"></div>

View File

@@ -249,7 +249,9 @@ export default function Page() {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
if (productsQuery.isLoading) return null;
if (productsQuery.isLoading) {
return null;
}
return (
<div>
<h1 className={styles.title}>Page products</h1>

View File

@@ -249,7 +249,9 @@ export default function Page() {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
if (productsQuery.isLoading) return null;
if (productsQuery.isLoading) {
return null;
}
return (
<div>
<h1 className={styles.title}>Page products</h1>

View File

@@ -30,7 +30,7 @@ We provide comprehensive design guidelines, best practices, resources, and tools
## Front-end Implementation
[React](http://facebook.github.io/react/) is used to encapsulate a library of components which embody our design language. We welcome the community to implement [our design system](/docs/spec/introduce) in other front-end frameworks of their choice.
[React](https://react.dev/) is used to encapsulate a library of components which embody our design language. We welcome the community to implement [our design system](/docs/spec/introduce) in other front-end frameworks of their choice.
- [Ant Design of React](/docs/react/introduce)official implementation
- [NG-ZORRO - Ant Design of Angular](http://ng.ant.design)

View File

@@ -30,7 +30,7 @@ title: 介绍
## 前端实现
我们采用 [React](https://reactjs.org) 封装了一套 Ant Design 的组件库,也欢迎社区其他框架的实现版本。
我们采用 [React](https://zh-hans.react.dev/) 封装了一套 Ant Design 的组件库,也欢迎社区其他框架的实现版本。
- [Ant Design of React](/docs/react/introduce)(官方实现)
- [NG-ZORRO - Ant Design of Angular](http://ng.ant.design)(社区实现)