Skip to content

Commit

Permalink
feat(ui): add custom trigger node
Browse files Browse the repository at this point in the history
  • Loading branch information
xiejay97 committed Nov 22, 2021
1 parent 0185082 commit 78e6fb6
Show file tree
Hide file tree
Showing 9 changed files with 427 additions and 313 deletions.
232 changes: 149 additions & 83 deletions packages/ui/src/components/_popup/Popup.tsx

Large diffs are not rendered by default.

164 changes: 119 additions & 45 deletions packages/ui/src/components/_trigger/Trigger.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,151 @@
import React, { useMemo, useState } from 'react';
import type { DElementSelector } from '../../hooks/element';

import { isUndefined } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';

import { useAsync } from '../../hooks';
import { useElement } from '../../hooks/element';

export type DTriggerType = 'hover' | 'focus' | 'click';

export interface DTriggerProps {
dTrigger?: DTriggerType | DTriggerType[];
dMouseEnterDelay?: number;
dMouseLeaveDelay?: number;
children: React.ReactNode;
dTriggerNode?: DElementSelector;
children?: React.ReactNode;
onTrigger?: (state?: boolean) => void;
}

export function DTrigger(props: DTriggerProps) {
const { dTrigger, dMouseEnterDelay = 150, dMouseLeaveDelay = 200, children, onTrigger } = props;
const { dTrigger, dMouseEnterDelay = 150, dMouseLeaveDelay = 200, dTriggerNode, children, onTrigger } = props;

const [currentData] = useState<{ tid: number | null }>({
tid: null,
});

const asyncCapture = useAsync();

const child = useMemo(() => {
const _child = React.Children.only(children) as React.ReactElement<React.HTMLAttributes<HTMLElement>>;
return React.cloneElement<React.HTMLAttributes<HTMLElement>>(_child, {
..._child.props,
onMouseEnter: (e) => {
_child.props.onMouseEnter?.(e);

if (dTrigger === 'hover') {
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
currentData.tid = asyncCapture.setTimeout(() => {
currentData.tid = null;
onTrigger?.(true);
}, dMouseEnterDelay);
}
},
onMouseLeave: (e) => {
_child.props.onMouseLeave?.(e);
const triggerEl = useElement(dTriggerNode ?? null);

//#region DidUpdate
useEffect(() => {
if (!isUndefined(dTriggerNode)) {
const [asyncGroup, asyncId] = asyncCapture.createGroup();
if (triggerEl.current) {
if (dTrigger === 'hover') {
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
currentData.tid = asyncCapture.setTimeout(() => {
currentData.tid = null;
onTrigger?.(false);
}, dMouseLeaveDelay);
asyncGroup.fromEvent(triggerEl.current, 'mouseenter').subscribe({
next: () => {
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
currentData.tid = asyncCapture.setTimeout(() => {
currentData.tid = null;
onTrigger?.(true);
}, dMouseEnterDelay);
},
});
asyncGroup.fromEvent(triggerEl.current, 'mouseleave').subscribe({
next: () => {
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
currentData.tid = asyncCapture.setTimeout(() => {
currentData.tid = null;
onTrigger?.(false);
}, dMouseLeaveDelay);
},
});
}
},
onFocus: (e) => {
_child.props.onFocus?.(e);

if (dTrigger === 'focus') {
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
onTrigger?.(true);
asyncGroup.fromEvent(triggerEl.current, 'focus').subscribe({
next: () => {
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
onTrigger?.(true);
},
});
asyncGroup.fromEvent(triggerEl.current, 'blur').subscribe({
next: () => {
currentData.tid = asyncCapture.setTimeout(() => onTrigger?.(false), 20);
},
});
}
},
onBlur: (e) => {
_child.props.onBlur?.(e);

if (dTrigger === 'focus') {
currentData.tid = asyncCapture.setTimeout(() => onTrigger?.(false), 20);
}
},
onClick: (e) => {
_child.props.onClick?.(e);

if (dTrigger === 'click') {
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
onTrigger?.();
asyncGroup.fromEvent(triggerEl.current, 'click').subscribe({
next: () => {
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
onTrigger?.();
},
});
}
},
});
}, [asyncCapture, children, currentData, dMouseEnterDelay, dMouseLeaveDelay, dTrigger, onTrigger]);
}

return () => {
asyncCapture.deleteGroup(asyncId);
};
}
}, [asyncCapture, currentData, dMouseEnterDelay, dMouseLeaveDelay, dTrigger, dTriggerNode, onTrigger, triggerEl]);
//#endregion

const child = useMemo(() => {
if (isUndefined(dTriggerNode)) {
const _child = React.Children.only(children) as React.ReactElement<React.HTMLAttributes<HTMLElement>>;
let childProps: React.HTMLAttributes<HTMLElement> = {};

if (dTrigger === 'hover') {
childProps = {
onMouseEnter: (e) => {
_child.props.onMouseEnter?.(e);

currentData.tid && asyncCapture.clearTimeout(currentData.tid);
currentData.tid = asyncCapture.setTimeout(() => {
currentData.tid = null;
onTrigger?.(true);
}, dMouseEnterDelay);
},
onMouseLeave: (e) => {
_child.props.onMouseLeave?.(e);

currentData.tid && asyncCapture.clearTimeout(currentData.tid);
currentData.tid = asyncCapture.setTimeout(() => {
currentData.tid = null;
onTrigger?.(false);
}, dMouseLeaveDelay);
},
};
}
if (dTrigger === 'focus') {
childProps = {
onFocus: (e) => {
_child.props.onFocus?.(e);

currentData.tid && asyncCapture.clearTimeout(currentData.tid);
onTrigger?.(true);
},
onBlur: (e) => {
_child.props.onBlur?.(e);

currentData.tid = asyncCapture.setTimeout(() => onTrigger?.(false), 20);
},
};
}
if (dTrigger === 'click') {
childProps = {
onClick: (e) => {
_child.props.onClick?.(e);

currentData.tid && asyncCapture.clearTimeout(currentData.tid);
onTrigger?.();
},
};
}

return React.cloneElement<React.HTMLAttributes<HTMLElement>>(_child, {
..._child.props,
...childProps,
});
}

return null;
}, [asyncCapture, children, currentData, dMouseEnterDelay, dMouseLeaveDelay, dTrigger, dTriggerNode, onTrigger]);

return child;
}
36 changes: 4 additions & 32 deletions packages/ui/src/components/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { isUndefined } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useImmer } from 'use-immer';

import { useDPrefixConfig, useDComponentConfig, useManualOrAutoState, useCustomRef, useAsync } from '../../hooks';
import { useDPrefixConfig, useDComponentConfig, useManualOrAutoState, useCustomRef } from '../../hooks';
import { getClassName } from '../../utils';
import { DCollapseTransition } from '../_transition';
import { DTrigger } from '../_trigger';
Expand All @@ -23,9 +23,7 @@ export interface DMenuContextData {
menuCurrentData: {
navIds: Set<string>;
ids: Map<string, Set<string>>;
mode: [DMenuMode, DMenuMode];
};
menuPopup: boolean;
onActiveChange: (id: string) => void;
onExpandChange: (id: string, expand: boolean) => void;
onFocus: (dId: string, id: string) => void;
Expand Down Expand Up @@ -70,20 +68,13 @@ export function DMenu(props: DMenuProps) {
const [currentData] = useState<DMenuContextData['menuCurrentData']>({
navIds: new Set(),
ids: new Map(),
mode: [dMode, dMode],
});
if (currentData.mode[1] !== dMode) {
currentData.mode[0] = currentData.mode[1];
currentData.mode[1] = dMode;
}

const asyncCapture = useAsync();
const [focusId, setFocusId] = useImmer<DMenuContextData['menuFocusId']>(null);
const [activedescendant, setActiveDescendant] = useImmer<string | undefined>(undefined);
const [expandIds, setExpandIds] = useImmer(() => new Set(dDefaultExpands));
const [popup, setPopup] = useImmer(dMode !== 'vertical');

const [activeId, setActiveId] = useManualOrAutoState(dDefaultActive ?? null, dActive, onActiveChange);
const [activeId, dispatchActiveId] = useManualOrAutoState(dDefaultActive ?? null, dActive, onActiveChange);
const expandTrigger = isUndefined(dExpandTrigger) ? (dMode === 'vertical' ? 'click' : 'hover') : dExpandTrigger;

const handleTrigger = useCallback(
Expand Down Expand Up @@ -113,24 +104,6 @@ export function DMenu(props: DMenuProps) {
useEffect(() => {
onExpandsChange?.(Array.from(expandIds));
}, [expandIds, onExpandsChange]);

useEffect(() => {
const [asyncGroup, asyncId] = asyncCapture.createGroup();

if (dMode !== 'vertical') {
asyncGroup.setTimeout(() => {
setPopup(true);
}, 200 + 10);
} else {
asyncGroup.setTimeout(() => {
setPopup(false);
}, 200 + 10);
}

return () => {
asyncCapture.deleteGroup(asyncId);
};
}, [asyncCapture, dMode, setPopup]);
//#endregion

const contextValue = useMemo<DMenuContextData>(
Expand All @@ -140,10 +113,9 @@ export function DMenu(props: DMenuProps) {
menuActiveId: activeId,
menuExpandIds: expandIds,
menuFocusId: focusId,
menuPopup: popup,
menuCurrentData: currentData,
onActiveChange: (id) => {
setActiveId(id);
dispatchActiveId({ value: id });
},
onExpandChange: (id, expand) => {
setExpandIds((draft) => {
Expand Down Expand Up @@ -171,7 +143,7 @@ export function DMenu(props: DMenuProps) {
setFocusId(null);
},
}),
[activeId, currentData, dExpandOne, dMode, expandIds, expandTrigger, focusId, popup, setActiveId, setExpandIds, setFocusId]
[activeId, currentData, dExpandOne, dMode, dispatchActiveId, expandIds, expandTrigger, focusId, setExpandIds, setFocusId]
);

const childs = useMemo(() => {
Expand Down
82 changes: 37 additions & 45 deletions packages/ui/src/components/menu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { isUndefined } from 'lodash';
import React from 'react';
import { useCallback } from 'react';

import { useDPrefixConfig, useDComponentConfig, useCustomContext } from '../../hooks';
import { useDPrefixConfig, useDComponentConfig, useCustomContext, useCustomRef } from '../../hooks';
import { getClassName, toId } from '../../utils';
import { DTooltip } from '../tooltip';
import { DMenuContext } from './Menu';
Expand Down Expand Up @@ -33,15 +33,11 @@ export function DMenuItem(props: DMenuItemProps) {

//#region Context
const dPrefix = useDPrefixConfig();
const {
menuMode,
menuActiveId,
menuCurrentData,
menuPopup,
onActiveChange,
onFocus: _onFocus,
onBlur: _onBlur,
} = useCustomContext(DMenuContext);
const { menuMode, menuActiveId, menuCurrentData, onActiveChange, onFocus: _onFocus, onBlur: _onBlur } = useCustomContext(DMenuContext);
//#endregion

//#region Ref
const [liEl, liRef] = useCustomRef<HTMLLIElement>();
//#endregion

const inNav = menuCurrentData?.navIds.has(dId) ?? false;
Expand Down Expand Up @@ -73,40 +69,36 @@ export function DMenuItem(props: DMenuItemProps) {
[_onBlur, onBlur]
);

const node = (
<li
{...restProps}
id={_id}
className={getClassName(className, `${dPrefix}menu-item`, {
'is-active': menuActiveId === dId,
'is-horizontal': menuMode === 'horizontal' && inNav,
'is-icon': menuMode === 'icon' && inNav,
'is-disabled': dDisabled,
})}
style={{
...style,
paddingLeft: 16 + __level * 20,
}}
role="menuitem"
tabIndex={isUndefined(tabIndex) ? -1 : tabIndex}
aria-disabled={dDisabled}
onClick={handleClick}
onFocus={handleFocus}
onBlur={handleBlur}
>
<div className={`${dPrefix}menu-item__indicator`}>
<div style={{ backgroundColor: __level === 0 ? 'transparent' : undefined }}></div>
</div>
{dIcon && <div className={`${dPrefix}menu-item__icon`}>{dIcon}</div>}
<div className={`${dPrefix}menu-item__title`}>{children}</div>
</li>
);

return inNav && (menuMode === 'icon' || menuCurrentData?.mode[0] === 'icon') && menuPopup ? (
<DTooltip dTitle={children} dPlacement="right">
{node}
</DTooltip>
) : (
node
return (
<>
<li
{...restProps}
ref={liRef}
id={_id}
className={getClassName(className, `${dPrefix}menu-item`, {
'is-active': menuActiveId === dId,
'is-horizontal': menuMode === 'horizontal' && inNav,
'is-icon': menuMode === 'icon' && inNav,
'is-disabled': dDisabled,
})}
style={{
...style,
paddingLeft: 16 + __level * 20,
}}
role="menuitem"
tabIndex={isUndefined(tabIndex) ? -1 : tabIndex}
aria-disabled={dDisabled}
onClick={handleClick}
onFocus={handleFocus}
onBlur={handleBlur}
>
<div className={`${dPrefix}menu-item__indicator`}>
<div style={{ backgroundColor: __level === 0 ? 'transparent' : undefined }}></div>
</div>
{dIcon && <div className={`${dPrefix}menu-item__icon`}>{dIcon}</div>}
<div className={`${dPrefix}menu-item__title`}>{children}</div>
</li>
{inNav && menuMode === 'icon' && <DTooltip dTitle={children} dTriggerNode={liEl} dPlacement="right" />}
</>
);
}

0 comments on commit 78e6fb6

Please sign in to comment.