diff --git a/packages/ui/src/components/_popup/Popup.tsx b/packages/ui/src/components/_popup/Popup.tsx index 28226e08..579e7868 100644 --- a/packages/ui/src/components/_popup/Popup.tsx +++ b/packages/ui/src/components/_popup/Popup.tsx @@ -16,8 +16,6 @@ export interface DPopupRenderProps { 'data-popup-triggerid': string; pOnMouseEnter?: React.MouseEventHandler; pOnMouseLeave?: React.MouseEventHandler; - pOnFocus?: React.FocusEventHandler; - pOnBlur?: React.FocusEventHandler; pOnClick?: React.MouseEventHandler; } @@ -26,7 +24,7 @@ export interface DPopupProps { dPopup: (props: DPopupPopupRenderProps) => JSX.Element | null; dVisible?: boolean; dContainer?: HTMLElement | null; - dTrigger?: 'hover' | 'focus' | 'click'; + dTrigger?: 'hover' | 'click'; dDisabled?: boolean; dEscClosable?: boolean; dMouseEnterDelay?: number; @@ -212,15 +210,6 @@ export function DPopup(props: DPopupProps): JSX.Element | null { }; break; - case 'focus': - childProps.pOnFocus = () => { - handleTrigger(true); - }; - childProps.pOnBlur = () => { - handleTrigger(false); - }; - break; - case 'click': childProps.pOnClick = () => { clickOut.current = false; diff --git a/packages/ui/src/components/drawer/Drawer.tsx b/packages/ui/src/components/drawer/Drawer.tsx index 0c303956..e95dcfea 100644 --- a/packages/ui/src/components/drawer/Drawer.tsx +++ b/packages/ui/src/components/drawer/Drawer.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useId, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import { usePrefixConfig, useComponentConfig, useElement, useLockScroll, useMaxIndex, useDValue } from '../../hooks'; -import { registerComponentMate, getClassName, toPx } from '../../utils'; +import { registerComponentMate, getClassName, toPx, handleModalKeyDown } from '../../utils'; import { TTANSITION_DURING_BASE } from '../../utils/global'; import { DMask } from '../_mask'; import { DTransition } from '../_transition'; @@ -66,12 +66,11 @@ export function DDrawer(props: DDrawerProps): JSX.Element | null { //#region Ref const drawerRef = useRef(null); - const drawerContentRef = useRef(null); //#endregion const uniqueId = useId(); const headerId = `${dPrefix}drawer-header-${uniqueId}`; - const contentId = `${dPrefix}drawer-content-${uniqueId}`; + const bodyId = `${dPrefix}drawer-content-${uniqueId}`; const [distance, setDistance] = useState<{ visible: boolean; top: number; right: number; bottom: number; left: number }>({ visible: false, @@ -157,8 +156,8 @@ export function DDrawer(props: DDrawerProps): JSX.Element | null { if (visible) { prevActiveEl.current = document.activeElement as HTMLElement | null; - if (drawerContentRef.current) { - drawerContentRef.current.focus({ preventScroll: true }); + if (drawerRef.current) { + drawerRef.current.focus({ preventScroll: true }); } } else if (prevActiveEl.current) { prevActiveEl.current.focus({ preventScroll: true }); @@ -233,10 +232,20 @@ export function DDrawer(props: DDrawerProps): JSX.Element | null { position: isFixed ? undefined : 'absolute', zIndex, }} + tabIndex={-1} role={restProps.role ?? 'dialog'} aria-modal={restProps['aria-modal'] ?? 'true'} aria-labelledby={restProps['aria-labelledby'] ?? (headerNode ? headerId : undefined)} - aria-describedby={restProps['aria-describedby'] ?? contentId} + aria-describedby={restProps['aria-describedby'] ?? bodyId} + onKeyDown={(e) => { + restProps.onKeyDown?.(e); + + if (dEscClosable && e.code === 'Escape') { + changeVisible(false); + } + + handleModalKeyDown(e); + }} > {dMask && ( )}
{ - if (dEscClosable && e.code === 'Escape') { - changeVisible(false); - } - }} > {headerNode} -
{children}
+
+ {children} +
{dFooter && React.cloneElement(dFooter, { ...dFooter.props, diff --git a/packages/ui/src/components/dropdown/Dropdown.tsx b/packages/ui/src/components/dropdown/Dropdown.tsx index 8a43a076..48f6e13a 100644 --- a/packages/ui/src/components/dropdown/Dropdown.tsx +++ b/packages/ui/src/components/dropdown/Dropdown.tsx @@ -443,7 +443,7 @@ function Dropdown>( dUpdatePosition={updatePosition} onVisibleChange={changeVisible} > - {({ pOnClick, pOnFocus, pOnBlur, pOnMouseEnter, pOnMouseLeave, ...restPProps }) => ( + {({ pOnClick, pOnMouseEnter, pOnMouseLeave, ...restPProps }) => ( {({ fvOnFocus, fvOnBlur, fvOnKeyDown }) => React.cloneElement>(children, { @@ -461,7 +461,6 @@ function Dropdown>( }, onFocus: (e) => { children.props.onFocus?.(e); - pOnFocus?.(e); fvOnFocus(e); setIsFocus(true); @@ -469,7 +468,6 @@ function Dropdown>( }, onBlur: (e) => { children.props.onBlur?.(e); - pOnBlur?.(e); fvOnBlur(e); setIsFocus(false); diff --git a/packages/ui/src/components/dropdown/DropdownSub.tsx b/packages/ui/src/components/dropdown/DropdownSub.tsx index 1e0c0436..7ef4d159 100644 --- a/packages/ui/src/components/dropdown/DropdownSub.tsx +++ b/packages/ui/src/components/dropdown/DropdownSub.tsx @@ -150,7 +150,7 @@ export function DDropdownSub(props: DDropdownSubProps): JSX.Element | null { dUpdatePosition={updatePosition} onVisibleChange={onVisibleChange} > - {({ pOnClick, pOnFocus, pOnBlur, pOnMouseEnter, pOnMouseLeave, ...restPProps }) => ( + {({ pOnClick, pOnMouseEnter, pOnMouseLeave, ...restPProps }) => (
  • diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 881cc16c..ed5758c4 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -67,6 +67,9 @@ export { NotificationService } from './notification'; export type { DPaginationProps } from './pagination'; export { DPagination } from './pagination'; +export type { DPopoverProps, DPopoverHeaderProps, DPopoverFooterProps } from './popover'; +export { DPopover, DPopoverHeader, DPopoverFooter } from './popover'; + export type { DProgressProps } from './progress'; export { DProgress } from './progress'; diff --git a/packages/ui/src/components/menu/MenuSub.tsx b/packages/ui/src/components/menu/MenuSub.tsx index 126d08d3..9d55a880 100644 --- a/packages/ui/src/components/menu/MenuSub.tsx +++ b/packages/ui/src/components/menu/MenuSub.tsx @@ -202,7 +202,7 @@ export function DMenuSub(props: DMenuSubProps): JSX.Element | null { dUpdatePosition={updatePosition} onVisibleChange={onVisibleChange} > - {({ pOnClick, pOnFocus, pOnBlur, pOnMouseEnter, pOnMouseLeave, ...restPProps }) => ( + {({ pOnClick, pOnMouseEnter, pOnMouseLeave, ...restPProps }) => (
  • diff --git a/packages/ui/src/components/modal/Modal.tsx b/packages/ui/src/components/modal/Modal.tsx index ee99dd03..ec3e056c 100644 --- a/packages/ui/src/components/modal/Modal.tsx +++ b/packages/ui/src/components/modal/Modal.tsx @@ -7,7 +7,7 @@ import ReactDOM from 'react-dom'; import { usePrefixConfig, useComponentConfig, useElement, useLockScroll, useMaxIndex, useAsync, useDValue } from '../../hooks'; import { CheckCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, WarningOutlined } from '../../icons'; -import { registerComponentMate, getClassName } from '../../utils'; +import { registerComponentMate, getClassName, handleModalKeyDown } from '../../utils'; import { TTANSITION_DURING_BASE } from '../../utils/global'; import { DMask } from '../_mask'; import { DTransition } from '../_transition'; @@ -73,7 +73,7 @@ export function DModal(props: DModalProps): JSX.Element | null { const uniqueId = useId(); const headerId = `${dPrefix}modal-header-${uniqueId}`; - const contentId = `${dPrefix}modal-content-${uniqueId}`; + const bodyId = `${dPrefix}modal-content-${uniqueId}`; const topStyle = dTop + (isNumber(dTop) ? 'px' : ''); @@ -104,8 +104,8 @@ export function DModal(props: DModalProps): JSX.Element | null { if (visible) { prevActiveEl.current = document.activeElement as HTMLElement | null; - if (modalContentRef.current) { - modalContentRef.current.focus({ preventScroll: true }); + if (modalRef.current) { + modalRef.current.focus({ preventScroll: true }); } } else if (prevActiveEl.current) { prevActiveEl.current.focus({ preventScroll: true }); @@ -206,10 +206,20 @@ export function DModal(props: DModalProps): JSX.Element | null { display: state === 'leaved' ? 'none' : undefined, zIndex, }} + tabIndex={-1} role={restProps.role ?? 'dialog'} aria-modal={restProps['aria-modal'] ?? 'true'} aria-labelledby={restProps['aria-labelledby'] ?? (headerNode ? headerId : undefined)} - aria-describedby={restProps['aria-describedby'] ?? contentId} + aria-describedby={restProps['aria-describedby'] ?? bodyId} + onKeyDown={(e) => { + restProps.onKeyDown?.(e); + + if (dEscClosable && e.code === 'Escape') { + changeVisible(false); + } + + handleModalKeyDown(e); + }} > {dMask && ( { - if (dEscClosable && e.code === 'Escape') { - changeVisible(false); - } - }} > {headerNode} -
    +
    {dType ? ( <>
    diff --git a/packages/ui/src/components/popover/Popover.tsx b/packages/ui/src/components/popover/Popover.tsx new file mode 100644 index 00000000..82c03874 --- /dev/null +++ b/packages/ui/src/components/popover/Popover.tsx @@ -0,0 +1,391 @@ +import type { DElementSelector } from '../../hooks/ui/useElement'; +import type { DPopupPlacement } from '../../utils/position'; +import type { DPopoverFooterPropsWithPrivate } from './PopoverFooter'; +import type { DPopoverHeaderPropsWithPrivate } from './PopoverHeader'; + +import { isString, isUndefined } from 'lodash'; +import React, { useEffect, useId, useImperativeHandle, useRef, useState } from 'react'; + +import { usePrefixConfig, useComponentConfig, useEventCallback, useElement, useMaxIndex, useDValue, useLockScroll } from '../../hooks'; +import { registerComponentMate, getClassName, getPopupPosition, handleModalKeyDown } from '../../utils'; +import { DPopup } from '../_popup'; +import { DTransition } from '../_transition'; +import { DPopoverHeader } from './PopoverHeader'; + +export interface DPopoverRef { + updatePosition: () => void; +} + +export interface DPopoverProps extends Omit, 'children'> { + children: React.ReactElement; + dVisible?: boolean; + dTrigger?: 'hover' | 'click'; + dContainer?: DElementSelector | false; + dPlacement?: DPopupPlacement; + dEscClosable?: boolean; + dArrow?: boolean; + dModal?: boolean; + dDisabled?: boolean; + dDistance?: number; + dMouseEnterDelay?: number; + dMouseLeaveDelay?: number; + dZIndex?: number | string; + dHeader?: React.ReactElement | string; + dContent?: React.ReactNode; + dFooter?: React.ReactElement; + onVisibleChange?: (visible: boolean) => void; + afterVisibleChange?: (visible: boolean) => void; +} + +const TTANSITION_DURING = { enter: 86, leave: 100 }; +const { COMPONENT_NAME } = registerComponentMate({ COMPONENT_NAME: 'DPopover' }); +function Popover(props: DPopoverProps, ref: React.ForwardedRef): JSX.Element | null { + const { + children, + dVisible, + dTrigger = 'hover', + dContainer, + dPlacement = 'top', + dEscClosable = true, + dArrow = true, + dModal = false, + dDisabled = false, + dDistance = 10, + dMouseEnterDelay = 150, + dMouseLeaveDelay = 200, + dZIndex, + dHeader, + dContent, + dFooter, + onVisibleChange, + afterVisibleChange, + + ...restProps + } = useComponentConfig(COMPONENT_NAME, props); + + //#region Context + const dPrefix = usePrefixConfig(); + //#endregion + + //#region Ref + const popoverRef = useRef(null); + const popupRef = useRef(null); + //#endregion + + const uniqueId = useId(); + const headerId = `${dPrefix}popover-header-${uniqueId}`; + const bodyId = `${dPrefix}popover-content-${uniqueId}`; + + const [visible, changeVisible] = useDValue(false, dVisible, onVisibleChange); + + const isFixed = isUndefined(dContainer); + + const maxZIndex = useMaxIndex(visible); + const zIndex = (() => { + if (isUndefined(dZIndex)) { + if (isFixed) { + return maxZIndex; + } else { + return `var(--${dPrefix}zindex-absolute)`; + } + } else { + return dZIndex; + } + })(); + + const containerEl = useElement( + isUndefined(dContainer) || dContainer === false + ? () => { + if (dContainer === false) { + const triggerEl = document.querySelector(`[data-popover-triggerid="${uniqueId}"]`) as HTMLElement | null; + return triggerEl?.parentElement ?? null; + } + return null; + } + : dContainer + ); + + const [popupPositionStyle, setPopupPositionStyle] = useState({ + top: -9999, + left: -9999, + }); + const [placement, setPlacement] = useState(dPlacement); + const [transformOrigin, setTransformOrigin] = useState(); + const updatePosition = useEventCallback(() => { + const triggerEl = document.querySelector(`[data-popover-triggerid="${uniqueId}"]`) as HTMLElement | null; + + if (popupRef.current && triggerEl) { + let currentPlacement = dPlacement; + + let space: [number, number, number, number] = [0, 0, 0, 0]; + if (!isFixed && containerEl) { + const containerRect = containerEl.getBoundingClientRect(); + space = [ + containerRect.top, + window.innerWidth - containerRect.left - containerRect.width, + window.innerHeight - containerRect.top - containerRect.height, + containerRect.left, + ]; + } + const position = getPopupPosition(popupRef.current, triggerEl, dPlacement, dDistance, isFixed, space); + if (position) { + currentPlacement = position.placement; + setPlacement(position.placement); + setPopupPositionStyle({ + position: isFixed ? undefined : 'absolute', + top: position.top, + left: position.left, + }); + } else { + const position = getPopupPosition(popupRef.current, triggerEl, placement, dDistance, isFixed); + setPopupPositionStyle({ + position: isFixed ? undefined : 'absolute', + top: position.top, + left: position.left, + }); + } + + let transformOrigin = 'center bottom'; + switch (currentPlacement) { + case 'top': + transformOrigin = 'center bottom'; + break; + + case 'top-left': + transformOrigin = '20px bottom'; + break; + + case 'top-right': + transformOrigin = 'calc(100% - 20px) bottom'; + break; + + case 'right': + transformOrigin = 'left center'; + break; + + case 'right-top': + transformOrigin = 'left 12px'; + break; + + case 'right-bottom': + transformOrigin = 'left calc(100% - 12px)'; + break; + + case 'bottom': + transformOrigin = 'center top'; + break; + + case 'bottom-left': + transformOrigin = '20px top'; + break; + + case 'bottom-right': + transformOrigin = 'calc(100% - 20px) top'; + break; + + case 'left': + transformOrigin = 'right center'; + break; + + case 'left-top': + transformOrigin = 'right 12px'; + break; + + case 'left-bottom': + transformOrigin = 'right calc(100% - 12px)'; + break; + + default: + break; + } + setTransformOrigin(transformOrigin); + } + }); + + useLockScroll(dModal && visible); + + const prevActiveEl = useRef(null); + useEffect(() => { + if (dModal) { + if (visible) { + prevActiveEl.current = document.activeElement as HTMLElement | null; + + if (popoverRef.current) { + popoverRef.current.focus({ preventScroll: true }); + } + } else if (prevActiveEl.current) { + prevActiveEl.current.focus({ preventScroll: true }); + } + } + }, [dModal, visible]); + + const headerNode = (() => { + if (dHeader) { + const node = isString(dHeader) ? {dHeader} : dHeader; + return React.cloneElement(node, { + ...node.props, + __id: headerId, + __onClose: () => { + changeVisible(false); + }, + }); + } + })(); + + useImperativeHandle( + ref, + () => ({ + updatePosition, + }), + [updatePosition] + ); + + return ( + { + afterVisibleChange?.(true); + }} + afterLeave={() => { + afterVisibleChange?.(false); + }} + > + {(state) => { + let transitionStyle: React.CSSProperties = {}; + switch (state) { + case 'enter': + transitionStyle = { transform: 'scale(0.3)', opacity: 0 }; + break; + + case 'entering': + transitionStyle = { + transition: `transform ${TTANSITION_DURING.enter}ms ease-out, opacity ${TTANSITION_DURING.enter}ms ease-out`, + transformOrigin, + }; + break; + + case 'leaving': + transitionStyle = { + transform: 'scale(0.3)', + opacity: 0, + transition: `transform ${TTANSITION_DURING.leave}ms ease-in, opacity ${TTANSITION_DURING.leave}ms ease-in`, + transformOrigin, + }; + break; + + default: + break; + } + + return ( + ( +
    { + restProps.onKeyDown?.(e); + + if (dEscClosable && e.code === 'Escape') { + changeVisible(false); + } + + if (dModal) { + handleModalKeyDown(e); + } + }} + > + {dModal && ( +
    { + changeVisible(false); + }} + >
    + )} +
    { + restProps.onClick?.(e); + pOnClick?.(e); + }} + onMouseEnter={(e) => { + restProps.onMouseEnter?.(e); + pOnMouseEnter?.(e); + }} + onMouseLeave={(e) => { + restProps.onMouseLeave?.(e); + pOnMouseLeave?.(e); + }} + > + {dArrow &&
    } + {headerNode} +
    + {dContent} +
    + {dFooter && + React.cloneElement(dFooter, { + ...dFooter.props, + __onClose: () => { + changeVisible(false); + }, + })} +
    +
    + )} + dVisible={visible} + dContainer={isFixed ? undefined : containerEl} + dTrigger={dTrigger} + dDisabled={dDisabled} + dEscClosable={dEscClosable} + dMouseEnterDelay={dMouseEnterDelay} + dMouseLeaveDelay={dMouseLeaveDelay} + dUpdatePosition={updatePosition} + onVisibleChange={changeVisible} + > + {({ pOnClick, pOnMouseEnter, pOnMouseLeave, ...restPProps }) => + React.cloneElement>(children, { + ...children.props, + ...restPProps, + 'data-popover-triggerid': uniqueId, + onClick: (e) => { + children.props.onClick?.(e); + pOnClick?.(e); + }, + onMouseEnter: (e) => { + children.props.onMouseEnter?.(e); + pOnMouseEnter?.(e); + }, + onMouseLeave: (e) => { + children.props.onMouseLeave?.(e); + pOnMouseLeave?.(e); + }, + }) + } +
    + ); + }} +
    + ); +} + +export const DPopover = React.forwardRef(Popover); diff --git a/packages/ui/src/components/popover/PopoverFooter.tsx b/packages/ui/src/components/popover/PopoverFooter.tsx new file mode 100644 index 00000000..201608bd --- /dev/null +++ b/packages/ui/src/components/popover/PopoverFooter.tsx @@ -0,0 +1,22 @@ +import type { DFooterProps } from '../_footer'; + +import { useComponentConfig } from '../../hooks'; +import { registerComponentMate } from '../../utils'; +import { DFooter } from '../_footer'; + +export type DPopoverFooterProps = Omit; + +export interface DPopoverFooterPropsWithPrivate extends DPopoverFooterProps { + __onClose?: () => void; +} + +const { COMPONENT_NAME } = registerComponentMate({ COMPONENT_NAME: 'DPopoverFooter' }); +export function DPopoverFooter(props: DPopoverFooterProps): JSX.Element | null { + const { + __onClose, + + ...restProps + } = useComponentConfig(COMPONENT_NAME, props as DPopoverFooterPropsWithPrivate); + + return ; +} diff --git a/packages/ui/src/components/popover/PopoverHeader.tsx b/packages/ui/src/components/popover/PopoverHeader.tsx new file mode 100644 index 00000000..ffa09fd1 --- /dev/null +++ b/packages/ui/src/components/popover/PopoverHeader.tsx @@ -0,0 +1,25 @@ +import type { DHeaderProps } from '../_header'; + +import { useComponentConfig } from '../../hooks'; +import { registerComponentMate } from '../../utils'; +import { DHeader } from '../_header'; + +export type DPopoverHeaderProps = Omit; + +export interface DPopoverHeaderPropsWithPrivate extends DPopoverHeaderProps { + __id?: string; + __onClose?: () => void; +} + +const { COMPONENT_NAME } = registerComponentMate({ COMPONENT_NAME: 'DPopoverHeader' }); +export function DPopoverHeader(props: DPopoverHeaderProps): JSX.Element | null { + const { + dActions = [], + __id, + __onClose, + + ...restProps + } = useComponentConfig(COMPONENT_NAME, props as DPopoverHeaderPropsWithPrivate); + + return ; +} diff --git a/packages/ui/src/components/popover/README.md b/packages/ui/src/components/popover/README.md new file mode 100644 index 00000000..ba3217fa --- /dev/null +++ b/packages/ui/src/components/popover/README.md @@ -0,0 +1,6 @@ +--- +group: General +title: Popover +--- + +## API diff --git a/packages/ui/src/components/popover/README.zh-Hant.md b/packages/ui/src/components/popover/README.zh-Hant.md new file mode 100644 index 00000000..fe187049 --- /dev/null +++ b/packages/ui/src/components/popover/README.zh-Hant.md @@ -0,0 +1,5 @@ +--- +title: 弹出框 +--- + +## API diff --git a/packages/ui/src/components/popover/demos/1.Basic.md b/packages/ui/src/components/popover/demos/1.Basic.md new file mode 100644 index 00000000..09462382 --- /dev/null +++ b/packages/ui/src/components/popover/demos/1.Basic.md @@ -0,0 +1,54 @@ +--- +title: + en-US: Basic + zh-Hant: 基本 +--- + +# en-US + +The simplest usage. + +# zh-Hant + +最简单的用法。 + +```tsx +import { DPopover, DButton } from '@react-devui/ui'; + +export default function Demo() { + return ( +
    + +
    Some contents...
    +
    Some contents...
    + + } + > + Hover +
    + +
    Some contents...
    +
    Some contents...
    + + } + dTrigger="click" + > + Click +
    +
    + ); +} +``` + +```scss +.container { + .d-button { + margin-right: 8px; + margin-bottom: 12px; + } +} +``` diff --git a/packages/ui/src/components/popover/demos/2.Placement.md b/packages/ui/src/components/popover/demos/2.Placement.md new file mode 100644 index 00000000..fe36425c --- /dev/null +++ b/packages/ui/src/components/popover/demos/2.Placement.md @@ -0,0 +1,114 @@ +--- +title: + en-US: Placement + zh-Hant: 位置 +--- + +# en-US + +Set the pop-up position through `dPlacement`. + +# zh-Hant + +通过 `dPlacement` 设置弹出位置。 + +```tsx +import { DPopover, DButton } from '@react-devui/ui'; + +export default function Demo() { + const content = ( + <> +
    Some contents...
    +
    Some contents...
    + + ); + + return ( +
    +
    + + TL + + + T + + + TR + +
    +
    +
    + + LT + + + L + + + LB + +
    +
    + + RT + + + R + + + RB + +
    +
    +
    + + BL + + + B + + + BR + +
    +
    + ); +} +``` + +```scss +.container { + min-width: 360px; + + .d-button { + width: 64px; + } +} + +.container-top, +.container-bottom { + display: flex; + gap: 0 10px; + justify-content: center; + width: 220px; +} + +.container-top { + margin: 0 0 8px 64px; +} + +.container-bottom { + margin: 8px 0 0 64px; +} + +.container-left, +.container-right { + display: inline-flex; + flex-direction: column; + gap: 10px 0; +} + +.container-right { + margin-left: 220px; +} +``` diff --git a/packages/ui/src/components/popover/demos/3.AutoPlace.md b/packages/ui/src/components/popover/demos/3.AutoPlace.md new file mode 100644 index 00000000..c60706c8 --- /dev/null +++ b/packages/ui/src/components/popover/demos/3.AutoPlace.md @@ -0,0 +1,54 @@ +--- +title: + en-US: Auto place + zh-Hant: 自动调整位置 +--- + +# en-US + +Adjust the position automatically through `dAutoPlace` and `dContainer`. + +# zh-Hant + +通过 `dAutoPlace` 和 `dContainer` 自动调整位置。 + +```tsx +import { DPopover, DButton } from '@react-devui/ui'; + +export default function Demo() { + return ( +
    +
    + +
    Some contents...
    +
    Some contents...
    + + } + dContainer=".auto-place-container" + > + Auto Place +
    +
    +
    + ); +} +``` + +```scss +.auto-place-container { + width: 400px; + max-width: 100%; + height: 200px; + overflow: auto; + background-color: var(--d-background-color-primary); + + > .overflow { + width: 500px; + height: 400px; + padding: 100px; + } +} +``` diff --git a/packages/ui/src/components/popover/demos/4.Modal.md b/packages/ui/src/components/popover/demos/4.Modal.md new file mode 100644 index 00000000..0f78f1ef --- /dev/null +++ b/packages/ui/src/components/popover/demos/4.Modal.md @@ -0,0 +1,38 @@ +--- +title: + en-US: Modal + zh-Hant: 模态 +--- + +# en-US + +Set the popover to modal via `dModal`. + +Refer to [aria-modal](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-modal). + +# zh-Hant + +通过 `dModal` 设置弹出框为模态。 + +参考 [aria-modal](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-modal)。 + +```tsx +import { DPopover, DButton } from '@react-devui/ui'; + +export default function Demo() { + return ( + +
    Some contents...
    +
    Some contents...
    + + } + dModal + > + Modal +
    + ); +} +``` diff --git a/packages/ui/src/components/popover/demos/5.UseCases.md b/packages/ui/src/components/popover/demos/5.UseCases.md new file mode 100644 index 00000000..c1ca66a3 --- /dev/null +++ b/packages/ui/src/components/popover/demos/5.UseCases.md @@ -0,0 +1,73 @@ +--- +title: + en-US: Common Use Cases + zh-Hant: 常见用例 +--- + +# en-US + +This example shows common usage. + +# zh-Hant + +该例展示了常见的使用。 + +```tsx +import { DPopover, DPopoverHeader, DPopoverFooter, DButton } from '@react-devui/ui'; +import { useAsync } from '@react-devui/ui/hooks'; +import { WarningOutlined } from '@react-devui/ui/icons'; + +export default function Demo() { + const asyncCapture = useAsync(); + + return ( +
    + +
    Some contents...
    +
    Some contents...
    + + } + > + Content display +
    + + +
    Are you sure to delete this?
    +
    + } + dFooter={ + { + const [asyncGroup] = asyncCapture.createGroup('handleOk'); + + return new Promise((r) => { + asyncGroup.setTimeout(() => { + r(true); + }, 3000); + }); + }} + > + } + dTrigger="click" + dModal + > + Delete confirm + +
    + ); +} +``` + +```scss +.container { + .d-button { + margin-right: 8px; + margin-bottom: 12px; + } +} +``` diff --git a/packages/ui/src/components/popover/index.ts b/packages/ui/src/components/popover/index.ts new file mode 100644 index 00000000..6ac339a4 --- /dev/null +++ b/packages/ui/src/components/popover/index.ts @@ -0,0 +1,3 @@ +export * from './Popover'; +export * from './PopoverHeader'; +export * from './PopoverFooter'; diff --git a/packages/ui/src/components/tooltip/Tooltip.tsx b/packages/ui/src/components/tooltip/Tooltip.tsx index 2766ba8d..2ffe83be 100644 --- a/packages/ui/src/components/tooltip/Tooltip.tsx +++ b/packages/ui/src/components/tooltip/Tooltip.tsx @@ -1,26 +1,25 @@ import type { DElementSelector } from '../../hooks/ui/useElement'; -import type { DPlacement } from './utils'; +import type { DPopupPlacement } from '../../utils/position'; import { isUndefined } from 'lodash'; import React, { useId, useImperativeHandle, useRef, useState } from 'react'; import { usePrefixConfig, useComponentConfig, useEventCallback, useElement, useMaxIndex, useDValue } from '../../hooks'; -import { registerComponentMate, getClassName } from '../../utils'; +import { registerComponentMate, getClassName, getPopupPosition } from '../../utils'; import { DPopup } from '../_popup'; import { DTransition } from '../_transition'; -import { getPopupPlacementStyle } from './utils'; export interface DTooltipRef { updatePosition: () => void; } -export interface DTooltipProps extends React.HTMLAttributes { +export interface DTooltipProps extends Omit, 'children'> { children: React.ReactElement; dVisible?: boolean; - dTrigger?: 'hover' | 'focus' | 'click'; + dTrigger?: 'hover' | 'click'; dTitle: React.ReactNode; dContainer?: DElementSelector | false; - dPlacement?: DPlacement; + dPlacement?: DPopupPlacement; dEscClosable?: boolean; dArrow?: boolean; dDisabled?: boolean; @@ -99,7 +98,7 @@ function Tooltip(props: DTooltipProps, ref: React.ForwardedRef): JS top: -9999, left: -9999, }); - const [placement, setPlacement] = useState(dPlacement); + const [placement, setPlacement] = useState(dPlacement); const [transformOrigin, setTransformOrigin] = useState(); const updatePosition = useEventCallback(() => { const triggerEl = document.querySelector(`[aria-describedby="${id}"]`) as HTMLElement | null; @@ -117,7 +116,7 @@ function Tooltip(props: DTooltipProps, ref: React.ForwardedRef): JS containerRect.left, ]; } - const position = getPopupPlacementStyle(popupRef.current, triggerEl, dPlacement, dDistance, isFixed, space); + const position = getPopupPosition(popupRef.current, triggerEl, dPlacement, dDistance, isFixed, space); if (position) { currentPlacement = position.placement; setPlacement(position.placement); @@ -127,7 +126,7 @@ function Tooltip(props: DTooltipProps, ref: React.ForwardedRef): JS left: position.left, }); } else { - const position = getPopupPlacementStyle(popupRef.current, triggerEl, placement, dDistance, isFixed); + const position = getPopupPosition(popupRef.current, triggerEl, placement, dDistance, isFixed); setPopupPositionStyle({ position: isFixed ? undefined : 'absolute', top: position.top, @@ -286,7 +285,7 @@ function Tooltip(props: DTooltipProps, ref: React.ForwardedRef): JS dUpdatePosition={updatePosition} onVisibleChange={changeVisible} > - {({ pOnClick, pOnFocus, pOnBlur, pOnMouseEnter, pOnMouseLeave, ...restPProps }) => + {({ pOnClick, pOnMouseEnter, pOnMouseLeave, ...restPProps }) => React.cloneElement>(children, { ...children.props, ...restPProps, @@ -295,14 +294,6 @@ function Tooltip(props: DTooltipProps, ref: React.ForwardedRef): JS children.props.onClick?.(e); pOnClick?.(e); }, - onFocus: (e) => { - children.props.onFocus?.(e); - pOnFocus?.(e); - }, - onBlur: (e) => { - children.props.onBlur?.(e); - pOnBlur?.(e); - }, onMouseEnter: (e) => { children.props.onMouseEnter?.(e); pOnMouseEnter?.(e); diff --git a/packages/ui/src/components/tooltip/demos/1.Basic.md b/packages/ui/src/components/tooltip/demos/1.Basic.md index db87fd53..d72cbfab 100644 --- a/packages/ui/src/components/tooltip/demos/1.Basic.md +++ b/packages/ui/src/components/tooltip/demos/1.Basic.md @@ -13,22 +13,15 @@ The simplest usage. 最简单的用法。 ```tsx -import { useState } from 'react'; - import { DTooltip, DButton } from '@react-devui/ui'; export default function Demo() { - const [visible, seVisible] = useState(false); - return (
    Hover - - Focus - - + Click
    diff --git a/packages/ui/src/components/tooltip/demos/3.AutoPlace.md b/packages/ui/src/components/tooltip/demos/3.AutoPlace.md index 8fc63fcf..ceacb0ac 100644 --- a/packages/ui/src/components/tooltip/demos/3.AutoPlace.md +++ b/packages/ui/src/components/tooltip/demos/3.AutoPlace.md @@ -19,7 +19,7 @@ export default function Demo() { return (
    - + Auto Place
    diff --git a/packages/ui/src/components/tooltip/utils.ts b/packages/ui/src/components/tooltip/utils.ts deleted file mode 100644 index 783ffd9c..00000000 --- a/packages/ui/src/components/tooltip/utils.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { isUndefined } from 'lodash'; - -import { getNoTransformSize, getPositionedParent } from '../../utils'; - -export type DPlacement = - | 'top' - | 'top-left' - | 'top-right' - | 'right' - | 'right-top' - | 'right-bottom' - | 'bottom' - | 'bottom-left' - | 'bottom-right' - | 'left' - | 'left-top' - | 'left-bottom'; - -export function getPopupPlacementStyle( - popupEl: HTMLElement, - targetEl: HTMLElement, - placement: DPlacement, - offset: number, - fixed: boolean -): { top: number; left: number }; -export function getPopupPlacementStyle( - popupEl: HTMLElement, - targetEl: HTMLElement, - placement: DPlacement, - offset: number, - fixed: boolean, - space: [number, number, number, number] -): { top: number; left: number; placement: DPlacement } | undefined; -export function getPopupPlacementStyle( - popupEl: HTMLElement, - targetEl: HTMLElement, - placement: DPlacement, - offset = 10, - fixed = true, - space?: [number, number, number, number] -): { top: number; left: number; placement?: DPlacement } | undefined { - const { width, height } = getNoTransformSize(popupEl); - - const targetRect = targetEl.getBoundingClientRect(); - - let offsetTop = 0; - let offsetLeft = 0; - if (!fixed) { - const parentEl = getPositionedParent(popupEl); - const parentRect = parentEl.getBoundingClientRect(); - offsetTop = parentEl.scrollTop - parentRect.top; - offsetLeft = parentEl.scrollLeft - parentRect.left; - } - - const getFixedPosition = (placement: DPlacement) => { - let top = 0; - let left = 0; - - switch (placement) { - case 'top': - top = targetRect.top - height - offset; - left = targetRect.left + (targetRect.width - width) / 2; - break; - - case 'top-left': - top = targetRect.top - height - offset; - left = targetRect.left; - break; - - case 'top-right': - top = targetRect.top - height - offset; - left = targetRect.left + targetRect.width - width; - break; - - case 'right': - top = targetRect.top + (targetRect.height - height) / 2; - left = targetRect.left + targetRect.width + offset; - break; - - case 'right-top': - top = targetRect.top; - left = targetRect.left + targetRect.width + offset; - break; - - case 'right-bottom': - top = targetRect.top + targetRect.height - height; - left = targetRect.left + targetRect.width + offset; - break; - - case 'bottom': - top = targetRect.top + targetRect.height + offset; - left = targetRect.left + (targetRect.width - width) / 2; - break; - - case 'bottom-left': - top = targetRect.top + targetRect.height + offset; - left = targetRect.left; - break; - - case 'bottom-right': - top = targetRect.top + targetRect.height + offset; - left = targetRect.left + targetRect.width - width; - break; - - case 'left': - top = targetRect.top + (targetRect.height - height) / 2; - left = targetRect.left - width - offset; - break; - - case 'left-top': - top = targetRect.top; - left = targetRect.left - width - offset; - break; - - case 'left-bottom': - top = targetRect.top + targetRect.height - height; - left = targetRect.left - width - offset; - - break; - - default: - break; - } - return { top, left }; - }; - - if (!isUndefined(space)) { - const getAutoFixedPosition = (placements: DPlacement[]) => { - for (const placement of placements) { - const { top, left } = getFixedPosition(placement); - const noOver = [top, window.innerWidth - left - width, window.innerHeight - top - height, left].every( - (num, index) => num >= space[index] - ); - if (noOver) { - return { top, left, placement }; - } - } - }; - - let positionStyle: { top: number; left: number; placement: DPlacement } | undefined; - if (placement.startsWith('top')) { - positionStyle = getAutoFixedPosition([ - placement, - 'right', - 'right-top', - 'right-bottom', - 'left', - 'left-top', - 'left-bottom', - ...(placement === 'top' - ? (['bottom', 'bottom-left', 'bottom-right'] as const) - : placement === 'top-left' - ? (['bottom-left', 'bottom', 'bottom-right'] as const) - : (['bottom-right', 'bottom', 'bottom-left'] as const)), - ]); - } - if (placement.startsWith('right')) { - positionStyle = getAutoFixedPosition([ - placement, - 'top', - 'top-left', - 'top-right', - 'bottom', - 'bottom-left', - 'bottom-right', - ...(placement === 'right' - ? (['left', 'left-top', 'left-bottom'] as const) - : placement === 'right-top' - ? (['left-top', 'left', 'left-bottom'] as const) - : (['left-bottom', 'left', 'left-top'] as const)), - ]); - } - if (placement.startsWith('bottom')) { - positionStyle = getAutoFixedPosition([ - placement, - 'right', - 'right-top', - 'right-bottom', - 'left', - 'left-top', - 'left-bottom', - ...(placement === 'bottom' - ? (['top', 'top-left', 'top-right'] as const) - : placement === 'bottom-left' - ? (['top-left', 'top', 'top-right'] as const) - : (['top-right', 'top', 'top-left'] as const)), - ]); - } - if (placement.startsWith('left')) { - positionStyle = getAutoFixedPosition([ - placement, - 'top', - 'top-left', - 'top-right', - 'bottom', - 'bottom-left', - 'bottom-right', - ...(placement === 'left' - ? (['right', 'right-top', 'right-bottom'] as const) - : placement === 'left-top' - ? (['right-top', 'right', 'right-bottom'] as const) - : (['right-bottom', 'right', 'right-top'] as const)), - ]); - } - return positionStyle - ? { - top: positionStyle.top + offsetTop, - left: positionStyle.left + offsetLeft, - placement: positionStyle.placement, - } - : undefined; - } else { - const positionStyle = getFixedPosition(placement); - return { - top: positionStyle.top + offsetTop, - left: positionStyle.left + offsetLeft, - }; - } -} diff --git a/packages/ui/src/hooks/d-config/contex.ts b/packages/ui/src/hooks/d-config/contex.ts index b3853123..ef8e16ca 100644 --- a/packages/ui/src/hooks/d-config/contex.ts +++ b/packages/ui/src/hooks/d-config/contex.ts @@ -34,6 +34,9 @@ import type { DModalFooterProps, DNotificationProps, DPaginationProps, + DPopoverProps, + DPopoverHeaderProps, + DPopoverFooterProps, DProgressProps, DRadioProps, DRadioGroupProps, @@ -101,6 +104,9 @@ export type DComponentConfig = { DModalFooter: DModalFooterProps; DNotification: DNotificationProps; DPagination: DPaginationProps; + DPopover: DPopoverProps; + DPopoverHeader: DPopoverHeaderProps; + DPopoverFooter: DPopoverFooterProps; DProgress: DProgressProps; DRadio: DRadioProps; DRadioGroup: DRadioGroupProps; diff --git a/packages/ui/src/styles/_components.scss b/packages/ui/src/styles/_components.scss index f41cb339..6ad3d992 100644 --- a/packages/ui/src/styles/_components.scss +++ b/packages/ui/src/styles/_components.scss @@ -21,6 +21,7 @@ @import 'components/modal'; @import 'components/notification'; @import 'components/pagination'; +@import 'components/popover'; @import 'components/progress'; @import 'components/radio'; @import 'components/rating'; diff --git a/packages/ui/src/styles/components/drawer.scss b/packages/ui/src/styles/components/drawer.scss index 1103a28c..364b05f8 100644 --- a/packages/ui/src/styles/components/drawer.scss +++ b/packages/ui/src/styles/components/drawer.scss @@ -3,11 +3,9 @@ @include component-footer; position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; + inset: 0; overflow: hidden; + outline: none; @include e(content) { position: absolute; @@ -17,7 +15,6 @@ max-width: 100%; max-height: 100%; background-color: var(--#{$variable-prefix}background-color); - outline: none; box-shadow: 0 8px 40px 0 var(--#{$variable-prefix}shadow-color); @include m(top) { diff --git a/packages/ui/src/styles/components/modal.scss b/packages/ui/src/styles/components/modal.scss index a64bac87..a42df69b 100644 --- a/packages/ui/src/styles/components/modal.scss +++ b/packages/ui/src/styles/components/modal.scss @@ -3,11 +3,9 @@ @include component-footer; position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; + inset: 0; overflow: hidden; + outline: none; @include m(center) { display: flex; @@ -24,6 +22,7 @@ } @include e(footer) { + padding: 0 16px 12px; border: none; } } @@ -37,7 +36,6 @@ margin: 0 auto; background-color: var(--#{$variable-prefix}background-color); border-radius: var(--#{$variable-prefix}border-radius); - outline: none; box-shadow: 0 8px 40px 0 var(--#{$variable-prefix}shadow-color); } diff --git a/packages/ui/src/styles/components/popover.scss b/packages/ui/src/styles/components/popover.scss new file mode 100644 index 00000000..41e6058b --- /dev/null +++ b/packages/ui/src/styles/components/popover.scss @@ -0,0 +1,161 @@ +@mixin popover-arrow() { + @include e(arrow) { + @content; + } +} + +@include b(popover) { + @include component-header; + @include component-footer; + + position: fixed; + inset: 0; + pointer-events: none; + outline: none; + + @include m(top) { + @include popover-arrow { + bottom: 0; + left: 50%; + transform: translate(-50%, 50%) rotate(45deg); + } + } + + @include m(top-left) { + @include popover-arrow { + bottom: 0; + left: 20px; + transform: translate(0, 50%) rotate(45deg); + } + } + + @include m(top-right) { + @include popover-arrow { + right: 20px; + bottom: 0; + transform: translate(0, 50%) rotate(45deg); + } + } + + @include m(right) { + @include popover-arrow { + top: 50%; + left: 0; + transform: translate(-50%, -50%) rotate(45deg); + } + } + + @include m(right-top) { + @include popover-arrow { + top: 12px; + left: 0; + transform: translate(-50%, 0) rotate(45deg); + } + } + + @include m(right-bottom) { + @include popover-arrow { + bottom: 12px; + left: 0; + transform: translate(-50%, 0) rotate(45deg); + } + } + + @include m(bottom) { + @include popover-arrow { + top: 0; + left: 50%; + transform: translate(-50%, -50%) rotate(45deg); + } + } + + @include m(bottom-left) { + @include popover-arrow { + top: 0; + left: 20px; + transform: translate(0, -50%) rotate(45deg); + } + } + + @include m(bottom-right) { + @include popover-arrow { + top: 0; + right: 20px; + transform: translate(0, -50%) rotate(45deg); + } + } + + @include m(left) { + @include popover-arrow { + top: 50%; + right: 0; + transform: translate(50%, -50%) rotate(45deg); + } + } + + @include m(left-top) { + @include popover-arrow { + top: 12px; + right: 0; + transform: translate(50%, 0) rotate(45deg); + } + } + + @include m(left-bottom) { + @include popover-arrow { + right: 0; + bottom: 12px; + transform: translate(50%, 0) rotate(45deg); + } + } + + @include e(content) { + position: absolute; + z-index: 1; + max-width: calc(100% - 32px); + pointer-events: all; + background-color: var(--#{$variable-prefix}background-color); + border-radius: var(--#{$variable-prefix}border-radius); + outline: none; + box-shadow: var(--#{$variable-prefix}shadow-popup); + } + + @include e(header) { + padding: 6px 12px; + } + + @include e(header-title) { + font-size: 1.05em; + } + + @include e(header-actions) { + --#{$variable-prefix}size: 24px; + } + + @include e(body) { + padding: 12px; + } + + @include e(footer) { + --#{$variable-prefix}size: 28px; + --#{$variable-prefix}padding-size: 10px; + + gap: 0 6px; + padding: 0 12px 10px; + border-top: none; + } + + @include e(arrow) { + position: absolute; + width: 6px; + height: 6px; + pointer-events: none; + background-color: inherit; + } + + @include e(mask) { + position: absolute; + inset: 0; + pointer-events: all; + } +} diff --git a/packages/ui/src/utils/index.ts b/packages/ui/src/utils/index.ts index e1f90e08..2a97595f 100644 --- a/packages/ui/src/utils/index.ts +++ b/packages/ui/src/utils/index.ts @@ -1,7 +1,7 @@ export { findNested } from './array'; export { pSBC, convertHex } from './color'; export { registerComponentMate } from './component-mate'; -export { getFillingPosition, getHorizontalSidePosition, getVerticalSidePosition } from './position'; +export { getFillingPosition, getHorizontalSidePosition, getVerticalSidePosition, getPopupPosition } from './position'; export { scrollTo, scrollElementToView } from './scroll'; -export { toPx, getNoTransformSize, getClassName, getPositionedParent, copy, getUID } from './other'; +export { toPx, getNoTransformSize, getClassName, getPositionedParent, copy, getUID, handleModalKeyDown } from './other'; diff --git a/packages/ui/src/utils/other.ts b/packages/ui/src/utils/other.ts index c43a4ec1..217bd921 100644 --- a/packages/ui/src/utils/other.ts +++ b/packages/ui/src/utils/other.ts @@ -97,3 +97,23 @@ export function getUID(): string { return v.toString(16); }); } + +export function handleModalKeyDown(e: React.KeyboardEvent) { + if (e.code === 'Tab') { + const focusableEls = Array.from( + e.currentTarget.querySelectorAll('a[href], button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])') + ).filter((el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden')) as HTMLElement[]; + + if (e.shiftKey) { + if (document.activeElement === focusableEls[0]) { + e.preventDefault(); + focusableEls[focusableEls.length - 1].focus({ preventScroll: true }); + } + } else { + if (document.activeElement === focusableEls[focusableEls.length - 1]) { + e.preventDefault(); + focusableEls[0].focus({ preventScroll: true }); + } + } + } +} diff --git a/packages/ui/src/utils/position.ts b/packages/ui/src/utils/position.ts index ae4f087c..3a39caf5 100644 --- a/packages/ui/src/utils/position.ts +++ b/packages/ui/src/utils/position.ts @@ -1,4 +1,6 @@ -import { getPositionedParent, toPx } from './other'; +import { isUndefined } from 'lodash'; + +import { getNoTransformSize, getPositionedParent, toPx } from './other'; export function getFillingPosition( el: HTMLElement, @@ -184,3 +186,218 @@ export function getHorizontalSidePosition( transformOrigin, }; } + +export type DPopupPlacement = + | 'top' + | 'top-left' + | 'top-right' + | 'right' + | 'right-top' + | 'right-bottom' + | 'bottom' + | 'bottom-left' + | 'bottom-right' + | 'left' + | 'left-top' + | 'left-bottom'; +export function getPopupPosition( + popupEl: HTMLElement, + targetEl: HTMLElement, + placement: DPopupPlacement, + offset: number, + fixed: boolean +): { top: number; left: number }; +export function getPopupPosition( + popupEl: HTMLElement, + targetEl: HTMLElement, + placement: DPopupPlacement, + offset: number, + fixed: boolean, + space: [number, number, number, number] +): { top: number; left: number; placement: DPopupPlacement } | undefined; +export function getPopupPosition( + popupEl: HTMLElement, + targetEl: HTMLElement, + placement: DPopupPlacement, + offset = 10, + fixed = true, + space?: [number, number, number, number] +): { top: number; left: number; placement?: DPopupPlacement } | undefined { + const { width, height } = getNoTransformSize(popupEl); + + const targetRect = targetEl.getBoundingClientRect(); + + let offsetTop = 0; + let offsetLeft = 0; + if (!fixed) { + const parentEl = getPositionedParent(popupEl); + const parentRect = parentEl.getBoundingClientRect(); + offsetTop = parentEl.scrollTop - parentRect.top; + offsetLeft = parentEl.scrollLeft - parentRect.left; + } + + const getFixedPosition = (placement: DPopupPlacement) => { + let top = 0; + let left = 0; + + switch (placement) { + case 'top': + top = targetRect.top - height - offset; + left = targetRect.left + (targetRect.width - width) / 2; + break; + + case 'top-left': + top = targetRect.top - height - offset; + left = targetRect.left; + break; + + case 'top-right': + top = targetRect.top - height - offset; + left = targetRect.left + targetRect.width - width; + break; + + case 'right': + top = targetRect.top + (targetRect.height - height) / 2; + left = targetRect.left + targetRect.width + offset; + break; + + case 'right-top': + top = targetRect.top; + left = targetRect.left + targetRect.width + offset; + break; + + case 'right-bottom': + top = targetRect.top + targetRect.height - height; + left = targetRect.left + targetRect.width + offset; + break; + + case 'bottom': + top = targetRect.top + targetRect.height + offset; + left = targetRect.left + (targetRect.width - width) / 2; + break; + + case 'bottom-left': + top = targetRect.top + targetRect.height + offset; + left = targetRect.left; + break; + + case 'bottom-right': + top = targetRect.top + targetRect.height + offset; + left = targetRect.left + targetRect.width - width; + break; + + case 'left': + top = targetRect.top + (targetRect.height - height) / 2; + left = targetRect.left - width - offset; + break; + + case 'left-top': + top = targetRect.top; + left = targetRect.left - width - offset; + break; + + case 'left-bottom': + top = targetRect.top + targetRect.height - height; + left = targetRect.left - width - offset; + + break; + + default: + break; + } + return { top, left }; + }; + + if (!isUndefined(space)) { + const getAutoFixedPosition = (placements: DPopupPlacement[]) => { + for (const placement of placements) { + const { top, left } = getFixedPosition(placement); + const noOver = [top, window.innerWidth - left - width, window.innerHeight - top - height, left].every( + (num, index) => num >= space[index] + ); + if (noOver) { + return { top, left, placement }; + } + } + }; + + let positionStyle: { top: number; left: number; placement: DPopupPlacement } | undefined; + if (placement.startsWith('top')) { + positionStyle = getAutoFixedPosition([ + placement, + 'right', + 'right-top', + 'right-bottom', + 'left', + 'left-top', + 'left-bottom', + ...(placement === 'top' + ? (['bottom', 'bottom-left', 'bottom-right'] as const) + : placement === 'top-left' + ? (['bottom-left', 'bottom', 'bottom-right'] as const) + : (['bottom-right', 'bottom', 'bottom-left'] as const)), + ]); + } + if (placement.startsWith('right')) { + positionStyle = getAutoFixedPosition([ + placement, + 'top', + 'top-left', + 'top-right', + 'bottom', + 'bottom-left', + 'bottom-right', + ...(placement === 'right' + ? (['left', 'left-top', 'left-bottom'] as const) + : placement === 'right-top' + ? (['left-top', 'left', 'left-bottom'] as const) + : (['left-bottom', 'left', 'left-top'] as const)), + ]); + } + if (placement.startsWith('bottom')) { + positionStyle = getAutoFixedPosition([ + placement, + 'right', + 'right-top', + 'right-bottom', + 'left', + 'left-top', + 'left-bottom', + ...(placement === 'bottom' + ? (['top', 'top-left', 'top-right'] as const) + : placement === 'bottom-left' + ? (['top-left', 'top', 'top-right'] as const) + : (['top-right', 'top', 'top-left'] as const)), + ]); + } + if (placement.startsWith('left')) { + positionStyle = getAutoFixedPosition([ + placement, + 'top', + 'top-left', + 'top-right', + 'bottom', + 'bottom-left', + 'bottom-right', + ...(placement === 'left' + ? (['right', 'right-top', 'right-bottom'] as const) + : placement === 'left-top' + ? (['right-top', 'right', 'right-bottom'] as const) + : (['right-bottom', 'right', 'right-top'] as const)), + ]); + } + return positionStyle + ? { + top: positionStyle.top + offsetTop, + left: positionStyle.left + offsetLeft, + placement: positionStyle.placement, + } + : undefined; + } else { + const positionStyle = getFixedPosition(placement); + return { + top: positionStyle.top + offsetTop, + left: positionStyle.left + offsetLeft, + }; + } +}