Skip to content

Commit

Permalink
feat(sheet): allow menu scroll when it over viewport (#2215)
Browse files Browse the repository at this point in the history
* feat(sheet): allow menu scroll when it over viewport

* refactor(sheet): use canUseDom instead of assert window
  • Loading branch information
siam-ese committed May 10, 2024
1 parent 3e02de5 commit 184b98b
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 16 deletions.
9 changes: 4 additions & 5 deletions packages/design/src/components/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,19 @@
* limitations under the License.
*/

import type { MenuItemGroupProps, MenuItemProps, MenuProps, SubMenuProps } from 'rc-menu';
import type { MenuItemGroupProps, MenuItemProps, MenuProps, MenuRef, SubMenuProps } from 'rc-menu';
import RcMenu, { MenuItem as RcMenuItem, MenuItemGroup as RcMenuItemGroup, SubMenu as RcSubMenu } from 'rc-menu';
import React, { useContext } from 'react';

import { ConfigContext } from '../config-provider/ConfigProvider';
import styles from './index.module.less';

export function Menu(props: MenuProps) {
export const Menu = React.forwardRef<MenuRef, MenuProps>((props, ref) => {
const { mountContainer } = useContext(ConfigContext);

return mountContainer && React.cloneElement(<RcMenu prefixCls={styles.menu} getPopupContainer={() => mountContainer} />, {
return mountContainer && React.cloneElement(<RcMenu ref={ref} prefixCls={styles.menu} getPopupContainer={() => mountContainer} />, {
...props,
});
}
});

export function MenuItem(props: MenuItemProps) {
return React.cloneElement(<RcMenuItem />, { ...props });
Expand Down
1 change: 1 addition & 0 deletions packages/design/src/components/menu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
*/

export { Menu, MenuItem, MenuItemGroup, SubMenu } from './Menu';
export type { MenuRef } from 'rc-menu';
2 changes: 1 addition & 1 deletion packages/design/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export { Dropdown, type IDropdownProps } from './components/dropdown';
export { FormDualColumnLayout, type IFormDualColumnLayoutProps, FormLayout, type IFormLayoutProps } from './components/form-layout';
export { type IInputProps, type IInputWithSlotProps, Input, InputWithSlot } from './components/input';
export { type IInputNumberProps, InputNumber } from './components/input-number';
export { Menu, MenuItem, MenuItemGroup, SubMenu } from './components/menu';
export { type MenuRef, Menu, MenuItem, MenuItemGroup, SubMenu } from './components/menu';
export { type IMessageMethodOptions, type IMessageProps, Message, MessageType } from './components/message';
export { type IPagerProps, Pager } from './components/pager';
export { type IPopupProps, Popup, RectPopup, type IRectPopupProps } from './components/popup';
Expand Down
106 changes: 106 additions & 0 deletions packages/ui/src/components/hooks/layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http:https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { useEffect } from 'react';
import { BehaviorSubject } from 'rxjs';
import canUseDom from 'rc-util/lib/Dom/canUseDom';
/**
* These hooks are used for browser layout
* Prefer to client-side
*/


/**
* To detect whether the element is displayed over the viewport
* @param element
* @returns $value To notice you detected result
*/
function detectElementOverViewport(element: HTMLElement) {
const state$ = new BehaviorSubject<{
x: boolean;
y: boolean;
xe: boolean;
ye: boolean;
}>({
/** Element displayed on x-axis is not fully show */
x: false,
/** Element displayed on y-axis is not fully show */
y: false,
/** Element border is equal to viewport x edge */
xe: false,
/** Element border is equal to viewport y edge */
ye: false,
});

function update() {
const rect = element.getBoundingClientRect();
const { innerHeight, innerWidth } = window;

const overX = rect.x >= 0;
const overY = rect.y >= 0;

state$.next({
x: overX && rect.x + rect.width > innerWidth,
xe: overX && rect.x + rect.width === innerWidth,
y: overY && rect.y + rect.height > innerHeight,
ye: overY && rect.y + rect.height === innerHeight,
});
}

const observer = new ResizeObserver(update);
observer.observe(element);
window.addEventListener('resize', update);

update();

return {
value$: state$.asObservable(),
dispose() {
observer.disconnect();
window.removeEventListener('resize', update);
state$.complete();
},
};
}

/** Allow the element to scroll when its height over the viewport height */
export function useScrollOnOverViewport(element: HTMLElement | undefined | null, disabled: boolean = false) {
useEffect(() => {
if (canUseDom() || !element || disabled) {
return;
}

const detector = detectElementOverViewport(element);
detector.value$.subscribe(({ y, ye }) => {
const elStyle = element.style;
const rect = element.getBoundingClientRect();
// When element height over viewport sets height to fit in viewport
if (y) {
elStyle.overflowY = 'scroll';
elStyle.maxHeight = `${window.innerHeight - rect.y}px`;
} else if (!ye) {
/**
* If element height is equal to viewport, it may be because of my previous adjustment
* On height is less than viewport then set to auto
*/
elStyle.overflowY = '';
elStyle.maxHeight = '';
}
});

return detector.dispose;
}, [element, disabled]);
}
29 changes: 21 additions & 8 deletions packages/ui/src/components/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/

import { isRealNum } from '@univerjs/core';
import type {
MenuRef as DesignMenuRef } from '@univerjs/design';
import {
Menu as DesignMenu,
MenuItem as DesignMenuItem,
Expand All @@ -24,7 +26,7 @@ import {
import { CheckMarkSingle, MoreSingle } from '@univerjs/icons';
import { useDependency } from '@wendellhu/redi/react-bindings';
import clsx from 'clsx';
import React, { useState } from 'react';
import React, { useRef, useState } from 'react';
import { isObservable } from 'rxjs';

import type {
Expand All @@ -39,6 +41,7 @@ import { MenuGroup, MenuItemType } from '../../services/menu/menu';
import { IMenuService } from '../../services/menu/menu.service';
import { CustomLabel } from '../custom-label/CustomLabel';
import { useObservable } from '../hooks/observable';
import { useScrollOnOverViewport } from '../hooks/layout.ts';
import styles from './index.module.less';

// TODO: @jikkai disabled and hidden are not working
Expand All @@ -49,7 +52,11 @@ export interface IBaseMenuProps {

value?: string | number;
options?: IValueOption[];

/**
* The menu will show scroll on it over viewport height
* Recommend that you use this prop when displaying menu overlays in Dropdown
*/
overViewport?: 'scroll';
onOptionSelect?: (option: IValueOption) => void;
}

Expand Down Expand Up @@ -163,12 +170,18 @@ function MenuOptionsWrapper(props: IBaseMenuProps) {
);
}

export const Menu = (props: IBaseMenuProps) => (
<DesignMenu selectable={false}>
<MenuOptionsWrapper {...props} />
<MenuWrapper {...props} />
</DesignMenu>
);

export const Menu = (props: IBaseMenuProps) => {
const { overViewport, ...restProps } = props;
const menuRef = useRef<DesignMenuRef>(null);
useScrollOnOverViewport(menuRef.current?.list, overViewport !== 'scroll');
return (
<DesignMenu ref={menuRef} selectable={false}>
<MenuOptionsWrapper {...restProps} />
<MenuWrapper {...restProps} />
</DesignMenu>
);
};

interface IMenuItemProps {
menuItem: IDisplayMenuItem<IMenuItem>;
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/views/components/doc-bars/ToolbarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export const ToolbarItem = forwardRef((props: IDisplayMenuItem<IMenuItem>, ref:
{!disabled
? (
<Dropdown
overlay={<Menu menuType={id} options={options} onOptionSelect={handleSelect} value={value} />}
overlay={<Menu overViewport="scroll" menuType={id} options={options} onOptionSelect={handleSelect} value={value} />}
onVisibleChange={handleDropdownVisibleChange}
>
<div
Expand Down Expand Up @@ -217,7 +217,7 @@ export const ToolbarItem = forwardRef((props: IDisplayMenuItem<IMenuItem>, ref:
: !disabled
? (
<Dropdown
overlay={<Menu menuType={id} options={options} onOptionSelect={handleSelect} value={value} />}
overlay={<Menu overViewport="scroll" menuType={id} options={options} onOptionSelect={handleSelect} value={value} />}
onVisibleChange={handleDropdownVisibleChange}
>
<div
Expand Down

0 comments on commit 184b98b

Please sign in to comment.