diff --git a/components/cascader/Panel.tsx b/components/cascader/Panel.tsx new file mode 100644 index 0000000000..d9f51a6f2b --- /dev/null +++ b/components/cascader/Panel.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import { Panel } from 'rc-cascader'; +import type { PickType } from 'rc-cascader/lib/Panel'; + +import type { CascaderProps } from '.'; +import DefaultRenderEmpty from '../config-provider/defaultRenderEmpty'; +import useBase from './hooks/useBase'; +import useCheckable from './hooks/useCheckable'; +import useColumnIcons from './hooks/useColumnIcons'; +import useStyle from './style'; +import usePanelStyle from './style/panel'; + +export type PanelPickType = Exclude | 'multiple' | 'rootClassName'; + +export type CascaderPanelProps = Pick; + +export default function CascaderPanel(props: CascaderPanelProps) { + const { + prefixCls: customizePrefixCls, + className, + multiple, + rootClassName, + notFoundContent, + direction, + expandIcon, + } = props; + + const [prefixCls, cascaderPrefixCls, mergedDirection, renderEmpty] = useBase( + customizePrefixCls, + direction, + ); + + const [, hashId] = useStyle(cascaderPrefixCls); + usePanelStyle(cascaderPrefixCls); + + const isRtl = mergedDirection === 'rtl'; + + // ===================== Icon ====================== + const [mergedExpandIcon, loadingIcon] = useColumnIcons(prefixCls, isRtl, expandIcon); + + // ===================== Empty ===================== + const mergedNotFoundContent = notFoundContent || renderEmpty?.('Cascader') || ( + + ); + + // =================== Multiple ==================== + const checkable = useCheckable(cascaderPrefixCls, multiple); + + // ==================== Render ===================== + + return ( + + ); +} diff --git a/components/cascader/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/cascader/__tests__/__snapshots__/demo-extend.test.ts.snap index fe73c8645a..2361569fe4 100644 --- a/components/cascader/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/cascader/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -1729,6 +1729,248 @@ exports[`renders components/cascader/demo/multiple.tsx extend context correctly exports[`renders components/cascader/demo/multiple.tsx extend context correctly 2`] = `[]`; +exports[`renders components/cascader/demo/panel.tsx extend context correctly 1`] = ` +
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + + + + + + + + +
+
+ No data +
+
+
+
+`; + +exports[`renders components/cascader/demo/panel.tsx extend context correctly 2`] = `[]`; + exports[`renders components/cascader/demo/placement.tsx extend context correctly 1`] = ` Array [
`; +exports[`renders components/cascader/demo/panel.tsx correctly 1`] = ` +
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + + + + + + + + +
+
+ No data +
+
+
+
+`; + exports[`renders components/cascader/demo/placement.tsx correctly 1`] = ` Array [
{ + console.log(value); +}; + +const App: React.FC = () => ( + + + + + +); + +export default App; diff --git a/components/cascader/hooks/useBase.ts b/components/cascader/hooks/useBase.ts new file mode 100644 index 0000000000..96e1706626 --- /dev/null +++ b/components/cascader/hooks/useBase.ts @@ -0,0 +1,22 @@ +import * as React from 'react'; + +import { ConfigContext, type RenderEmptyHandler } from '../../config-provider'; + +export default function useBase( + customizePrefixCls?: string, + direction?: 'ltr' | 'rtl', +): [ + prefixCls: string, + cascaderPrefixCls: string, + direction?: 'ltr' | 'rtl', + renderEmpty?: RenderEmptyHandler, +] { + const { getPrefixCls, direction: rootDirection, renderEmpty } = React.useContext(ConfigContext); + + const mergedDirection = direction || rootDirection; + + const prefixCls = getPrefixCls('select', customizePrefixCls); + const cascaderPrefixCls = getPrefixCls('cascader', customizePrefixCls); + + return [prefixCls, cascaderPrefixCls, mergedDirection, renderEmpty]; +} diff --git a/components/cascader/hooks/useCheckable.tsx b/components/cascader/hooks/useCheckable.tsx new file mode 100644 index 0000000000..bb47be504d --- /dev/null +++ b/components/cascader/hooks/useCheckable.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; + +export default function useCheckable(cascaderPrefixCls: string, multiple?: boolean) { + return React.useMemo( + () => (multiple ? : false), + [multiple], + ); +} diff --git a/components/cascader/hooks/useColumnIcons.tsx b/components/cascader/hooks/useColumnIcons.tsx new file mode 100644 index 0000000000..bce3b82a5e --- /dev/null +++ b/components/cascader/hooks/useColumnIcons.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import LeftOutlined from '@ant-design/icons/LeftOutlined'; +import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; +import RightOutlined from '@ant-design/icons/RightOutlined'; + +export default function useColumnIcons( + prefixCls: string, + rtl: boolean, + expandIcon?: React.ReactNode, +) { + let mergedExpandIcon = expandIcon; + if (!expandIcon) { + mergedExpandIcon = rtl ? : ; + } + + const loadingIcon = ( + + + + ); + + return [mergedExpandIcon, loadingIcon]; +} diff --git a/components/cascader/index.en-US.md b/components/cascader/index.en-US.md index 2993186b80..dbe1a55134 100644 --- a/components/cascader/index.en-US.md +++ b/components/cascader/index.en-US.md @@ -36,6 +36,7 @@ Cascade selection box. Custom dropdown Placement Status +Panel _InternalPanelDoNotUseOrYouWillBeFired ## API diff --git a/components/cascader/index.tsx b/components/cascader/index.tsx index ee85219d14..50b680400c 100644 --- a/components/cascader/index.tsx +++ b/components/cascader/index.tsx @@ -1,7 +1,4 @@ import * as React from 'react'; -import LeftOutlined from '@ant-design/icons/LeftOutlined'; -import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; -import RightOutlined from '@ant-design/icons/RightOutlined'; import classNames from 'classnames'; import type { BaseOptionType, @@ -29,9 +26,13 @@ import type { SizeType } from '../config-provider/SizeContext'; import { FormItemInputContext } from '../form/context'; import useSelectStyle from '../select/style'; import useBuiltinPlacements from '../select/useBuiltinPlacements'; -import useShowArrow from '../select/useShowArrow'; import useIcons from '../select/useIcons'; +import useShowArrow from '../select/useShowArrow'; import { useCompactItemContext } from '../space/Compact'; +import useBase from './hooks/useBase'; +import useCheckable from './hooks/useCheckable'; +import useColumnIcons from './hooks/useColumnIcons'; +import CascaderPanel from './Panel'; import useStyle from './style'; // Align the design since we use `rc-select` in root. This help: @@ -174,15 +175,10 @@ const Cascader = React.forwardRef>((props, ref) const { getPopupContainer: getContextPopupContainer, getPrefixCls, - renderEmpty, - direction: rootDirection, popupOverflow, cascader, } = React.useContext(ConfigContext); - const mergedDirection = direction || rootDirection; - const isRtl = mergedDirection === 'rtl'; - // =================== Form ===================== const { status: contextStatus, @@ -205,20 +201,25 @@ const Cascader = React.forwardRef>((props, ref) ); } - // =================== No Found ==================== - const mergedNotFoundContent = notFoundContent || renderEmpty?.('Cascader') || ( - - ); - // ==================== Prefix ===================== + const [prefixCls, cascaderPrefixCls, mergedDirection, renderEmpty] = useBase( + customizePrefixCls, + direction, + ); + const isRtl = mergedDirection === 'rtl'; + const rootPrefixCls = getPrefixCls(); - const prefixCls = getPrefixCls('select', customizePrefixCls); - const cascaderPrefixCls = getPrefixCls('cascader', customizePrefixCls); const [wrapSelectSSR, hashId] = useSelectStyle(prefixCls); const [wrapCascaderSSR] = useStyle(cascaderPrefixCls); const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction); + + // =================== No Found ==================== + const mergedNotFoundContent = notFoundContent || renderEmpty?.('Cascader') || ( + + ); + // =================== Dropdown ==================== const mergedDropdownClassName = classNames( popupClassName || dropdownClassName, @@ -258,22 +259,10 @@ const Cascader = React.forwardRef>((props, ref) const mergedDisabled = customDisabled ?? disabled; // ===================== Icon ====================== - let mergedExpandIcon = expandIcon; - if (!expandIcon) { - mergedExpandIcon = isRtl ? : ; - } - - const loadingIcon = ( - - - - ); + const [mergedExpandIcon, loadingIcon] = useColumnIcons(prefixCls, isRtl, expandIcon); // =================== Multiple ==================== - const checkable = React.useMemo( - () => (multiple ? : false), - [multiple], - ); + const checkable = useCheckable(cascaderPrefixCls, multiple); // ===================== Icons ===================== const showSuffixIcon = useShowArrow(props.suffixIcon, showArrow); @@ -349,6 +338,7 @@ const Cascader = React.forwardRef>((props, ref) displayName: string; SHOW_PARENT: typeof SHOW_PARENT; SHOW_CHILD: typeof SHOW_CHILD; + Panel: typeof CascaderPanel; _InternalPanelDoNotUseOrYouWillBeFired: typeof PurePanel; }; if (process.env.NODE_ENV !== 'production') { @@ -361,6 +351,7 @@ const PurePanel = genPurePanel(Cascader); Cascader.SHOW_PARENT = SHOW_PARENT; Cascader.SHOW_CHILD = SHOW_CHILD; +Cascader.Panel = CascaderPanel; Cascader._InternalPanelDoNotUseOrYouWillBeFired = PurePanel; export default Cascader; diff --git a/components/cascader/index.zh-CN.md b/components/cascader/index.zh-CN.md index d41debe236..6dd6d25215 100644 --- a/components/cascader/index.zh-CN.md +++ b/components/cascader/index.zh-CN.md @@ -37,6 +37,7 @@ demo: 扩展菜单 弹出位置 自定义状态 +面板使用 _InternalPanelDoNotUseOrYouWillBeFired ## API diff --git a/components/cascader/style/columns.ts b/components/cascader/style/columns.ts new file mode 100644 index 0000000000..32a5310502 --- /dev/null +++ b/components/cascader/style/columns.ts @@ -0,0 +1,119 @@ +import type { CSSInterpolation } from '@ant-design/cssinjs'; + +import type { CascaderToken } from '.'; +import { getStyle as getCheckboxStyle } from '../../checkbox/style'; +import { textEllipsis } from '../../style'; +import type { GenerateStyle } from '../../theme/internal'; + +const getColumnsStyle: GenerateStyle = (token: CascaderToken): CSSInterpolation => { + const { prefixCls, componentCls } = token; + + const cascaderMenuItemCls = `${componentCls}-menu-item`; + const iconCls = ` + &${cascaderMenuItemCls}-expand ${cascaderMenuItemCls}-expand-icon, + ${cascaderMenuItemCls}-loading-icon +`; + + return [ + // ==================== Checkbox ==================== + getCheckboxStyle(`${prefixCls}-checkbox`, token), + + { + [componentCls]: { + // ================== Checkbox ================== + '&-checkbox': { + top: 0, + marginInlineEnd: token.paddingXS, + }, + + // ==================== Menu ==================== + // >>> Menus + '&-menus': { + display: 'flex', + flexWrap: 'nowrap', + alignItems: 'flex-start', + + [`&${componentCls}-menu-empty`]: { + [`${componentCls}-menu`]: { + width: '100%', + height: 'auto', + + [cascaderMenuItemCls]: { + color: token.colorTextDisabled, + }, + }, + }, + }, + + // >>> Menu + '&-menu': { + flexGrow: 1, + flexShrink: 0, + minWidth: token.controlItemWidth, + height: token.dropdownHeight, + margin: 0, + padding: token.menuPadding, + overflow: 'auto', + verticalAlign: 'top', + listStyle: 'none', + '-ms-overflow-style': '-ms-autohiding-scrollbar', // https://github.com/ant-design/ant-design/issues/11857 + + '&:not(:last-child)': { + borderInlineEnd: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`, + }, + + '&-item': { + ...textEllipsis, + display: 'flex', + flexWrap: 'nowrap', + alignItems: 'center', + padding: token.optionPadding, + lineHeight: token.lineHeight, + cursor: 'pointer', + transition: `all ${token.motionDurationMid}`, + borderRadius: token.borderRadiusSM, + + '&:hover': { + background: token.controlItemBgHover, + }, + '&-disabled': { + color: token.colorTextDisabled, + cursor: 'not-allowed', + + '&:hover': { + background: 'transparent', + }, + + [iconCls]: { + color: token.colorTextDisabled, + }, + }, + + [`&-active:not(${cascaderMenuItemCls}-disabled)`]: { + [`&, &:hover`]: { + fontWeight: token.optionSelectedFontWeight, + backgroundColor: token.optionSelectedBg, + }, + }, + + '&-content': { + flex: 'auto', + }, + + [iconCls]: { + marginInlineStart: token.paddingXXS, + color: token.colorTextDescription, + fontSize: token.fontSizeIcon, + }, + + '&-keyword': { + color: token.colorHighlight, + }, + }, + }, + }, + }, + ]; +}; + +export default getColumnsStyle; diff --git a/components/cascader/style/index.ts b/components/cascader/style/index.ts index fd9f46bf9e..3e25c9f0d5 100644 --- a/components/cascader/style/index.ts +++ b/components/cascader/style/index.ts @@ -1,9 +1,10 @@ import type { CSSProperties } from 'react'; -import { getStyle as getCheckboxStyle } from '../../checkbox/style'; -import { textEllipsis } from '../../style'; + import { genCompactItemStyle } from '../../style/compact-item'; +import type { GlobalToken } from '../../theme'; import type { FullToken, GenerateStyle } from '../../theme/internal'; import { genComponentStyleHook } from '../../theme/internal'; +import getColumnsStyle from './columns'; export interface ComponentToken { /** @@ -43,16 +44,11 @@ export interface ComponentToken { menuPadding: CSSProperties['padding']; } -type CascaderToken = FullToken<'Cascader'>; +export type CascaderToken = FullToken<'Cascader'>; // =============================== Base =============================== const genBaseStyle: GenerateStyle = (token) => { - const { prefixCls, componentCls, antCls } = token; - const cascaderMenuItemCls = `${componentCls}-menu-item`; - const iconCls = ` - &${cascaderMenuItemCls}-expand ${cascaderMenuItemCls}-expand-icon, - ${cascaderMenuItemCls}-loading-icon - `; + const { componentCls, antCls } = token; return [ // ===================================================== @@ -68,107 +64,12 @@ const genBaseStyle: GenerateStyle = (token) => { // ===================================================== { [`${componentCls}-dropdown`]: [ - // ==================== Checkbox ==================== - getCheckboxStyle(`${prefixCls}-checkbox`, token), { [`&${antCls}-select-dropdown`]: { padding: 0, }, }, - { - [componentCls]: { - // ================== Checkbox ================== - '&-checkbox': { - top: 0, - marginInlineEnd: token.paddingXS, - }, - - // ==================== Menu ==================== - // >>> Menus - '&-menus': { - display: 'flex', - flexWrap: 'nowrap', - alignItems: 'flex-start', - - [`&${componentCls}-menu-empty`]: { - [`${componentCls}-menu`]: { - width: '100%', - height: 'auto', - - [cascaderMenuItemCls]: { - color: token.colorTextDisabled, - }, - }, - }, - }, - - // >>> Menu - '&-menu': { - flexGrow: 1, - minWidth: token.controlItemWidth, - height: token.dropdownHeight, - margin: 0, - padding: token.menuPadding, - overflow: 'auto', - verticalAlign: 'top', - listStyle: 'none', - '-ms-overflow-style': '-ms-autohiding-scrollbar', // https://github.com/ant-design/ant-design/issues/11857 - - '&:not(:last-child)': { - borderInlineEnd: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`, - }, - - '&-item': { - ...textEllipsis, - display: 'flex', - flexWrap: 'nowrap', - alignItems: 'center', - padding: token.optionPadding, - lineHeight: token.lineHeight, - cursor: 'pointer', - transition: `all ${token.motionDurationMid}`, - borderRadius: token.borderRadiusSM, - - '&:hover': { - background: token.controlItemBgHover, - }, - '&-disabled': { - color: token.colorTextDisabled, - cursor: 'not-allowed', - - '&:hover': { - background: 'transparent', - }, - - [iconCls]: { - color: token.colorTextDisabled, - }, - }, - - [`&-active:not(${cascaderMenuItemCls}-disabled)`]: { - [`&, &:hover`]: { - fontWeight: token.optionSelectedFontWeight, - backgroundColor: token.optionSelectedBg, - }, - }, - - '&-content': { - flex: 'auto', - }, - - [iconCls]: { - marginInlineStart: token.paddingXXS, - color: token.colorTextDescription, - fontSize: token.fontSizeIcon, - }, - - '&-keyword': { - color: token.colorHighlight, - }, - }, - }, - }, - }, + getColumnsStyle(token), ], }, // ===================================================== @@ -187,22 +88,24 @@ const genBaseStyle: GenerateStyle = (token) => { }; // ============================== Export ============================== +export const prepareComponentToken = (token: GlobalToken) => { + const itemPaddingVertical = Math.round( + (token.controlHeight - token.fontSize * token.lineHeight) / 2, + ); + + return { + controlWidth: 184, + controlItemWidth: 111, + dropdownHeight: 180, + optionSelectedBg: token.controlItemBgActive, + optionSelectedFontWeight: token.fontWeightStrong, + optionPadding: `${itemPaddingVertical}px ${token.paddingSM}px`, + menuPadding: token.paddingXXS, + }; +}; + export default genComponentStyleHook( 'Cascader', (token) => [genBaseStyle(token)], - (token) => { - const itemPaddingVertical = Math.round( - (token.controlHeight - token.fontSize * token.lineHeight) / 2, - ); - - return { - controlWidth: 184, - controlItemWidth: 111, - dropdownHeight: 180, - optionSelectedBg: token.controlItemBgActive, - optionSelectedFontWeight: token.fontWeightStrong, - optionPadding: `${itemPaddingVertical}px ${token.paddingSM}px`, - menuPadding: token.paddingXXS, - }; - }, + prepareComponentToken, ); diff --git a/components/cascader/style/panel.ts b/components/cascader/style/panel.ts new file mode 100644 index 0000000000..1e12c8e0f9 --- /dev/null +++ b/components/cascader/style/panel.ts @@ -0,0 +1,41 @@ +import type { CSSObject } from '@ant-design/cssinjs'; + +import { prepareComponentToken, type CascaderToken } from '.'; +import { genComponentStyleHook, type GenerateStyle } from '../../theme/internal'; +import getColumnsStyle from './columns'; + +// ============================== Panel =============================== +const genPanelStyle: GenerateStyle = (token: CascaderToken): CSSObject => { + const { componentCls } = token; + + return { + [`${componentCls}-panel`]: [ + getColumnsStyle(token), + { + display: 'inline-flex', + border: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`, + borderRadius: token.borderRadiusLG, + overflowX: 'auto', + maxWidth: '100%', + + [`${componentCls}-menus`]: { + alignItems: 'stretch', + }, + [`${componentCls}-menu`]: { + height: 'auto', + }, + + '&-empty': { + padding: token.paddingXXS, + }, + }, + ], + }; +}; + +// ============================== Export ============================== +export default genComponentStyleHook( + ['Cascader', 'Panel'], + (token) => genPanelStyle(token), + prepareComponentToken, +); diff --git a/package.json b/package.json index 8f3f86b404..adf0bf9b7c 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "copy-to-clipboard": "^3.2.0", "dayjs": "^1.11.1", "qrcode.react": "^3.1.0", - "rc-cascader": "~3.17.0", + "rc-cascader": "~3.18.1", "rc-checkbox": "~3.1.0", "rc-collapse": "~3.7.1", "rc-dialog": "~9.3.3",