diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index e50d28b18..6395a0541 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -41,6 +41,8 @@ import type { FocusEventHandler, MouseEventHandler } from '../../_util/EventInte import collapseMotion from '../../_util/collapseMotion'; import type { ItemType } from './hooks/useItems'; import useItems from './hooks/useItems'; +import useStyle from '../style'; +import { useInjectOverride } from './OverrideContext'; export const menuProps = () => ({ id: String, @@ -95,7 +97,17 @@ export default defineComponent({ props: menuProps(), slots: ['expandIcon', 'overflowedIndicator'], setup(props, { slots, emit, attrs }) { - const { prefixCls, direction, getPrefixCls } = useConfigInject('menu', props); + const { direction, getPrefixCls } = useConfigInject('menu', props); + const override = useInjectOverride(); + const prefixCls = computed(() => { + return getPrefixCls('menu', props.prefixCls || override?.prefixCls?.value); + }); + const [wrapSSR, hashId] = useStyle( + prefixCls, + computed(() => { + return !override; + }), + ); const store = shallowRef>(new Map()); const siderCollapsed = inject(SiderCollapsedKey, ref(undefined)); const inlineCollapsed = computed(() => { @@ -265,6 +277,9 @@ export default defineComponent({ mergedMode.value = props.mode; mergedInlineCollapsed.value = false; } + if (override?.mode?.value) { + mergedMode.value = override.mode.value; + } }); const isInlineMode = computed(() => mergedMode.value === 'inline'); @@ -346,6 +361,7 @@ export default defineComponent({ const onInternalClick = (info: MenuInfo) => { emit('click', info); triggerSelection(info); + override?.onClick?.(); }; const onInternalOpenChange = (key: Key, open: boolean) => { @@ -406,7 +422,7 @@ export default defineComponent({ triggerSubMenuAction: computed(() => props.triggerSubMenuAction), getPopupContainer: computed(() => props.getPopupContainer), inlineCollapsed: mergedInlineCollapsed, - antdMenuTheme: computed(() => props.theme), + theme: computed(() => props.theme), siderCollapsed, defaultMotions: computed(() => (isMounted.value ? defaultMotions.value : null)), motion: computed(() => (isMounted.value ? props.motion : null)), @@ -419,7 +435,7 @@ export default defineComponent({ isRootMenu: ref(true), expandIcon, forceSubMenuRender: computed(() => props.forceSubMenuRender), - rootClassName: computed(() => ''), + rootClassName: hashId, }); return () => { const childList = itemsNodes.value || flattenChildren(slots.default?.()); @@ -442,14 +458,14 @@ export default defineComponent({ )); const overflowedIndicator = slots.overflowedIndicator?.() || ; - return ( + return wrapSSR( {wrappedChildList} - + , ); }; }, diff --git a/components/menu/src/OverrideContext.ts b/components/menu/src/OverrideContext.ts new file mode 100644 index 000000000..8a4eb064f --- /dev/null +++ b/components/menu/src/OverrideContext.ts @@ -0,0 +1,27 @@ +import type { ComputedRef, InjectionKey } from 'vue'; +import { provide, computed, inject } from 'vue'; +import type { MenuProps } from './menu'; + +// Used for Dropdown only +export interface OverrideContextProps { + prefixCls?: ComputedRef; + mode?: ComputedRef; + selectable?: ComputedRef; + validator?: (menuProps: Pick) => void; + onClick?: () => void; +} +export const OverrideContextKey: InjectionKey = Symbol('OverrideContextKey'); +export const useInjectOverride = () => { + return inject(OverrideContextKey, undefined); +}; + +export const useProvideOverride = (props: OverrideContextProps) => { + const { prefixCls, mode, selectable, validator, onClick } = useInjectOverride() || {}; + provide(OverrideContextKey, { + prefixCls: computed(() => (props.prefixCls?.value ?? prefixCls?.value) as string), + mode: computed(() => props.mode?.value ?? mode?.value), + selectable: computed(() => (props.selectable?.value ?? selectable?.value) as boolean), + validator: props.validator ?? validator, + onClick: props.onClick ?? onClick, + }); +}; diff --git a/components/menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx index 99599fd1f..468007d3e 100644 --- a/components/menu/src/SubMenu.tsx +++ b/components/menu/src/SubMenu.tsx @@ -93,7 +93,6 @@ export default defineComponent({ changeActiveKeys, mode, inlineCollapsed, - antdMenuTheme, openKeys, overflowDisabled, onOpenChange, @@ -101,6 +100,7 @@ export default defineComponent({ unRegisterMenuInfo, selectedSubMenuKeys, expandIcon: menuExpandIcon, + theme, } = useInjectMenu(); const hasKey = vnodeKey !== undefined && vnodeKey !== null; @@ -196,7 +196,7 @@ export default defineComponent({ const popupClassName = computed(() => classNames( prefixCls.value, - `${prefixCls.value}-${props.theme || antdMenuTheme.value}`, + `${prefixCls.value}-${props.theme || theme.value}`, props.popupClassName, ), ); @@ -279,13 +279,14 @@ export default defineComponent({ const subMenuPrefixClsValue = subMenuPrefixCls.value; let titleNode = () => null; if (!overflowDisabled.value && mode.value !== 'inline') { + const popupOffset = mode.value === 'horizontal' ? [0, 8] : [10, 0]; titleNode = () => ( { +export interface SubMenuType extends Omit { icon?: any; theme?: 'dark' | 'light'; children: ItemType[]; } -interface MenuItemGroupType extends Omit { +export interface MenuItemGroupType extends Omit { children?: MenuItemType[]; key?: Key; } -interface MenuDividerType extends VcMenuDividerType { +export interface MenuDividerType extends VcMenuDividerType { dashed?: boolean; key?: Key; } diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts index c02aac01a..1ea88bf4b 100644 --- a/components/menu/src/hooks/useMenuContext.ts +++ b/components/menu/src/hooks/useMenuContext.ts @@ -31,7 +31,7 @@ export interface MenuContextProps { rtl?: ComputedRef; inlineCollapsed: Ref; - antdMenuTheme?: ComputedRef; + theme?: ComputedRef; siderCollapsed?: ComputedRef; diff --git a/components/menu/style/dark.less b/components/menu/style/dark.less deleted file mode 100644 index fe110330b..000000000 --- a/components/menu/style/dark.less +++ /dev/null @@ -1,177 +0,0 @@ -.accessibility-focus-dark() { - box-shadow: 0 0 0 2px @primary-7; -} - -.@{menu-prefix-cls} { - &&-root:focus-visible { - .accessibility-focus-dark(); - } - - &-dark &-item, - &-dark &-submenu-title { - &:focus-visible { - .accessibility-focus-dark(); - } - } - - // dark theme - &&-dark, - &-dark &-sub, - &&-dark &-sub { - color: @menu-dark-color; - background: @menu-dark-bg; - .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { - opacity: 0.45; - transition: all 0.3s; - - &::after, - &::before { - background: @menu-dark-arrow-color; - } - } - } - - &-dark&-submenu-popup { - background: transparent; - } - - &-dark &-inline&-sub { - background: @menu-dark-inline-submenu-bg; - } - - &-dark&-horizontal { - border-bottom: 0; - } - - &-dark&-horizontal > &-item, - &-dark&-horizontal > &-submenu { - top: 0; - margin-top: 0; - padding: @menu-item-padding; - border-color: @menu-dark-bg; - border-bottom: 0; - } - - &-dark&-horizontal > &-item:hover { - background-color: @menu-dark-item-active-bg; - } - - &-dark&-horizontal > &-item > a::before { - bottom: 0; - } - - &-dark &-item, - &-dark &-item-group-title, - &-dark &-item > a, - &-dark &-item > span > a { - color: @menu-dark-color; - } - - &-dark&-inline, - &-dark&-vertical, - &-dark&-vertical-left, - &-dark&-vertical-right { - border-right: 0; - } - - &-dark&-inline &-item, - &-dark&-vertical &-item, - &-dark&-vertical-left &-item, - &-dark&-vertical-right &-item { - left: 0; - margin-left: 0; - border-right: 0; - - &::after { - border-right: 0; - } - } - - &-dark&-inline &-item, - &-dark&-inline &-submenu-title { - width: 100%; - } - - &-dark &-item:hover, - &-dark &-item-active, - &-dark &-submenu-active, - &-dark &-submenu-open, - &-dark &-submenu-selected, - &-dark &-submenu-title:hover { - color: @menu-dark-highlight-color; - background-color: transparent; - - > a, - > span > a { - color: @menu-dark-highlight-color; - } - > .@{menu-prefix-cls}-submenu-title { - > .@{menu-prefix-cls}-submenu-arrow { - opacity: 1; - - &::after, - &::before { - background: @menu-dark-highlight-color; - } - } - } - } - - &-dark &-item:hover { - background-color: @menu-dark-item-hover-bg; - } - - &-dark&-dark:not(&-horizontal) &-item-selected { - background-color: @menu-dark-item-active-bg; - } - - &-dark &-item-selected { - color: @menu-dark-highlight-color; - border-right: 0; - - &::after { - border-right: 0; - } - - > a, - > span > a, - > a:hover, - > span > a:hover { - color: @menu-dark-highlight-color; - } - - .@{menu-prefix-cls}-item-icon, - .@{iconfont-css-prefix} { - color: @menu-dark-selected-item-icon-color; - - + span { - color: @menu-dark-selected-item-text-color; - } - } - } - - &&-dark &-item-selected, - &-submenu-popup&-dark &-item-selected { - background-color: @menu-dark-item-active-bg; - } - - // Disabled state sets text to dark gray and nukes hover/tab effects - &-dark &-item-disabled, - &-dark &-submenu-disabled { - &, - > a, - > span > a { - color: @disabled-color-dark !important; - opacity: 0.8; - } - > .@{menu-prefix-cls}-submenu-title { - color: @disabled-color-dark !important; - > .@{menu-prefix-cls}-submenu-arrow { - &::before, - &::after { - background: @disabled-color-dark !important; - } - } - } - } -} diff --git a/components/menu/style/horizontal.ts b/components/menu/style/horizontal.ts new file mode 100644 index 000000000..31bec2c1f --- /dev/null +++ b/components/menu/style/horizontal.ts @@ -0,0 +1,57 @@ +import type { MenuToken } from '.'; +import type { GenerateStyle } from '../../theme/internal'; + +const getHorizontalStyle: GenerateStyle = token => { + const { + componentCls, + motionDurationSlow, + menuHorizontalHeight, + colorSplit, + lineWidth, + lineType, + menuItemPaddingInline, + } = token; + + return { + [`${componentCls}-horizontal`]: { + lineHeight: `${menuHorizontalHeight}px`, + border: 0, + borderBottom: `${lineWidth}px ${lineType} ${colorSplit}`, + boxShadow: 'none', + + '&::after': { + display: 'block', + clear: 'both', + height: 0, + content: '"\\20"', + }, + + // ======================= Item ======================= + [`${componentCls}-item, ${componentCls}-submenu`]: { + position: 'relative', + display: 'inline-block', + verticalAlign: 'bottom', + paddingInline: menuItemPaddingInline, + }, + + [`> ${componentCls}-item:hover, + > ${componentCls}-item-active, + > ${componentCls}-submenu ${componentCls}-submenu-title:hover`]: { + backgroundColor: 'transparent', + }, + + [`${componentCls}-item, ${componentCls}-submenu-title`]: { + transition: [`border-color ${motionDurationSlow}`, `background ${motionDurationSlow}`].join( + ',', + ), + }, + + // ===================== Sub Menu ===================== + [`${componentCls}-submenu-arrow`]: { + display: 'none', + }, + }, + }; +}; + +export default getHorizontalStyle; diff --git a/components/menu/style/index.less b/components/menu/style/index.less deleted file mode 100644 index 7d42a6666..000000000 --- a/components/menu/style/index.less +++ /dev/null @@ -1,700 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; -@import './status'; - -@menu-prefix-cls: ~'@{ant-prefix}-menu'; -@menu-animation-duration-normal: 0.15s; - -.accessibility-focus() { - box-shadow: 0 0 0 2px @primary-2; -} - -// TODO: Should remove icon style compatible in v5 - -// default theme -.@{menu-prefix-cls} { - .reset-component(); - - margin-bottom: 0; - padding-left: 0; // Override default ul/ol - color: @menu-item-color; - font-size: @menu-item-font-size; - line-height: 0; // Fix display inline-block gap - text-align: left; - list-style: none; - background: @menu-bg; - outline: none; - box-shadow: @box-shadow-base; - transition: background @animation-duration-slow, - width @animation-duration-slow cubic-bezier(0.2, 0, 0, 1) 0s; - .clearfix(); - - &&-root:focus-visible { - .accessibility-focus(); - } - - ul, - ol { - margin: 0; - padding: 0; - list-style: none; - } - - // Overflow ellipsis - &-overflow { - display: flex; - - &-item { - flex: none; - } - } - - &-hidden, - &-submenu-hidden { - display: none; - } - - &-item-group-title { - height: @menu-item-group-height; - padding: 8px 16px; - color: @menu-item-group-title-color; - font-size: @menu-item-group-title-font-size; - line-height: @menu-item-group-height; - transition: all @animation-duration-slow; - } - - &-horizontal &-submenu { - transition: border-color @animation-duration-slow @ease-in-out, - background @animation-duration-slow @ease-in-out; - } - - &-submenu, - &-submenu-inline { - transition: border-color @animation-duration-slow @ease-in-out, - background @animation-duration-slow @ease-in-out, - padding @menu-animation-duration-normal @ease-in-out; - } - - &-submenu-selected { - color: @menu-highlight-color; - } - - &-item:active, - &-submenu-title:active { - background: @menu-item-active-bg; - } - - &-submenu &-sub { - cursor: initial; - transition: background @animation-duration-slow @ease-in-out, - padding @animation-duration-slow @ease-in-out; - } - - &-title-content { - transition: color @animation-duration-slow; - } - - &-item a { - color: @menu-item-color; - - &:hover { - color: @menu-highlight-color; - } - - &::before { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background-color: transparent; - content: ''; - } - } - - // https://github.com/ant-design/ant-design/issues/19809 - &-item > .@{ant-prefix}-badge a { - color: @menu-item-color; - - &:hover { - color: @menu-highlight-color; - } - } - - &-item-divider { - overflow: hidden; - line-height: 0; - border-color: @border-color-split; - border-style: solid; - border-width: 1px 0 0; - } - - &-item-divider-dashed { - border-style: dashed; - } - - &-horizontal &-item, - &-horizontal &-submenu { - margin-top: -1px; - } - - &-horizontal > &-item:hover, - &-horizontal > &-item-active, - &-horizontal > &-submenu &-submenu-title:hover { - background-color: transparent; - } - - &-item-selected { - color: @menu-highlight-color; - - a, - a:hover { - color: @menu-highlight-color; - } - } - - &:not(&-horizontal) &-item-selected { - background-color: @menu-item-active-bg; - } - - &-inline, - &-vertical, - &-vertical-left { - border-right: @border-width-base @border-style-base @border-color-split; - } - - &-vertical-right { - border-left: @border-width-base @border-style-base @border-color-split; - } - - &-vertical&-sub, - &-vertical-left&-sub, - &-vertical-right&-sub { - min-width: 160px; - max-height: calc(100vh - 100px); - padding: 0; - overflow: hidden; - border-right: 0; - - // https://github.com/ant-design/ant-design/issues/22244 - // https://github.com/ant-design/ant-design/issues/26812 - &:not([class*='-active']) { - overflow-x: hidden; - overflow-y: auto; - } - - .@{menu-prefix-cls}-item { - left: 0; - margin-left: 0; - border-right: 0; - - &::after { - border-right: 0; - } - } - > .@{menu-prefix-cls}-item, - > .@{menu-prefix-cls}-submenu { - transform-origin: 0 0; - } - } - - &-horizontal&-sub { - min-width: 114px; // in case of submenu width is too big: https://codesandbox.io/s/qvpwm6mk66 - } - - &-horizontal &-item, - &-horizontal &-submenu-title { - transition: border-color @animation-duration-slow, background @animation-duration-slow; - } - - &-item, - &-submenu-title { - position: relative; - display: block; - margin: 0; - padding: @menu-item-padding; - white-space: nowrap; - cursor: pointer; - transition: border-color @animation-duration-slow, background @animation-duration-slow, - padding @animation-duration-slow @ease-in-out; - - .@{menu-prefix-cls}-item-icon, - .@{iconfont-css-prefix} { - min-width: 14px; - font-size: @menu-icon-size; - transition: font-size @menu-animation-duration-normal @ease-out, - margin @animation-duration-slow @ease-in-out, color @animation-duration-slow; - - + span { - margin-left: @menu-icon-margin-right; - opacity: 1; - transition: opacity @animation-duration-slow @ease-in-out, margin @animation-duration-slow, - color @animation-duration-slow; - } - } - - .@{menu-prefix-cls}-item-icon.svg { - vertical-align: -0.125em; - } - - &.@{menu-prefix-cls}-item-only-child { - > .@{iconfont-css-prefix}, - > .@{menu-prefix-cls}-item-icon { - margin-right: 0; - } - } - - &:focus-visible { - .accessibility-focus(); - } - } - - & > &-item-divider { - margin: 1px 0; - padding: 0; - } - - &-submenu { - &-popup { - position: absolute; - z-index: @zindex-dropdown; - background: transparent; - border-radius: @border-radius-base; - box-shadow: none; - transform-origin: 0 0; - - // https://github.com/ant-design/ant-design/issues/13955 - &::before { - position: absolute; - top: -7px; - right: 0; - bottom: 0; - left: 0; - z-index: -1; - width: 100%; - height: 100%; - opacity: 0.0001; - content: ' '; - } - } - - // https://github.com/ant-design/ant-design/issues/13955 - &-placement-rightTop::before { - top: 0; - left: -7px; - } - - > .@{menu-prefix-cls} { - background-color: @menu-bg; - border-radius: @border-radius-base; - - &-submenu-title::after { - transition: transform @animation-duration-slow @ease-in-out; - } - } - - &-popup > .@{menu-prefix-cls} { - background-color: @menu-popup-bg; - } - - &-expand-icon, - &-arrow { - position: absolute; - top: 50%; - right: 16px; - width: 10px; - color: @menu-item-color; - transform: translateY(-50%); - transition: transform @animation-duration-slow @ease-in-out; - } - - &-arrow { - // → - &::before, - &::after { - position: absolute; - width: 6px; - height: 1.5px; - background-color: currentcolor; - border-radius: 2px; - transition: background @animation-duration-slow @ease-in-out, - transform @animation-duration-slow @ease-in-out, top @animation-duration-slow @ease-in-out, - color @animation-duration-slow @ease-in-out; - content: ''; - } - - &::before { - transform: rotate(45deg) translateY(-2.5px); - } - - &::after { - transform: rotate(-45deg) translateY(2.5px); - } - } - - &:hover > &-title > &-expand-icon, - &:hover > &-title > &-arrow { - color: @menu-highlight-color; - } - - .@{menu-prefix-cls}-inline-collapsed &-arrow, - &-inline &-arrow { - // ↓ - &::before { - transform: rotate(-45deg) translateX(2.5px); - } - - &::after { - transform: rotate(45deg) translateX(-2.5px); - } - } - - &-horizontal &-arrow { - display: none; - } - - &-open&-inline > &-title > &-arrow { - // ↑ - transform: translateY(-2px); - - &::after { - transform: rotate(-45deg) translateX(-2.5px); - } - - &::before { - transform: rotate(45deg) translateX(2.5px); - } - } - } - - &-vertical &-submenu-selected, - &-vertical-left &-submenu-selected, - &-vertical-right &-submenu-selected { - color: @menu-highlight-color; - } - - &-horizontal { - line-height: @menu-horizontal-line-height; - border: 0; - border-bottom: @border-width-base @border-style-base @border-color-split; - box-shadow: none; - - &:not(.@{menu-prefix-cls}-dark) { - > .@{menu-prefix-cls}-item, - > .@{menu-prefix-cls}-submenu { - margin-top: -1px; - margin-bottom: 0; - padding: @menu-item-padding; - - &:hover, - &-active, - &-open, - &-selected { - color: @menu-highlight-color; - - &::after { - border-bottom: 2px solid @menu-highlight-color; - } - } - } - } - - > .@{menu-prefix-cls}-item, - > .@{menu-prefix-cls}-submenu { - position: relative; - top: 1px; - display: inline-block; - vertical-align: bottom; - - &::after { - position: absolute; - right: @menu-item-padding-horizontal; - bottom: 0; - left: @menu-item-padding-horizontal; - border-bottom: 2px solid transparent; - transition: border-color @animation-duration-slow @ease-in-out; - content: ''; - } - } - - > .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title { - padding: 0; - } - - > .@{menu-prefix-cls}-item { - a { - color: @menu-item-color; - - &:hover { - color: @menu-highlight-color; - } - - &::before { - bottom: -2px; - } - } - - &-selected a { - color: @menu-highlight-color; - } - } - - &::after { - display: block; - clear: both; - height: 0; - content: '\20'; - } - } - - &-vertical, - &-vertical-left, - &-vertical-right, - &-inline { - .@{menu-prefix-cls}-item { - position: relative; - - &::after { - position: absolute; - top: 0; - right: 0; - bottom: 0; - border-right: @menu-item-active-border-width solid @menu-highlight-color; - transform: scaleY(0.0001); - opacity: 0; - transition: transform @menu-animation-duration-normal @ease-out, - opacity @menu-animation-duration-normal @ease-out; - content: ''; - } - } - - .@{menu-prefix-cls}-item, - .@{menu-prefix-cls}-submenu-title { - height: @menu-item-height; - margin-top: @menu-item-vertical-margin; - margin-bottom: @menu-item-vertical-margin; - padding: 0 16px; - overflow: hidden; - line-height: @menu-item-height; - text-overflow: ellipsis; - } - - // disable margin collapsed - .@{menu-prefix-cls}-submenu { - padding-bottom: 0.02px; - } - - .@{menu-prefix-cls}-item:not(:last-child) { - margin-bottom: @menu-item-boundary-margin; - } - - > .@{menu-prefix-cls}-item, - > .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title { - height: @menu-inline-toplevel-item-height; - line-height: @menu-inline-toplevel-item-height; - } - } - - &-vertical { - .@{menu-prefix-cls}-item-group-list .@{menu-prefix-cls}-submenu-title, - .@{menu-prefix-cls}-submenu-title { - padding-right: 34px; - } - } - - &-inline { - width: 100%; - .@{menu-prefix-cls}-selected, - .@{menu-prefix-cls}-item-selected { - &::after { - transform: scaleY(1); - opacity: 1; - transition: transform @menu-animation-duration-normal @ease-in-out, - opacity @menu-animation-duration-normal @ease-in-out; - } - } - - .@{menu-prefix-cls}-item, - .@{menu-prefix-cls}-submenu-title { - width: ~'calc(100% + 1px)'; - } - - .@{menu-prefix-cls}-item-group-list .@{menu-prefix-cls}-submenu-title, - .@{menu-prefix-cls}-submenu-title { - padding-right: 34px; - } - - // Motion enhance for first level - &.@{menu-prefix-cls}-root { - .@{menu-prefix-cls}-item, - .@{menu-prefix-cls}-submenu-title { - display: flex; - align-items: center; - transition: border-color @animation-duration-slow, background @animation-duration-slow, - padding 0.1s @ease-out; - - > .@{menu-prefix-cls}-title-content { - flex: auto; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - } - - > * { - flex: none; - } - } - } - } - - &&-inline-collapsed { - width: @menu-collapsed-width; - - > .@{menu-prefix-cls}-item, - > .@{menu-prefix-cls}-item-group - > .@{menu-prefix-cls}-item-group-list - > .@{menu-prefix-cls}-item, - > .@{menu-prefix-cls}-item-group - > .@{menu-prefix-cls}-item-group-list - > .@{menu-prefix-cls}-submenu - > .@{menu-prefix-cls}-submenu-title, - > .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title { - left: 0; - padding: 0 ~'calc(50% - @{menu-icon-size-lg} / 2)'; - text-overflow: clip; - - .@{menu-prefix-cls}-submenu-arrow { - opacity: 0; - } - - .@{menu-prefix-cls}-item-icon, - .@{iconfont-css-prefix} { - margin: 0; - font-size: @menu-icon-size-lg; - line-height: @menu-item-height; - - + span { - display: inline-block; - opacity: 0; - } - } - } - - .@{menu-prefix-cls}-item-icon, - .@{iconfont-css-prefix} { - display: inline-block; - } - - &-tooltip { - pointer-events: none; - - .@{menu-prefix-cls}-item-icon, - .@{iconfont-css-prefix} { - display: none; - } - - a { - color: @text-color-dark; - } - } - - .@{menu-prefix-cls}-item-group-title { - padding-right: 4px; - padding-left: 4px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - } - - &-item-group-list { - margin: 0; - padding: 0; - .@{menu-prefix-cls}-item, - .@{menu-prefix-cls}-submenu-title { - padding: 0 16px 0 28px; - } - } - - &-root&-vertical, - &-root&-vertical-left, - &-root&-vertical-right, - &-root&-inline { - box-shadow: none; - } - - &-root&-inline-collapsed { - .@{menu-prefix-cls}-item, - .@{menu-prefix-cls}-submenu .@{menu-prefix-cls}-submenu-title { - > .@{menu-prefix-cls}-inline-collapsed-noicon { - font-size: @menu-icon-size-lg; - text-align: center; - } - } - } - - &-sub&-inline { - padding: 0; - background: @menu-inline-submenu-bg; - border: 0; - border-radius: 0; - box-shadow: none; - & > .@{menu-prefix-cls}-item, - & > .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title { - height: @menu-item-height; - line-height: @menu-item-height; - list-style-position: inside; - list-style-type: disc; - } - - & .@{menu-prefix-cls}-item-group-title { - padding-left: 32px; - } - } - - // Disabled state sets text to gray and nukes hover/tab effects - &-item-disabled, - &-submenu-disabled { - color: @disabled-color !important; - background: none; - cursor: not-allowed; - - &::after { - border-color: transparent !important; - } - - a { - color: @disabled-color !important; - pointer-events: none; - } - > .@{menu-prefix-cls}-submenu-title { - color: @disabled-color !important; - cursor: not-allowed; - > .@{menu-prefix-cls}-submenu-arrow { - &::before, - &::after { - background: @disabled-color !important; - } - } - } - } -} - -// Integration with header element so menu items have the same height -.@{ant-prefix}-layout-header { - .@{menu-prefix-cls} { - line-height: inherit; - } -} - -// https://github.com/ant-design/ant-design/issues/32950 -.@{ant-prefix}-menu-inline-collapsed-tooltip { - a, - a:hover { - color: @white; - } -} - -@import './light'; -@import './dark'; -@import './rtl'; diff --git a/components/menu/style/index.ts b/components/menu/style/index.ts new file mode 100644 index 000000000..815dc26bd --- /dev/null +++ b/components/menu/style/index.ts @@ -0,0 +1,581 @@ +import { TinyColor } from '@ctrl/tinycolor'; +import type { CSSObject } from '../../_util/cssinjs'; +import { genCollapseMotion, initSlideMotion, initZoomMotion } from '../../_style/motion'; +import type { FullToken, GenerateStyle, UseComponentStyleResult } from '../../theme/internal'; +import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import getHorizontalStyle from './horizontal'; +import getRTLStyle from './rtl'; +import getThemeStyle from './theme'; +import getVerticalStyle from './vertical'; +import { clearFix, resetComponent, resetIcon } from '../../_style'; +import { Ref } from 'vue'; + +/** Component only token. Which will handle additional calculation of alias token */ +export interface ComponentToken { + dropdownWidth: number; + zIndexPopup: number; + + // Group + colorGroupTitle: string; + + // radius + radiusItem: number; + radiusSubMenuItem: number; + + // Item Text + // > Default + colorItemText: string; + colorItemTextHover: string; + colorItemTextHoverHorizontal: string; + colorItemTextSelected: string; + colorItemTextSelectedHorizontal: string; + + // > Disabled + colorItemTextDisabled: string; + + // > Danger + colorDangerItemText: string; + colorDangerItemTextHover: string; + colorDangerItemTextSelected: string; + colorDangerItemBgActive: string; + colorDangerItemBgSelected: string; + + // Item Bg + colorItemBg: string; + colorItemBgHover: string; + colorSubItemBg: string; + + // > Default + colorItemBgActive: string; + colorItemBgSelected: string; + colorItemBgSelectedHorizontal: string; + + // Ink Bar + colorActiveBarWidth: number; + colorActiveBarHeight: number; + colorActiveBarBorderSize: number; + + itemMarginInline: number; +} + +export interface MenuToken extends FullToken<'Menu'> { + menuItemHeight: number; + menuHorizontalHeight: number; + menuItemPaddingInline: number; + menuArrowSize: number; + menuArrowOffset: string; + menuPanelMaskInset: number; + menuSubMenuBg: string; +} + +const genMenuItemStyle = (token: MenuToken): CSSObject => { + const { + componentCls, + fontSize, + motionDurationSlow, + motionDurationMid, + motionEaseInOut, + motionEaseOut, + iconCls, + controlHeightSM, + } = token; + + return { + // >>>>> Item + [`${componentCls}-item, ${componentCls}-submenu-title`]: { + position: 'relative', + display: 'block', + margin: 0, + 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', + }, + + [`> ${componentCls}-submenu-title`]: { + color: 'inherit !important', + cursor: 'not-allowed', + }, + }, + }; +}; + +const genSubMenuArrowStyle = (token: MenuToken): CSSObject => { + const { + componentCls, + motionDurationSlow, + motionEaseInOut, + borderRadius, + menuArrowSize, + menuArrowOffset, + } = token; + + return { + [`${componentCls}-submenu`]: { + [`&-expand-icon, &-arrow`]: { + position: 'absolute', + top: '50%', + insetInlineEnd: token.margin, + width: menuArrowSize, + color: 'currentcolor', + transform: 'translateY(-50%)', + transition: `transform ${motionDurationSlow} ${motionEaseInOut}, opacity ${motionDurationSlow}`, + }, + + '&-arrow': { + // → + '&::before, &::after': { + position: 'absolute', + width: menuArrowSize * 0.6, + height: menuArrowSize * 0.15, + backgroundColor: 'currentcolor', + borderRadius, + 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})`, + }, + }, + }, + }; +}; + +// =============================== Base =============================== +const getBaseStyle: GenerateStyle = token => { + const { + antCls, + componentCls, + fontSize, + motionDurationSlow, + motionDurationMid, + motionEaseInOut, + lineHeight, + paddingXS, + padding, + colorSplit, + lineWidth, + zIndexPopup, + borderRadiusLG, + radiusSubMenuItem, + menuArrowSize, + menuArrowOffset, + lineType, + menuPanelMaskInset, + } = token; + + return [ + // Misc + { + '': { + [`${componentCls}`]: { + ...clearFix(), + + // Hidden + [`&-hidden`]: { + display: 'none', + }, + }, + }, + [`${componentCls}-submenu-hidden`]: { + display: 'none', + }, + }, + { + [componentCls]: { + ...resetComponent(token), + ...clearFix(), + + 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, ${componentCls}-submenu, ${componentCls}-submenu-title`]: { + borderRadius: token.radiusItem, + }, + + [`${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}`, + ].join(','), + }, + + [`${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, + borderWidth: 0, + borderTopWidth: lineWidth, + marginBlock: lineWidth, + padding: 0, + + '&-dashed': { + borderStyle: 'dashed', + }, + }, + + // Item + ...genMenuItemStyle(token), + + [`${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: borderRadiusLG, + 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: borderRadiusLG, + + ...genMenuItemStyle(token), + ...genSubMenuArrowStyle(token), + + [`${componentCls}-item, ${componentCls}-submenu > ${componentCls}-submenu-title`]: { + borderRadius: radiusSubMenuItem, + }, + + [`${componentCls}-submenu-title::after`]: { + transition: `transform ${motionDurationSlow} ${motionEaseInOut}`, + }, + }, + }, + + ...genSubMenuArrowStyle(token), + + [`&-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: Ref, injectStyle: Ref): UseComponentStyleResult => { + const useOriginHook = genComponentStyleHook( + 'Menu', + (token, { overrideComponentToken }) => { + // Dropdown will handle menu style self. We do not need to handle this. + if (injectStyle.value === false) { + return []; + } + + const { colorBgElevated, colorPrimary, colorError, colorErrorHover, colorTextLightSolid } = + token; + + const { controlHeightLG, fontSize } = token; + + const menuArrowSize = (fontSize / 7) * 5; + + // Menu Token + const menuToken = mergeToken(token, { + menuItemHeight: controlHeightLG, + menuItemPaddingInline: token.margin, + menuArrowSize, + menuHorizontalHeight: controlHeightLG * 1.15, + menuArrowOffset: `${menuArrowSize * 0.25}px`, + menuPanelMaskInset: -7, // Still a hardcode here since it's offset by rc-align + menuSubMenuBg: colorBgElevated, + }); + + const colorTextDark = new TinyColor(colorTextLightSolid).setAlpha(0.65).toRgbString(); + + const menuDarkToken = mergeToken( + menuToken, + { + colorItemText: colorTextDark, + colorItemTextHover: colorTextLightSolid, + colorGroupTitle: colorTextDark, + colorItemTextSelected: colorTextLightSolid, + colorItemBg: '#001529', + colorSubItemBg: '#000c17', + colorItemBgActive: 'transparent', + colorItemBgSelected: colorPrimary, + colorActiveBarWidth: 0, + colorActiveBarHeight: 0, + colorActiveBarBorderSize: 0, + + // Disabled + colorItemTextDisabled: new TinyColor(colorTextLightSolid).setAlpha(0.25).toRgbString(), + + // Danger + colorDangerItemText: colorError, + colorDangerItemTextHover: colorErrorHover, + colorDangerItemTextSelected: colorTextLightSolid, + colorDangerItemBgActive: colorError, + colorDangerItemBgSelected: colorError, + + menuSubMenuBg: '#001529', + + // Horizontal + colorItemTextSelectedHorizontal: colorTextLightSolid, + colorItemBgSelectedHorizontal: colorPrimary, + }, + { + ...overrideComponentToken, + }, + ); + + return [ + // Basic + getBaseStyle(menuToken), + + // Horizontal + getHorizontalStyle(menuToken), // Hard code for some light style + + // Vertical + getVerticalStyle(menuToken), // Hard code for some light style + + // Theme + getThemeStyle(menuToken, 'light'), + getThemeStyle(menuDarkToken, 'dark'), + + // RTL + getRTLStyle(menuToken), + + // Motion + genCollapseMotion(menuToken), + + initSlideMotion(menuToken, 'slide-up'), + initSlideMotion(menuToken, 'slide-down'), + initZoomMotion(menuToken, 'zoom-big'), + ]; + }, + token => { + const { + colorPrimary, + colorError, + colorTextDisabled, + colorErrorBg, + colorText, + colorTextDescription, + colorBgContainer, + colorFillAlter, + colorFillContent, + lineWidth, + lineWidthBold, + controlItemBgActive, + colorBgTextHover, + } = token; + + return { + dropdownWidth: 160, + zIndexPopup: token.zIndexPopupBase + 50, + radiusItem: token.borderRadiusLG, + radiusSubMenuItem: token.borderRadiusSM, + colorItemText: colorText, + colorItemTextHover: colorText, + colorItemTextHoverHorizontal: colorPrimary, + colorGroupTitle: colorTextDescription, + colorItemTextSelected: colorPrimary, + colorItemTextSelectedHorizontal: colorPrimary, + colorItemBg: colorBgContainer, + colorItemBgHover: colorBgTextHover, + colorItemBgActive: colorFillContent, + colorSubItemBg: colorFillAlter, + colorItemBgSelected: controlItemBgActive, + colorItemBgSelectedHorizontal: 'transparent', + colorActiveBarWidth: 0, + colorActiveBarHeight: lineWidthBold, + colorActiveBarBorderSize: lineWidth, + + // Disabled + colorItemTextDisabled: colorTextDisabled, + + // Danger + colorDangerItemText: colorError, + colorDangerItemTextHover: colorError, + colorDangerItemTextSelected: colorError, + colorDangerItemBgActive: colorErrorBg, + colorDangerItemBgSelected: colorErrorBg, + + itemMarginInline: token.marginXXS, + }; + }, + ); + + return useOriginHook(prefixCls); +}; diff --git a/components/menu/style/index.tsx b/components/menu/style/index.tsx deleted file mode 100644 index be4998634..000000000 --- a/components/menu/style/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import '../../style/index.less'; -import './index.less'; - -// style dependencies -// deps-lint-skip: layout -import '../../tooltip/style'; diff --git a/components/menu/style/light.less b/components/menu/style/light.less deleted file mode 100644 index 06cd6d0c0..000000000 --- a/components/menu/style/light.less +++ /dev/null @@ -1,12 +0,0 @@ -.@{menu-prefix-cls} { - // light theme - &-light { - .@{menu-prefix-cls}-item:hover, - .@{menu-prefix-cls}-item-active, - .@{menu-prefix-cls}:not(.@{menu-prefix-cls}-inline) .@{menu-prefix-cls}-submenu-open, - .@{menu-prefix-cls}-submenu-active, - .@{menu-prefix-cls}-submenu-title:hover { - color: @menu-highlight-color; - } - } -} diff --git a/components/menu/style/rtl.less b/components/menu/style/rtl.less deleted file mode 100644 index cc7a152af..000000000 --- a/components/menu/style/rtl.less +++ /dev/null @@ -1,165 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@menu-prefix-cls: ~'@{ant-prefix}-menu'; - -.@{menu-prefix-cls} { - &&-rtl { - direction: rtl; - text-align: right; - } - - &-item-group-title { - .@{menu-prefix-cls}-rtl & { - text-align: right; - } - } - - &-inline, - &-vertical { - .@{menu-prefix-cls}-rtl& { - border-right: none; - border-left: @border-width-base @border-style-base @border-color-split; - } - } - - &-dark&-inline, - &-dark&-vertical { - .@{menu-prefix-cls}-rtl& { - border-left: none; - } - } - - &-vertical&-sub, - &-vertical-left&-sub, - &-vertical-right&-sub { - > .@{menu-prefix-cls}-item, - > .@{menu-prefix-cls}-submenu { - .@{menu-prefix-cls}-rtl& { - transform-origin: top right; - } - } - } - - &-item, - &-submenu-title { - .@{menu-prefix-cls}-item-icon, - .@{iconfont-css-prefix} { - .@{menu-prefix-cls}-rtl & { - margin-right: auto; - margin-left: @menu-icon-margin-right; - } - } - - &.@{menu-prefix-cls}-item-only-child { - > .@{menu-prefix-cls}-item-icon, - > .@{iconfont-css-prefix} { - .@{menu-prefix-cls}-rtl & { - margin-left: 0; - } - } - } - } - - &-submenu { - &-rtl.@{menu-prefix-cls}-submenu-popup { - transform-origin: 100% 0; - } - - &-vertical, - &-vertical-left, - &-vertical-right, - &-inline { - > .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { - .@{menu-prefix-cls}-rtl & { - right: auto; - left: 16px; - } - } - } - - &-vertical, - &-vertical-left, - &-vertical-right { - > .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { - &::before { - .@{menu-prefix-cls}-rtl & { - transform: rotate(-45deg) translateY(-2px); - } - } - - &::after { - .@{menu-prefix-cls}-rtl & { - transform: rotate(45deg) translateY(2px); - } - } - } - } - } - - &-vertical, - &-vertical-left, - &-vertical-right, - &-inline { - .@{menu-prefix-cls}-item { - &::after { - .@{menu-prefix-cls}-rtl& { - right: auto; - left: 0; - } - } - } - - .@{menu-prefix-cls}-item, - .@{menu-prefix-cls}-submenu-title { - .@{menu-prefix-cls}-rtl& { - text-align: right; - } - } - } - - &-inline { - .@{menu-prefix-cls}-submenu-title { - .@{menu-prefix-cls}-rtl& { - padding-right: 0; - padding-left: 34px; - } - } - } - - &-vertical { - .@{menu-prefix-cls}-submenu-title { - .@{menu-prefix-cls}-rtl& { - padding-right: 16px; - padding-left: 34px; - } - } - } - - &-inline-collapsed&-vertical { - .@{menu-prefix-cls}-submenu-title { - .@{menu-prefix-cls}-rtl& { - padding: 0 ~'calc(50% - @{menu-icon-size-lg} / 2)'; - } - } - } - - &-item-group-list { - .@{menu-prefix-cls}-item, - .@{menu-prefix-cls}-submenu-title { - .@{menu-prefix-cls}-rtl & { - padding: 0 28px 0 16px; - } - } - } - - &-sub&-inline { - border: 0; - & .@{menu-prefix-cls}-item-group-title { - .@{menu-prefix-cls}-rtl& { - padding-right: 32px; - padding-left: 0; - } - } - } -} diff --git a/components/menu/style/rtl.ts b/components/menu/style/rtl.ts new file mode 100644 index 000000000..a13fd7d8d --- /dev/null +++ b/components/menu/style/rtl.ts @@ -0,0 +1,28 @@ +import type { MenuToken } from '.'; +import type { GenerateStyle } from '../../theme/internal'; + +const getRTLStyle: GenerateStyle = ({ componentCls, menuArrowOffset }) => ({ + [`${componentCls}-rtl`]: { + direction: 'rtl', + }, + + [`${componentCls}-submenu-rtl`]: { + transformOrigin: '100% 0', + }, + + // Vertical Arrow + [`${componentCls}-rtl${componentCls}-vertical, + ${componentCls}-submenu-rtl ${componentCls}-vertical`]: { + [`${componentCls}-submenu-arrow`]: { + '&::before': { + transform: `rotate(-45deg) translateY(-${menuArrowOffset})`, + }, + + '&::after': { + transform: `rotate(45deg) translateY(${menuArrowOffset})`, + }, + }, + }, +}); + +export default getRTLStyle; diff --git a/components/menu/style/status.less b/components/menu/style/status.less deleted file mode 100644 index 5e8a13438..000000000 --- a/components/menu/style/status.less +++ /dev/null @@ -1,49 +0,0 @@ -@import (reference) '../../style/themes/index'; -@menu-prefix-cls: ~'@{ant-prefix}-menu'; - -.@{menu-prefix-cls} { - // Danger - &-item-danger&-item { - color: @menu-highlight-danger-color; - - &:hover, - &-active { - color: @menu-highlight-danger-color; - } - - &:active { - background: @menu-item-active-danger-bg; - } - - &-selected { - color: @menu-highlight-danger-color; - - > a, - > a:hover { - color: @menu-highlight-danger-color; - } - } - - .@{menu-prefix-cls}:not(.@{menu-prefix-cls}-horizontal) &-selected { - background-color: @menu-item-active-danger-bg; - } - - .@{menu-prefix-cls}-inline &::after { - border-right-color: @menu-highlight-danger-color; - } - } - - // ==================== Dark ==================== - &-dark &-item-danger&-item { - &, - &:hover, - & > a { - color: @menu-dark-danger-color; - } - } - - &-dark&-dark:not(&-horizontal) &-item-danger&-item-selected { - color: @menu-dark-highlight-color; - background-color: @menu-dark-item-active-danger-bg; - } -} diff --git a/components/menu/style/theme.ts b/components/menu/style/theme.ts new file mode 100644 index 000000000..f8c33e333 --- /dev/null +++ b/components/menu/style/theme.ts @@ -0,0 +1,262 @@ +import type { CSSInterpolation } from '../../_util/cssinjs'; +import { genFocusOutline } from '../../_style'; +import type { MenuToken } from '.'; + +const accessibilityFocus = (token: MenuToken) => ({ + ...genFocusOutline(token), +}); + +const getThemeStyle = (token: MenuToken, themeSuffix: string): CSSInterpolation => { + const { + componentCls, + colorItemText, + colorItemTextSelected, + colorGroupTitle, + colorItemBg, + colorSubItemBg, + colorItemBgSelected, + colorActiveBarHeight, + colorActiveBarWidth, + colorActiveBarBorderSize, + motionDurationSlow, + motionEaseInOut, + motionEaseOut, + menuItemPaddingInline, + motionDurationMid, + colorItemTextHover, + lineType, + colorSplit, + + // Disabled + colorItemTextDisabled, + + // Danger + colorDangerItemText, + colorDangerItemTextHover, + colorDangerItemTextSelected, + colorDangerItemBgActive, + colorDangerItemBgSelected, + + colorItemBgHover, + menuSubMenuBg, + + // Horizontal + colorItemTextSelectedHorizontal, + colorItemBgSelectedHorizontal, + } = token; + + return { + [`${componentCls}-${themeSuffix}`]: { + color: colorItemText, + background: colorItemBg, + + [`&${componentCls}-root:focus-visible`]: { + ...accessibilityFocus(token), + }, + + // ======================== Item ======================== + [`${componentCls}-item-group-title`]: { + color: colorGroupTitle, + }, + + [`${componentCls}-submenu-selected`]: { + [`> ${componentCls}-submenu-title`]: { + color: colorItemTextSelected, + }, + }, + + // Disabled + [`${componentCls}-item-disabled, ${componentCls}-submenu-disabled`]: { + color: `${colorItemTextDisabled} !important`, + }, + + // Hover + [`${componentCls}-item:hover, ${componentCls}-submenu-title:hover`]: { + [`&:not(${componentCls}-item-selected):not(${componentCls}-submenu-selected)`]: { + color: colorItemTextHover, + }, + }, + + [`&:not(${componentCls}-horizontal)`]: { + [`${componentCls}-item:not(${componentCls}-item-selected)`]: { + '&:hover': { + backgroundColor: colorItemBgHover, + }, + + '&:active': { + backgroundColor: colorItemBgSelected, + }, + }, + + [`${componentCls}-submenu-title`]: { + '&:hover': { + backgroundColor: colorItemBgHover, + }, + + '&:active': { + backgroundColor: colorItemBgSelected, + }, + }, + }, + + // Danger - only Item has + [`${componentCls}-item-danger`]: { + color: colorDangerItemText, + + [`&${componentCls}-item:hover`]: { + [`&:not(${componentCls}-item-selected):not(${componentCls}-submenu-selected)`]: { + color: colorDangerItemTextHover, + }, + }, + + [`&${componentCls}-item:active`]: { + background: colorDangerItemBgActive, + }, + }, + + [`${componentCls}-item a`]: { + '&, &:hover': { + color: 'inherit', + }, + }, + + [`${componentCls}-item-selected`]: { + color: colorItemTextSelected, + + // Danger + [`&${componentCls}-item-danger`]: { + color: colorDangerItemTextSelected, + }, + + [`a, a:hover`]: { + color: 'inherit', + }, + }, + + [`& ${componentCls}-item-selected`]: { + backgroundColor: colorItemBgSelected, + + // Danger + [`&${componentCls}-item-danger`]: { + backgroundColor: colorDangerItemBgSelected, + }, + }, + + [`${componentCls}-item, ${componentCls}-submenu-title`]: { + [`&:not(${componentCls}-item-disabled):focus-visible`]: { + ...accessibilityFocus(token), + }, + }, + + [`&${componentCls}-submenu > ${componentCls}`]: { + backgroundColor: menuSubMenuBg, + }, + + [`&${componentCls}-popup > ${componentCls}`]: { + backgroundColor: colorItemBg, + }, + + // ====================== Horizontal ====================== + [`&${componentCls}-horizontal`]: { + ...(themeSuffix === 'dark' + ? { + borderBottom: 0, + } + : {}), + + [`> ${componentCls}-item, > ${componentCls}-submenu`]: { + top: colorActiveBarBorderSize, + marginTop: -colorActiveBarBorderSize, + marginBottom: 0, + borderRadius: 0, + + '&::after': { + position: 'absolute', + insetInline: menuItemPaddingInline, + bottom: 0, + borderBottom: `${colorActiveBarHeight}px solid transparent`, + transition: `border-color ${motionDurationSlow} ${motionEaseInOut}`, + content: '""', + }, + + [`&:hover, &-active, &-open`]: { + '&::after': { + borderBottomWidth: colorActiveBarHeight, + borderBottomColor: colorItemTextSelectedHorizontal, + }, + }, + [`&-selected`]: { + color: colorItemTextSelectedHorizontal, + backgroundColor: colorItemBgSelectedHorizontal, + '&::after': { + borderBottomWidth: colorActiveBarHeight, + borderBottomColor: colorItemTextSelectedHorizontal, + }, + }, + }, + }, + + // ================== Inline & Vertical =================== + // + [`&${componentCls}-root`]: { + [`&${componentCls}-inline, &${componentCls}-vertical`]: { + borderInlineEnd: `${colorActiveBarBorderSize}px ${lineType} ${colorSplit}`, + }, + }, + + // ======================== Inline ======================== + [`&${componentCls}-inline`]: { + // Sub + [`${componentCls}-sub${componentCls}-inline`]: { + background: colorSubItemBg, + }, + + // Item + [`${componentCls}-item, ${componentCls}-submenu-title`]: + colorActiveBarBorderSize && colorActiveBarWidth + ? { + width: `calc(100% + ${colorActiveBarBorderSize}px)`, + } + : {}, + + [`${componentCls}-item`]: { + position: 'relative', + + '&::after': { + position: 'absolute', + insetBlock: 0, + insetInlineEnd: 0, + borderInlineEnd: `${colorActiveBarWidth}px solid ${colorItemTextSelected}`, + transform: 'scaleY(0.0001)', + opacity: 0, + transition: [ + `transform ${motionDurationMid} ${motionEaseOut}`, + `opacity ${motionDurationMid} ${motionEaseOut}`, + ].join(','), + content: '""', + }, + + // Danger + [`&${componentCls}-item-danger`]: { + '&::after': { + borderInlineEndColor: colorDangerItemTextSelected, + }, + }, + }, + + [`${componentCls}-selected, ${componentCls}-item-selected`]: { + '&::after': { + transform: 'scaleY(1)', + opacity: 1, + transition: [ + `transform ${motionDurationMid} ${motionEaseInOut}`, + `opacity ${motionDurationMid} ${motionEaseInOut}`, + ].join(','), + }, + }, + }, + }, + }; +}; + +export default getThemeStyle; diff --git a/components/menu/style/vertical.ts b/components/menu/style/vertical.ts new file mode 100644 index 000000000..5ce3fb2e5 --- /dev/null +++ b/components/menu/style/vertical.ts @@ -0,0 +1,231 @@ +import type { CSSObject } from '../../_util/cssinjs'; +import { textEllipsis } from '../../_style'; +import type { MenuToken } from '.'; +import type { GenerateStyle } from '../../theme/internal'; + +const getVerticalInlineStyle: GenerateStyle = token => { + const { + componentCls, + menuItemHeight, + itemMarginInline, + padding, + menuArrowSize, + marginXS, + marginXXS, + } = token; + + const paddingWithArrow = padding + menuArrowSize + marginXS; + + return { + [`${componentCls}-item`]: { + position: 'relative', + }, + + [`${componentCls}-item, ${componentCls}-submenu-title`]: { + height: menuItemHeight, + lineHeight: `${menuItemHeight}px`, + paddingInline: padding, + overflow: 'hidden', + textOverflow: 'ellipsis', + + marginInline: itemMarginInline, + marginBlock: marginXXS, + width: `calc(100% - ${itemMarginInline * 2}px)`, + }, + + // disable margin collapsed + [`${componentCls}-submenu`]: { + paddingBottom: 0.02, + }, + + [`> ${componentCls}-item, + > ${componentCls}-submenu > ${componentCls}-submenu-title`]: { + height: menuItemHeight, + lineHeight: `${menuItemHeight}px`, + }, + + [`${componentCls}-item-group-list ${componentCls}-submenu-title, + ${componentCls}-submenu-title`]: { + paddingInlineEnd: paddingWithArrow, + }, + }; +}; + +const getVerticalStyle: GenerateStyle = token => { + const { + componentCls, + iconCls, + menuItemHeight, + colorTextLightSolid, + dropdownWidth, + controlHeightLG, + motionDurationMid, + motionEaseOut, + paddingXL, + fontSizeSM, + fontSizeLG, + motionDurationSlow, + paddingXS, + boxShadowSecondary, + } = token; + + const inlineItemStyle: CSSObject = { + height: menuItemHeight, + lineHeight: `${menuItemHeight}px`, + listStylePosition: 'inside', + listStyleType: 'disc', + }; + + return [ + { + [componentCls]: { + [`&-inline, &-vertical`]: { + [`&${componentCls}-root`]: { + boxShadow: 'none', + }, + + ...getVerticalInlineStyle(token), + }, + }, + + [`${componentCls}-submenu-popup`]: { + [`${componentCls}-vertical`]: { + ...getVerticalInlineStyle(token), + boxShadow: boxShadowSecondary, + }, + }, + }, + + // Vertical only + { + [`${componentCls}-submenu-popup ${componentCls}-vertical${componentCls}-sub`]: { + minWidth: dropdownWidth, + maxHeight: `calc(100vh - ${controlHeightLG * 2.5}px)`, + padding: '0', + overflow: 'hidden', + borderInlineEnd: 0, + + // https://github.com/ant-design/ant-design/issues/22244 + // https://github.com/ant-design/ant-design/issues/26812 + "&:not([class*='-active'])": { + overflowX: 'hidden', + overflowY: 'auto', + }, + }, + }, + + // Inline Only + { + [`${componentCls}-inline`]: { + width: '100%', + + // Motion enhance for first level + [`&${componentCls}-root`]: { + [`${componentCls}-item, ${componentCls}-submenu-title`]: { + display: 'flex', + alignItems: 'center', + transition: [ + `border-color ${motionDurationSlow}`, + `background ${motionDurationSlow}`, + `padding ${motionDurationMid} ${motionEaseOut}`, + ].join(','), + + [`> ${componentCls}-title-content`]: { + flex: 'auto', + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + + '> *': { + flex: 'none', + }, + }, + }, + + // >>>>> Sub + [`${componentCls}-sub${componentCls}-inline`]: { + padding: 0, + border: 0, + borderRadius: 0, + boxShadow: 'none', + + [`& > ${componentCls}-submenu > ${componentCls}-submenu-title`]: inlineItemStyle, + + [`& ${componentCls}-item-group-title`]: { + paddingInlineStart: paddingXL, + }, + }, + + // >>>>> Item + [`${componentCls}-item`]: inlineItemStyle, + }, + }, + + // Inline Collapse Only + { + [`${componentCls}-inline-collapsed`]: { + width: menuItemHeight * 2, + + [`&${componentCls}-root`]: { + [`${componentCls}-item, ${componentCls}-submenu ${componentCls}-submenu-title`]: { + [`> ${componentCls}-inline-collapsed-noicon`]: { + fontSize: fontSizeLG, + textAlign: 'center', + }, + }, + }, + + [`> ${componentCls}-item, + > ${componentCls}-item-group > ${componentCls}-item-group-list > ${componentCls}-item, + > ${componentCls}-item-group > ${componentCls}-item-group-list > ${componentCls}-submenu > ${componentCls}-submenu-title, + > ${componentCls}-submenu > ${componentCls}-submenu-title`]: { + insetInlineStart: 0, + paddingInline: `calc(50% - ${fontSizeSM}px)`, + textOverflow: 'clip', + + [` + ${componentCls}-submenu-arrow, + ${componentCls}-submenu-expand-icon + `]: { + opacity: 0, + }, + + [`${componentCls}-item-icon, ${iconCls}`]: { + margin: 0, + fontSize: fontSizeLG, + lineHeight: `${menuItemHeight}px`, + + '+ span': { + display: 'inline-block', + opacity: 0, + }, + }, + }, + + [`${componentCls}-item-icon, ${iconCls}`]: { + display: 'inline-block', + }, + + '&-tooltip': { + pointerEvents: 'none', + + [`${componentCls}-item-icon, ${iconCls}`]: { + display: 'none', + }, + + 'a, a:hover': { + color: colorTextLightSolid, + }, + }, + + [`${componentCls}-item-group-title`]: { + ...textEllipsis, + paddingInline: paddingXS, + }, + }, + }, + ]; +}; + +export default getVerticalStyle; diff --git a/components/style.ts b/components/style.ts index 8f26f68d2..a39c73fbb 100644 --- a/components/style.ts +++ b/components/style.ts @@ -13,7 +13,7 @@ import './input/style'; import './tooltip/style'; import './popover/style'; import './popconfirm/style'; -import './menu/style'; +// import './menu/style'; import './mentions/style'; import './dropdown/style'; // import './divider/style'; diff --git a/components/theme/interface/components.ts b/components/theme/interface/components.ts index a8f91fc46..9db269bd3 100644 --- a/components/theme/interface/components.ts +++ b/components/theme/interface/components.ts @@ -20,7 +20,7 @@ import type { ComponentToken as DividerComponentToken } from '../../divider/styl // import type { ComponentToken as LayoutComponentToken } from '../../layout/style'; // import type { ComponentToken as ListComponentToken } from '../../list/style'; // import type { ComponentToken as MentionsComponentToken } from '../../mentions/style'; -// import type { ComponentToken as MenuComponentToken } from '../../menu/style'; +import type { ComponentToken as MenuComponentToken } from '../../menu/style'; import type { ComponentToken as MessageComponentToken } from '../../message/style'; import type { ComponentToken as ModalComponentToken } from '../../modal/style'; import type { ComponentToken as NotificationComponentToken } from '../../notification/style'; @@ -102,7 +102,7 @@ export interface ComponentTokenMap { // Tabs?: TabsComponentToken; // Calendar?: CalendarComponentToken; // Steps?: StepsComponentToken; - // Menu?: MenuComponentToken; + Menu?: MenuComponentToken; Modal?: ModalComponentToken; Message?: MessageComponentToken; // Upload?: UploadComponentToken;