Skip to content

Commit 926f7d4

Browse files
authored
Merge pull request #1535 from szhsin/feat/mouse-over
Set hover state on menu items when the mouse moves over them
2 parents 29cd43b + 441b802 commit 926f7d4

25 files changed

+137
-58
lines changed

README.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
# React-Menu
22

3-
> An accessible and keyboard-friendly React menu library.
3+
> An accessible, keyboard-friendly React menu library
44
55
**[Live examples and docs](https://szhsin.github.io/react-menu/)**
66

77
[![NPM](https://img.shields.io/npm/v/@szhsin/react-menu.svg)](https://www.npmjs.com/package/@szhsin/react-menu)
88
[![NPM](https://img.shields.io/npm/dm/@szhsin/react-menu)](https://www.npmjs.com/package/@szhsin/react-menu)
9-
[![TypeScript](https://img.shields.io/badge/TypeScript-.d.ts-blue.svg)](https://github.com/szhsin/react-menu/blob/master/types/index.d.ts)
9+
[![bundlephobia](https://img.shields.io/bundlephobia/minzip/@szhsin/react-menu)](https://bundlephobia.com/package/@szhsin/react-menu)
1010
[![Known Vulnerabilities](https://snyk.io/test/github/szhsin/react-menu/badge.svg)](https://snyk.io/test/github/szhsin/react-menu)
1111

1212
## Features
1313

14-
- Unstyled and lightweight [(8kB)](https://bundlephobia.com/package/@szhsin/react-menu) React menu components
15-
- Unlimited levels of submenu
16-
- Supports dropdown, hover, and context menu
17-
- Supports radio and checkbox menu items
18-
- Flexible menu positioning
19-
- Comprehensive keyboard interactions
20-
- Customisable [styling](https://szhsin.github.io/react-menu/#styling)
21-
- [Level 3 support](https://github.com/reactwg/react-18/discussions/70) of React 18 concurrent rendering
14+
- [Lightweight](https://bundlephobia.com/package/@szhsin/react-menu), unstyled React menu components
15+
- Unlimited submenu nesting
16+
- Supports dropdown, hover, and context menus
17+
- Radio and checkbox menu items
18+
- Flexible positioning options
19+
- Full keyboard interaction support
20+
- Compatible with React 18+ concurrent rendering
2221
- Supports server-side rendering
2322
- Implements [WAI-ARIA menu](https://www.w3.org/WAI/ARIA/apg/patterns/menu/) pattern
2423

dist/cjs/components/FocusableItem.cjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const FocusableItem = /*#__PURE__*/withHovering.withHovering('FocusableItem', fu
2121
const isDisabled = !!disabled;
2222
const ref = react.useRef(null);
2323
const {
24+
mouseOver,
2425
setHover,
2526
onPointerLeave,
2627
...restStateProps
@@ -30,9 +31,9 @@ const FocusableItem = /*#__PURE__*/withHovering.withHovering('FocusableItem', fu
3031
} = react.useContext(constants.EventHandlersContext);
3132
const modifiers = react.useMemo(() => ({
3233
disabled: isDisabled,
33-
hover: isHovering,
34+
hover: mouseOver || isHovering,
3435
focusable: true
35-
}), [isDisabled, isHovering]);
36+
}), [isDisabled, isHovering, mouseOver]);
3637
const renderChildren = react.useMemo(() => utils.safeCall(children, {
3738
...modifiers,
3839
ref,

dist/cjs/components/MenuItem.cjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const MenuItem = /*#__PURE__*/withHovering.withHovering('MenuItem', function Men
2525
}) {
2626
const isDisabled = !!disabled;
2727
const {
28+
mouseOver,
2829
setHover,
2930
...restStateProps
3031
} = useItemState.useItemState(itemRef, itemRef, isHovering, isDisabled);
@@ -63,10 +64,10 @@ const MenuItem = /*#__PURE__*/withHovering.withHovering('MenuItem', function Men
6364
const modifiers = react.useMemo(() => ({
6465
type,
6566
disabled: isDisabled,
66-
hover: isHovering,
67+
hover: mouseOver || isHovering,
6768
checked: isChecked,
6869
anchor: isAnchor
69-
}), [type, isDisabled, isHovering, isChecked, isAnchor]);
70+
}), [type, isDisabled, mouseOver, isHovering, isChecked, isAnchor]);
7071
const mergedProps = utils.mergeProps({
7172
...restStateProps,
7273
onPointerDown: setHover,

dist/cjs/components/SubMenu.cjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ var MenuList = require('./MenuList.cjs');
66
var jsxRuntime = require('react/jsx-runtime');
77
var withHovering = require('../utils/withHovering.cjs');
88
var useMenuStateAndFocus = require('../hooks/useMenuStateAndFocus.cjs');
9+
var useMouseOver = require('../hooks/useMouseOver.cjs');
910
var useItemEffect = require('../hooks/useItemEffect.cjs');
1011
var constants = require('../utils/constants.cjs');
1112
var useBEM = require('../hooks/useBEM.cjs');
@@ -50,6 +51,7 @@ const SubMenu = /*#__PURE__*/withHovering.withHovering('SubMenu', function SubMe
5051
...settings,
5152
onMenuChange
5253
});
54+
const [mouseOver, mouseOverStart, mouseOverEnd] = useMouseOver.useMouseOver(isHovering);
5355
const {
5456
state
5557
} = stateProps;
@@ -79,10 +81,12 @@ const SubMenu = /*#__PURE__*/withHovering.withHovering('SubMenu', function SubMe
7981
const onPointerMove = e => {
8082
if (isDisabled) return;
8183
e.stopPropagation();
84+
mouseOverStart();
8285
if (timerId.v || isOpen) return;
8386
submenuCtx.on(submenuCloseDelay, () => delayOpen(submenuOpenDelay - submenuCloseDelay), () => delayOpen(submenuOpenDelay));
8487
};
8588
const onPointerLeave = () => {
89+
mouseOverEnd();
8690
stopTimer();
8791
if (!isOpen) dispatch(constants.HoverActionTypes.UNSET, itemRef.current);
8892
};
@@ -141,10 +145,10 @@ const SubMenu = /*#__PURE__*/withHovering.withHovering('SubMenu', function SubMe
141145
}));
142146
const modifiers = react.useMemo(() => ({
143147
open: isOpen,
144-
hover: isHovering,
148+
hover: mouseOver || isHovering,
145149
disabled: isDisabled,
146150
submenu: true
147-
}), [isOpen, isHovering, isDisabled]);
151+
}), [isOpen, isHovering, isDisabled, mouseOver]);
148152
const {
149153
ref: externalItemRef,
150154
className: itemClassName,

dist/cjs/hooks/useItemState.cjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
var react = require('react');
44
var useItemEffect = require('./useItemEffect.cjs');
5+
var useMouseOver = require('./useMouseOver.cjs');
56
var constants = require('../utils/constants.cjs');
67

78
const useItemState = (itemRef, focusRef, isHovering, isDisabled) => {
9+
const [mouseOver, mouseOverStart, mouseOverEnd] = useMouseOver.useMouseOver(isHovering);
810
const {
911
submenuCloseDelay
1012
} = react.useContext(constants.SettingsContext);
@@ -26,10 +28,12 @@ const useItemState = (itemRef, focusRef, isHovering, isDisabled) => {
2628
const onPointerMove = e => {
2729
if (!isDisabled) {
2830
e.stopPropagation();
31+
mouseOverStart();
2932
submenuCtx.on(submenuCloseDelay, setHover, setHover);
3033
}
3134
};
3235
const onPointerLeave = (_, keepHover) => {
36+
mouseOverEnd();
3337
submenuCtx.off();
3438
!keepHover && unsetHover();
3539
};
@@ -40,6 +44,7 @@ const useItemState = (itemRef, focusRef, isHovering, isDisabled) => {
4044
}
4145
}, [focusRef, isHovering, isParentOpen]);
4246
return {
47+
mouseOver,
4348
setHover,
4449
onBlur,
4550
onPointerMove,

dist/cjs/hooks/useMouseOver.cjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
3+
var react = require('react');
4+
5+
const useMouseOver = isHovering => {
6+
const [mouseOver, setMouseOver] = react.useState(false);
7+
react.useEffect(() => {
8+
!isHovering && setMouseOver(false);
9+
}, [isHovering]);
10+
return [mouseOver, () => !mouseOver && setMouseOver(true), () => setMouseOver(false)];
11+
};
12+
13+
exports.useMouseOver = useMouseOver;

dist/esm/components/FocusableItem.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const FocusableItem = /*#__PURE__*/withHovering('FocusableItem', function Focusa
1919
const isDisabled = !!disabled;
2020
const ref = useRef(null);
2121
const {
22+
mouseOver,
2223
setHover,
2324
onPointerLeave,
2425
...restStateProps
@@ -28,9 +29,9 @@ const FocusableItem = /*#__PURE__*/withHovering('FocusableItem', function Focusa
2829
} = useContext(EventHandlersContext);
2930
const modifiers = useMemo(() => ({
3031
disabled: isDisabled,
31-
hover: isHovering,
32+
hover: mouseOver || isHovering,
3233
focusable: true
33-
}), [isDisabled, isHovering]);
34+
}), [isDisabled, isHovering, mouseOver]);
3435
const renderChildren = useMemo(() => safeCall(children, {
3536
...modifiers,
3637
ref,

dist/esm/components/MenuItem.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const MenuItem = /*#__PURE__*/withHovering('MenuItem', function MenuItem({
2323
}) {
2424
const isDisabled = !!disabled;
2525
const {
26+
mouseOver,
2627
setHover,
2728
...restStateProps
2829
} = useItemState(itemRef, itemRef, isHovering, isDisabled);
@@ -61,10 +62,10 @@ const MenuItem = /*#__PURE__*/withHovering('MenuItem', function MenuItem({
6162
const modifiers = useMemo(() => ({
6263
type,
6364
disabled: isDisabled,
64-
hover: isHovering,
65+
hover: mouseOver || isHovering,
6566
checked: isChecked,
6667
anchor: isAnchor
67-
}), [type, isDisabled, isHovering, isChecked, isAnchor]);
68+
}), [type, isDisabled, mouseOver, isHovering, isChecked, isAnchor]);
6869
const mergedProps = mergeProps({
6970
...restStateProps,
7071
onPointerDown: setHover,

dist/esm/components/SubMenu.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { MenuList } from './MenuList.mjs';
44
import { jsxs, jsx } from 'react/jsx-runtime';
55
import { withHovering } from '../utils/withHovering.mjs';
66
import { useMenuStateAndFocus } from '../hooks/useMenuStateAndFocus.mjs';
7+
import { useMouseOver } from '../hooks/useMouseOver.mjs';
78
import { useItemEffect } from '../hooks/useItemEffect.mjs';
89
import { SettingsContext, MenuListContext, MenuListItemContext, roleNone, roleMenuitem, menuClass, menuItemClass, subMenuClass, HoverActionTypes, Keys, FocusPositions } from '../utils/constants.mjs';
910
import { useBEM } from '../hooks/useBEM.mjs';
@@ -48,6 +49,7 @@ const SubMenu = /*#__PURE__*/withHovering('SubMenu', function SubMenu({
4849
...settings,
4950
onMenuChange
5051
});
52+
const [mouseOver, mouseOverStart, mouseOverEnd] = useMouseOver(isHovering);
5153
const {
5254
state
5355
} = stateProps;
@@ -77,10 +79,12 @@ const SubMenu = /*#__PURE__*/withHovering('SubMenu', function SubMenu({
7779
const onPointerMove = e => {
7880
if (isDisabled) return;
7981
e.stopPropagation();
82+
mouseOverStart();
8083
if (timerId.v || isOpen) return;
8184
submenuCtx.on(submenuCloseDelay, () => delayOpen(submenuOpenDelay - submenuCloseDelay), () => delayOpen(submenuOpenDelay));
8285
};
8386
const onPointerLeave = () => {
87+
mouseOverEnd();
8488
stopTimer();
8589
if (!isOpen) dispatch(HoverActionTypes.UNSET, itemRef.current);
8690
};
@@ -139,10 +143,10 @@ const SubMenu = /*#__PURE__*/withHovering('SubMenu', function SubMenu({
139143
}));
140144
const modifiers = useMemo(() => ({
141145
open: isOpen,
142-
hover: isHovering,
146+
hover: mouseOver || isHovering,
143147
disabled: isDisabled,
144148
submenu: true
145-
}), [isOpen, isHovering, isDisabled]);
149+
}), [isOpen, isHovering, isDisabled, mouseOver]);
146150
const {
147151
ref: externalItemRef,
148152
className: itemClassName,

dist/esm/hooks/useItemState.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useContext, useEffect } from 'react';
22
import { useItemEffect } from './useItemEffect.mjs';
3+
import { useMouseOver } from './useMouseOver.mjs';
34
import { SettingsContext, MenuListItemContext, HoverActionTypes } from '../utils/constants.mjs';
45

56
const useItemState = (itemRef, focusRef, isHovering, isDisabled) => {
7+
const [mouseOver, mouseOverStart, mouseOverEnd] = useMouseOver(isHovering);
68
const {
79
submenuCloseDelay
810
} = useContext(SettingsContext);
@@ -24,10 +26,12 @@ const useItemState = (itemRef, focusRef, isHovering, isDisabled) => {
2426
const onPointerMove = e => {
2527
if (!isDisabled) {
2628
e.stopPropagation();
29+
mouseOverStart();
2730
submenuCtx.on(submenuCloseDelay, setHover, setHover);
2831
}
2932
};
3033
const onPointerLeave = (_, keepHover) => {
34+
mouseOverEnd();
3135
submenuCtx.off();
3236
!keepHover && unsetHover();
3337
};
@@ -38,6 +42,7 @@ const useItemState = (itemRef, focusRef, isHovering, isDisabled) => {
3842
}
3943
}, [focusRef, isHovering, isParentOpen]);
4044
return {
45+
mouseOver,
4146
setHover,
4247
onBlur,
4348
onPointerMove,

dist/esm/hooks/useMouseOver.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { useState, useEffect } from 'react';
2+
3+
const useMouseOver = isHovering => {
4+
const [mouseOver, setMouseOver] = useState(false);
5+
useEffect(() => {
6+
!isHovering && setMouseOver(false);
7+
}, [isHovering]);
8+
return [mouseOver, () => !mouseOver && setMouseOver(true), () => setMouseOver(false)];
9+
};
10+
11+
export { useMouseOver };

example/package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/src/data/codeExamples.js

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,22 +1255,15 @@ export const features = {
12551255
desc: (
12561256
<ul className="features">
12571257
<li>
1258-
Unstyled and lightweight{' '}
1259-
<a href="https://bundlephobia.com/package/@szhsin/react-menu">(8kB)</a> React menu
1260-
components.
1261-
</li>
1262-
<li>Unlimited levels of submenu</li>
1263-
<li>Supports dropdown, hover, and context menu</li>
1264-
<li>Supports radio and checkbox menu items</li>
1265-
<li>Flexible menu positioning</li>
1266-
<li>Comprehensive keyboard interactions</li>
1267-
<li>
1268-
Customisable <Link href={'#styling'}>styling</Link>
1269-
</li>
1270-
<li>
1271-
<a href="https://github.com/reactwg/react-18/discussions/70">Level 3 support</a> of React 18
1272-
concurrent rendering
1258+
<a href="https://bundlephobia.com/package/@szhsin/react-menu">Lightweight</a>, unstyled
1259+
React menu components
12731260
</li>
1261+
<li>Unlimited submenu nesting</li>
1262+
<li>Supports dropdown, hover, and context menus</li>
1263+
<li>Radio and checkbox menu items</li>
1264+
<li>Flexible positioning options</li>
1265+
<li>Full keyboard interaction support</li>
1266+
<li>Compatible with React 18+ concurrent rendering</li>
12741267
<li>Supports server-side rendering</li>
12751268
<li>
12761269
Implements{' '}

example/src/utils/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { useEffect, useLayoutEffect } from 'react';
22
import { useTheme } from '../store';
33

4-
export const version = '4.4.0';
5-
export const build = '141';
4+
export const version = '4.4.1';
5+
export const build = '142';
66

77
export const bem = (block, element, modifiers = {}) => {
88
let blockElement = element ? `${block}__${element}` : block;

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@szhsin/react-menu",
3-
"version": "4.4.0",
4-
"description": "React component for building accessible menu, dropdown, submenu, context menu and more.",
3+
"version": "4.4.1",
4+
"description": "React component for building accessible menu, dropdown, submenu, context menu, and more",
55
"author": "Zheng Song",
66
"license": "MIT",
77
"repository": "szhsin/react-menu",

0 commit comments

Comments
 (0)