diff --git a/src/components/AsideHeader/AsideHeader.tsx b/src/components/AsideHeader/AsideHeader.tsx index ea26b10..2203a74 100644 --- a/src/components/AsideHeader/AsideHeader.tsx +++ b/src/components/AsideHeader/AsideHeader.tsx @@ -1,224 +1,58 @@ -import React from 'react'; -import {block} from '../utils/cn'; +import React, {useCallback, useMemo} from 'react'; -import {MenuItem, LogoProps, SubheaderMenuItem} from '../types'; +import {MenuItem} from '../types'; import {ASIDE_HEADER_COMPACT_WIDTH, ASIDE_HEADER_EXPANDED_WIDTH} from '../constants'; -import {Button, Icon} from '@gravity-ui/uikit'; +import {Content} from '../Content'; -import {Drawer, DrawerItem, DrawerItemProps} from '../Drawer/Drawer'; -import {Logo} from '../Logo/Logo'; -import {CompositeBar} from '../CompositeBar/CompositeBar'; -import {Content, RenderContentType} from '../Content'; -import {fakeDisplayName} from '../helpers'; -import i18n from './i18n'; +import {AsideHeaderContextProvider, AsideHeaderInnerContextProvider} from './AsideHeaderContext'; +import {AsideHeaderGeneralProps, AsideHeaderDefaultProps, AsideHeaderInnerProps} from './types'; -import controlMenuButtonIcon from '../../../assets/icons/control-menu-button.svg'; -import headerDividerCollapsedIcon from '../../../assets/icons/divider-collapsed.svg'; - -import {AsideHeaderContextProvider} from './AsideHeaderContext'; +import {FirstPanel} from './components'; +import {b} from './utils'; import './AsideHeader.scss'; -// TODO: remove temporary fix for expand button -const NotIcon = fakeDisplayName('NotIcon', Icon); - -const b = block('aside-header'); - -interface AsideHeaderGeneralProps { - logo: LogoProps; - compact: boolean; - multipleTooltip?: boolean; - className?: string; - collapseTitle?: string; - expandTitle?: string; - menuMoreTitle?: string; - renderContent?: RenderContentType; - renderFooter?: (data: { - size: number; - compact: boolean; - asideRef: React.RefObject; - }) => React.ReactNode; - onClosePanel?: () => void; - onChangeCompact?: (compact: boolean) => void; -} - -interface AsideHeaderDefaultProps { - panelItems: DrawerItemProps[]; - subheaderItems: SubheaderMenuItem[]; - menuItems: MenuItem[]; - headerDecoration: boolean; -} - export interface AsideHeaderProps extends AsideHeaderGeneralProps, Partial {} -type AsideHeaderInnerProps = AsideHeaderGeneralProps & AsideHeaderDefaultProps; - -export class AsideHeader extends React.Component { - static defaultProps: AsideHeaderDefaultProps = { - panelItems: [], - subheaderItems: [], - menuItems: [], - headerDecoration: true, - }; - - asideRef = React.createRef(); - - render() { - const {className, compact} = this.props; +export const AsideHeader = (props: AsideHeaderInnerProps) => { + const {className, compact, onClosePanel} = props; + + const size = compact ? ASIDE_HEADER_COMPACT_WIDTH : ASIDE_HEADER_EXPANDED_WIDTH; + + const onItemClick = useCallback( + ( + item: MenuItem, + collapsed: boolean, + event: React.MouseEvent, + ) => { + onClosePanel?.(); + item.onItemClick?.(item, collapsed, event); + }, + [onClosePanel], + ); - const size = compact ? ASIDE_HEADER_COMPACT_WIDTH : ASIDE_HEADER_EXPANDED_WIDTH; + const asideHeaderContextValue = useMemo(() => ({size, compact}), [compact, size]); - return ( - + return ( + +
- {this.renderFirstPane(size)} - {this.renderSecondPane(size)} + {/* First Panel */} + + {/* Second Panel */} +
-
- ); - } - - private renderFirstPane = (size: number) => { - const {menuItems, panelItems, headerDecoration, multipleTooltip, menuMoreTitle} = - this.props; - - return ( - -
-
-
- {this.renderHeader()} - {menuItems?.length ? ( - - ) : ( -
- )} - {this.renderFooter(size)} - {this.renderCollapseButton()} -
-
- - {panelItems && this.renderPanels(size)} - - ); - }; - - private renderSecondPane = (size: number) => { - return ( - - ); - }; - - private renderLogo = () => ; - - private renderHeader = () => ( -
- {this.renderLogo()} - - - - -
+ + ); - - private renderFooter = (size: number) => { - const {renderFooter, compact} = this.props; - - return ( -
- {renderFooter?.({ - size, - compact, - asideRef: this.asideRef, - })} -
- ); - }; - - private renderPanels = (size: number) => { - const {panelItems} = this.props; - - return ( - - {panelItems.map((item) => ( - - ))} - - ); - }; - - private renderCollapseButton = () => { - const {expandTitle, collapseTitle, compact} = this.props; - const buttonTitle = compact - ? expandTitle || i18n('button_expand') - : collapseTitle || i18n('button_collapse'); - - return ( - - ); - }; - - private onCollapseButtonClick = () => { - this.props.onChangeCompact?.(!this.props.compact); - }; - - private onCloseDrawer = () => { - this.props.onClosePanel?.(); - }; - - private onItemClick = ( - item: MenuItem, - collapsed: boolean, - event: React.MouseEvent, - ) => { - this.props.onClosePanel?.(); - item.onItemClick?.(item, collapsed, event); - }; - - private onLogoClick = (event: React.MouseEvent) => { - this.props.onClosePanel?.(); - this.props.logo.onClick?.(event); - }; -} +}; diff --git a/src/components/AsideHeader/AsideHeaderContext.ts b/src/components/AsideHeader/AsideHeaderContext.ts index 5b71357..2ff2cee 100644 --- a/src/components/AsideHeader/AsideHeaderContext.ts +++ b/src/components/AsideHeader/AsideHeaderContext.ts @@ -1,16 +1,50 @@ import React from 'react'; -import {ASIDE_HEADER_COMPACT_WIDTH} from '../constants'; +import {MenuItem} from '../types'; +import {AsideHeaderInnerProps} from './types'; -interface AsideHeaderContextType { - compact: boolean; +export interface AsideHeaderInnerContextType extends AsideHeaderInnerProps { size: number; + onItemClick: ( + item: MenuItem, + collapsed: boolean, + event: React.MouseEvent, + ) => void; } -export const AsideHeaderContext = React.createContext({ - compact: false, - size: ASIDE_HEADER_COMPACT_WIDTH, -}); +export const AsideHeaderInnerContext = React.createContext( + undefined, +); +AsideHeaderInnerContext.displayName = 'AsideHeaderInnerContext'; + +export const AsideHeaderInnerContextProvider = AsideHeaderInnerContext.Provider; + +export const useAsideHeaderInnerContext = (): AsideHeaderInnerContextType => { + const contextValue = React.useContext(AsideHeaderInnerContext); + if (contextValue === undefined) { + throw new Error(`AsideHeaderInnerContext is not initialized. + Please check if you wrapped your component with AsideHeaderInnerContext.Provider`); + } + return contextValue; +}; + +export interface AsideHeaderContextType { + compact?: boolean; + size: number; +} + +export const AsideHeaderContext = React.createContext( + undefined, +); +AsideHeaderContext.displayName = 'AsideHeaderContext'; export const AsideHeaderContextProvider = AsideHeaderContext.Provider; -export const useAsideHeaderContext = () => React.useContext(AsideHeaderContext); +export const useAsideHeaderContext = (): AsideHeaderContextType => { + const contextValue = React.useContext(AsideHeaderContext); + if (contextValue === undefined) { + throw new Error(`AsideHeaderContext is not initialized. + Please check if you wrapped your component with AsideHeader + Context.Provider`); + } + return contextValue; +}; diff --git a/src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx b/src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx index 3a48266..7f0ba9f 100644 --- a/src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx +++ b/src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx @@ -42,8 +42,6 @@ export const AsideHeaderShowcase: FC = ({ const [headerDecoration, setHeaderDecoration] = React.useState(BOOLEAN_OPTIONS.Yes); const [isModalOpen, setIsModalOpen] = React.useState(false); - const navRef = React.useRef(null); - const openModalSubscriber = (callback: OpenModalSubscriber) => { // @ts-ignore eventBroker.subscribe((data: EventBrokerData<{layersCount: number}>) => { @@ -61,7 +59,6 @@ export const AsideHeaderShowcase: FC = ({
= ({ }, ]} onClosePanel={() => setVisiblePanel(undefined)} - onChangeCompact={setCompact} + onChangeCompact={(v) => { + setCompact(v); + }} />
); diff --git a/src/components/AsideHeader/__stories__/moc.tsx b/src/components/AsideHeader/__stories__/moc.tsx index 438aaf0..a7991db 100644 --- a/src/components/AsideHeader/__stories__/moc.tsx +++ b/src/components/AsideHeader/__stories__/moc.tsx @@ -1,12 +1,18 @@ import React from 'react'; import {Gear, Plus} from '@gravity-ui/icons'; -import {MenuItem} from 'src/components/types'; +import {MenuItem} from '../../types'; +import {ASIDE_HEADER_EXPANDED_WIDTH} from '../../constants'; +import {AsideHeaderContextType} from '../AsideHeaderContext'; function renderTag(tag: string) { return
{tag.toUpperCase()}
; } +export const EMPTY_CONTEXT_VALUE: AsideHeaderContextType = { + size: ASIDE_HEADER_EXPANDED_WIDTH, +}; + export const menuItemsShowcase: MenuItem[] = [ { id: 'overview', diff --git a/src/components/AsideHeader/components/CollapseButton.tsx b/src/components/AsideHeader/components/CollapseButton.tsx new file mode 100644 index 0000000..8a56fda --- /dev/null +++ b/src/components/AsideHeader/components/CollapseButton.tsx @@ -0,0 +1,40 @@ +import React, {useCallback} from 'react'; +import {Button, Icon} from '@gravity-ui/uikit'; + +import {fakeDisplayName} from '../../helpers'; +import i18n from '../i18n'; + +import {b} from '../utils'; +import {useAsideHeaderInnerContext} from '../AsideHeaderContext'; + +import controlMenuButtonIcon from '../../../../assets/icons/control-menu-button.svg'; + +// TODO: remove temporary fix for expand button +const NotIcon = fakeDisplayName('NotIcon', Icon); + +export const CollapseButton = () => { + const {onChangeCompact, compact, expandTitle, collapseTitle} = useAsideHeaderInnerContext(); + + const onCollapseButtonClick = useCallback(() => { + onChangeCompact?.(!compact); + }, [compact]); + + const buttonTitle = compact + ? expandTitle || i18n('button_expand') + : collapseTitle || i18n('button_collapse'); + return ( + + ); +}; diff --git a/src/components/AsideHeader/components/FirstPanel.tsx b/src/components/AsideHeader/components/FirstPanel.tsx new file mode 100644 index 0000000..50d2a07 --- /dev/null +++ b/src/components/AsideHeader/components/FirstPanel.tsx @@ -0,0 +1,55 @@ +import React, {useRef} from 'react'; +import {CompositeBar} from '../../CompositeBar/CompositeBar'; +import {useAsideHeaderInnerContext} from '../AsideHeaderContext'; +import {b} from '../utils'; + +import i18n from '../i18n'; +import {Header} from './Header'; +import {CollapseButton} from './CollapseButton'; +import {Panels} from './Panels'; + +export const FirstPanel = () => { + const { + size, + onItemClick, + menuItems, + headerDecoration, + multipleTooltip, + menuMoreTitle, + renderFooter, + compact, + } = useAsideHeaderInnerContext(); + + const asideRef = useRef(null); + + return ( + <> +
+
+
+
+ {menuItems?.length ? ( + + ) : ( +
+ )} +
+ {renderFooter?.({ + size, + compact: Boolean(compact), + asideRef, + })} +
+ +
+
+ + + ); +}; diff --git a/src/components/AsideHeader/components/Header.tsx b/src/components/AsideHeader/components/Header.tsx new file mode 100644 index 0000000..5adb189 --- /dev/null +++ b/src/components/AsideHeader/components/Header.tsx @@ -0,0 +1,47 @@ +import React, {useCallback} from 'react'; + +import {Icon} from '@gravity-ui/uikit'; + +import {SubheaderMenuItem} from '../../types'; +import {ASIDE_HEADER_COMPACT_WIDTH} from '../../constants'; +import {Logo} from '../../Logo/Logo'; +import {CompositeBar} from '../../CompositeBar/CompositeBar'; + +import headerDividerCollapsedIcon from '../../../../assets/icons/divider-collapsed.svg'; + +import {useAsideHeaderInnerContext} from '../AsideHeaderContext'; +import {b} from '../utils'; + +const DEFAULT_SUBHEADER_ITEMS: SubheaderMenuItem[] = []; + +export const Header = () => { + const {logo, onItemClick, onClosePanel, headerDecoration, subheaderItems} = + useAsideHeaderInnerContext(); + + const onLogoClick = useCallback( + (event: React.MouseEvent) => { + onClosePanel?.(); + logo.onClick?.(event); + }, + [onClosePanel, logo.onClick], + ); + + return ( +
+ + + + + +
+ ); +}; diff --git a/src/components/AsideHeader/components/Panels.tsx b/src/components/AsideHeader/components/Panels.tsx new file mode 100644 index 0000000..82c22f8 --- /dev/null +++ b/src/components/AsideHeader/components/Panels.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import {Drawer, DrawerItem} from '../../Drawer/Drawer'; + +import {useAsideHeaderInnerContext} from '../AsideHeaderContext'; +import {b} from '../utils'; + +export const Panels = () => { + const {panelItems, onClosePanel, size} = useAsideHeaderInnerContext(); + + return panelItems ? ( + + {panelItems.map((item) => ( + + ))} + + ) : null; +}; diff --git a/src/components/AsideHeader/components/index.ts b/src/components/AsideHeader/components/index.ts new file mode 100644 index 0000000..6b3499c --- /dev/null +++ b/src/components/AsideHeader/components/index.ts @@ -0,0 +1 @@ +export {FirstPanel} from './FirstPanel'; diff --git a/src/components/AsideHeader/types.tsx b/src/components/AsideHeader/types.tsx new file mode 100644 index 0000000..6efbb6c --- /dev/null +++ b/src/components/AsideHeader/types.tsx @@ -0,0 +1,30 @@ +import {RenderContentType} from '../Content'; +import {DrawerItemProps} from '../Drawer/Drawer'; +import {LogoProps, MenuItem, SubheaderMenuItem} from '../types'; + +export interface AsideHeaderGeneralProps { + logo: LogoProps; + compact: boolean; + multipleTooltip?: boolean; + className?: string; + collapseTitle?: string; + expandTitle?: string; + menuMoreTitle?: string; + renderContent?: RenderContentType; + renderFooter?: (data: { + size: number; + compact: boolean; + asideRef: React.RefObject; + }) => React.ReactNode; + onClosePanel?: () => void; + onChangeCompact?: (compact: boolean) => void; +} + +export interface AsideHeaderDefaultProps { + panelItems?: DrawerItemProps[]; + subheaderItems?: SubheaderMenuItem[]; + menuItems?: MenuItem[]; + headerDecoration?: boolean; +} + +export type AsideHeaderInnerProps = AsideHeaderGeneralProps & AsideHeaderDefaultProps; diff --git a/src/components/AsideHeader/utils.ts b/src/components/AsideHeader/utils.ts new file mode 100644 index 0000000..f321a35 --- /dev/null +++ b/src/components/AsideHeader/utils.ts @@ -0,0 +1,3 @@ +import {block} from '../utils/cn'; + +export const b = block('aside-header'); diff --git a/src/components/CompositeBar/Item/Item.tsx b/src/components/CompositeBar/Item/Item.tsx index f0ec330..e351d30 100644 --- a/src/components/CompositeBar/Item/Item.tsx +++ b/src/components/CompositeBar/Item/Item.tsx @@ -213,7 +213,7 @@ export const Item: React.FC = (props) => { const params = {icon: iconNode, title: titleNode}; let node; - const opts = {compact, collapsed: false, item, ref}; + const opts = {compact: Boolean(compact), collapsed: false, item, ref}; if (typeof item.itemWrapper === 'function') { node = item.itemWrapper(params, makeNode, opts) as React.ReactElement; @@ -296,7 +296,12 @@ function CollapsedPopup({ const titleNode = renderItemTitle(collapseItem); const params = {title: titleNode}; - const opts = {compact, collapsed: true, item: collapseItem, ref: anchorRef}; + const opts = { + compact: Boolean(compact), + collapsed: true, + item: collapseItem, + ref: anchorRef, + }; if (typeof collapseItem.itemWrapper === 'function') { return collapseItem.itemWrapper(params, makeCollapseNode, opts); } else { diff --git a/src/components/FooterItem/__stories__/FooterItem.stories.tsx b/src/components/FooterItem/__stories__/FooterItem.stories.tsx index 0042978..de898b7 100644 --- a/src/components/FooterItem/__stories__/FooterItem.stories.tsx +++ b/src/components/FooterItem/__stories__/FooterItem.stories.tsx @@ -7,6 +7,7 @@ import {FooterItem, FooterItemProps} from '../FooterItem'; import {ASIDE_HEADER_COMPACT_WIDTH, ASIDE_HEADER_EXPANDED_WIDTH} from '../../constants'; import {AsideHeaderContextProvider} from '../../AsideHeader/AsideHeaderContext'; +import {EMPTY_CONTEXT_VALUE} from '../../AsideHeader/__stories__/moc'; import './FooterItemShowcase.scss'; @@ -22,7 +23,9 @@ export default { return (
- +
diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx index e4622ec..7dc5468 100644 --- a/src/components/Logo/Logo.tsx +++ b/src/components/Logo/Logo.tsx @@ -64,11 +64,11 @@ export const Logo: React.FC = ({ return (
- {hasWrapper ? wrapper(button, compact) : button} + {hasWrapper ? wrapper(button, Boolean(compact)) : button}
{!compact && (hasWrapper ? ( -
{wrapper(logo, compact)}
+
{wrapper(logo, Boolean(compact))}
) : (