diff --git a/packages/react-ui/internal/InternalMenu/InternalMenu.tsx b/packages/react-ui/internal/InternalMenu/InternalMenu.tsx
index 906739d853..86ac108464 100644
--- a/packages/react-ui/internal/InternalMenu/InternalMenu.tsx
+++ b/packages/react-ui/internal/InternalMenu/InternalMenu.tsx
@@ -64,7 +64,9 @@ type DefaultProps = Required<
'width' | 'maxHeight' | 'hasShadow' | 'preventWindowScroll' | 'cyclicSelection' | 'initialSelectedItemIndex'
>
>;
-
+/**
+ * @deprecated use Menu component instead
+ */
@responsiveLayout
@rootNode
export class InternalMenu extends React.PureComponent
{
diff --git a/packages/react-ui/internal/Menu/Menu.styles.ts b/packages/react-ui/internal/Menu/Menu.styles.ts
index fe4b0b4846..e371a6e37a 100644
--- a/packages/react-ui/internal/Menu/Menu.styles.ts
+++ b/packages/react-ui/internal/Menu/Menu.styles.ts
@@ -7,14 +7,13 @@ export const styles = memoizeStyle({
background: ${t.menuBgDefault};
border-radius: ${t.menuBorderRadius};
box-sizing: ${t.menuBoxSizing};
- overflow: auto;
- padding: 0 ${t.menuPaddingX};
+ outline: none;
+ padding: ${t.menuPaddingY} ${t.menuPaddingX};
margin: ${t.menuOffsetY} 0;
- border-radius: ${t.menuBorderRadius};
`;
},
- rootMobile(t: Theme) {
+ mobileRoot(t: Theme) {
return css`
border-radius: 0;
margin: 0;
@@ -44,13 +43,13 @@ export const styles = memoizeStyle({
scrollContainer(t: Theme) {
return css`
- padding: ${t.menuPaddingY} 0;
+ padding: ${t.menuScrollContainerContentWrapperPaddingY} 0;
`;
},
scrollContainerMobile(t: Theme) {
return css`
- padding: ${t.mobileMenuPaddingY} 0;
+ padding: ${t.mobileMenuScrollContainerContentWrapperPaddingY} 0;
`;
},
@@ -60,4 +59,39 @@ export const styles = memoizeStyle({
box-shadow: ${t.menuShadow};
`;
},
+
+ wrapper() {
+ return css`
+ position: relative;
+ z-index: 1;
+ width: 100%;
+ overflow: hidden;
+ line-height: 18px;
+ box-sizing: border-box;
+ `;
+ },
+
+ headerWrapper() {
+ return css`
+ top: -5px;
+ `;
+ },
+
+ footerWrapper() {
+ return css`
+ bottom: -5px;
+ `;
+ },
+
+ contentWrapper() {
+ return css`
+ padding: 6px 18px 7px 8px;
+ `;
+ },
+
+ menuSeparatorWrapper(t: Theme) {
+ return css`
+ height: ${t.menuSeparatorBorderWidth};
+ `;
+ },
});
diff --git a/packages/react-ui/internal/Menu/Menu.tsx b/packages/react-ui/internal/Menu/Menu.tsx
index b56e952160..de44425131 100644
--- a/packages/react-ui/internal/Menu/Menu.tsx
+++ b/packages/react-ui/internal/Menu/Menu.tsx
@@ -1,8 +1,13 @@
import React, { CSSProperties, HTMLAttributes } from 'react';
+import { isKeyArrowDown, isKeyArrowUp, isKeyEnter } from '../../lib/events/keyboard/identifiers';
+import { MenuSeparator } from '../../components/MenuSeparator';
+import { ThemeFactory } from '../../lib/theming/ThemeFactory';
+import { getDOMRect } from '../../lib/dom/getDOMRect';
+import { isHTMLElement } from '../../lib/SSRSafe';
import { responsiveLayout } from '../../components/ResponsiveLayout/decorator';
-import { isNonNullable } from '../../lib/utils';
-import { ScrollContainer } from '../../components/ScrollContainer';
+import { isNonNullable, isNullable } from '../../lib/utils';
+import { ScrollContainer, ScrollContainerScrollState } from '../../components/ScrollContainer';
import { MenuItem, MenuItemProps } from '../../components/MenuItem';
import { Nullable } from '../../typings/utility-types';
import { ThemeContext } from '../../lib/theming/ThemeContext';
@@ -13,19 +18,21 @@ import { addIconPaddingIfPartOfMenu } from '../InternalMenu/addIconPaddingIfPart
import { isIE11 } from '../../lib/client';
import { createPropsGetter } from '../../lib/createPropsGetter';
import { isTheme2022 } from '../../lib/theming/ThemeHelpers';
-import { InternalMenuProps } from '../InternalMenu';
import { isIconPaddingEnabled } from '../InternalMenu/isIconPaddingEnabled';
import { styles } from './Menu.styles';
import { isActiveElement } from './isActiveElement';
-export interface MenuProps
- extends Pick,
- Pick, 'id'> {
+export interface MenuProps extends Pick, 'id'> {
children: React.ReactNode;
hasShadow?: boolean;
+ /**
+ * Максимальная высота применяется только для скролл контейнера
+ *
+ * Высота `header` и `footer` в нее не включены
+ */
maxHeight?: number | string;
- onItemClick?: () => void;
+ onItemClick?: (event: React.SyntheticEvent) => void;
width?: number | string;
preventWindowScroll?: boolean;
/**
@@ -33,21 +40,48 @@ export interface MenuProps
*/
disableScrollContainer?: boolean;
align?: 'left' | 'right';
+ /**
+ * Предотвращает выравнивание текста всех пунктов меню относительно друг друга.
+ * Так, если хотя бы у одного пункта меню есть иконка, текст в остальных пунктах меню будет выровнен относительно пункта меню с иконкой
+ */
+ preventIconsOffset?: boolean;
+ onKeyDown?: (event: React.KeyboardEvent) => void;
+
+ header?: React.ReactNode;
+ footer?: React.ReactNode;
+ /**
+ * Циклический перебор айтемов меню (по-дефолтну включен)
+ */
+ cyclicSelection?: boolean;
+ initialSelectedItemIndex?: number;
}
export interface MenuState {
highlightedIndex: number;
+ maxHeight: number | string;
+ scrollState: ScrollContainerScrollState;
}
export const MenuDataTids = {
root: 'Menu__root',
} as const;
-type DefaultProps = Required>;
+type DefaultProps = Required<
+ Pick<
+ MenuProps,
+ | 'align'
+ | 'width'
+ | 'maxHeight'
+ | 'hasShadow'
+ | 'preventWindowScroll'
+ | 'cyclicSelection'
+ | 'initialSelectedItemIndex'
+ >
+>;
@responsiveLayout
@rootNode
-export class Menu extends React.Component {
+export class Menu extends React.PureComponent {
public static __KONTUR_REACT_UI__ = 'Menu';
public static defaultProps: DefaultProps = {
@@ -56,12 +90,16 @@ export class Menu extends React.Component {
maxHeight: 300,
hasShadow: true,
preventWindowScroll: true,
+ cyclicSelection: true,
+ initialSelectedItemIndex: -1,
};
private getProps = createPropsGetter(Menu.defaultProps);
- public state = {
+ public state: MenuState = {
highlightedIndex: -1,
+ maxHeight: this.getProps().maxHeight || 'none',
+ scrollState: 'top',
};
private theme!: Theme;
@@ -70,11 +108,41 @@ export class Menu extends React.Component {
private highlighted: Nullable
+ {this.props.footer && this.renderFooter()}
);
}
+ private renderHeader = () => {
+ return (
+ (this.header = el)}
+ >
+
{this.props.header}
+
+ {this.state.scrollState !== 'top' && this.renderMenuSeparatorWithNoMargin()}
+
+
+ );
+ };
+
+ private renderFooter = () => {
+ return (
+ (this.footer = el)}
+ >
+
+ {this.state.scrollState !== 'bottom' && this.renderMenuSeparatorWithNoMargin()}
+
+
{this.props.footer}
+
+ );
+ };
+
+ private renderMenuSeparatorWithNoMargin = () => {
+ return (
+