Skip to content

Commit

Permalink
Merge pull request #9 from icflorescu/next
Browse files Browse the repository at this point in the history
Backport imperative hiding functionality
  • Loading branch information
icflorescu authored Nov 10, 2023
2 parents 5525dae + 612b42c commit 395e9a7
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 197 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
The following is a list of notable changes to the Mantine ContextMenu component.
Minor versions that are not listed in the changelog are minor bug fixes and small internal improvements or refactorings.

## 6.1.0 (2023-11-10)

- Allow imperative hiding by using the `hideContextMenu` function that is exposed via `useContextMenu()`.
- `useContextMenu()` is now also an object that can be destructured into 3 properties: `showContextMenu`, `hideContextMenu`, and `isContextMenuVisible `.

## 6.0.0 (2023-10-01)

- Bump version to `6.0.0` to match the compatible versions of `@mantine/hooks` and `@mantine/core`. From now on, we'll make sure to keep the major version of `mantine-contextmenu` in sync with the major version of Mantine core
Expand Down
5 changes: 5 additions & 0 deletions docs/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ export const PAGES: ({ external?: true; title: string; color?: MantineColor; des
title: 'Submenus (nested menus)',
description: 'Example: How to create Mantine ContextMenu Submenus (nested menus)',
},
{
path: 'imperative-hiding',
title: 'Imperative hiding',
description: `Example: Mantine ContextMenu hides itself automatically when the user scrolls the page or clicks outside of it, but you can also hide it imperatively`,
},
],
},
{
Expand Down
45 changes: 45 additions & 0 deletions docs/examples/ImperativeHidingExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useHotkeys, usePageLeave, useTimeout } from '@mantine/hooks';
import { useContextMenu } from 'mantine-contextmenu';
import { useEffect } from 'react';
import Picture from '~/components/Picture';
import { copyImageToClipboard, downloadImage, unsplashImages } from '~/lib/image';

export default function ImperativeHidingExample() {
const { showContextMenu, hideContextMenu, isContextMenuVisible } = useContextMenu();

// 👇 hide the context menu after five seconds have elapsed
const { start: startHiding, clear: cancelHiding } = useTimeout(hideContextMenu, 5000);
useEffect(() => {
if (isContextMenuVisible) {
startHiding();
} else {
cancelHiding();
}
}, [cancelHiding, isContextMenuVisible, startHiding]);

// 👇 hide the context menu when the user hits the `H` key
useHotkeys([['H', hideContextMenu]]);

// 👇 hide the context menu when the mouse cursor leaves the page
usePageLeave(hideContextMenu);

// example-skip
const image = unsplashImages[2];
const { src } = image.file;
// example-resume
return (
<Picture
image={image}
onContextMenu={showContextMenu([
{
key: 'copy',
onClick: () => copyImageToClipboard(src),
},
{
key: 'download',
onClick: () => downloadImage(src),
},
])}
/>
);
}
12 changes: 6 additions & 6 deletions docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mantine-contextmenu-docs",
"version": "6.0.6",
"version": "6.1.0",
"description": "Docs website for mantine-contextmenu; see ../package/package.json for more info",
"private": true,
"scripts": {
Expand All @@ -17,18 +17,18 @@
"@mantine/next": "^6.0.21",
"@mantine/notifications": "^6.0.21",
"@mantine/prism": "^6.0.21",
"@tabler/icons-react": "^2.39.0",
"@tabler/icons-react": "^2.40.0",
"lodash": "^4.17.21",
"mantine-contextmenu": "*",
"next": "^13.5.6",
"next": "^14.0.2",
"next-sitemap": "^4.2.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/lodash": "^4.14.200",
"@types/node": "^20.8.8",
"@types/react": "^18.2.31",
"@types/lodash": "^4.14.201",
"@types/node": "^20.9.0",
"@types/react": "^18.2.37",
"typescript": "^5.2.2",
"webpack": "^5.89.0"
}
Expand Down
50 changes: 50 additions & 0 deletions docs/pages/examples/imperative-hiding.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Code, Container } from '@mantine/core';
import { GetStaticProps, InferGetStaticPropsType } from 'next';
import CodeBlock from '~/components/CodeBlock';
import PageNavigation from '~/components/PageNavigation';
import PageText from '~/components/PageText';
import PageTitle from '~/components/PageTitle';
import ImperativeHidingExample from '~/examples/ImperativeHidingExample';
import readCodeExample from '~/lib/readCodeExample';

const PATH = 'examples/imperative-hiding';

export const getStaticProps: GetStaticProps<{ code: string }> = async () => ({
props: { code: (await readCodeExample('examples/ImperativeHidingExample.tsx')) as string },
});

export default function Page({ code }: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<Container>
<PageTitle of={PATH} />
<PageText>
A visible context menu hides automatically when the user clicks anywhere on the page, hits the{' '}
<Code>Escape</Code> key, scrolls the or resizes the browser window.
</PageText>
<PageText>
However, you can also hide the context menu <em>imperatively</em> by destructuring the result returned by the{' '}
<Code>useContextMenu</Code> hook into:
</PageText>
<ul>
<li>
<Code>showContextMenu</Code> → a function that can be used to show the context menu;
</li>
<li>
<Code>hideContextMenu</Code> → a function that can be used to hide the context menu;
</li>
<li>
<Code>isContextMenuVisible</Code> → a <Code>boolean</Code> representing whether the context menu is currently
visible or not.
</li>
</ul>
<PageText>
In the example below, we’ll hide the context menu automatically when the user presses the <Code>H</Code> key,
his mouse cursor leaves the page, or after five seconds have elapsed:
</PageText>
<CodeBlock language="typescript" content={code} />
<PageText>Right-click on the image below to show the context menu:</PageText>
<ImperativeHidingExample />
<PageNavigation of={PATH} />
</Container>
);
}
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mantine-contextmenu-turborepo",
"version": "6.0.6",
"version": "6.1.0",
"description": "This is a monorepo; see package/package.json for more info",
"private": true,
"workspaces": [
Expand All @@ -18,10 +18,10 @@
"test:watch": "turbo run test:watch"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.9.0",
"@typescript-eslint/parser": "^6.9.0",
"eslint": "^8.52.0",
"eslint-config-next": "^13.5.6",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"eslint": "^8.53.0",
"eslint-config-next": "^14.0.2",
"eslint-config-prettier": "^9.0.0",
"prettier": "^3.0.3",
"turbo": "^1.10.16",
Expand Down
25 changes: 19 additions & 6 deletions package/ContextMenuProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { createContext, useContext, useState } from 'react';
import { ContextMenuInstanceOptions } from './ContextMenu';
import { ContextMenuPortal } from './ContextMenuPortal';
import type { ContextMenuOptions, ContextMenuProviderProps, ShowContextMenuFunction } from './types';
import type { ContextMenuOptions, ContextMenuProviderProps, ShowContextMenuFunctionObject } from './types';

const MenuContext = createContext<ShowContextMenuFunction>(() => () => undefined);
const defaultMenuContextValue = () => () => undefined;

defaultMenuContextValue.showContextMenu = defaultMenuContextValue;
defaultMenuContextValue.hideContextMenu = () => undefined;
defaultMenuContextValue.isContextMenuVisible = false;

const MenuContext = createContext<ShowContextMenuFunctionObject>(defaultMenuContextValue);

/**
* Provider that allows to show a context menu anywhere in your application.
Expand All @@ -18,11 +24,11 @@ export function ContextMenuProvider({
}: ContextMenuProviderProps) {
const [data, setData] = useState<(ContextMenuInstanceOptions & ContextMenuOptions) | null>(null);

const destroy = () => {
const hideContextMenu = () => {
setData(null);
};

const showContextMenu: ShowContextMenuFunction = (content, options) => (e) => {
const showContextMenu: ShowContextMenuFunctionObject = (content, options) => (e) => {
e.preventDefault();
e.stopPropagation();
setData({
Expand All @@ -40,16 +46,23 @@ export function ContextMenuProvider({
});
};

showContextMenu.showContextMenu = showContextMenu;
showContextMenu.hideContextMenu = hideContextMenu;
showContextMenu.isContextMenuVisible = !!data;

return (
<MenuContext.Provider value={showContextMenu}>
{children}
{data && <ContextMenuPortal onHide={destroy} {...data} />}
{data && <ContextMenuPortal onHide={hideContextMenu} {...data} />}
</MenuContext.Provider>
);
}

/**
* Hook returning a function that shows a context menu.
* Hook returning a function object that shows a context menu.
* The returned object can also be destructured into
* `showContextMenu`, `hideContextMenu` functions and a `isContextMenuVisible` boolean,
* which can be used for finer-grained control (such as imperatively hiding the context menu).
*/
export function useContextMenu() {
return useContext(MenuContext);
Expand Down
6 changes: 3 additions & 3 deletions package/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mantine-contextmenu",
"version": "6.0.6",
"version": "6.1.0",
"description": "Enhance your Mantine UI applications usability with customizable context menus",
"keywords": [
"ui",
Expand Down Expand Up @@ -55,8 +55,8 @@
"devDependencies": {
"@mantine/core": "^6.0.21",
"@mantine/hooks": "^6.0.21",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"esbuild": "^0.19.5",
"react": "^18.2.0",
"typescript": "^5.2.2"
Expand Down
25 changes: 25 additions & 0 deletions package/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ export type ContextMenuItemOptions = {

export type ContextMenuContent = ContextMenuItemOptions[] | ((close: () => void) => JSX.Element);

/**
* Show context menu function
*/
export type ShowContextMenuFunction = (
/**
* Context menu content - either an array of context menu items
Expand All @@ -136,3 +139,25 @@ export type ShowContextMenuFunction = (
*/
options?: ContextMenuOptions
) => MouseEventHandler;

/**
* Hide context menu function
*/
export type HideContextMenuFunction = () => void;

export interface ShowContextMenuFunctionObject extends ShowContextMenuFunction {
/**
* A function that shows the context menu
*/
showContextMenu: ShowContextMenuFunction;

/**
* A function that hides the context menu
*/
hideContextMenu: HideContextMenuFunction;

/**
* Boolean indicating whether the context menu is visible
*/
isContextMenuVisible: boolean;
}
Loading

0 comments on commit 395e9a7

Please sign in to comment.