Skip to content

Commit 78e6fb6

Browse files
committed
feat(ui): add custom trigger node
1 parent 0185082 commit 78e6fb6

9 files changed

Lines changed: 427 additions & 313 deletions

File tree

packages/ui/src/components/_popup/Popup.tsx

Lines changed: 149 additions & 83 deletions
Large diffs are not rendered by default.
Lines changed: 119 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,151 @@
1-
import React, { useMemo, useState } from 'react';
1+
import type { DElementSelector } from '../../hooks/element';
2+
3+
import { isUndefined } from 'lodash';
4+
import React, { useEffect, useMemo, useState } from 'react';
25

36
import { useAsync } from '../../hooks';
7+
import { useElement } from '../../hooks/element';
48

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

711
export interface DTriggerProps {
812
dTrigger?: DTriggerType | DTriggerType[];
913
dMouseEnterDelay?: number;
1014
dMouseLeaveDelay?: number;
11-
children: React.ReactNode;
15+
dTriggerNode?: DElementSelector;
16+
children?: React.ReactNode;
1217
onTrigger?: (state?: boolean) => void;
1318
}
1419

1520
export function DTrigger(props: DTriggerProps) {
16-
const { dTrigger, dMouseEnterDelay = 150, dMouseLeaveDelay = 200, children, onTrigger } = props;
21+
const { dTrigger, dMouseEnterDelay = 150, dMouseLeaveDelay = 200, dTriggerNode, children, onTrigger } = props;
1722

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

2227
const asyncCapture = useAsync();
2328

24-
const child = useMemo(() => {
25-
const _child = React.Children.only(children) as React.ReactElement<React.HTMLAttributes<HTMLElement>>;
26-
return React.cloneElement<React.HTMLAttributes<HTMLElement>>(_child, {
27-
..._child.props,
28-
onMouseEnter: (e) => {
29-
_child.props.onMouseEnter?.(e);
30-
31-
if (dTrigger === 'hover') {
32-
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
33-
currentData.tid = asyncCapture.setTimeout(() => {
34-
currentData.tid = null;
35-
onTrigger?.(true);
36-
}, dMouseEnterDelay);
37-
}
38-
},
39-
onMouseLeave: (e) => {
40-
_child.props.onMouseLeave?.(e);
29+
const triggerEl = useElement(dTriggerNode ?? null);
4130

31+
//#region DidUpdate
32+
useEffect(() => {
33+
if (!isUndefined(dTriggerNode)) {
34+
const [asyncGroup, asyncId] = asyncCapture.createGroup();
35+
if (triggerEl.current) {
4236
if (dTrigger === 'hover') {
43-
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
44-
currentData.tid = asyncCapture.setTimeout(() => {
45-
currentData.tid = null;
46-
onTrigger?.(false);
47-
}, dMouseLeaveDelay);
37+
asyncGroup.fromEvent(triggerEl.current, 'mouseenter').subscribe({
38+
next: () => {
39+
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
40+
currentData.tid = asyncCapture.setTimeout(() => {
41+
currentData.tid = null;
42+
onTrigger?.(true);
43+
}, dMouseEnterDelay);
44+
},
45+
});
46+
asyncGroup.fromEvent(triggerEl.current, 'mouseleave').subscribe({
47+
next: () => {
48+
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
49+
currentData.tid = asyncCapture.setTimeout(() => {
50+
currentData.tid = null;
51+
onTrigger?.(false);
52+
}, dMouseLeaveDelay);
53+
},
54+
});
4855
}
49-
},
50-
onFocus: (e) => {
51-
_child.props.onFocus?.(e);
5256

5357
if (dTrigger === 'focus') {
54-
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
55-
onTrigger?.(true);
58+
asyncGroup.fromEvent(triggerEl.current, 'focus').subscribe({
59+
next: () => {
60+
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
61+
onTrigger?.(true);
62+
},
63+
});
64+
asyncGroup.fromEvent(triggerEl.current, 'blur').subscribe({
65+
next: () => {
66+
currentData.tid = asyncCapture.setTimeout(() => onTrigger?.(false), 20);
67+
},
68+
});
5669
}
57-
},
58-
onBlur: (e) => {
59-
_child.props.onBlur?.(e);
60-
61-
if (dTrigger === 'focus') {
62-
currentData.tid = asyncCapture.setTimeout(() => onTrigger?.(false), 20);
63-
}
64-
},
65-
onClick: (e) => {
66-
_child.props.onClick?.(e);
6770

6871
if (dTrigger === 'click') {
69-
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
70-
onTrigger?.();
72+
asyncGroup.fromEvent(triggerEl.current, 'click').subscribe({
73+
next: () => {
74+
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
75+
onTrigger?.();
76+
},
77+
});
7178
}
72-
},
73-
});
74-
}, [asyncCapture, children, currentData, dMouseEnterDelay, dMouseLeaveDelay, dTrigger, onTrigger]);
79+
}
80+
81+
return () => {
82+
asyncCapture.deleteGroup(asyncId);
83+
};
84+
}
85+
}, [asyncCapture, currentData, dMouseEnterDelay, dMouseLeaveDelay, dTrigger, dTriggerNode, onTrigger, triggerEl]);
86+
//#endregion
87+
88+
const child = useMemo(() => {
89+
if (isUndefined(dTriggerNode)) {
90+
const _child = React.Children.only(children) as React.ReactElement<React.HTMLAttributes<HTMLElement>>;
91+
let childProps: React.HTMLAttributes<HTMLElement> = {};
92+
93+
if (dTrigger === 'hover') {
94+
childProps = {
95+
onMouseEnter: (e) => {
96+
_child.props.onMouseEnter?.(e);
97+
98+
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
99+
currentData.tid = asyncCapture.setTimeout(() => {
100+
currentData.tid = null;
101+
onTrigger?.(true);
102+
}, dMouseEnterDelay);
103+
},
104+
onMouseLeave: (e) => {
105+
_child.props.onMouseLeave?.(e);
106+
107+
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
108+
currentData.tid = asyncCapture.setTimeout(() => {
109+
currentData.tid = null;
110+
onTrigger?.(false);
111+
}, dMouseLeaveDelay);
112+
},
113+
};
114+
}
115+
if (dTrigger === 'focus') {
116+
childProps = {
117+
onFocus: (e) => {
118+
_child.props.onFocus?.(e);
119+
120+
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
121+
onTrigger?.(true);
122+
},
123+
onBlur: (e) => {
124+
_child.props.onBlur?.(e);
125+
126+
currentData.tid = asyncCapture.setTimeout(() => onTrigger?.(false), 20);
127+
},
128+
};
129+
}
130+
if (dTrigger === 'click') {
131+
childProps = {
132+
onClick: (e) => {
133+
_child.props.onClick?.(e);
134+
135+
currentData.tid && asyncCapture.clearTimeout(currentData.tid);
136+
onTrigger?.();
137+
},
138+
};
139+
}
140+
141+
return React.cloneElement<React.HTMLAttributes<HTMLElement>>(_child, {
142+
..._child.props,
143+
...childProps,
144+
});
145+
}
146+
147+
return null;
148+
}, [asyncCapture, children, currentData, dMouseEnterDelay, dMouseLeaveDelay, dTrigger, dTriggerNode, onTrigger]);
75149

76150
return child;
77151
}

packages/ui/src/components/menu/Menu.tsx

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { isUndefined } from 'lodash';
55
import React, { useCallback, useEffect, useMemo, useState } from 'react';
66
import { useImmer } from 'use-immer';
77

8-
import { useDPrefixConfig, useDComponentConfig, useManualOrAutoState, useCustomRef, useAsync } from '../../hooks';
8+
import { useDPrefixConfig, useDComponentConfig, useManualOrAutoState, useCustomRef } from '../../hooks';
99
import { getClassName } from '../../utils';
1010
import { DCollapseTransition } from '../_transition';
1111
import { DTrigger } from '../_trigger';
@@ -23,9 +23,7 @@ export interface DMenuContextData {
2323
menuCurrentData: {
2424
navIds: Set<string>;
2525
ids: Map<string, Set<string>>;
26-
mode: [DMenuMode, DMenuMode];
2726
};
28-
menuPopup: boolean;
2927
onActiveChange: (id: string) => void;
3028
onExpandChange: (id: string, expand: boolean) => void;
3129
onFocus: (dId: string, id: string) => void;
@@ -70,20 +68,13 @@ export function DMenu(props: DMenuProps) {
7068
const [currentData] = useState<DMenuContextData['menuCurrentData']>({
7169
navIds: new Set(),
7270
ids: new Map(),
73-
mode: [dMode, dMode],
7471
});
75-
if (currentData.mode[1] !== dMode) {
76-
currentData.mode[0] = currentData.mode[1];
77-
currentData.mode[1] = dMode;
78-
}
7972

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

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

8980
const handleTrigger = useCallback(
@@ -113,24 +104,6 @@ export function DMenu(props: DMenuProps) {
113104
useEffect(() => {
114105
onExpandsChange?.(Array.from(expandIds));
115106
}, [expandIds, onExpandsChange]);
116-
117-
useEffect(() => {
118-
const [asyncGroup, asyncId] = asyncCapture.createGroup();
119-
120-
if (dMode !== 'vertical') {
121-
asyncGroup.setTimeout(() => {
122-
setPopup(true);
123-
}, 200 + 10);
124-
} else {
125-
asyncGroup.setTimeout(() => {
126-
setPopup(false);
127-
}, 200 + 10);
128-
}
129-
130-
return () => {
131-
asyncCapture.deleteGroup(asyncId);
132-
};
133-
}, [asyncCapture, dMode, setPopup]);
134107
//#endregion
135108

136109
const contextValue = useMemo<DMenuContextData>(
@@ -140,10 +113,9 @@ export function DMenu(props: DMenuProps) {
140113
menuActiveId: activeId,
141114
menuExpandIds: expandIds,
142115
menuFocusId: focusId,
143-
menuPopup: popup,
144116
menuCurrentData: currentData,
145117
onActiveChange: (id) => {
146-
setActiveId(id);
118+
dispatchActiveId({ value: id });
147119
},
148120
onExpandChange: (id, expand) => {
149121
setExpandIds((draft) => {
@@ -171,7 +143,7 @@ export function DMenu(props: DMenuProps) {
171143
setFocusId(null);
172144
},
173145
}),
174-
[activeId, currentData, dExpandOne, dMode, expandIds, expandTrigger, focusId, popup, setActiveId, setExpandIds, setFocusId]
146+
[activeId, currentData, dExpandOne, dMode, dispatchActiveId, expandIds, expandTrigger, focusId, setExpandIds, setFocusId]
175147
);
176148

177149
const childs = useMemo(() => {

packages/ui/src/components/menu/MenuItem.tsx

Lines changed: 37 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { isUndefined } from 'lodash';
22
import React from 'react';
33
import { useCallback } from 'react';
44

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

3434
//#region Context
3535
const dPrefix = useDPrefixConfig();
36-
const {
37-
menuMode,
38-
menuActiveId,
39-
menuCurrentData,
40-
menuPopup,
41-
onActiveChange,
42-
onFocus: _onFocus,
43-
onBlur: _onBlur,
44-
} = useCustomContext(DMenuContext);
36+
const { menuMode, menuActiveId, menuCurrentData, onActiveChange, onFocus: _onFocus, onBlur: _onBlur } = useCustomContext(DMenuContext);
37+
//#endregion
38+
39+
//#region Ref
40+
const [liEl, liRef] = useCustomRef<HTMLLIElement>();
4541
//#endregion
4642

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

76-
const node = (
77-
<li
78-
{...restProps}
79-
id={_id}
80-
className={getClassName(className, `${dPrefix}menu-item`, {
81-
'is-active': menuActiveId === dId,
82-
'is-horizontal': menuMode === 'horizontal' && inNav,
83-
'is-icon': menuMode === 'icon' && inNav,
84-
'is-disabled': dDisabled,
85-
})}
86-
style={{
87-
...style,
88-
paddingLeft: 16 + __level * 20,
89-
}}
90-
role="menuitem"
91-
tabIndex={isUndefined(tabIndex) ? -1 : tabIndex}
92-
aria-disabled={dDisabled}
93-
onClick={handleClick}
94-
onFocus={handleFocus}
95-
onBlur={handleBlur}
96-
>
97-
<div className={`${dPrefix}menu-item__indicator`}>
98-
<div style={{ backgroundColor: __level === 0 ? 'transparent' : undefined }}></div>
99-
</div>
100-
{dIcon && <div className={`${dPrefix}menu-item__icon`}>{dIcon}</div>}
101-
<div className={`${dPrefix}menu-item__title`}>{children}</div>
102-
</li>
103-
);
104-
105-
return inNav && (menuMode === 'icon' || menuCurrentData?.mode[0] === 'icon') && menuPopup ? (
106-
<DTooltip dTitle={children} dPlacement="right">
107-
{node}
108-
</DTooltip>
109-
) : (
110-
node
72+
return (
73+
<>
74+
<li
75+
{...restProps}
76+
ref={liRef}
77+
id={_id}
78+
className={getClassName(className, `${dPrefix}menu-item`, {
79+
'is-active': menuActiveId === dId,
80+
'is-horizontal': menuMode === 'horizontal' && inNav,
81+
'is-icon': menuMode === 'icon' && inNav,
82+
'is-disabled': dDisabled,
83+
})}
84+
style={{
85+
...style,
86+
paddingLeft: 16 + __level * 20,
87+
}}
88+
role="menuitem"
89+
tabIndex={isUndefined(tabIndex) ? -1 : tabIndex}
90+
aria-disabled={dDisabled}
91+
onClick={handleClick}
92+
onFocus={handleFocus}
93+
onBlur={handleBlur}
94+
>
95+
<div className={`${dPrefix}menu-item__indicator`}>
96+
<div style={{ backgroundColor: __level === 0 ? 'transparent' : undefined }}></div>
97+
</div>
98+
{dIcon && <div className={`${dPrefix}menu-item__icon`}>{dIcon}</div>}
99+
<div className={`${dPrefix}menu-item__title`}>{children}</div>
100+
</li>
101+
{inNav && menuMode === 'icon' && <DTooltip dTitle={children} dTriggerNode={liEl} dPlacement="right" />}
102+
</>
111103
);
112104
}

0 commit comments

Comments
 (0)