import { TinyColor } from '@ctrl/tinycolor'; import { genCollapseMotion } from '../../style/motion'; import type { FullToken, GenerateStyle, UseComponentStyleResult } from '../../theme'; import { genComponentStyleHook, mergeToken } from '../../theme'; import getHorizontalStyle from './horizontal'; import getRTLStyle from './rtl'; import getThemeStyle from './theme'; import getVerticalStyle from './vertical'; import { clearFix, resetComponent, resetIcon } from '../../style'; /** Component only token. Which will handle additional calculation of alias token */ export interface ComponentToken { dropdownWidth: number; zIndexPopup: number; } export interface MenuToken extends FullToken<'Menu'> { menuItemHeight: number; menuHorizontalHeight: number; menuItemPaddingInline: number; menuArrowSize: number; menuArrowOffset: string; menuPanelMaskInset: number; } export interface MenuThemeToken extends MenuToken { // Group themeColorTextSecondary: string; // Item Text // > Default themeColorText: string; themeColorTextHover: string; themeColorTextSelect: string; // > Disabled themeColorDisabledText: string; // > Danger themeColorDangerText: string; themeColorDangerTextHover: string; themeColorDangerTextSelect: string; // Item Bg themeColorBg: string; themeColorBgSecondary: string; // > Default themeColorBgActive: string; themeColorBgSelect: string; // > Danger themeColorDangerBgActive: string; themeColorDangerBgSelect: string; // Ink Bar themeInkBarWidth: number; themeInkBarHeight: number; themeInkBorderSize: number; } // =============================== Base =============================== const getBaseStyle: GenerateStyle = token => { const { antCls, componentCls, fontSize, motionDurationSlow, motionDurationMid, motionEaseInOut, motionEaseOut, lineHeight, paddingXS, padding, colorSplit, lineWidth, iconCls, zIndexPopup, radiusBase, menuArrowSize, controlHeightSM, menuArrowOffset, lineType, menuPanelMaskInset, } = token; return [ // Misc { '': { [`${componentCls}`]: { ...clearFix(), // Hidden [`&-hidden`]: { display: 'none', }, }, }, [`${componentCls}-submenu-hidden`]: { display: 'none', }, }, { [componentCls]: { ...resetComponent(token), marginBottom: 0, paddingInlineStart: 0, // Override default ul/ol fontSize, lineHeight: 0, // Fix display inline-block gap listStyle: 'none', outline: 'none', transition: [ `background ${motionDurationSlow}`, // Magic cubic here but smooth transition `width ${motionDurationSlow} cubic-bezier(0.2, 0, 0, 1) 0s`, ].join(','), [`ul, ol`]: { margin: 0, padding: 0, listStyle: 'none', }, // Overflow ellipsis [`&-overflow`]: { display: 'flex', [`${componentCls}-item`]: { flex: 'none', }, }, [`${componentCls}-item-group-title`]: { padding: `${paddingXS}px ${padding}px`, fontSize, lineHeight, transition: `all ${motionDurationSlow}`, }, [`&-horizontal ${componentCls}-submenu`]: { transition: [ `border-color ${motionDurationSlow} ${motionEaseInOut}`, `background ${motionDurationSlow} ${motionEaseInOut}`, ].join(','), }, [`${componentCls}-submenu, ${componentCls}-submenu-inline`]: { transition: [ `border-color ${motionDurationSlow} ${motionEaseInOut}`, `background ${motionDurationSlow} ${motionEaseInOut}`, `padding ${motionDurationMid} ${motionEaseInOut}`, ].join(','), }, [`${componentCls}-submenu ${componentCls}-sub`]: { cursor: 'initial', transition: [ `background ${motionDurationSlow} ${motionEaseInOut}`, `padding ${motionDurationSlow} ${motionEaseInOut}`, ], }, [`${componentCls}-title-content`]: { transition: `color ${motionDurationSlow}`, }, [`${componentCls}-item a`]: { '&::before': { position: 'absolute', inset: 0, backgroundColor: 'transparent', content: '""', }, }, // Removed a Badge related style seems it's safe // https://github.com/ant-design/ant-design/issues/19809 // >>>>> Divider [`${componentCls}-item-divider`]: { overflow: 'hidden', lineHeight: 0, borderColor: colorSplit, borderStyle: lineType, borderTopWidth: lineWidth, marginBlock: lineWidth, padding: 0, '&-dashed': { borderStyle: 'dashed', }, }, // >>>>> Item [`${componentCls}-item, ${componentCls}-submenu-title`]: { position: 'relative', display: 'block', margin: 0, // paddingInline: menuItemPaddingInline, whiteSpace: 'nowrap', cursor: 'pointer', transition: [ `border-color ${motionDurationSlow}`, `background ${motionDurationSlow}`, `padding ${motionDurationSlow} ${motionEaseInOut}`, ].join(','), [`${componentCls}-item-icon, ${iconCls}`]: { minWidth: fontSize, fontSize, transition: [ `font-size ${motionDurationMid} ${motionEaseOut}`, `margin ${motionDurationSlow} ${motionEaseInOut}`, `color ${motionDurationSlow}`, ].join(','), '+ span': { marginInlineStart: controlHeightSM - fontSize, opacity: 1, transition: [ `opacity ${motionDurationSlow} ${motionEaseInOut}`, `margin ${motionDurationSlow}`, `color ${motionDurationSlow}`, ].join(','), }, }, [`${componentCls}-item-icon`]: { ...resetIcon(), }, [`&.${componentCls}-item-only-child`]: { [`> ${iconCls}, > ${componentCls}-item-icon`]: { marginInlineEnd: 0, }, }, }, // Disabled state sets text to gray and nukes hover/tab effects [`${componentCls}-item-disabled, ${componentCls}-submenu-disabled`]: { background: 'none !important', cursor: 'not-allowed', '&::after': { borderColor: 'transparent !important', }, a: { color: 'inherit !important', pointerEvents: 'none', }, [`> ${componentCls}-submenu-title`]: { color: 'inherit !important', cursor: 'not-allowed', }, }, [`${componentCls}-item-group`]: { [`${componentCls}-item-group-list`]: { margin: 0, padding: 0, [`${componentCls}-item, ${componentCls}-submenu-title`]: { paddingInline: `${fontSize * 2}px ${padding}px`, }, }, }, // ======================= Sub Menu ======================= '&-submenu': { '&-popup': { position: 'absolute', zIndex: zIndexPopup, background: 'transparent', borderRadius: radiusBase, boxShadow: 'none', transformOrigin: '0 0', // https://github.com/ant-design/ant-design/issues/13955 '&::before': { position: 'absolute', inset: `${menuPanelMaskInset}px 0 0`, zIndex: -1, width: '100%', height: '100%', opacity: 0, content: '""', }, }, // https://github.com/ant-design/ant-design/issues/13955 '&-placement-rightTop::before': { top: 0, insetInlineStart: menuPanelMaskInset, }, [`> ${componentCls}`]: { borderRadius: radiusBase, [`${componentCls}-submenu-title::after`]: { transition: `transform ${motionDurationSlow} ${motionEaseInOut}`, }, }, }, [`${componentCls}-submenu`]: { [`&-expand-icon, &-arrow`]: { position: 'absolute', top: '50%', insetInlineEnd: token.margin, width: menuArrowSize, color: 'currentcolor', transform: 'translateY(-50%)', transition: `transform ${motionDurationSlow} ${motionEaseInOut}`, }, '&-arrow': { // → '&::before, &::after': { position: 'absolute', width: menuArrowSize * 0.6, height: menuArrowSize * 0.15, backgroundColor: 'currentcolor', borderRadius: radiusBase, transition: [ `background ${motionDurationSlow} ${motionEaseInOut}`, `transform ${motionDurationSlow} ${motionEaseInOut}`, `top ${motionDurationSlow} ${motionEaseInOut}`, `color ${motionDurationSlow} ${motionEaseInOut}`, ].join(','), content: '""', }, '&::before': { transform: `rotate(45deg) translateY(-${menuArrowOffset})`, }, '&::after': { transform: `rotate(-45deg) translateY(${menuArrowOffset})`, }, }, }, [`&-inline-collapsed ${componentCls}-submenu-arrow, &-inline ${componentCls}-submenu-arrow`]: { // ↓ '&::before': { transform: `rotate(-45deg) translateX(${menuArrowOffset})`, }, '&::after': { transform: `rotate(45deg) translateX(-${menuArrowOffset})`, }, }, [`${componentCls}-submenu-open${componentCls}-submenu-inline > ${componentCls}-submenu-title > ${componentCls}-submenu-arrow`]: { // ↑ transform: `translateY(-${menuArrowSize * 0.2}px)`, '&::after': { transform: `rotate(-45deg) translateX(-${menuArrowOffset})`, }, '&::before': { transform: `rotate(45deg) translateX(${menuArrowOffset})`, }, }, }, }, // Integration with header element so menu items have the same height { [`${antCls}-layout-header`]: { [componentCls]: { lineHeight: 'inherit', }, }, }, ]; }; // ============================== Export ============================== export default (prefixCls: string, injectStyle: boolean): UseComponentStyleResult => { const useOriginHook = genComponentStyleHook( 'Menu', token => { // Dropdown will handle menu style self. We do not need to handle this. if (injectStyle === false) { return []; } const { colorPrimary, colorError, colorTextDisabled, colorErrorHover, colorErrorBg, colorText, colorTextLightSolid, colorTextSecondary, colorBgContainer, colorFillAlter, controlHeightLG, fontSize, controlItemBgActive, lineWidth, lineWidthBold, } = token; const menuArrowSize = (fontSize / 7) * 5; // Menu Token const menuToken = mergeToken(token, { menuItemHeight: controlHeightLG, menuItemPaddingInline: controlHeightLG / 2, menuArrowSize, menuHorizontalHeight: controlHeightLG * 1.15, menuArrowOffset: `${menuArrowSize * 0.25}px`, menuPanelMaskInset: -7, // Still a hardcode here since it's offset by rc-align }); // Theme Token const menuLightToken = mergeToken(menuToken, { themeColorText: colorText, themeColorTextHover: colorPrimary, themeColorTextSecondary: colorTextSecondary, themeColorTextSelect: colorPrimary, themeColorBg: colorBgContainer, themeColorBgSecondary: colorFillAlter, themeColorBgActive: controlItemBgActive, themeColorBgSelect: controlItemBgActive, themeInkBarWidth: lineWidthBold + lineWidth, themeInkBarHeight: lineWidthBold, themeInkBorderSize: lineWidth, // Disabled themeColorDisabledText: colorTextDisabled, // Danger themeColorDangerText: colorError, themeColorDangerTextHover: colorError, themeColorDangerTextSelect: colorError, themeColorDangerBgActive: colorErrorBg, themeColorDangerBgSelect: colorErrorBg, }); const menuDarkToken = mergeToken(menuToken, { themeColorText: new TinyColor(colorTextLightSolid).setAlpha(0.65).toRgbString(), themeColorTextHover: colorTextLightSolid, themeColorTextSecondary: colorTextSecondary, themeColorTextSelect: colorTextLightSolid, themeColorBg: '#001529', themeColorBgSecondary: '#000c17', themeColorBgActive: 'transparent', themeColorBgSelect: colorPrimary, themeInkBarWidth: 0, themeInkBarHeight: 0, themeInkBorderSize: 0, // Disabled themeColorDisabledText: new TinyColor(colorTextLightSolid).setAlpha(0.25).toRgbString(), // Danger themeColorDangerText: colorError, themeColorDangerTextHover: colorErrorHover, themeColorDangerTextSelect: colorTextLightSolid, themeColorDangerBgActive: colorError, themeColorDangerBgSelect: colorError, }); return [ // Basic getBaseStyle(menuToken), // Horizontal getHorizontalStyle(menuLightToken), // Hard code for some light style // Vertical getVerticalStyle(menuLightToken), // Hard code for some light style // Theme getThemeStyle(menuLightToken, 'light'), getThemeStyle(menuDarkToken, 'dark'), // RTL getRTLStyle(menuToken), // Motion genCollapseMotion(menuToken), ]; }, token => ({ dropdownWidth: 160, zIndexPopup: token.zIndexPopupBase + 50, }), ); return useOriginHook(prefixCls); };