From 73c9d3f9c17457dd05f6e0f7d035e3ca2927a0e0 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Fri, 15 Jan 2021 17:15:59 +0800 Subject: [PATCH] refactor(menu): ts --- src/_styles/new-common/dark.ts | 14 +- src/_styles/new-common/index.ts | 1 + src/_styles/new-common/light.ts | 12 +- src/_utils/composable/use-adjusted-to.ts | 11 +- src/_utils/cssr/create-key.ts | 98 ++++++++ src/_utils/cssr/index.ts | 15 +- src/_utils/index.ts | 1 + src/_utils/vue/index.ts | 1 + src/_utils/vue/keep.ts | 8 +- src/_utils/vue/keysOf.ts | 3 + src/dropdown/src/Dropdown.ts | 14 +- src/menu/index.js | 1 - src/menu/index.ts | 1 + src/menu/src/{Menu.js => Menu.ts} | 189 ++++++++++----- src/menu/src/MenuItem.ts | 118 ++++++++++ src/menu/src/MenuItem.vue | 107 --------- src/menu/src/MenuItemContent.ts | 123 ++++++++++ src/menu/src/MenuItemContent.vue | 109 --------- src/menu/src/MenuItemGroup.js | 47 ---- src/menu/src/MenuItemGroup.ts | 54 +++++ src/menu/src/Submenu.js | 171 -------------- src/menu/src/Submenu.ts | 196 ++++++++++++++++ src/menu/src/menu-child-mixin.js | 106 --------- src/menu/src/styles/index.cssr.js | 234 ------------------- src/menu/src/styles/index.cssr.ts | 285 +++++++++++++++++++++++ src/menu/src/use-menu-child.ts | 139 +++++++++++ src/menu/src/utils.js | 46 ---- src/menu/src/utils.ts | 44 ++++ src/menu/styles/{dark.js => dark.ts} | 4 +- src/menu/styles/index.js | 2 - src/menu/styles/index.ts | 3 + src/menu/styles/{light.js => light.ts} | 9 +- 32 files changed, 1247 insertions(+), 919 deletions(-) create mode 100644 src/_utils/cssr/create-key.ts create mode 100644 src/_utils/vue/keysOf.ts delete mode 100644 src/menu/index.js create mode 100644 src/menu/index.ts rename src/menu/src/{Menu.js => Menu.ts} (57%) create mode 100644 src/menu/src/MenuItem.ts delete mode 100644 src/menu/src/MenuItem.vue create mode 100644 src/menu/src/MenuItemContent.ts delete mode 100644 src/menu/src/MenuItemContent.vue delete mode 100644 src/menu/src/MenuItemGroup.js create mode 100644 src/menu/src/MenuItemGroup.ts delete mode 100644 src/menu/src/Submenu.js create mode 100644 src/menu/src/Submenu.ts delete mode 100644 src/menu/src/menu-child-mixin.js delete mode 100644 src/menu/src/styles/index.cssr.js create mode 100644 src/menu/src/styles/index.cssr.ts create mode 100644 src/menu/src/use-menu-child.ts delete mode 100644 src/menu/src/utils.js create mode 100644 src/menu/src/utils.ts rename src/menu/styles/{dark.js => dark.ts} (87%) delete mode 100644 src/menu/styles/index.js create mode 100644 src/menu/styles/index.ts rename src/menu/styles/{light.js => light.ts} (83%) diff --git a/src/_styles/new-common/dark.ts b/src/_styles/new-common/dark.ts index 9a38c501a..9bd381ef5 100644 --- a/src/_styles/new-common/dark.ts +++ b/src/_styles/new-common/dark.ts @@ -1,5 +1,6 @@ import { rgba, composite } from 'seemly' -import commonVariables from './_common.js' +import type { ThemeCommonVars } from './light' +import commonVariables from './_common' const base = { neutralBase: '#000', @@ -72,15 +73,18 @@ const baseBackgroundRgb = rgba(base.neutralBase) const baseInvertBackgroundRgb = rgba(base.neutralInvertBase) const overlayPrefix = 'rgba(' + baseInvertBackgroundRgb.slice(0, 3).join(', ') + ', ' -function overlay (alpha) { +function overlay (alpha: number | string) { return overlayPrefix + String(alpha) + ')' } -function neutral (alpha) { +function neutral (alpha: number | string) { const overlayRgba = Array.from(baseInvertBackgroundRgb) overlayRgba[3] = Number(alpha) - return composite(baseBackgroundRgb, overlayRgba as any) + return composite( + baseBackgroundRgb, + overlayRgba as [number, number, number, number] + ) } -const derived = { +const derived: ThemeCommonVars = { ...commonVariables, baseColor: base.neutralBase, diff --git a/src/_styles/new-common/index.ts b/src/_styles/new-common/index.ts index fea4336cb..9663eb476 100644 --- a/src/_styles/new-common/index.ts +++ b/src/_styles/new-common/index.ts @@ -1,2 +1,3 @@ export { default as commonDark } from './dark' export { default as commonLight } from './light' +export type { ThemeCommonVars } from './light' diff --git a/src/_styles/new-common/light.ts b/src/_styles/new-common/light.ts index 956dcbba0..2ee8e4bf3 100644 --- a/src/_styles/new-common/light.ts +++ b/src/_styles/new-common/light.ts @@ -1,5 +1,5 @@ import { rgba, composite } from 'seemly' -import commonVariables from './_common.js' +import commonVariables from './_common' const base = { neutralBase: '#FFF', @@ -74,13 +74,16 @@ const baseBackgroundRgb = rgba(base.neutralBase) const baseInvertBackgroundRgb = rgba(base.neutralInvertBase) const overlayPrefix = 'rgba(' + baseInvertBackgroundRgb.slice(0, 3).join(', ') + ', ' -function overlay (alpha) { +function overlay (alpha: string | number) { return overlayPrefix + String(alpha) + ')' } -function neutral (alpha) { +function neutral (alpha: string | number) { const overlayRgba = Array.from(baseInvertBackgroundRgb) overlayRgba[3] = Number(alpha) - return composite(baseBackgroundRgb, overlayRgba as any) + return composite( + baseBackgroundRgb, + overlayRgba as [number, number, number, number] + ) } const derived = { ...commonVariables, @@ -203,3 +206,4 @@ const derived = { } export default derived +export type ThemeCommonVars = typeof derived diff --git a/src/_utils/composable/use-adjusted-to.ts b/src/_utils/composable/use-adjusted-to.ts index 91fc4a27b..513774e23 100644 --- a/src/_utils/composable/use-adjusted-to.ts +++ b/src/_utils/composable/use-adjusted-to.ts @@ -1,19 +1,22 @@ import { useMemo } from 'vooks' -import { inject } from 'vue' +import { ComputedRef, inject } from 'vue' interface UseAdjustedToProps { to?: string + [key: string]: unknown } interface ModalInjection { - bodyRef: Element + bodyRef: HTMLElement } interface DrawerInjection { - bodyRef: Element + bodyRef: HTMLElement } -export function useAdjustedTo (props: UseAdjustedToProps) { +export function useAdjustedTo ( + props: UseAdjustedToProps +): ComputedRef { const modal = inject('NModalBody', null) const drawer = inject('NDrawerBody', null) return useMemo(() => { diff --git a/src/_utils/cssr/create-key.ts b/src/_utils/cssr/create-key.ts new file mode 100644 index 000000000..2521bedf8 --- /dev/null +++ b/src/_utils/cssr/create-key.ts @@ -0,0 +1,98 @@ +type characterMap = { + 1: '1' + 2: '2' + 3: '3' + 4: '4' + 5: '5' + 6: '6' + 7: '7' + 8: '8' + 9: '9' + 0: '0' + q: 'Q' + w: 'W' + e: 'E' + r: 'R' + t: 'T' + y: 'Y' + u: 'U' + i: 'I' + o: 'O' + p: 'P' + a: 'A' + s: 'S' + d: 'D' + f: 'F' + g: 'G' + h: 'H' + j: 'J' + k: 'K' + l: 'L' + z: 'Z' + x: 'X' + c: 'C' + v: 'V' + b: 'B' + n: 'N' + m: 'M' + [key: string]: string +} + +type char = + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9' + | '0' + | 'q' + | 'w' + | 'e' + | 'r' + | 't' + | 'y' + | 'u' + | 'i' + | 'o' + | 'p' + | 'a' + | 's' + | 'd' + | 'f' + | 'g' + | 'h' + | 'j' + | 'k' + | 'l' + | 'z' + | 'x' + | 'c' + | 'v' + | 'b' + | 'n' + | 'm' + +type RestChars = T extends `${char}${infer P}` ? P : '' + +type UpperFirst = T extends `${infer P}${string}` + ? `${characterMap[P]}${RestChars}` + : T + +export function createKey

( + prefix: P, + suffix: S +): S extends 'default' ? P : `${P}${UpperFirst}` { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (prefix + + (suffix === 'default' + ? '' + : suffix.replace(/^[a-z]/, (startChar) => + startChar.toUpperCase() + ))) as any +} + +createKey('abc', 'def') diff --git a/src/_utils/cssr/index.ts b/src/_utils/cssr/index.ts index 19758bdb9..0d651d55e 100644 --- a/src/_utils/cssr/index.ts +++ b/src/_utils/cssr/index.ts @@ -31,18 +31,6 @@ function insideModal (style: CNode) { return c(`${prefix}modal, ${prefix}drawer`, [style]) } -function createKey (keyPrefix: string, ...suffixs: string[]) { - return ( - keyPrefix + - suffixs - .map((suffix) => { - if (suffix === 'default') return '' - return suffix.replace(/^[a-z]/, (startChar) => startChar.toUpperCase()) - }) - .join('') - ) -} - function cRB (selector: string, ...rest: any[]): CNode { return (c as any)(`${prefix}${selector}`, ...rest) } @@ -63,6 +51,7 @@ export { withPrefix, prefix, namespace, - createKey, find } + +export { createKey } from './create-key' diff --git a/src/_utils/index.ts b/src/_utils/index.ts index 050186c2e..f39c8d178 100644 --- a/src/_utils/index.ts +++ b/src/_utils/index.ts @@ -5,6 +5,7 @@ export { flatten, getSlot, getVNodeChildren, + keysOf, render } from './vue' export { warn, warnOnce } from './naive' diff --git a/src/_utils/vue/index.ts b/src/_utils/vue/index.ts index f1c9da8dc..820905a2f 100644 --- a/src/_utils/vue/index.ts +++ b/src/_utils/vue/index.ts @@ -4,4 +4,5 @@ export { keep } from './keep' export { omit } from './omit' export { flatten } from './flatten' export { call } from './call' +export { keysOf } from './keysOf' export { render } from './render' diff --git a/src/_utils/vue/keep.ts b/src/_utils/vue/keep.ts index 82aa2956d..075b1207d 100644 --- a/src/_utils/vue/keep.ts +++ b/src/_utils/vue/keep.ts @@ -1,7 +1,11 @@ -export function keep (object: T, keys: K[] = [], rest: R): Pick & R { +export function keep ( + object: T, + keys: K[] = [], + rest?: R +): Pick & R { const keepedObject: any = {} keys.forEach((key) => { - keepedObject[key] = object[key] + keepedObject[key] = (object as any)[key] }) return Object.assign(keepedObject, rest) } diff --git a/src/_utils/vue/keysOf.ts b/src/_utils/vue/keysOf.ts new file mode 100644 index 000000000..db4f59089 --- /dev/null +++ b/src/_utils/vue/keysOf.ts @@ -0,0 +1,3 @@ +export function keysOf (obj: T): (keyof T)[] { + return Object.keys(obj) as any +} diff --git a/src/dropdown/src/Dropdown.ts b/src/dropdown/src/Dropdown.ts index 5139cf17c..a76d69c8e 100644 --- a/src/dropdown/src/Dropdown.ts +++ b/src/dropdown/src/Dropdown.ts @@ -9,7 +9,7 @@ import { provide, reactive } from 'vue' -import { RawNode, TreeMate } from 'treemate' +import { RawNode, TreeMate, Key } from 'treemate' import { useMergedState, useKeyboard, useMemo } from 'vooks' import { useTheme } from '../../_mixins' import { NPopover, popoverProps } from '../../popover' @@ -19,7 +19,6 @@ import type { DropdownThemeVars } from '../styles' import NDropdownMenu from './DropdownMenu' import style from './styles/index.cssr' -type Key = string | number type OnSelect = (key: Key, rawNode: RawNode) => void const treemateOptions = { @@ -79,7 +78,7 @@ const dropdownProps = { }, // for menu value: { - type: String, + type: [String, Number] as PropType, default: undefined } } as const @@ -92,7 +91,8 @@ export default defineComponent({ name: 'Dropdown', props: { ...popoverProps, - ...dropdownProps + ...dropdownProps, + ...useTheme.createProps() }, setup (props) { const uncontrolledShowRef = ref(false) @@ -162,7 +162,7 @@ export default defineComponent({ keyboardEnabledRef ) - const themeRef = useTheme( + const themeRef = useTheme( 'Dropdown', 'Dropdown', style, @@ -332,7 +332,9 @@ export default defineComponent({ 'onUpdate:show': this.doUpdateShow, showArrow: false, raw: true, - shadow: false + shadow: false, + // TODO: using peers + unstableTheme: undefined }), { trigger: this.$slots.default, diff --git a/src/menu/index.js b/src/menu/index.js deleted file mode 100644 index 8033d3eef..000000000 --- a/src/menu/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as NMenu } from './src/Menu.js' diff --git a/src/menu/index.ts b/src/menu/index.ts new file mode 100644 index 000000000..c2001d921 --- /dev/null +++ b/src/menu/index.ts @@ -0,0 +1 @@ +export { default as NMenu } from './src/Menu' diff --git a/src/menu/src/Menu.js b/src/menu/src/Menu.ts similarity index 57% rename from src/menu/src/Menu.js rename to src/menu/src/Menu.ts index 5d39537b8..913f5f23c 100644 --- a/src/menu/src/Menu.js +++ b/src/menu/src/Menu.ts @@ -1,23 +1,29 @@ -import { h, ref, toRef, computed, defineComponent } from 'vue' -import { createTreeMate } from 'treemate' +import { + h, + ref, + toRef, + computed, + defineComponent, + provide, + reactive, + PropType +} from 'vue' +import { createTreeMate, Key, RawNode } from 'treemate' import { useCompitable, useMergedState } from 'vooks' import { useTheme } from '../../_mixins' +import { call, warn } from '../../_utils' import { itemRenderer } from './utils' import { menuLight } from '../styles' -import style from './styles/index.cssr.js' +import type { MenuThemeVars } from '../styles' +import { MenuInjection } from './use-menu-child' +import style from './styles/index.cssr' export default defineComponent({ name: 'Menu', - provide () { - return { - NMenu: this, - NSubmenu: null - } - }, props: { - ...useTheme.props, + ...useTheme.createProps(), items: { - type: Array, + type: Array as PropType, required: true }, collapsed: { @@ -49,25 +55,23 @@ export default defineComponent({ default: false }, defaultExpandedKeys: { - type: Array, + type: Array as PropType, default: () => [] }, expandedKeys: { - type: Array, + type: Array as PropType, default: undefined }, value: { - type: String, + type: [String, Number] as PropType, default: undefined }, defaultValue: { - type: String, + type: [String, Number] as PropType, default: null }, mode: { - validator (value) { - return ['vertical', 'horizontal'].includes(value) - }, + type: String as PropType<'vertical' | 'horizontal'>, default: 'vertical' }, disabled: { @@ -76,38 +80,74 @@ export default defineComponent({ }, // eslint-disable-next-line vue/prop-name-casing 'onUpdate:expandedKeys': { - type: Function, - default: () => {} + type: Function as PropType<(value: Key[]) => void>, + default: undefined }, // eslint-disable-next-line vue/prop-name-casing 'onUpdate:value': { - type: Function, - default: () => {} + type: Function as PropType<(value: Key) => void>, + default: undefined }, // deprecated onOpenNamesChange: { - type: Function, - default: () => {} + type: Function as PropType<(value: Key[]) => void>, + validator: () => { + warn( + 'menu', + '`on-open-names-change` is deprecated, please use `on-update:expanded-keys` instead.' + ) + return true + }, + default: undefined }, onSelect: { - type: Function, - default: () => {} + type: Function as PropType<(value: Key) => void>, + validator: () => { + warn( + 'menu', + '`on-select` is deprecated, please use `on-update:value` instead.' + ) + return true + }, + default: undefined }, onExpandedNamesChange: { - type: Function, - default: () => {} + type: Function as PropType<(value: Key[]) => void>, + validator: () => { + warn( + 'menu', + '`on-expanded-names-change` is deprecated, please use `on-update:expanded-keys` instead.' + ) + return true + }, + default: undefined }, expandedNames: { - type: Array, + type: Array as PropType, + validator: () => { + warn( + 'menu', + '`expanded-names` is deprecated, please use `expanded-keys` instead.' + ) + return true + }, default: undefined }, defaultExpandedNames: { - type: Array, + type: Array as PropType, + validator: () => { + warn( + 'menu', + '`default-expanded-names` is deprecated, please use `default-expanded-keys` instead.' + ) + return true + }, default: undefined } }, setup (props) { const themeRef = useTheme('Menu', 'Menu', style, menuLight, props) + const treeMateRef = computed(() => createTreeMate(props.items, { getKey (node) { @@ -120,11 +160,10 @@ export default defineComponent({ ? treeMateRef.value.getNonLeafKeys() : props.defaultExpandedNames || props.defaultExpandedKeys ) - const controlledExpandedKeysRef = useCompitable( - props, + const controlledExpandedKeysRef = useCompitable(props, [ 'expandedNames', 'expandedKeys' - ) + ]) const mergedExpandedKeysRef = useMergedState( controlledExpandedKeysRef, uncontrolledExpandedKeysRef @@ -139,6 +178,64 @@ export default defineComponent({ const activePathRef = computed(() => { return treeMateRef.value.getPath(mergedValueRef.value).keyPath }) + provide( + 'NMenu', + reactive({ + mergedTheme: themeRef, + mode: toRef(props, 'mode'), + collapsed: toRef(props, 'collapsed'), + iconSize: toRef(props, 'iconSize'), + indent: toRef(props, 'indent'), + rootIndent: toRef(props, 'rootIndent'), + collapsedWidth: toRef(props, 'collapsedWidth'), + disabled: toRef(props, 'disabled'), + mergedValue: mergedValueRef, + mergedExpandedKeys: mergedExpandedKeysRef, + activePath: activePathRef, + doSelect, + toggleExpand + }) + ) + function doSelect (value: Key, item: RawNode) { + const { 'onUpdate:value': onUpdateValue, onSelect } = props + if (onUpdateValue) { + call(onUpdateValue, value, item) + } + if (onSelect) { + call(onSelect, value, item) + } + uncontrolledValueRef.value = value + } + function doUpdateExpandedKeys (value: Key[]) { + const { + 'onUpdate:expandedKeys': onUpdateExpandedKeys, + onExpandedNamesChange, + onOpenNamesChange + } = props + if (onUpdateExpandedKeys) { + call(onUpdateExpandedKeys, value) + } + // deprecated + if (onExpandedNamesChange) { + call(onExpandedNamesChange, value) + } + if (onOpenNamesChange) { + call(onOpenNamesChange, value) + } + uncontrolledExpandedKeysRef.value = value + } + function toggleExpand (key: Key) { + const currentExpandedKeys = Array.from(mergedExpandedKeysRef.value) + const index = currentExpandedKeys.findIndex( + (expanededKey) => expanededKey === key + ) + if (~index) { + currentExpandedKeys.splice(index, 1) + } else { + currentExpandedKeys.push(key) + } + doUpdateExpandedKeys(currentExpandedKeys) + } return { controlledExpandedKeys: controlledExpandedKeysRef, uncontrolledExpanededKeys: uncontrolledExpandedKeysRef, @@ -190,32 +287,6 @@ export default defineComponent({ }) } }, - methods: { - doSelect (value, item) { - this['onUpdate:value'](value, item) - // deprecated - this.onSelect(value, item) - this.uncontrolledValue = value - }, - toggleExpand (key) { - const currentExpandedKeys = Array.from(this.mergedExpandedKeys) - const index = currentExpandedKeys.findIndex( - (expanededKey) => expanededKey === key - ) - if (~index) { - currentExpandedKeys.splice(index, 1) - } else { - currentExpandedKeys.push(key) - } - if (this.controlledExpandedKeys === undefined) { - this.uncontrolledExpanededKeys = currentExpandedKeys - } - this['onUpdate:expandedKeys'](currentExpandedKeys) - // deprecated - this.onExpandedNamesChange(currentExpandedKeys) - this.onOpenNamesChange(currentExpandedKeys) - } - }, render () { return h( 'div', diff --git a/src/menu/src/MenuItem.ts b/src/menu/src/MenuItem.ts new file mode 100644 index 000000000..194d927a9 --- /dev/null +++ b/src/menu/src/MenuItem.ts @@ -0,0 +1,118 @@ +import { h, computed, defineComponent, toRef, PropType } from 'vue' +import { useMemo } from 'vooks' +import { NTooltip } from '../../tooltip' +import NMenuItemContent from './MenuItemContent' +import { useMenuChild } from './use-menu-child' +import { TreeNode } from 'treemate' + +export const menuItemProps = { + ...useMenuChild.props, + tmNode: { + type: Object as PropType, + required: true + }, + disabled: { + type: Boolean, + default: false + }, + icon: { + type: Function, + default: undefined + }, + onClick: { + type: Function, + default: undefined + } +} as const + +export default defineComponent({ + name: 'MenuItem', + components: { + NMenuItemContent, + NTooltip + }, + props: menuItemProps, + setup (props) { + const MenuChild = useMenuChild(props) + const { NSubmenu, NMenu } = MenuChild + const submenuDisabledRef = NSubmenu + ? toRef(NSubmenu, 'mergedDisabled') + : { value: false } + const mergedDisabledRef = computed(() => { + return submenuDisabledRef.value || props.disabled + }) + function doClick (e: MouseEvent) { + const { onClick } = props + if (onClick) onClick(e) + } + function handleClick (e: MouseEvent): void { + if (!mergedDisabledRef.value) { + NMenu.doSelect(props.internalKey, props.tmNode.rawNode) + doClick(e) + } + } + return { + dropdownPlacement: MenuChild.dropdownPlacement, + paddingLeft: MenuChild.paddingLeft, + iconMarginRight: MenuChild.iconMarginRight, + maxIconSize: MenuChild.maxIconSize, + activeIconSize: MenuChild.activeIconSize, + mergedTheme: NMenu.mergedTheme, + dropdownEnabled: useMemo(() => { + return ( + props.root && + NMenu.collapsed && + NMenu.mode === 'horizontal' && + !mergedDisabledRef.value + ) + }), + selected: useMemo(() => { + if (NMenu.mergedValue === props.internalKey) return true + return false + }), + mergedDisabled: mergedDisabledRef, + handleClick + } + }, + render () { + return h( + 'div', + { + class: [ + 'n-menu-item', + { + 'n-menu-item--selected': this.selected, + 'n-menu-item--disabled': this.mergedDisabled + } + ] + }, + [ + h( + NTooltip, + { + unstableTheme: this.mergedTheme.peers.Tooltip, + unstableThemeOverrides: this.mergedTheme.overrides.Tooltip, + trigger: 'hover', + placement: this.dropdownPlacement, + disabled: !this.dropdownEnabled + }, + { + default: () => this.title, + trigger: () => { + return h(NMenuItemContent, { + paddingLeft: this.paddingLeft, + iconMarginRight: this.iconMarginRight, + maxIconSize: this.maxIconSize, + activeIconSize: this.activeIconSize, + title: this.title, + disabled: this.mergedDisabled, + icon: this.icon, + onClick: this.handleClick + }) + } + } + ) + ] + ) + } +}) diff --git a/src/menu/src/MenuItem.vue b/src/menu/src/MenuItem.vue deleted file mode 100644 index 82f3a24e3..000000000 --- a/src/menu/src/MenuItem.vue +++ /dev/null @@ -1,107 +0,0 @@ - - - diff --git a/src/menu/src/MenuItemContent.ts b/src/menu/src/MenuItemContent.ts new file mode 100644 index 000000000..119b4c23f --- /dev/null +++ b/src/menu/src/MenuItemContent.ts @@ -0,0 +1,123 @@ +import { computed, defineComponent, h } from 'vue' +import { ChevronDownFilledIcon } from '../../_base/icons' +import { render } from '../../_utils' +import { NBaseIcon } from '../../_base' + +export default defineComponent({ + name: 'MenuItemContent', + props: { + collapsed: { + type: Boolean, + default: false + }, + disabled: { + type: Boolean, + default: false + }, + paddingLeft: { + type: Number, + default: undefined + }, + maxIconSize: { + type: Number, + default: undefined + }, + activeIconSize: { + type: Number, + default: undefined + }, + title: { + type: [String, Function], + default: undefined + }, + icon: { + type: [String, Function], + default: undefined + }, + showArrow: { + type: Boolean, + default: false + }, + childActive: { + type: Boolean, + default: false + }, + hover: { + type: Boolean, + default: false + }, + iconMarginRight: { + type: Number, + required: true + } + }, + setup (props) { + return { + style: computed(() => { + const { paddingLeft } = props + return { paddingLeft: paddingLeft && paddingLeft + 'px' } + }), + iconStyle: computed(() => { + const { maxIconSize, activeIconSize, iconMarginRight } = props + return { + width: maxIconSize + 'px', + height: maxIconSize + 'px', + fontSize: activeIconSize + 'px', + marginRight: iconMarginRight + 'px' + } + }) + } + }, + render () { + return h( + 'div', + { + class: [ + 'n-menu-item-content', + { + 'n-menu-item-content--collapsed': this.collapsed, + 'n-menu-item-content--child-active': this.childActive, + 'n-menu-item-content--disabled': this.disabled, + 'n-menu-item-content--hover': this.hover + } + ], + style: this.style + }, + [ + this.icon && + h( + 'div', + { + class: 'n-menu-item-content__icon', + style: this.iconStyle + }, + [ + h(render, { + render: this.icon + }) + ] + ), + h( + 'div', + { + class: 'n-menu-item-content-header' + }, + [ + h(render, { + render: this.title + }) + ] + ), + this.showArrow + ? h( + NBaseIcon, + { + class: 'n-menu-item-content__arrow' + }, + [h(ChevronDownFilledIcon)] + ) + : null + ] + ) + } +}) diff --git a/src/menu/src/MenuItemContent.vue b/src/menu/src/MenuItemContent.vue deleted file mode 100644 index 1c33655de..000000000 --- a/src/menu/src/MenuItemContent.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - diff --git a/src/menu/src/MenuItemGroup.js b/src/menu/src/MenuItemGroup.js deleted file mode 100644 index d69a9332f..000000000 --- a/src/menu/src/MenuItemGroup.js +++ /dev/null @@ -1,47 +0,0 @@ -import { h, defineComponent } from 'vue' -import { render } from '../../_utils' -import { itemRenderer } from './utils' -import menuChildMixin from './menu-child-mixin' - -export default defineComponent({ - name: 'MenuItemGroup', - mixins: [menuChildMixin], - provide () { - return { - NMenuItemGroup: this, - NSubmenu: null - } - }, - props: { - tmNodes: { - type: Array, - required: true - } - }, - render () { - return h( - 'div', - { - class: 'n-menu-item-group' - }, - [ - h( - 'span', - { - class: 'n-menu-item-group-title', - style: `padding-left: ${this.paddingLeft}px;` - }, - [ - h(render, { - render: this.title - }) - ] - ), - h( - 'div', - this.tmNodes.map((item) => itemRenderer(item)) - ) - ] - ) - } -}) diff --git a/src/menu/src/MenuItemGroup.ts b/src/menu/src/MenuItemGroup.ts new file mode 100644 index 000000000..9bbd12e07 --- /dev/null +++ b/src/menu/src/MenuItemGroup.ts @@ -0,0 +1,54 @@ +import { h, defineComponent, provide, PropType, reactive } from 'vue' +import { render } from '../../_utils' +import { useMenuChild } from './use-menu-child' +import type { MenuItemGroupInjection } from './use-menu-child' +import { itemRenderer } from './utils' +import { TreeNode } from 'treemate' + +export const menuItemGroupProps = { + ...useMenuChild.props, + tmNodes: { + type: Array as PropType, + required: true + } +} as const + +export default defineComponent({ + name: 'MenuItemGroup', + props: menuItemGroupProps, + setup (props) { + provide('NSubmenu', null) + const MenuChild = useMenuChild(props) + provide( + 'NMenuItemGroup', + reactive({ paddingLeft: MenuChild.paddingLeft }) + ) + return function () { + const paddingLeft = MenuChild.paddingLeft.value + return h( + 'div', + { + class: 'n-menu-item-group' + }, + [ + h( + 'span', + { + class: 'n-menu-item-group-title', + style: paddingLeft && `padding-left: ${paddingLeft}px;` + }, + [ + h(render, { + render: props.title + }) + ] + ), + h( + 'div', + props.tmNodes.map((tmNode) => itemRenderer(tmNode)) + ) + ] + ) + } + } +}) diff --git a/src/menu/src/Submenu.js b/src/menu/src/Submenu.js deleted file mode 100644 index d74ca80bb..000000000 --- a/src/menu/src/Submenu.js +++ /dev/null @@ -1,171 +0,0 @@ -import { h, ref, defineComponent } from 'vue' -import { NFadeInExpandTransition } from '../../_base' -import { NDropdown } from '../../dropdown' -import NMenuItemContent from './MenuItemContent.vue' -import menuChildMixin from './menu-child-mixin' -import { itemRenderer } from './utils' -import { useMemo } from 'vooks' -import { useInjectionRef } from '../../_utils/composable' - -export default defineComponent({ - name: 'Submenu', - mixins: [menuChildMixin], - provide () { - return { - NSubmenu: this, - NMenuItemGroup: null - } - }, - props: { - rawNodes: { - type: Array, - required: true - }, - tmNodes: { - type: Array, - required: true - }, - disabled: { - type: Boolean, - default: false - }, - icon: { - type: Function, - default: undefined - }, - onClick: { - type: Function, - default: undefined - } - }, - setup (props) { - const activePathRef = useInjectionRef('NMenu', 'activePath') - return { - childActive: useMemo(() => { - return activePathRef.value.includes(props.internalKey) - }), - dropdownShow: ref(false) - } - }, - computed: { - mergedDisabled () { - const { NMenu, NSubmenu, disabled } = this - if (NSubmenu && NSubmenu.mergedDisabled) return true - if (NMenu.disabled) return true - return disabled - }, - collapsed () { - if (this.horizontal) return false - if (this.menuCollapsed) { - return true - } - return !this.NMenu.mergedExpandedKeys.includes(this.internalKey) - }, - dropdownEnabled () { - return !this.mergedDisabled && (this.horizontal || this.menuCollapsed) - } - }, - methods: { - doClick () { - const { onClick } = this - if (onClick) onClick() - }, - handleClick () { - if (!this.mergedDisabled) { - if (!this.menuCollapsed) { - this.NMenu.toggleExpand(this.internalKey) - } - this.doClick() - } - }, - handlePopoverShowChange (value) { - this.dropdownShow = value - } - }, - render () { - const createSubmenuItem = () => { - const { - paddingLeft, - collapsed, - mergedDisabled, - maxIconSize, - activeIconSize, - title, - horizontal, - childActive, - icon, - handleClick, - dropdownShow, - iconMarginRight - } = this - return h(NMenuItemContent, { - paddingLeft, - collapsed, - disabled: mergedDisabled, - iconMarginRight, - maxIconSize, - activeIconSize, - title, - showArrow: !horizontal, - childActive: childActive, - icon, - hover: dropdownShow, - onClick: handleClick - }) - } - const createSubmenuChildren = () => { - return h(NFadeInExpandTransition, null, { - default: () => { - const { tmNodes, collapsed } = this - return !collapsed - ? h( - 'div', - { - class: 'n-submenu-children' - }, - tmNodes.map((item) => itemRenderer(item)) - ) - : null - } - }) - } - return this.root - ? h( - NDropdown, - { - builtinThemeOverrides: { - fontSizeLarge: '14px', - optionIconSizeLarge: '18px' - }, - value: this.NMenu.mergedValue, - size: 'large', - trigger: 'hover', - disabled: !this.dropdownEnabled, - placement: this.dropdownPlacement, - 'onUpdate:show': this.handlePopoverShowChange, - options: this.rawNodes, - onSelect: this.NMenu.doSelect - }, - { - default: () => - h( - 'div', - { - class: 'n-submenu' - }, - [ - createSubmenuItem(), - this.horizontal ? null : createSubmenuChildren() - ] - ) - } - ) - : h( - 'div', - { - class: 'n-submenu' - }, - [createSubmenuItem(), createSubmenuChildren()] - ) - } -}) diff --git a/src/menu/src/Submenu.ts b/src/menu/src/Submenu.ts new file mode 100644 index 000000000..fc8e694cc --- /dev/null +++ b/src/menu/src/Submenu.ts @@ -0,0 +1,196 @@ +import { + h, + ref, + defineComponent, + PropType, + provide, + computed, + reactive +} from 'vue' +import { useMemo } from 'vooks' +import { NFadeInExpandTransition } from '../../_base' +import { NDropdown } from '../../dropdown' +import NMenuItemContent from './MenuItemContent' +import { itemRenderer } from './utils' +import { useMenuChild } from './use-menu-child' +import type { SubmenuInjection } from './use-menu-child' +import { TreeNode, RawNode } from 'treemate' + +export const submenuProps = { + ...useMenuChild.props, + rawNodes: { + type: Array as PropType, + required: true + }, + tmNodes: { + type: Array as PropType, + required: true + }, + disabled: { + type: Boolean, + default: false + }, + icon: { + type: Function, + default: undefined + }, + onClick: { + type: Function, + default: undefined + } +} as const + +export default defineComponent({ + name: 'Submenu', + props: submenuProps, + setup (props) { + const MenuChild = useMenuChild(props) + const { NMenu, NSubmenu } = MenuChild + const mergedDisabledRef = computed(() => { + const { disabled } = props + if (NSubmenu && NSubmenu.mergedDisabled) return true + if (NMenu.disabled) return true + return disabled + }) + const dropdownShowRef = ref(false) + provide( + 'NSubmenu', + reactive({ + paddingLeft: MenuChild.paddingLeft, + mergedDisabled: mergedDisabledRef + }) + ) + provide('NMenuItemGroup', null) + function doClick () { + const { onClick } = props + if (onClick) onClick() + } + function handleClick () { + if (!mergedDisabledRef.value) { + if (!NMenu.collapsed) { + NMenu.toggleExpand(props.internalKey) + } + doClick() + } + } + function handlePopoverShowChange (value: boolean) { + dropdownShowRef.value = value + } + return { + NMenu, + maxIconSize: MenuChild.maxIconSize, + activeIconSize: MenuChild.activeIconSize, + iconMarginRight: MenuChild.iconMarginRight, + dropdownPlacement: MenuChild.dropdownPlacement, + dropdownShow: dropdownShowRef, + paddingLeft: MenuChild.paddingLeft, + mergedDisabled: mergedDisabledRef, + childActive: useMemo(() => { + return NMenu.activePath.includes(props.internalKey) + }), + collapsed: computed(() => { + if (NMenu.mode === 'horizontal') return false + if (NMenu.collapsed) { + return true + } + return !NMenu.mergedExpandedKeys.includes(props.internalKey) + }), + dropdownEnabled: computed(() => { + return ( + !mergedDisabledRef.value && + (NMenu.mode === 'horizontal' || NMenu.collapsed) + ) + }), + handlePopoverShowChange, + handleClick + } + }, + render () { + const createSubmenuItem = () => { + const { + NMenu, + paddingLeft, + collapsed, + mergedDisabled, + maxIconSize, + activeIconSize, + title, + childActive, + icon, + handleClick, + dropdownShow, + iconMarginRight + } = this + return h(NMenuItemContent, { + paddingLeft, + collapsed, + disabled: mergedDisabled, + iconMarginRight, + maxIconSize, + activeIconSize, + title, + showArrow: !(NMenu.mode === 'horizontal'), + childActive: childActive, + icon, + hover: dropdownShow, + onClick: handleClick + }) + } + const createSubmenuChildren = () => { + return h(NFadeInExpandTransition, null, { + default: () => { + const { tmNodes, collapsed } = this + return !collapsed + ? h( + 'div', + { + class: 'n-submenu-children' + }, + tmNodes.map((item) => itemRenderer(item)) + ) + : null + } + }) + } + return this.root + ? h( + NDropdown, + { + builtinThemeOverrides: { + fontSizeLarge: '14px', + optionIconSizeLarge: '18px' + }, + value: this.NMenu.mergedValue, + size: 'large', + trigger: 'hover', + disabled: !this.dropdownEnabled, + placement: this.dropdownPlacement, + 'onUpdate:show': this.handlePopoverShowChange, + options: this.rawNodes, + onSelect: this.NMenu.doSelect + }, + { + default: () => + h( + 'div', + { + class: 'n-submenu' + }, + [ + createSubmenuItem(), + this.NMenu.mode === 'horizontal' + ? null + : createSubmenuChildren() + ] + ) + } + ) + : h( + 'div', + { + class: 'n-submenu' + }, + [createSubmenuItem(), createSubmenuChildren()] + ) + } +}) diff --git a/src/menu/src/menu-child-mixin.js b/src/menu/src/menu-child-mixin.js deleted file mode 100644 index e2e38cf8a..000000000 --- a/src/menu/src/menu-child-mixin.js +++ /dev/null @@ -1,106 +0,0 @@ -export default { - inject: { - NMenu: { - default: null - }, - NSubmenu: { - default: null - }, - NMenuItemGroup: { - default: null - } - }, - props: { - internalKey: { - type: String, - required: true - }, - root: { - type: Boolean, - default: false - }, - level: { - type: Number, - required: true - }, - title: { - type: [String, Function], - default: undefined - } - }, - computed: { - horizontal () { - return this.NMenu.mode === 'horizontal' - }, - dropdownPlacement () { - if (this.horizontal) { - return 'bottom' - } - if ('tmNodes' in this) return 'right-start' - return 'right' - }, - menuCollapsed () { - return this.NMenu.collapsed - }, - maxIconSize () { - return Math.max(this.collapsedIconSize, this.iconSize) - }, - activeIconSize () { - if (!this.horizontal && this.root && this.menuCollapsed) { - return this.collapsedIconSize - } else { - return this.iconSize - } - }, - iconSize () { - const { NMenu } = this - return NMenu.iconSize - }, - collapsedIconSize () { - const { - NMenu: { iconSize, collapsedIconSize } - } = this - return collapsedIconSize === undefined ? iconSize : collapsedIconSize - }, - paddingLeft () { - const { - NMenu: { collapsedWidth, indent, rootIndent }, - NSubmenu, - NMenuItemGroup, - root, - horizontal, - maxIconSize, - menuCollapsed - } = this - const mergedRootIndent = rootIndent === undefined ? indent : rootIndent - if (root) { - if (horizontal) return undefined - if (menuCollapsed) return collapsedWidth / 2 - maxIconSize / 2 - return mergedRootIndent - } - if (NMenuItemGroup) { - return indent / 2 + NMenuItemGroup.paddingLeft - } - if (NSubmenu) { - return indent + NSubmenu.paddingLeft - } - return undefined - }, - iconMarginRight () { - const { - NMenu: { collapsedWidth, indent, rootIndent }, - root, - maxIconSize, - horizontal, - menuCollapsed - } = this - if (horizontal) return 8 - if (!root) return 8 - if (!menuCollapsed) return 8 - const mergedRootIndent = rootIndent === undefined ? indent : rootIndent - return ( - mergedRootIndent + maxIconSize + 8 - (collapsedWidth + maxIconSize) / 2 - ) - } - } -} diff --git a/src/menu/src/styles/index.cssr.js b/src/menu/src/styles/index.cssr.js deleted file mode 100644 index 055fa7e5d..000000000 --- a/src/menu/src/styles/index.cssr.js +++ /dev/null @@ -1,234 +0,0 @@ -import { c, cB, cE, cM, cNotM } from '../../../_utils/cssr' -import fadeInHeightExpandTransition from '../../../_styles/transitions/fade-in-height-expand' - -// vars: -// --group-text-color -// --bezier -// --font-size -// --border-color-horizontal -// --border-radius -// --arrow-color -// --item-color-active -// --item-text-color -// --item-icon-color -// --item-text-color-hover -// --item-icon-color-hover -// --item-text-color-active -// --item-icon-color-active -// --item-icon-color-collapsed -// --item-text-color-child-active -// --item-icon-color-child-active -export default cB('menu', { - color: 'var(--item-text-color)', - overflow: 'hidden', - transition: 'background-color .3s var(--bezier)', - width: '100%', - boxSizing: 'border-box', - fontSize: 'var(--font-size)', - paddingBottom: '6px' -}, [ - cM('horizontal', { - display: 'flex', - paddingBottom: 0 - }, [ - cB('submenu', { - margin: 0 - }), - cB('menu-item', { - margin: 0 - }, [ - c('&::after', { - backgroundColor: 'transparent !important' - }), - cM('selected', [ - cB('menu-item-content', { - borderBottom: '2px solid var(--border-color-horizontal)' - }) - ]) - ]), - cB('menu-item-content', { - padding: '0 20px', - borderBottom: '2px solid transparent' - }, [ - cM('child-active', { - borderBottom: '2px solid var(--border-color-horizontal)' - }), - cNotM('disabled', [ - hoverStyle({ - borderBottom: '2px solid var(--border-color-horizontal)' - }) - ]) - ]) - ]), - cM('collapsed', [ - cB('menu-item', [ - c('&::after', { - backgroundColor: 'transparent !important' - }) - ]), - cB('menu-item-content', [ - cB('menu-item-content-header', { - opacity: 0 - }), - cE('arrow', { - opacity: 0 - }), - cE('icon', { - color: 'var(--item-icon-color-collapsed)' - }) - ]) - ]), - cB('menu-item', { - transition: 'background-color .3s var(--bezier)', - height: '42px', - marginTop: '6px', - position: 'relative' - }, [ - c('&::after', ` - content: ""; - background-color: transparent; - position: absolute; - left: 8px; - right: 8px; - top: 0; - bottom: 0; - pointer-events: none; - border-radius: var(--border-radius); - transition: background-color .3s var(--bezier); - `), - cNotM('disabled', [ - c('&:active::after', { - backgroundColor: 'var(--item-color-active)' - }) - ]), - cM('selected', [ - c('&::after', { - backgroundColor: 'var(--item-color-active)' - }), - cB('menu-item-content', [ - cE('icon', { - color: 'var(--item-icon-color-active)' - }), - cB('menu-item-content-header', { - color: 'var(--item-text-color-active)' - }) - ]) - ]) - ]), - cB('menu-item-content', ` - box-sizing: border-box; - line-height: 1.75; - height: 100%; - display: grid; - grid-template-areas: "icon content arrow"; - grid-template-columns: auto 1fr auto; - align-items: center; - cursor: pointer; - position: relative; - z-index: auto; - padding-right: 18px; - transition: - background-color .3s var(--bezier), - padding-left .3s var(--bezier), - border-color .3s var(--bezier); - `, [ - cM('disabled', { - opacity: '.45', - cursor: 'not-allowed' - }), - cM('collapsed', [ - cE('arrow', ` - transform: rotate(0); - `) - ]), - cM('child-active', [ - cB('menu-item-content-header', { - color: 'var(--item-text-color-child-active)' - }), - cE('icon', { - color: 'var(--item-icon-color-child-active)' - }) - ]), - cNotM('disabled', [ - hoverStyle(null, [ - cE('icon', { - color: 'var(--item-icon-color-hover)' - }), - cB('menu-item-content-header', { - color: 'var(--item-text-color-hover)' - }) - ]) - ]), - cE('icon', ` - grid-area: icon; - color: var(--item-icon-color); - transition: - color .3s var(--bezier), - font-size .3s var(--bezier), - margin-right .3s var(--bezier); - box-sizing: content-box; - display: inline-flex; - align-items: center; - justify-content: center; - `), - cE('arrow', ` - grid-area: arrow; - font-size: 16px; - color: var(--arrow-color); - transform: rotate(180deg); - opacity: 1; - transition: - transform 0.2s var(--bezier), - opacity 0.2s var(--bezier); - `), - cB('menu-item-content-header', ` - grid-area: content; - transition: - color .3s var(--bezier), - opacity .3s var(--bezier); - opacity: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: var(--item-text-color); - `) - ]), - cB('submenu', { - cursor: 'pointer', - position: 'relative', - marginTop: '6px' - }, [ - cB('menu-item-content', { - height: '42px' - }), - cB('submenu-children', { - overflow: 'hidden', - padding: 0 - }, [ - fadeInHeightExpandTransition({ - duration: '.2s' - }) - ]) - ]), - cB('menu-item-group', [ - cB('menu-item-group-title', ` - margin-top: 6px; - color: var(--group-text-color); - cursor: default; - font-size: 13px; - height: 36px; - display: flex; - align-items: center; - transition: - padding-left .3s var(--bezier), - color .3s var(--bezier); - `) - ]) -]) - -function hoverStyle (props, children) { - return [ - cM('hover', props, children), - c('&:hover', props, children) - ] -} diff --git a/src/menu/src/styles/index.cssr.ts b/src/menu/src/styles/index.cssr.ts new file mode 100644 index 000000000..553c1641a --- /dev/null +++ b/src/menu/src/styles/index.cssr.ts @@ -0,0 +1,285 @@ +import { CNodeChildren, CProperties } from 'css-render' +import { c, cB, cE, cM, cNotM } from '../../../_utils/cssr' +import fadeInHeightExpandTransition from '../../../_styles/transitions/fade-in-height-expand' + +// vars: +// --group-text-color +// --bezier +// --font-size +// --border-color-horizontal +// --border-radius +// --arrow-color +// --item-color-active +// --item-text-color +// --item-icon-color +// --item-text-color-hover +// --item-icon-color-hover +// --item-text-color-active +// --item-icon-color-active +// --item-icon-color-collapsed +// --item-text-color-child-active +// --item-icon-color-child-active +export default cB( + 'menu', + { + color: 'var(--item-text-color)', + overflow: 'hidden', + transition: 'background-color .3s var(--bezier)', + width: '100%', + boxSizing: 'border-box', + fontSize: 'var(--font-size)', + paddingBottom: '6px' + }, + [ + cM( + 'horizontal', + { + display: 'flex', + paddingBottom: 0 + }, + [ + cB('submenu', { + margin: 0 + }), + cB( + 'menu-item', + { + margin: 0 + }, + [ + c('&::after', { + backgroundColor: 'transparent !important' + }), + cM('selected', [ + cB('menu-item-content', { + borderBottom: '2px solid var(--border-color-horizontal)' + }) + ]) + ] + ), + cB( + 'menu-item-content', + { + padding: '0 20px', + borderBottom: '2px solid transparent' + }, + [ + cM('child-active', { + borderBottom: '2px solid var(--border-color-horizontal)' + }), + cNotM('disabled', [ + hoverStyle( + { + borderBottom: '2px solid var(--border-color-horizontal)' + }, + null + ) + ]) + ] + ) + ] + ), + cM('collapsed', [ + cB('menu-item', [ + c('&::after', { + backgroundColor: 'transparent !important' + }) + ]), + cB('menu-item-content', [ + cB('menu-item-content-header', { + opacity: 0 + }), + cE('arrow', { + opacity: 0 + }), + cE('icon', { + color: 'var(--item-icon-color-collapsed)' + }) + ]) + ]), + cB( + 'menu-item', + { + transition: 'background-color .3s var(--bezier)', + height: '42px', + marginTop: '6px', + position: 'relative' + }, + [ + c( + '&::after', + ` + content: ""; + background-color: transparent; + position: absolute; + left: 8px; + right: 8px; + top: 0; + bottom: 0; + pointer-events: none; + border-radius: var(--border-radius); + transition: background-color .3s var(--bezier); + ` + ), + cNotM('disabled', [ + c('&:active::after', { + backgroundColor: 'var(--item-color-active)' + }) + ]), + cM('selected', [ + c('&::after', { + backgroundColor: 'var(--item-color-active)' + }), + cB('menu-item-content', [ + cE('icon', { + color: 'var(--item-icon-color-active)' + }), + cB('menu-item-content-header', { + color: 'var(--item-text-color-active)' + }) + ]) + ]) + ] + ), + cB( + 'menu-item-content', + ` + box-sizing: border-box; + line-height: 1.75; + height: 100%; + display: grid; + grid-template-areas: "icon content arrow"; + grid-template-columns: auto 1fr auto; + align-items: center; + cursor: pointer; + position: relative; + z-index: auto; + padding-right: 18px; + transition: + background-color .3s var(--bezier), + padding-left .3s var(--bezier), + border-color .3s var(--bezier); + `, + [ + cM('disabled', { + opacity: '.45', + cursor: 'not-allowed' + }), + cM('collapsed', [ + cE( + 'arrow', + ` + transform: rotate(0); + ` + ) + ]), + cM('child-active', [ + cB('menu-item-content-header', { + color: 'var(--item-text-color-child-active)' + }), + cE('icon', { + color: 'var(--item-icon-color-child-active)' + }) + ]), + cNotM('disabled', [ + hoverStyle(null, [ + cE('icon', { + color: 'var(--item-icon-color-hover)' + }), + cB('menu-item-content-header', { + color: 'var(--item-text-color-hover)' + }) + ]) + ]), + cE( + 'icon', + ` + grid-area: icon; + color: var(--item-icon-color); + transition: + color .3s var(--bezier), + font-size .3s var(--bezier), + margin-right .3s var(--bezier); + box-sizing: content-box; + display: inline-flex; + align-items: center; + justify-content: center; + ` + ), + cE( + 'arrow', + ` + grid-area: arrow; + font-size: 16px; + color: var(--arrow-color); + transform: rotate(180deg); + opacity: 1; + transition: + transform 0.2s var(--bezier), + opacity 0.2s var(--bezier); + ` + ), + cB( + 'menu-item-content-header', + ` + grid-area: content; + transition: + color .3s var(--bezier), + opacity .3s var(--bezier); + opacity: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--item-text-color); + ` + ) + ] + ), + cB( + 'submenu', + { + cursor: 'pointer', + position: 'relative', + marginTop: '6px' + }, + [ + cB('menu-item-content', { + height: '42px' + }), + cB( + 'submenu-children', + { + overflow: 'hidden', + padding: 0 + }, + [ + fadeInHeightExpandTransition({ + duration: '.2s' + }) + ] + ) + ] + ), + cB('menu-item-group', [ + cB( + 'menu-item-group-title', + ` + margin-top: 6px; + color: var(--group-text-color); + cursor: default; + font-size: 13px; + height: 36px; + display: flex; + align-items: center; + transition: + padding-left .3s var(--bezier), + color .3s var(--bezier); + ` + ) + ]) + ] +) + +function hoverStyle (props: CProperties, children: CNodeChildren) { + return [cM('hover', props, children), c('&:hover', props, children)] +} diff --git a/src/menu/src/use-menu-child.ts b/src/menu/src/use-menu-child.ts new file mode 100644 index 000000000..6bbadfb59 --- /dev/null +++ b/src/menu/src/use-menu-child.ts @@ -0,0 +1,139 @@ +import { RawNode, Key } from 'treemate' +import { inject, computed, ComputedRef, PropType } from 'vue' +import { MergedTheme } from '../../_mixins/use-theme' +import { MenuThemeVars } from '../styles' + +const ICON_MARGIN_RIGHT = 8 + +export interface MenuInjection { + mergedValue: Key + mode: 'vertical' | 'horizontal' + collapsed: boolean + iconSize: number + collapsedIconSize?: number + indent: number + rootIndent: number + collapsedWidth: number + disabled: boolean + mergedExpandedKeys: Key[] + activePath: Key[] + mergedTheme: MergedTheme + doSelect: (key: Key, node: RawNode) => void + toggleExpand: (key: Key) => void +} + +export interface SubmenuInjection { + paddingLeft: number | undefined + mergedDisabled: boolean +} + +export interface MenuItemGroupInjection { + paddingLeft: number | undefined +} + +export interface UseMenuChildProps { + root: boolean +} + +export interface UseMenuChild { + dropdownPlacement: ComputedRef<'bottom' | 'right' | 'right-start'> + activeIconSize: ComputedRef + maxIconSize: ComputedRef + paddingLeft: ComputedRef + iconMarginRight: ComputedRef + NMenu: MenuInjection + NSubmenu: SubmenuInjection | null +} + +function useMenuChild (props: UseMenuChildProps): UseMenuChild { + const NMenu = inject('NMenu') as MenuInjection + const NSubmenu = inject('NSubmenu', null) + const NMenuItemGroup = inject( + 'NMenuItemGroup', + null + ) + const horizontalRef = computed(() => { + return NMenu.mode === 'horizontal' + }) + const dropdownPlacementRef = computed(() => { + if (horizontalRef.value) { + return 'bottom' + } + if ('tmNodes' in props) return 'right-start' + return 'right' + }) + const maxIconSizeRef = computed(() => { + return Math.max(NMenu.collapsedIconSize ?? NMenu.iconSize, NMenu.iconSize) + }) + const activeIconSizeRef = computed(() => { + if (!horizontalRef.value && props.root && NMenu.collapsed) { + return NMenu.collapsedIconSize ?? NMenu.iconSize + } else { + return NMenu.iconSize + } + }) + const paddingLeftRef = computed(() => { + if (horizontalRef.value) return undefined + const { collapsedWidth, indent, rootIndent } = NMenu + const { root } = props + const mergedRootIndent = rootIndent === undefined ? indent : rootIndent + if (root) { + if (NMenu.collapsed) return collapsedWidth / 2 - maxIconSizeRef.value / 2 + return mergedRootIndent + } + if (NMenuItemGroup) { + return indent / 2 + (NMenuItemGroup.paddingLeft as number) + } + if (NSubmenu) { + return indent + (NSubmenu.paddingLeft as number) + } + return undefined as never + }) + const iconMarginRightRef = computed(() => { + const { collapsedWidth, indent, rootIndent } = NMenu + const { value: maxIconSize } = maxIconSizeRef + const { root } = props + if (horizontalRef.value) return ICON_MARGIN_RIGHT + if (!root) return ICON_MARGIN_RIGHT + if (!NMenu.collapsed) return ICON_MARGIN_RIGHT + const mergedRootIndent = rootIndent === undefined ? indent : rootIndent + return ( + mergedRootIndent + + maxIconSize + + ICON_MARGIN_RIGHT - + (collapsedWidth + maxIconSize) / 2 + ) + }) + return { + dropdownPlacement: dropdownPlacementRef, + activeIconSize: activeIconSizeRef, + maxIconSize: maxIconSizeRef, + paddingLeft: paddingLeftRef, + iconMarginRight: iconMarginRightRef, + NMenu, + NSubmenu + } +} + +const menuChildProps = { + internalKey: { + type: [String, Number] as PropType, + required: true + } as const, + root: { + type: Boolean, + default: false + }, + level: { + type: Number, + required: true + }, + title: { + type: [String, Function], + default: undefined + } +} + +useMenuChild.props = menuChildProps + +export { useMenuChild } diff --git a/src/menu/src/utils.js b/src/menu/src/utils.js deleted file mode 100644 index 945bce177..000000000 --- a/src/menu/src/utils.js +++ /dev/null @@ -1,46 +0,0 @@ -import { h } from 'vue' -import { keep } from '../../_utils' -import NMenuItemGroup from './MenuItemGroup' -import NSubmenu from './Submenu' -import NMenuItem from './MenuItem.vue' -import menuChildMixin from './menu-child-mixin' - -const menuChildProps = Object.keys(menuChildMixin.props) -const menuItemProps = Object.keys(NMenuItem.props).concat(menuChildProps) -const submenuProps = Object.keys(NSubmenu.props).concat(menuChildProps) -const menuItemGroupProps = Object.keys(NMenuItemGroup.props).concat( - menuChildProps -) - -export function itemRenderer (tmNode) { - const { rawNode, key, level } = tmNode - const props = { - ...rawNode, - key, - internalKey: key, // since key can't be used as a prop - level, - root: level === 0 - } - if (tmNode.children) { - if (tmNode.isGroup) { - return h( - NMenuItemGroup, - keep(props, menuItemGroupProps, { tmNodes: tmNode.children }) - ) - } - return h( - NSubmenu, - keep(props, submenuProps, { - rawNodes: tmNode.rawNode.children, - tmNodes: tmNode.children - }) - ) - } else { - return h( - NMenuItem, - keep(props, menuItemProps, { - tmNode - }) - ) - } -} diff --git a/src/menu/src/utils.ts b/src/menu/src/utils.ts new file mode 100644 index 000000000..4dce2f4a6 --- /dev/null +++ b/src/menu/src/utils.ts @@ -0,0 +1,44 @@ +import { h, VNode } from 'vue' +import type { TreeNode, RawNode } from 'treemate' +import { keep, keysOf } from '../../_utils' +import NMenuItemGroup, { menuItemGroupProps } from './MenuItemGroup' +import NSubmenu, { submenuProps } from './Submenu' +import NMenuItem, { menuItemProps } from './MenuItem' + +const groupPropKeys = keysOf(menuItemGroupProps) +const itemPropKeys = keysOf(menuItemProps) +const submenuPropKeys = keysOf(submenuProps) + +export function itemRenderer (tmNode: TreeNode): VNode { + const { rawNode, key, level } = tmNode + const props = { + ...rawNode, + key, + internalKey: key, // since key can't be used as a prop + level, + root: level === 0 + } + + if (tmNode.children) { + if (tmNode.isGroup) { + return h( + NMenuItemGroup, + keep(props, groupPropKeys, { tmNodes: tmNode.children }) + ) + } + return h( + NSubmenu, + keep(props, submenuPropKeys, { + rawNodes: tmNode.rawNode.children as RawNode[], + tmNodes: tmNode.children + }) + ) + } else { + return h( + NMenuItem, + keep(props, itemPropKeys, { + tmNode + }) + ) + } +} diff --git a/src/menu/styles/dark.js b/src/menu/styles/dark.ts similarity index 87% rename from src/menu/styles/dark.js rename to src/menu/styles/dark.ts index 1cbe8b5bf..176bb4a2e 100644 --- a/src/menu/styles/dark.js +++ b/src/menu/styles/dark.ts @@ -1,6 +1,8 @@ import { changeColor } from 'seemly' import { tooltipDark } from '../../tooltip/styles' import { commonDark } from '../../_styles/new-common' +import type { ThemeCommonVars } from '../../_styles/new-common' +import type { MenuThemeVars } from './light' export default { name: 'Menu', @@ -8,7 +10,7 @@ export default { peers: { Tooltip: tooltipDark }, - self (vars) { + self (vars: ThemeCommonVars): MenuThemeVars { const { borderRadius, textColor3Overlay, diff --git a/src/menu/styles/index.js b/src/menu/styles/index.js deleted file mode 100644 index 687e62509..000000000 --- a/src/menu/styles/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as menuDark } from './dark.js' -export { default as menuLight } from './light.js' diff --git a/src/menu/styles/index.ts b/src/menu/styles/index.ts new file mode 100644 index 000000000..ce9684c4d --- /dev/null +++ b/src/menu/styles/index.ts @@ -0,0 +1,3 @@ +export { default as menuDark } from './dark' +export { default as menuLight } from './light' +export type { MenuThemeVars } from './light' diff --git a/src/menu/styles/light.js b/src/menu/styles/light.ts similarity index 83% rename from src/menu/styles/light.js rename to src/menu/styles/light.ts index 30127c7ce..6561e23da 100644 --- a/src/menu/styles/light.js +++ b/src/menu/styles/light.ts @@ -1,14 +1,15 @@ import { changeColor } from 'seemly' import { tooltipLight } from '../../tooltip/styles' import { commonLight } from '../../_styles/new-common' +import type { ThemeCommonVars } from '../../_styles/new-common' -export default { +const menuLight = { name: 'Menu', common: commonLight, peers: { Tooltip: tooltipLight }, - self (vars) { + self (vars: ThemeCommonVars) { const { borderRadius, textColor3, @@ -37,3 +38,7 @@ export default { } } } + +export default menuLight + +export type MenuThemeVars = ReturnType