Skip to content

Commit

Permalink
some api change and update README
Browse files Browse the repository at this point in the history
  • Loading branch information
cluk3 committed Jul 8, 2019
1 parent 024e112 commit 7b386c7
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 11 deletions.
164 changes: 164 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<div align="center">
<h1>use-context-menu</h1>

<p>A React hook for easily creating custom Context Menus! The hooks takes care of the logic and creating the a11y attributes, you take care of the UI!</p>

</div>

<hr />

## Table of Contents

- [Installation](#installation)
- [Features](#features)
- [Usage](#usage)
- [API](#api)
- [Examples](#example)
- [LICENSE](#license)

## Installation

This module is distributed via [npm][npm] which is bundled with [node][node] and
should be installed as one of your project's `dependencies`:

```
npm install --save react-use-context-menu
```

or

```
yarn add react-use-context-menu
```

##Features

- Supports Keyboard navigation
- Fully customizable
- Fully accessible
- No dependencies
- Only 1kb gzipped
- RTL support
- supports touch screen with hold to display!
- Detects the size of the menu and always places it inside the viewport when clicking near the borders.
- ESM and CommonJS dist

## Usage

Basic usage:

```jsx
import React from 'react'
import ReactDOM from 'react-dom'
import useContextMenu from 'react-use-context-menu'

function App() {
const [
bindMenu,
bindMenuItems,
useContextTrigger
] = useContextMenu();
const [bindTrigger] = useContextTrigger(});
return (
<div className="App">
<h1>useContextMenu</h1>
<h2 {...bindTrigger}>Right click me to see some magic happen!</h2>
<nav {...bindMenu}>
<div {...bindMenuItems}>First action</div>
<div {...bindMenuItems}>Second action</div>
<hr/>
<div {...bindMenuItems}>Last action</div>
</nav>
</div>
);
}
```

## API

```jsx
const [
bindMenu,
bindMenuItems,
useContextTrigger,
{
data, // the data collected by the collect function of useContextTrigger
coords, // a 2d Array [x, y] returning the coords of the point where the right click was triggered
setCoords, // manually set the coords of the menu
isVisible, // Boolean indicating if the menu is visible or not
setVisible, // manually set the context menu visible with setVisible(true) or hidden with setVisible(false)
}
] = useContextMenu({
rtl: false, // Optional, set to true to enable RightToLeft menu,
handleElementSelect: el => el.focus() // Handles the element being selected with keyboard. Optional, focus is the default behaviour
});
```

### bindMenu
The first element of the result array is an Object used to bind the context menu element.
It has 4 properties:
```js
{
style, // Mandatory
ref, // Mandatory
role: "menu", // Optional, if you don't care about a11y 😠
tabIndex: -1 // same as above, also this is needed for keayboard navigation
}
```
Keep in mind that you need to spread this over an actual element (like a div or a nav), if you spread it to a Component, then the Component should take care of passing the props to the underlying wrapper element.
If you as well need to access the ref of the element you can simply do:
```jsx
<div {...bindMenu} ref={(el) => {
bindMenu.ref(el);
// you logic here
}}
```

### bindMenuItems

The second element is an Object with 3 props:
```js
{
ref, // Mandatory
role: "menuitem", // Optional, if you don't care about a11y 😠
tabIndex: -1 // same as above, also this is needed for keayboard navigation
}
```
used to bind the menu items. You can spread it or assign the props one by one. Same as above applies.

### useContextTrigger
The third element of the result array is another hook which you should use to bind the trigger component(s), which is the component which will trigger the context menu when right clicked.
It accepts an optional config object for fine tuning and advanced usage
```js
const [bindTrigger] = useContextTrigger({
// those are the default values
disable: false, // disable the trigger
holdToDisplay: 1000, // time in ms after which the context menu will appear when holding the touch
posX: 0, // distance in pixel from which the context menu will appear related to the right click X coord
posY: 0, // distance in pixel from which the context menu will appear related to the right click y coord
mouseButton: MOUSE_BUTTON.RIGHT, // set to 0 for triggering the context menu with the left click
disableIfShiftIsPressed: false, // Self explanatory 😺
collect: () => 'useContextMenu is cool!' // collect data to be passed to the context menu, see the example to see this in action
});
```

## Examples:

You can check the example folder or [this codesandbox][codesandbox-example] for more advanced examples.

## LICENSE

MIT

## Comparison with other similar packages

This is the first package implemented using hooks so far for what I've seen!
Other packages are using components which requires lot of configuration and are way bigger in size.
Also this is the smallest and most configurable 💃

## Gratitude
This package have been deeply inspired by https://github.com/vkbansal/react-contextmenu, thanks a lot to @vkbansal! 🙏

[npm]: https://www.npmjs.com/
[node]: https://nodejs.org
[codesandbox-example]: https://codesandbox.io/s/hopeful-hopper-v4zzv?fontsize=14
6 changes: 4 additions & 2 deletions example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@ const ListItem = ({ name, useContextTrigger }) => {

function App() {
const [
bindMenu,
bindMenuItem,
useContextTrigger,
{ data, coords, bindMenu, hideMenu },
bindMenuItem
{ data, coords, setVisible }
] = useContextMenu();
const [bindTrigger] = useContextTrigger({
collect: () => "Title"
});
const [clickedCmd, setClickedCmd] = useState();
const hideMenu = () => setVisible(false);
return (
<div className="App">
<h1 {...bindTrigger}>useContextMenu</h1>
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "use-context-menu",
"version": "0.0.1",
"name": "react-use-context-menu",
"version": "0.1.1",
"description": "React hook for managing custom context menus!",
"main": "dist/use-context-menu.js",
"umd:main": "dist/react-hooks-lib.umd.js",
Expand Down
32 changes: 32 additions & 0 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,38 @@ export const getMenuPosition = (rect, [x, y]) => {
return menuStyles;
};

export const getRTLMenuPosition = (rect, [x, y]) => {
const menuStyles = {
top: y,
left: x
};

const { innerWidth, innerHeight } = window;

// Try to position the menu on the left side of the cursor
menuStyles.left = x - rect.width;

if (y + rect.height > innerHeight) {
menuStyles.top -= rect.height;
}

if (menuStyles.left < 0) {
menuStyles.left += rect.width;
}

if (menuStyles.top < 0) {
menuStyles.top =
rect.height < innerHeight ? (innerHeight - rect.height) / 2 : 0;
}

if (menuStyles.left + rect.width > innerWidth) {
menuStyles.left =
rect.width < innerWidth ? (innerWidth - rect.width) / 2 : 0;
}

return menuStyles;
};

export const getCoords = (event, config) =>
["X", "Y"].map(
axis =>
Expand Down
24 changes: 17 additions & 7 deletions src/useContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import {
useEffect,
useCallback
} from "react";
import { getMenuPosition } from "./helpers";
import { getMenuPosition, getRTLMenuPosition } from "./helpers";
import buildUseContextMenuTrigger from "./buildUseContextMenuTrigger";

const ESCAPE = 27;
const baseStyles = { position: "fixed", opacity: 0, pointerEvents: "none" };
const focusElement = el => el.focus();
const useContextMenu = ({ handleElementSelect = focusElement } = {}) => {
const useContextMenu = ({ rtl, handleElementSelect = focusElement } = {}) => {
const menuRef = useRef();
const selectables = useRef([]);
const [style, setStyles] = useState(baseStyles);
Expand Down Expand Up @@ -89,7 +89,9 @@ const useContextMenu = ({ handleElementSelect = focusElement } = {}) => {
useLayoutEffect(() => {
if (isVisible) {
const rect = menuRef.current.getBoundingClientRect();
const { top, left } = getMenuPosition(rect, coords);
const { top, left } = rtl
? getRTLMenuPosition(rect, coords)
: getMenuPosition(rect, coords);
setStyles(st => ({
...st,
top: `${top}px`,
Expand All @@ -103,13 +105,21 @@ const useContextMenu = ({ handleElementSelect = focusElement } = {}) => {
}, [menuRef, isVisible, coords]);

const bindMenu = { style, ref: menuRef, role: "menu", tabIndex: -1 };
const bindMenuItems = {
ref: markSelectable,
role: "menuitem",
tabIndex: -1
};
return [
bindMenu,
bindMenuItems,
buildUseContextMenuTrigger(triggerVisible, setCoords),
{ data: collectedData, isVisible, coords, bindMenu, hideMenu },
{
ref: markSelectable,
role: "menuitem",
tabIndex: -1
data: collectedData,
isVisible,
setVisible,
coords,
setCoords
}
];
};
Expand Down

0 comments on commit 7b386c7

Please sign in to comment.