feat: Menu support CSS Var (#8323)

Co-authored-by: songsichen <songsichen@qianxin.com>
feat-4.3
Shuhari 2025-08-23 09:16:56 +08:00 committed by GitHub
parent c9443e038b
commit 0410908cd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 847 additions and 281 deletions

View File

@ -43,6 +43,7 @@ import type { ItemType } from './hooks/useItems';
import useItems from './hooks/useItems';
import useStyle from '../style';
import { useInjectOverride } from './OverrideContext';
import useCSSVarCls from '../../config-provider/hooks/useCssVarCls';
export const menuProps = () => ({
id: String,
@ -106,8 +107,10 @@ export default defineComponent({
const prefixCls = computed(() => {
return getPrefixCls('menu', props.prefixCls || override?.prefixCls?.value);
});
const [wrapSSR, hashId] = useStyle(
const rootCls = useCSSVarCls(prefixCls);
const [wrapSSR, hashId, cssVarCls] = useStyle(
prefixCls,
rootCls,
computed(() => {
return !override;
}),
@ -336,6 +339,8 @@ export default defineComponent({
[`${prefixCls.value}-inline-collapsed`]: mergedInlineCollapsed.value,
[`${prefixCls.value}-rtl`]: isRtl.value,
[`${prefixCls.value}-${props.theme}`]: true,
[rootCls.value]: true,
[cssVarCls.value]: true,
};
});
const rootPrefixCls = computed(() => getPrefixCls());
@ -411,6 +416,7 @@ export default defineComponent({
);
useProvideMenu({
prefixCls,
cssVarCls: computed(() => `${rootCls.value} ${cssVarCls.value}`),
activeKeys,
openKeys: mergedOpenKeys,
selectedKeys: mergedSelectedKeys,

View File

@ -103,6 +103,7 @@ export default defineComponent({
const {
prefixCls,
cssVarCls,
activeKeys,
disabled: contextDisabled,
changeActiveKeys,
@ -210,6 +211,7 @@ export default defineComponent({
const popupClassName = computed(() =>
classNames(
cssVarCls.value,
prefixCls.value,
`${prefixCls.value}-${props.theme || theme.value}`,
props.popupClassName,

View File

@ -23,6 +23,7 @@ export interface MenuContextProps {
registerMenuInfo: (key: string, info: StoreMenuInfo) => void;
unRegisterMenuInfo: (key: string) => void;
prefixCls: ComputedRef<string>;
cssVarCls: ComputedRef<string>;
openKeys: Ref<Key[]>;
selectedKeys: Ref<Key[]>;

View File

@ -1,3 +1,4 @@
import { unit } from '../../_util/cssinjs';
import type { MenuToken } from '.';
import type { GenerateStyle } from '../../theme/internal';
@ -5,18 +6,18 @@ const getHorizontalStyle: GenerateStyle<MenuToken> = token => {
const {
componentCls,
motionDurationSlow,
menuHorizontalHeight,
horizontalLineHeight,
colorSplit,
lineWidth,
lineType,
menuItemPaddingInline,
itemPaddingInline,
} = token;
return {
[`${componentCls}-horizontal`]: {
lineHeight: `${menuHorizontalHeight}px`,
lineHeight: horizontalLineHeight,
border: 0,
borderBottom: `${lineWidth}px ${lineType} ${colorSplit}`,
borderBottom: `${unit(lineWidth)} ${lineType} ${colorSplit}`,
boxShadow: 'none',
'&::after': {
@ -31,7 +32,7 @@ const getHorizontalStyle: GenerateStyle<MenuToken> = token => {
position: 'relative',
display: 'inline-block',
verticalAlign: 'bottom',
paddingInline: menuItemPaddingInline,
paddingInline: itemPaddingInline,
},
[`> ${componentCls}-item:hover,

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,14 @@
import { unit } from '../../_util/cssinjs';
import type { CSSUtil } from '../../theme/util/genComponentStyleHook';
import type { MenuToken } from '.';
import type { GenerateStyle } from '../../theme/internal';
const getRTLStyle: GenerateStyle<MenuToken> = ({ componentCls, menuArrowOffset }) => ({
const getRTLStyle: GenerateStyle<MenuToken & CSSUtil> = ({
componentCls,
menuArrowOffset,
calc,
}) => ({
[`${componentCls}-rtl`]: {
direction: 'rtl',
},
@ -15,11 +22,11 @@ const getRTLStyle: GenerateStyle<MenuToken> = ({ componentCls, menuArrowOffset }
${componentCls}-submenu-rtl ${componentCls}-vertical`]: {
[`${componentCls}-submenu-arrow`]: {
'&::before': {
transform: `rotate(-45deg) translateY(-${menuArrowOffset})`,
transform: `rotate(-45deg) translateY(${unit(calc(menuArrowOffset).mul(-1).equal())})`,
},
'&::after': {
transform: `rotate(45deg) translateY(${menuArrowOffset})`,
transform: `rotate(45deg) translateY(${unit(menuArrowOffset)})`,
},
},
},

View File

@ -1,6 +1,7 @@
import type { CSSInterpolation } from '../../_util/cssinjs';
import { genFocusOutline } from '../../style';
import { unit, CSSInterpolation } from '../../_util/cssinjs';
import type { MenuToken } from '.';
import { genFocusOutline } from '../../style';
const accessibilityFocus = (token: MenuToken) => ({
...genFocusOutline(token),
@ -9,108 +10,121 @@ const accessibilityFocus = (token: MenuToken) => ({
const getThemeStyle = (token: MenuToken, themeSuffix: string): CSSInterpolation => {
const {
componentCls,
colorItemText,
colorItemTextSelected,
colorGroupTitle,
colorItemBg,
colorSubItemBg,
colorItemBgSelected,
colorActiveBarHeight,
colorActiveBarWidth,
colorActiveBarBorderSize,
itemColor,
itemSelectedColor,
subMenuItemSelectedColor,
groupTitleColor,
itemBg,
subMenuItemBg,
itemSelectedBg,
activeBarHeight,
activeBarWidth,
activeBarBorderWidth,
motionDurationSlow,
motionEaseInOut,
motionEaseOut,
menuItemPaddingInline,
itemPaddingInline,
motionDurationMid,
colorItemTextHover,
itemHoverColor,
lineType,
colorSplit,
// Disabled
colorItemTextDisabled,
itemDisabledColor,
// Danger
colorDangerItemText,
colorDangerItemTextHover,
colorDangerItemTextSelected,
colorDangerItemBgActive,
colorDangerItemBgSelected,
colorItemBgHover,
dangerItemColor,
dangerItemHoverColor,
dangerItemSelectedColor,
dangerItemActiveBg,
dangerItemSelectedBg,
// Bg
popupBg,
itemHoverBg,
itemActiveBg,
menuSubMenuBg,
// Horizontal
colorItemTextSelectedHorizontal,
colorItemBgSelectedHorizontal,
horizontalItemSelectedColor,
horizontalItemSelectedBg,
horizontalItemBorderRadius,
horizontalItemHoverBg,
} = token;
return {
[`${componentCls}-${themeSuffix}`]: {
color: colorItemText,
background: colorItemBg,
[`${componentCls}-${themeSuffix}, ${componentCls}-${themeSuffix} > ${componentCls}`]: {
color: itemColor,
background: itemBg,
[`&${componentCls}-root:focus-visible`]: {
...accessibilityFocus(token),
},
// ======================== Item ========================
[`${componentCls}-item-group-title`]: {
color: colorGroupTitle,
[`${componentCls}-item`]: {
'&-group-title, &-extra': {
color: groupTitleColor,
},
},
[`${componentCls}-submenu-selected`]: {
[`> ${componentCls}-submenu-title`]: {
color: colorItemTextSelected,
[`${componentCls}-submenu-selected > ${componentCls}-submenu-title`]: {
color: subMenuItemSelectedColor,
},
[`${componentCls}-item, ${componentCls}-submenu-title`]: {
color: itemColor,
[`&:not(${componentCls}-item-disabled):focus-visible`]: {
...accessibilityFocus(token),
},
},
// Disabled
[`${componentCls}-item-disabled, ${componentCls}-submenu-disabled`]: {
color: `${colorItemTextDisabled} !important`,
color: `${itemDisabledColor} !important`,
},
// Hover
[`${componentCls}-item:hover, ${componentCls}-submenu-title:hover`]: {
[`&:not(${componentCls}-item-selected):not(${componentCls}-submenu-selected)`]: {
color: colorItemTextHover,
[`${componentCls}-item:not(${componentCls}-item-selected):not(${componentCls}-submenu-selected)`]:
{
[`&:hover, > ${componentCls}-submenu-title:hover`]: {
color: itemHoverColor,
},
},
},
[`&:not(${componentCls}-horizontal)`]: {
[`${componentCls}-item:not(${componentCls}-item-selected)`]: {
'&:hover': {
backgroundColor: colorItemBgHover,
backgroundColor: itemHoverBg,
},
'&:active': {
backgroundColor: colorItemBgSelected,
backgroundColor: itemActiveBg,
},
},
[`${componentCls}-submenu-title`]: {
'&:hover': {
backgroundColor: colorItemBgHover,
backgroundColor: itemHoverBg,
},
'&:active': {
backgroundColor: colorItemBgSelected,
backgroundColor: itemActiveBg,
},
},
},
// Danger - only Item has
[`${componentCls}-item-danger`]: {
color: colorDangerItemText,
color: dangerItemColor,
[`&${componentCls}-item:hover`]: {
[`&:not(${componentCls}-item-selected):not(${componentCls}-submenu-selected)`]: {
color: colorDangerItemTextHover,
color: dangerItemHoverColor,
},
},
[`&${componentCls}-item:active`]: {
background: colorDangerItemBgActive,
background: dangerItemActiveBg,
},
},
@ -121,30 +135,24 @@ const getThemeStyle = (token: MenuToken, themeSuffix: string): CSSInterpolation
},
[`${componentCls}-item-selected`]: {
color: colorItemTextSelected,
color: itemSelectedColor,
// Danger
[`&${componentCls}-item-danger`]: {
color: colorDangerItemTextSelected,
color: dangerItemSelectedColor,
},
[`a, a:hover`]: {
'a, a:hover': {
color: 'inherit',
},
},
[`& ${componentCls}-item-selected`]: {
backgroundColor: colorItemBgSelected,
backgroundColor: itemSelectedBg,
// Danger
[`&${componentCls}-item-danger`]: {
backgroundColor: colorDangerItemBgSelected,
},
},
[`${componentCls}-item, ${componentCls}-submenu-title`]: {
[`&:not(${componentCls}-item-disabled):focus-visible`]: {
...accessibilityFocus(token),
backgroundColor: dangerItemSelectedBg,
},
},
@ -152,10 +160,17 @@ const getThemeStyle = (token: MenuToken, themeSuffix: string): CSSInterpolation
backgroundColor: menuSubMenuBg,
},
// ===== 设置浮层的颜色 =======
// dark 模式会被popupBg 会被rest 为 darkPopupBg
[`&${componentCls}-popup > ${componentCls}`]: {
backgroundColor: colorItemBg,
backgroundColor: popupBg,
},
[`&${componentCls}-submenu-popup > ${componentCls}`]: {
backgroundColor: popupBg,
},
// ===== 设置浮层的颜色 end =======
// ====================== Horizontal ======================
[`&${componentCls}-horizontal`]: {
...(themeSuffix === 'dark'
@ -165,32 +180,36 @@ const getThemeStyle = (token: MenuToken, themeSuffix: string): CSSInterpolation
: {}),
[`> ${componentCls}-item, > ${componentCls}-submenu`]: {
top: colorActiveBarBorderSize,
marginTop: -colorActiveBarBorderSize,
top: activeBarBorderWidth,
marginTop: token.calc(activeBarBorderWidth).mul(-1).equal(),
marginBottom: 0,
borderRadius: 0,
borderRadius: horizontalItemBorderRadius,
'&::after': {
position: 'absolute',
insetInline: menuItemPaddingInline,
insetInline: itemPaddingInline,
bottom: 0,
borderBottom: `${colorActiveBarHeight}px solid transparent`,
borderBottom: `${unit(activeBarHeight)} solid transparent`,
transition: `border-color ${motionDurationSlow} ${motionEaseInOut}`,
content: '""',
},
[`&:hover, &-active, &-open`]: {
'&:hover, &-active, &-open': {
background: horizontalItemHoverBg,
'&::after': {
borderBottomWidth: colorActiveBarHeight,
borderBottomColor: colorItemTextSelectedHorizontal,
borderBottomWidth: activeBarHeight,
borderBottomColor: horizontalItemSelectedColor,
},
},
[`&-selected`]: {
color: colorItemTextSelectedHorizontal,
backgroundColor: colorItemBgSelectedHorizontal,
'&-selected': {
color: horizontalItemSelectedColor,
backgroundColor: horizontalItemSelectedBg,
'&:hover': {
backgroundColor: horizontalItemSelectedBg,
},
'&::after': {
borderBottomWidth: colorActiveBarHeight,
borderBottomColor: colorItemTextSelectedHorizontal,
borderBottomWidth: activeBarHeight,
borderBottomColor: horizontalItemSelectedColor,
},
},
},
@ -200,7 +219,7 @@ const getThemeStyle = (token: MenuToken, themeSuffix: string): CSSInterpolation
//
[`&${componentCls}-root`]: {
[`&${componentCls}-inline, &${componentCls}-vertical`]: {
borderInlineEnd: `${colorActiveBarBorderSize}px ${lineType} ${colorSplit}`,
borderInlineEnd: `${unit(activeBarBorderWidth)} ${lineType} ${colorSplit}`,
},
},
@ -208,17 +227,9 @@ const getThemeStyle = (token: MenuToken, themeSuffix: string): CSSInterpolation
[`&${componentCls}-inline`]: {
// Sub
[`${componentCls}-sub${componentCls}-inline`]: {
background: colorSubItemBg,
background: subMenuItemBg,
},
// Item
[`${componentCls}-item, ${componentCls}-submenu-title`]:
colorActiveBarBorderSize && colorActiveBarWidth
? {
width: `calc(100% + ${colorActiveBarBorderSize}px)`,
}
: {},
[`${componentCls}-item`]: {
position: 'relative',
@ -226,7 +237,7 @@ const getThemeStyle = (token: MenuToken, themeSuffix: string): CSSInterpolation
position: 'absolute',
insetBlock: 0,
insetInlineEnd: 0,
borderInlineEnd: `${colorActiveBarWidth}px solid ${colorItemTextSelected}`,
borderInlineEnd: `${unit(activeBarWidth)} solid ${itemSelectedColor}`,
transform: 'scaleY(0.0001)',
opacity: 0,
transition: [
@ -239,7 +250,7 @@ const getThemeStyle = (token: MenuToken, themeSuffix: string): CSSInterpolation
// Danger
[`&${componentCls}-item-danger`]: {
'&::after': {
borderInlineEndColor: colorDangerItemTextSelected,
borderInlineEndColor: dangerItemSelectedColor,
},
},
},

View File

@ -1,47 +1,44 @@
import type { CSSObject } from '../../_util/cssinjs';
import { textEllipsis } from '../../style';
import { unit, CSSObject } from '../../_util/cssinjs';
import type { MenuToken } from '.';
import { textEllipsis } from '../../style';
import type { GenerateStyle } from '../../theme/internal';
const getVerticalInlineStyle: GenerateStyle<MenuToken, CSSObject> = token => {
const {
componentCls,
menuItemHeight,
itemHeight,
itemMarginInline,
padding,
menuArrowSize,
marginXS,
marginXXS,
itemMarginBlock,
itemWidth,
itemPaddingInline,
} = token;
const paddingWithArrow = padding + menuArrowSize + marginXS;
const paddingWithArrow = token.calc(menuArrowSize).add(padding).add(marginXS).equal();
return {
[`${componentCls}-item`]: {
position: 'relative',
overflow: 'hidden',
},
[`${componentCls}-item, ${componentCls}-submenu-title`]: {
height: menuItemHeight,
lineHeight: `${menuItemHeight}px`,
paddingInline: padding,
height: itemHeight,
lineHeight: unit(itemHeight),
paddingInline: itemPaddingInline,
overflow: 'hidden',
textOverflow: 'ellipsis',
marginInline: itemMarginInline,
marginBlock: marginXXS,
width: `calc(100% - ${itemMarginInline * 2}px)`,
},
// disable margin collapsed
[`${componentCls}-submenu`]: {
paddingBottom: 0.02,
marginBlock: itemMarginBlock,
width: itemWidth,
},
[`> ${componentCls}-item,
> ${componentCls}-submenu > ${componentCls}-submenu-title`]: {
height: menuItemHeight,
lineHeight: `${menuItemHeight}px`,
height: itemHeight,
lineHeight: unit(itemHeight),
},
[`${componentCls}-item-group-list ${componentCls}-submenu-title,
@ -55,23 +52,25 @@ const getVerticalStyle: GenerateStyle<MenuToken> = token => {
const {
componentCls,
iconCls,
menuItemHeight,
itemHeight,
colorTextLightSolid,
dropdownWidth,
controlHeightLG,
motionDurationMid,
motionEaseOut,
paddingXL,
fontSizeSM,
itemMarginInline,
fontSizeLG,
motionDurationFast,
motionDurationSlow,
paddingXS,
boxShadowSecondary,
collapsedWidth,
collapsedIconSize,
} = token;
const inlineItemStyle: CSSObject = {
height: menuItemHeight,
lineHeight: `${menuItemHeight}px`,
height: itemHeight,
lineHeight: unit(itemHeight),
listStylePosition: 'inside',
listStyleType: 'disc',
};
@ -79,7 +78,7 @@ const getVerticalStyle: GenerateStyle<MenuToken> = token => {
return [
{
[componentCls]: {
[`&-inline, &-vertical`]: {
'&-inline, &-vertical': {
[`&${componentCls}-root`]: {
boxShadow: 'none',
},
@ -100,7 +99,7 @@ const getVerticalStyle: GenerateStyle<MenuToken> = token => {
{
[`${componentCls}-submenu-popup ${componentCls}-vertical${componentCls}-sub`]: {
minWidth: dropdownWidth,
maxHeight: `calc(100vh - ${controlHeightLG * 2.5}px)`,
maxHeight: `calc(100vh - ${unit(token.calc(controlHeightLG).mul(2.5).equal())})`,
padding: '0',
overflow: 'hidden',
borderInlineEnd: 0,
@ -127,7 +126,7 @@ const getVerticalStyle: GenerateStyle<MenuToken> = token => {
transition: [
`border-color ${motionDurationSlow}`,
`background ${motionDurationSlow}`,
`padding ${motionDurationMid} ${motionEaseOut}`,
`padding ${motionDurationFast} ${motionEaseOut}`,
].join(','),
[`> ${componentCls}-title-content`]: {
@ -165,7 +164,7 @@ const getVerticalStyle: GenerateStyle<MenuToken> = token => {
// Inline Collapse Only
{
[`${componentCls}-inline-collapsed`]: {
width: menuItemHeight * 2,
width: collapsedWidth,
[`&${componentCls}-root`]: {
[`${componentCls}-item, ${componentCls}-submenu ${componentCls}-submenu-title`]: {
@ -181,7 +180,9 @@ const getVerticalStyle: GenerateStyle<MenuToken> = token => {
> ${componentCls}-item-group > ${componentCls}-item-group-list > ${componentCls}-submenu > ${componentCls}-submenu-title,
> ${componentCls}-submenu > ${componentCls}-submenu-title`]: {
insetInlineStart: 0,
paddingInline: `calc(50% - ${fontSizeSM}px)`,
paddingInline: `calc(50% - ${unit(token.calc(collapsedIconSize).div(2).equal())} - ${unit(
itemMarginInline,
)})`,
textOverflow: 'clip',
[`
@ -193,8 +194,8 @@ const getVerticalStyle: GenerateStyle<MenuToken> = token => {
[`${componentCls}-item-icon, ${iconCls}`]: {
margin: 0,
fontSize: fontSizeLG,
lineHeight: `${menuItemHeight}px`,
fontSize: collapsedIconSize,
lineHeight: unit(itemHeight),
'+ span': {
display: 'inline-block',