├── example
├── public
│ ├── .nojekyll
│ ├── gdbk.jpeg
│ ├── favicon.ico
│ ├── octocat.png
│ ├── GitHub-Mark-32px.png
│ ├── GitHub-Mark-64px.png
│ ├── GitHub-Mark-Light-32px.png
│ ├── GitHub-Mark-Light-64px.png
│ └── manifest.json
├── src
│ ├── pages
│ │ ├── 404.js
│ │ ├── docs.js
│ │ ├── index.js
│ │ ├── style-guide.js
│ │ ├── _app.js
│ │ └── _document.js
│ ├── components
│ │ ├── LibName.js
│ │ ├── Docs.js
│ │ ├── StyleGuide.js
│ │ ├── NotFound.js
│ │ ├── ExternalLink.js
│ │ ├── RightSection.js
│ │ ├── ThemeSwitch.js
│ │ ├── PromoSpot.js
│ │ ├── HeaderBanner.js
│ │ ├── PageView.js
│ │ ├── CascadingContents.js
│ │ ├── TableContentsList.js
│ │ ├── Promo.js
│ │ ├── Table.js
│ │ ├── HashHeading.js
│ │ ├── Icons.js
│ │ ├── Logo.js
│ │ ├── StyleExamples.js
│ │ ├── Footer.js
│ │ ├── App.js
│ │ ├── TableContents.js
│ │ └── Header.js
│ ├── styles
│ │ ├── _logo.scss
│ │ ├── _theme-switch.scss
│ │ ├── _hash-heading.scss
│ │ ├── _prism.scss
│ │ ├── _menu.scss
│ │ ├── _var.scss
│ │ ├── _mixins.scss
│ │ └── _table-contents.scss
│ ├── store.js
│ └── utils
│ │ └── index.js
├── README.md
├── next.config.js
├── package.json
└── gh-pages.sh
├── .browserslistrc
├── .prettierignore
├── setup-jest.js
├── types
├── tsconfig.json
└── style-utils.d.ts
├── src
├── utils
│ ├── index.js
│ ├── withHovering.js
│ ├── submenuCtx.js
│ ├── constants.js
│ └── utils.js
├── styles
│ ├── _mixins.scss
│ ├── _var.scss
│ ├── theme-dark.scss
│ ├── transitions
│ │ ├── slide.scss
│ │ └── zoom.scss
│ ├── index.scss
│ └── core.scss
├── positionUtils
│ ├── index.js
│ ├── placeArrowHorizontal.js
│ ├── placeArrowVertical.js
│ ├── getNormalizedClientRect.js
│ ├── placeLeftorRight.js
│ ├── placeToporBottom.js
│ ├── positionMenu.js
│ └── getPositionHelpers.js
├── __tests__
│ ├── entry.js
│ ├── utils.test.js
│ ├── SSR.test.js
│ ├── MenuGroup.test.js
│ ├── styling.test.js
│ ├── style-utils.test.js
│ └── useMenuState.test.js
├── hooks
│ ├── useMouseOver.js
│ ├── useMenuStateAndFocus.js
│ ├── index.js
│ ├── useClick.js
│ ├── useCombinedRef.js
│ ├── useItemEffect.js
│ ├── useIsomorphicLayoutEffect.js
│ ├── useHover.js
│ ├── useBEM.js
│ ├── useMenuState.js
│ └── useItemState.js
├── components
│ ├── MenuDivider.js
│ ├── MenuHeader.js
│ ├── MenuButton.js
│ ├── MenuRadioGroup.js
│ ├── MenuContainer.js
│ ├── MenuGroup.js
│ ├── FocusableItem.js
│ └── Menu.js
├── index.js
└── style-utils
│ └── index.js
├── .prettierrc.yaml
├── style-utils
└── package.json
├── jest.config.js
├── .gitignore
├── dist
├── esm
│ ├── hooks
│ │ ├── useIsomorphicLayoutEffect.mjs
│ │ ├── useMouseOver.mjs
│ │ ├── useCombinedRef.mjs
│ │ ├── useClick.mjs
│ │ ├── useMenuStateAndFocus.mjs
│ │ ├── useItemEffect.mjs
│ │ ├── useHover.mjs
│ │ ├── useBEM.mjs
│ │ ├── useMenuState.mjs
│ │ └── useItemState.mjs
│ ├── positionUtils
│ │ ├── placeArrowVertical.mjs
│ │ ├── placeArrowHorizontal.mjs
│ │ ├── getNormalizedClientRect.mjs
│ │ ├── placeLeftorRight.mjs
│ │ ├── placeToporBottom.mjs
│ │ ├── getPositionHelpers.mjs
│ │ └── positionMenu.mjs
│ ├── utils
│ │ ├── submenuCtx.mjs
│ │ ├── withHovering.mjs
│ │ ├── constants.mjs
│ │ └── utils.mjs
│ ├── components
│ │ ├── MenuDivider.mjs
│ │ ├── MenuHeader.mjs
│ │ ├── MenuButton.mjs
│ │ ├── MenuRadioGroup.mjs
│ │ ├── MenuContainer.mjs
│ │ ├── MenuGroup.mjs
│ │ ├── FocusableItem.mjs
│ │ └── Menu.mjs
│ └── index.mjs
├── cjs
│ ├── hooks
│ │ ├── useIsomorphicLayoutEffect.cjs
│ │ ├── useMouseOver.cjs
│ │ ├── useCombinedRef.cjs
│ │ ├── useClick.cjs
│ │ ├── useMenuStateAndFocus.cjs
│ │ ├── useItemEffect.cjs
│ │ ├── useHover.cjs
│ │ ├── useBEM.cjs
│ │ ├── useMenuState.cjs
│ │ └── useItemState.cjs
│ ├── positionUtils
│ │ ├── placeArrowVertical.cjs
│ │ ├── placeArrowHorizontal.cjs
│ │ ├── getNormalizedClientRect.cjs
│ │ ├── placeLeftorRight.cjs
│ │ ├── placeToporBottom.cjs
│ │ ├── getPositionHelpers.cjs
│ │ └── positionMenu.cjs
│ ├── utils
│ │ ├── submenuCtx.cjs
│ │ ├── withHovering.cjs
│ │ ├── utils.cjs
│ │ └── constants.cjs
│ ├── components
│ │ ├── MenuDivider.cjs
│ │ ├── MenuHeader.cjs
│ │ ├── MenuButton.cjs
│ │ ├── MenuRadioGroup.cjs
│ │ ├── MenuContainer.cjs
│ │ ├── MenuGroup.cjs
│ │ ├── FocusableItem.cjs
│ │ └── Menu.cjs
│ └── index.cjs
├── theme-dark.css
├── transitions
│ ├── zoom.css
│ └── slide.css
├── core.css
└── style-utils
│ ├── index.mjs
│ └── index.cjs
├── .github
├── ISSUE_TEMPLATE
│ ├── ---feature-request.md
│ ├── ---bug-report.md
│ └── ---questions-and-help.md
└── dependabot.yml
├── docs
├── migration
│ ├── index.md
│ └── v4.md
└── FAQs.md
├── babel.config.js
├── LICENSE
├── rollup.config.mjs
├── eslint.config.mjs
├── README.md
└── package.json
/example/public/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | last 2 versions
2 | > 0.2%
3 | not dead
4 | not op_mini all
--------------------------------------------------------------------------------
/example/src/pages/404.js:
--------------------------------------------------------------------------------
1 | export { default } from '../components/NotFound';
2 |
--------------------------------------------------------------------------------
/example/src/pages/docs.js:
--------------------------------------------------------------------------------
1 | export { default } from '../components/Docs';
2 |
--------------------------------------------------------------------------------
/example/src/pages/index.js:
--------------------------------------------------------------------------------
1 | export { default } from '../components/Usage';
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | build/
2 | coverage/
3 | dist/
4 | example/out/
5 | .next/
6 | _next/
--------------------------------------------------------------------------------
/example/src/pages/style-guide.js:
--------------------------------------------------------------------------------
1 | export { default } from '../components/StyleGuide';
2 |
--------------------------------------------------------------------------------
/example/public/gdbk.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szhsin/react-menu/HEAD/example/public/gdbk.jpeg
--------------------------------------------------------------------------------
/example/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szhsin/react-menu/HEAD/example/public/favicon.ico
--------------------------------------------------------------------------------
/example/public/octocat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szhsin/react-menu/HEAD/example/public/octocat.png
--------------------------------------------------------------------------------
/setup-jest.js:
--------------------------------------------------------------------------------
1 | const { toHaveNoViolations } = require('jest-axe');
2 | expect.extend(toHaveNoViolations);
3 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | React-Menu examples and documentation website, built with [Next.js](https://nextjs.org/).
2 |
--------------------------------------------------------------------------------
/types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "noEmit": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/example/public/GitHub-Mark-32px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szhsin/react-menu/HEAD/example/public/GitHub-Mark-32px.png
--------------------------------------------------------------------------------
/example/public/GitHub-Mark-64px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szhsin/react-menu/HEAD/example/public/GitHub-Mark-64px.png
--------------------------------------------------------------------------------
/example/src/components/LibName.js:
--------------------------------------------------------------------------------
1 | export function LibName() {
2 | return React-Menu;
3 | }
4 |
--------------------------------------------------------------------------------
/example/public/GitHub-Mark-Light-32px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szhsin/react-menu/HEAD/example/public/GitHub-Mark-Light-32px.png
--------------------------------------------------------------------------------
/example/public/GitHub-Mark-Light-64px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szhsin/react-menu/HEAD/example/public/GitHub-Mark-Light-64px.png
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export * from './constants';
2 | export * from './utils';
3 | export * from './submenuCtx';
4 | export * from './withHovering';
5 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | trailingComma: none
2 | singleQuote: true
3 | printWidth: 100
4 | overrides:
5 | - files: '*.md'
6 | options:
7 | printWidth: 80
8 |
--------------------------------------------------------------------------------
/src/styles/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin reset-list {
2 | margin: 0;
3 | padding: 0;
4 | list-style: none;
5 | }
6 |
7 | @mixin remove-focus {
8 | &:focus {
9 | outline: none;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/positionUtils/index.js:
--------------------------------------------------------------------------------
1 | export { getNormalizedClientRect } from './getNormalizedClientRect';
2 | export { getPositionHelpers } from './getPositionHelpers';
3 | export { positionMenu } from './positionMenu';
4 |
--------------------------------------------------------------------------------
/style-utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "sideEffects": false,
4 | "main": "../dist/style-utils/index.cjs",
5 | "module": "../dist/style-utils/index.mjs",
6 | "types": "../types/style-utils.d.ts"
7 | }
8 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | clearMocks: true,
3 | testEnvironment: 'jsdom',
4 | testMatch: ['**/*.test.js'],
5 | setupFilesAfterEnv: ['@testing-library/jest-dom', 'jest-dom-extended/jest', './setup-jest.js']
6 | };
7 |
--------------------------------------------------------------------------------
/example/src/components/Docs.js:
--------------------------------------------------------------------------------
1 | import { PageView } from './PageView';
2 | import documentation from '../data/documentation';
3 |
4 | const Docs = () => ;
5 |
6 | export default Docs;
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules/
3 |
4 | # builds
5 | build/
6 | .next/
7 | _next/
8 | example/out/
9 |
10 | # tests
11 | coverage/
12 |
13 | # logs
14 | logs
15 | *.log
16 |
17 | # misc
18 | .DS_Store
19 | .npm
20 | .env
21 | .eslintcache
--------------------------------------------------------------------------------
/example/next.config.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path');
2 |
3 | module.exports = {
4 | reactStrictMode: true,
5 | output: 'export',
6 | basePath: '/react-menu',
7 | turbopack: {
8 | root: path.join(__dirname, '..')
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/example/src/components/StyleGuide.js:
--------------------------------------------------------------------------------
1 | import { PageView } from './PageView';
2 | import styleGuide from '../data/styleGuide';
3 |
4 | const StyleGuide = () => ;
5 |
6 | export default StyleGuide;
7 |
--------------------------------------------------------------------------------
/example/src/components/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NotFound = React.memo(function NotFound() {
4 | return (
5 |
6 | 404 - Page Not Found
7 |
8 | );
9 | });
10 |
11 | export default NotFound;
12 |
--------------------------------------------------------------------------------
/example/src/components/ExternalLink.js:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { ExternalLinkIcon } from './Icons';
3 |
4 | export const ExternalLink = memo(({ href, children }) => (
5 |
6 | {children}
7 |
8 |
9 | ));
10 |
--------------------------------------------------------------------------------
/dist/esm/hooks/useIsomorphicLayoutEffect.mjs:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useEffect } from 'react';
2 |
3 | const useIsomorphicLayoutEffect = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined' ? useLayoutEffect : useEffect;
4 |
5 | export { useIsomorphicLayoutEffect as useLayoutEffect };
6 |
--------------------------------------------------------------------------------
/src/__tests__/entry.js:
--------------------------------------------------------------------------------
1 | export * from '../';
2 | export * from '../style-utils';
3 |
4 | // Test npm distribution before publishing
5 |
6 | // cjs
7 | // export * from '../../dist';
8 | // export * from '../../dist/style-utils/index.cjs';
9 |
10 | // es
11 | // export * from '../../dist/es';
12 | // export * from '../../dist/style-utils/index.js';
13 |
--------------------------------------------------------------------------------
/dist/cjs/hooks/useIsomorphicLayoutEffect.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var react = require('react');
4 |
5 | const useIsomorphicLayoutEffect = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined' ? react.useLayoutEffect : react.useEffect;
6 |
7 | exports.useLayoutEffect = useIsomorphicLayoutEffect;
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/---feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F4A1 Feature Request"
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: unconfirmed
6 | assignees: ''
7 | ---
8 |
9 |
13 |
--------------------------------------------------------------------------------
/src/positionUtils/placeArrowHorizontal.js:
--------------------------------------------------------------------------------
1 | export const placeArrowHorizontal = ({ arrowRef, menuX, anchorRect, containerRect, menuRect }) => {
2 | let x = anchorRect.left - containerRect.left - menuX + anchorRect.width / 2;
3 | const offset = arrowRef.current.offsetWidth * 1.25;
4 | x = Math.max(offset, x);
5 | x = Math.min(x, menuRect.width - offset);
6 | return x;
7 | };
8 |
--------------------------------------------------------------------------------
/src/positionUtils/placeArrowVertical.js:
--------------------------------------------------------------------------------
1 | export const placeArrowVertical = ({ arrowRef, menuY, anchorRect, containerRect, menuRect }) => {
2 | let y = anchorRect.top - containerRect.top - menuY + anchorRect.height / 2;
3 | const offset = arrowRef.current.offsetHeight * 1.25;
4 | y = Math.max(offset, y);
5 | y = Math.min(y, menuRect.height - offset);
6 | return y;
7 | };
8 |
--------------------------------------------------------------------------------
/docs/migration/index.md:
--------------------------------------------------------------------------------
1 | ## Migration guides
2 |
3 | - [Migrating to v4](v4.md)
4 | - [Migrating to v3](v3.md)
5 | - [Migrating to v2](v2.md)
6 |
7 | ## Looking for doc/example websites of older versions?
8 |
9 | - [v3 docs](https://szhsin.github.io/react-menu-v3/)
10 | - [v2 docs](https://szhsin.github.io/react-menu-v2/)
11 | - [v1 docs](https://szhsin.github.io/react-menu-v1/)
12 |
--------------------------------------------------------------------------------
/example/src/components/RightSection.js:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { useDomInfo } from '../store';
3 | import { Promo } from './Promo';
4 |
5 | export const RightSection = memo(function RightSection() {
6 | return (
7 |
10 | );
11 | });
12 |
--------------------------------------------------------------------------------
/example/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "react-menu",
3 | "name": "@szhsin/react-menu",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#ffffff",
14 | "background_color": "#000000"
15 | }
16 |
--------------------------------------------------------------------------------
/example/src/styles/_logo.scss:
--------------------------------------------------------------------------------
1 | .app-logo {
2 | display: flex;
3 | align-items: center;
4 | font-weight: 700;
5 | cursor: pointer;
6 | .version {
7 | margin-left: 0.5rem;
8 | font-size: 0.85rem;
9 | font-weight: normal;
10 | color: #888;
11 | }
12 | .drop-down {
13 | font-size: 1.75rem;
14 | }
15 | }
16 |
17 | .version-menu {
18 | font-weight: normal;
19 | }
20 |
--------------------------------------------------------------------------------
/dist/esm/hooks/useMouseOver.mjs:
--------------------------------------------------------------------------------
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 };
12 |
--------------------------------------------------------------------------------
/src/hooks/useMouseOver.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | const useMouseOver = (isHovering) => {
4 | const [mouseOver, setMouseOver] = useState(false);
5 |
6 | useEffect(() => {
7 | !isHovering && setMouseOver(false);
8 | }, [isHovering]);
9 |
10 | return [mouseOver, () => !mouseOver && setMouseOver(true), () => setMouseOver(false)];
11 | };
12 |
13 | export { useMouseOver };
14 |
--------------------------------------------------------------------------------
/dist/cjs/hooks/useMouseOver.cjs:
--------------------------------------------------------------------------------
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;
14 |
--------------------------------------------------------------------------------
/dist/esm/positionUtils/placeArrowVertical.mjs:
--------------------------------------------------------------------------------
1 | const placeArrowVertical = ({
2 | arrowRef,
3 | menuY,
4 | anchorRect,
5 | containerRect,
6 | menuRect
7 | }) => {
8 | let y = anchorRect.top - containerRect.top - menuY + anchorRect.height / 2;
9 | const offset = arrowRef.current.offsetHeight * 1.25;
10 | y = Math.max(offset, y);
11 | y = Math.min(y, menuRect.height - offset);
12 | return y;
13 | };
14 |
15 | export { placeArrowVertical };
16 |
--------------------------------------------------------------------------------
/dist/esm/positionUtils/placeArrowHorizontal.mjs:
--------------------------------------------------------------------------------
1 | const placeArrowHorizontal = ({
2 | arrowRef,
3 | menuX,
4 | anchorRect,
5 | containerRect,
6 | menuRect
7 | }) => {
8 | let x = anchorRect.left - containerRect.left - menuX + anchorRect.width / 2;
9 | const offset = arrowRef.current.offsetWidth * 1.25;
10 | x = Math.max(offset, x);
11 | x = Math.min(x, menuRect.width - offset);
12 | return x;
13 | };
14 |
15 | export { placeArrowHorizontal };
16 |
--------------------------------------------------------------------------------
/dist/esm/hooks/useCombinedRef.mjs:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | function setRef(ref, instance) {
4 | typeof ref === 'function' ? ref(instance) : ref.current = instance;
5 | }
6 | const useCombinedRef = (refA, refB) => useMemo(() => {
7 | if (!refA) return refB;
8 | if (!refB) return refA;
9 | return instance => {
10 | setRef(refA, instance);
11 | setRef(refB, instance);
12 | };
13 | }, [refA, refB]);
14 |
15 | export { useCombinedRef };
16 |
--------------------------------------------------------------------------------
/dist/cjs/positionUtils/placeArrowVertical.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const placeArrowVertical = ({
4 | arrowRef,
5 | menuY,
6 | anchorRect,
7 | containerRect,
8 | menuRect
9 | }) => {
10 | let y = anchorRect.top - containerRect.top - menuY + anchorRect.height / 2;
11 | const offset = arrowRef.current.offsetHeight * 1.25;
12 | y = Math.max(offset, y);
13 | y = Math.min(y, menuRect.height - offset);
14 | return y;
15 | };
16 |
17 | exports.placeArrowVertical = placeArrowVertical;
18 |
--------------------------------------------------------------------------------
/dist/esm/positionUtils/getNormalizedClientRect.mjs:
--------------------------------------------------------------------------------
1 | const getNativeDimension = (transformed, untransformed) => Math.round(transformed) === untransformed ? transformed : untransformed;
2 | const getNormalizedClientRect = element => {
3 | const rect = element.getBoundingClientRect();
4 | rect.width = getNativeDimension(rect.width, element.offsetWidth);
5 | rect.height = getNativeDimension(rect.height, element.offsetHeight);
6 | return rect;
7 | };
8 |
9 | export { getNormalizedClientRect };
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/---bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F41B Bug Report"
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: unconfirmed
6 | assignees: ''
7 | ---
8 |
9 | React/React-dom version:
10 | React-Menu version:
11 |
12 | ## Describe the bug
13 |
14 | ## To Reproduce
15 |
16 | 1.
17 | 2.
18 | 3.
19 |
20 | ## Expected behavior
21 |
22 | ## Code snippets/CodeSandbox examples/Screenshots
23 |
24 |
25 |
--------------------------------------------------------------------------------
/dist/cjs/hooks/useCombinedRef.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var react = require('react');
4 |
5 | function setRef(ref, instance) {
6 | typeof ref === 'function' ? ref(instance) : ref.current = instance;
7 | }
8 | const useCombinedRef = (refA, refB) => react.useMemo(() => {
9 | if (!refA) return refB;
10 | if (!refB) return refA;
11 | return instance => {
12 | setRef(refA, instance);
13 | setRef(refB, instance);
14 | };
15 | }, [refA, refB]);
16 |
17 | exports.useCombinedRef = useCombinedRef;
18 |
--------------------------------------------------------------------------------
/dist/cjs/positionUtils/placeArrowHorizontal.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const placeArrowHorizontal = ({
4 | arrowRef,
5 | menuX,
6 | anchorRect,
7 | containerRect,
8 | menuRect
9 | }) => {
10 | let x = anchorRect.left - containerRect.left - menuX + anchorRect.width / 2;
11 | const offset = arrowRef.current.offsetWidth * 1.25;
12 | x = Math.max(offset, x);
13 | x = Math.min(x, menuRect.width - offset);
14 | return x;
15 | };
16 |
17 | exports.placeArrowHorizontal = placeArrowHorizontal;
18 |
--------------------------------------------------------------------------------
/src/positionUtils/getNormalizedClientRect.js:
--------------------------------------------------------------------------------
1 | const getNativeDimension = (transformed, untransformed) =>
2 | Math.round(transformed) === untransformed ? transformed : untransformed;
3 |
4 | const getNormalizedClientRect = (element) => {
5 | const rect = element.getBoundingClientRect();
6 | rect.width = getNativeDimension(rect.width, element.offsetWidth);
7 | rect.height = getNativeDimension(rect.height, element.offsetHeight);
8 | return rect;
9 | };
10 |
11 | export { getNormalizedClientRect };
12 |
--------------------------------------------------------------------------------
/example/src/components/ThemeSwitch.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTheme, themeState } from '../store';
3 | import { bem } from '../utils';
4 |
5 | export const ThemeSwitch = React.memo(function ThemeSwitch() {
6 | const { isDark, theme } = useTheme();
7 |
8 | return (
9 | themeState.set(e.target.checked ? 'dark' : 'light')}
13 | checked={isDark}
14 | />
15 | );
16 | });
17 |
--------------------------------------------------------------------------------
/src/hooks/useMenuStateAndFocus.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useMenuState } from './useMenuState';
3 |
4 | export const useMenuStateAndFocus = (options) => {
5 | const [menuProps, toggleMenu] = useMenuState(options);
6 | const [menuItemFocus, setMenuItemFocus] = useState();
7 |
8 | const openMenu = (position, alwaysUpdate) => {
9 | setMenuItemFocus({ position, alwaysUpdate });
10 | toggleMenu(true);
11 | };
12 |
13 | return [{ menuItemFocus, ...menuProps }, toggleMenu, openMenu];
14 | };
15 |
--------------------------------------------------------------------------------
/dist/cjs/positionUtils/getNormalizedClientRect.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const getNativeDimension = (transformed, untransformed) => Math.round(transformed) === untransformed ? transformed : untransformed;
4 | const getNormalizedClientRect = element => {
5 | const rect = element.getBoundingClientRect();
6 | rect.width = getNativeDimension(rect.width, element.offsetWidth);
7 | rect.height = getNativeDimension(rect.height, element.offsetHeight);
8 | return rect;
9 | };
10 |
11 | exports.getNormalizedClientRect = getNormalizedClientRect;
12 |
--------------------------------------------------------------------------------
/src/components/MenuDivider.js:
--------------------------------------------------------------------------------
1 | import { memo, forwardRef } from 'react';
2 | import { useBEM } from '../hooks';
3 | import { menuClass, menuDividerClass } from '../utils';
4 |
5 | export const MenuDivider = memo(
6 | forwardRef(function MenuDivider({ className, ...restProps }, externalRef) {
7 | return (
8 |
14 | );
15 | })
16 | );
17 |
--------------------------------------------------------------------------------
/src/components/MenuHeader.js:
--------------------------------------------------------------------------------
1 | import { memo, forwardRef } from 'react';
2 | import { useBEM } from '../hooks';
3 | import { menuClass, menuHeaderClass, roleNone } from '../utils';
4 |
5 | export const MenuHeader = memo(
6 | forwardRef(function MenuHeader({ className, ...restProps }, externalRef) {
7 | return (
8 |
14 | );
15 | })
16 | );
17 |
--------------------------------------------------------------------------------
/example/src/styles/_theme-switch.scss:
--------------------------------------------------------------------------------
1 | @use 'var';
2 | @use 'mixins';
3 |
4 | .theme-switch {
5 | display: flex;
6 | align-items: center;
7 | font-family: 'Material Icons';
8 | cursor: pointer;
9 | color: var.$navbar-dark;
10 | border: none;
11 | @include mixins.remove-focus;
12 |
13 | &::before {
14 | content: 'nights_stay';
15 | }
16 | &:checked::before {
17 | content: 'light_mode';
18 | }
19 | &:hover {
20 | opacity: 0.75;
21 | }
22 |
23 | &--theme-dark {
24 | color: var.$navbar-light;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/styles/_var.scss:
--------------------------------------------------------------------------------
1 | @use 'sass:math';
2 |
3 | $color: #212529;
4 | $color-dark: #cad1d8;
5 | $color-disabled: #aaa;
6 | $color-disabled-dark: #666;
7 |
8 | $header-color: #888;
9 | $divider-color: rgba(0, 0, 0, 0.12);
10 | $divider-color-dark: #3a3a3a;
11 | $border-color: rgba(0, 0, 0, 0.1);
12 | $border-color-dark: #333;
13 |
14 | $background-color: #fff;
15 | $background-color-dark: #22262c;
16 | $background-color-hover: #ebebeb;
17 | $background-color-hover-dark: #31363c;
18 |
19 | $arrow-size: 0.75rem;
20 | $arrow-pos: -(math.div($arrow-size, 2));
21 |
--------------------------------------------------------------------------------
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | export { useBEM } from './useBEM';
2 | export { useClick } from './useClick';
3 | export { useCombinedRef } from './useCombinedRef';
4 | export { useHover } from './useHover';
5 | export { useLayoutEffect } from './useIsomorphicLayoutEffect';
6 | export { useItems } from './useItems';
7 | export { useItemEffect } from './useItemEffect';
8 | export { useItemState } from './useItemState';
9 | export { useMenuState } from './useMenuState';
10 | export { useMenuStateAndFocus } from './useMenuStateAndFocus';
11 | export { useMouseOver } from './useMouseOver';
12 |
--------------------------------------------------------------------------------
/dist/esm/hooks/useClick.mjs:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const useClick = (state, onToggle) => {
4 | if (process.env.NODE_ENV !== 'production' && typeof onToggle !== 'function') {
5 | throw new Error('[React-Menu] useClick/useHover requires a function in the second parameter.');
6 | }
7 | const [skipOpen] = useState({});
8 | return {
9 | onMouseDown: () => {
10 | skipOpen.v = state && state !== 'closed';
11 | },
12 | onClick: e => skipOpen.v ? skipOpen.v = false : onToggle(true, e)
13 | };
14 | };
15 |
16 | export { useClick };
17 |
--------------------------------------------------------------------------------
/example/src/styles/_hash-heading.scss:
--------------------------------------------------------------------------------
1 | @use 'mixins';
2 |
3 | #content .hash-heading {
4 | display: flex;
5 | align-items: flex-end;
6 | margin-top: -4.5rem;
7 | width: max-content;
8 |
9 | &__heading {
10 | padding: 4.5rem 0.5rem 0 0;
11 | position: relative;
12 | z-index: -1;
13 | }
14 |
15 | &__link {
16 | opacity: 0;
17 | @include mixins.transition(opacity, 0.15s);
18 | line-height: 1.2;
19 | margin-bottom: 0.25rem;
20 | &--hover,
21 | &:focus {
22 | opacity: 1;
23 | text-decoration: none;
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/hooks/useClick.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const useClick = (state, onToggle) => {
4 | if (process.env.NODE_ENV !== 'production' && typeof onToggle !== 'function') {
5 | throw new Error('[React-Menu] useClick/useHover requires a function in the second parameter.');
6 | }
7 |
8 | const [skipOpen] = useState({});
9 |
10 | return {
11 | onMouseDown: () => {
12 | skipOpen.v = state && state !== 'closed';
13 | },
14 | onClick: (e) => (skipOpen.v ? (skipOpen.v = false) : onToggle(true, e))
15 | };
16 | };
17 |
18 | export { useClick };
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/---questions-and-help.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F914 Questions and Help"
3 | about: Ask questions or request support
4 | title: ''
5 | labels: question
6 | assignees: ''
7 | ---
8 |
9 |
13 |
14 | React/React-dom version:
15 | React-Menu version:
16 |
17 |
21 |
--------------------------------------------------------------------------------
/dist/cjs/hooks/useClick.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var react = require('react');
4 |
5 | const useClick = (state, onToggle) => {
6 | if (process.env.NODE_ENV !== 'production' && typeof onToggle !== 'function') {
7 | throw new Error('[React-Menu] useClick/useHover requires a function in the second parameter.');
8 | }
9 | const [skipOpen] = react.useState({});
10 | return {
11 | onMouseDown: () => {
12 | skipOpen.v = state && state !== 'closed';
13 | },
14 | onClick: e => skipOpen.v ? skipOpen.v = false : onToggle(true, e)
15 | };
16 | };
17 |
18 | exports.useClick = useClick;
19 |
--------------------------------------------------------------------------------
/dist/esm/hooks/useMenuStateAndFocus.mjs:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useMenuState } from './useMenuState.mjs';
3 |
4 | const useMenuStateAndFocus = options => {
5 | const [menuProps, toggleMenu] = useMenuState(options);
6 | const [menuItemFocus, setMenuItemFocus] = useState();
7 | const openMenu = (position, alwaysUpdate) => {
8 | setMenuItemFocus({
9 | position,
10 | alwaysUpdate
11 | });
12 | toggleMenu(true);
13 | };
14 | return [{
15 | menuItemFocus,
16 | ...menuProps
17 | }, toggleMenu, openMenu];
18 | };
19 |
20 | export { useMenuStateAndFocus };
21 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@szhsin/react-menu-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": ".",
6 | "dependencies": {
7 | "@szhsin/react-menu": "file:..",
8 | "next": "^16.0.10",
9 | "prism-react-renderer": "^2.4.1",
10 | "react": "file:../node_modules/react",
11 | "react-dom": "file:../node_modules/react-dom",
12 | "reactish-state": "^2.0.0"
13 | },
14 | "devDependencies": {
15 | "eslint-config-next": "^16.0.10"
16 | },
17 | "scripts": {
18 | "dev": "next dev",
19 | "build": "next build",
20 | "start": "next start"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/hooks/useCombinedRef.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | // Adapted from material-ui
4 | // https://github.com/mui-org/material-ui/blob/f996027d00e7e4bff3fc040786c1706f9c6c3f82/packages/material-ui-utils/src/useForkRef.ts
5 |
6 | function setRef(ref, instance) {
7 | typeof ref === 'function' ? ref(instance) : (ref.current = instance);
8 | }
9 |
10 | export const useCombinedRef = (refA, refB) =>
11 | useMemo(() => {
12 | if (!refA) return refB;
13 | if (!refB) return refA;
14 |
15 | return (instance) => {
16 | setRef(refA, instance);
17 | setRef(refB, instance);
18 | };
19 | }, [refA, refB]);
20 |
--------------------------------------------------------------------------------
/example/src/components/PromoSpot.js:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { bem } from '../utils';
3 |
4 | const blockName = 'promo-spot';
5 |
6 | export const PromoSpot = memo(function PromoSpot({ label, title, desc, link }) {
7 | const modifier = { ...(label && { [label.toLowerCase()]: true }) };
8 | return (
9 |
15 | {title}
16 | {desc}
17 |
18 | );
19 | });
20 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { useClick, useHover, useMenuState } from './hooks';
2 | export { MenuButton } from './components/MenuButton';
3 | export { Menu } from './components/Menu';
4 | export { ControlledMenu } from './components/ControlledMenu';
5 | export { SubMenu } from './components/SubMenu';
6 | export { MenuItem } from './components/MenuItem';
7 | export { FocusableItem } from './components/FocusableItem';
8 | export { MenuDivider } from './components/MenuDivider';
9 | export { MenuHeader } from './components/MenuHeader';
10 | export { MenuGroup } from './components/MenuGroup';
11 | export { MenuRadioGroup } from './components/MenuRadioGroup';
12 |
--------------------------------------------------------------------------------
/src/hooks/useItemEffect.js:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect } from './useIsomorphicLayoutEffect';
2 |
3 | export const useItemEffect = (isDisabled, itemRef, updateItems) => {
4 | useLayoutEffect(() => {
5 | if (process.env.NODE_ENV !== 'production' && !updateItems) {
6 | throw new Error(
7 | `[React-Menu] This menu item or submenu should be rendered under a menu: ${itemRef.current.outerHTML}`
8 | );
9 | }
10 | if (isDisabled) return;
11 | const item = itemRef.current;
12 | updateItems(item, true);
13 | return () => {
14 | updateItems(item);
15 | };
16 | }, [isDisabled, itemRef, updateItems]);
17 | };
18 |
--------------------------------------------------------------------------------
/example/gh-pages.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | check_str=$(pwd | grep "/example")
6 | if [ -z "$check_str" ]; then
7 | echo "Not in /example"
8 | exit 1
9 | fi
10 |
11 | rm -Rf out/
12 | npm run build
13 |
14 | tmpdir="$HOME/gh-pages"
15 | rm -Rf "$tmpdir"
16 | mkdir "$tmpdir"
17 | mv out "$tmpdir"
18 | cd ..
19 |
20 | git checkout gh-pages
21 | check_str=$(git branch | grep "*" | grep "gh-pages")
22 | if [ -z "$check_str" ]; then
23 | echo "Not on branch gh-pages"
24 | exit 1
25 | fi
26 |
27 | rm -Rf _next
28 | cp -Rf "$tmpdir/out/" .
29 | git add .
30 | git commit -m "Updates"
31 | rm -Rf "$tmpdir"
32 | echo "Ready to push gh-pages"
33 |
--------------------------------------------------------------------------------
/dist/cjs/hooks/useMenuStateAndFocus.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var react = require('react');
4 | var useMenuState = require('./useMenuState.cjs');
5 |
6 | const useMenuStateAndFocus = options => {
7 | const [menuProps, toggleMenu] = useMenuState.useMenuState(options);
8 | const [menuItemFocus, setMenuItemFocus] = react.useState();
9 | const openMenu = (position, alwaysUpdate) => {
10 | setMenuItemFocus({
11 | position,
12 | alwaysUpdate
13 | });
14 | toggleMenu(true);
15 | };
16 | return [{
17 | menuItemFocus,
18 | ...menuProps
19 | }, toggleMenu, openMenu];
20 | };
21 |
22 | exports.useMenuStateAndFocus = useMenuStateAndFocus;
23 |
--------------------------------------------------------------------------------
/dist/esm/utils/submenuCtx.mjs:
--------------------------------------------------------------------------------
1 | const createSubmenuCtx = () => {
2 | let timer,
3 | count = 0;
4 | return {
5 | toggle: isOpen => {
6 | isOpen ? count++ : count--;
7 | count = Math.max(count, 0);
8 | },
9 | on: (closeDelay, pending, settled) => {
10 | if (count) {
11 | if (!timer) timer = setTimeout(() => {
12 | timer = 0;
13 | pending();
14 | }, closeDelay);
15 | } else {
16 | settled?.();
17 | }
18 | },
19 | off: () => {
20 | if (timer) {
21 | clearTimeout(timer);
22 | timer = 0;
23 | }
24 | }
25 | };
26 | };
27 |
28 | export { createSubmenuCtx };
29 |
--------------------------------------------------------------------------------
/src/utils/withHovering.js:
--------------------------------------------------------------------------------
1 | import { memo, forwardRef, useContext, useRef } from 'react';
2 | import { HoverItemContext } from './constants';
3 |
4 | export const withHovering = (name, WrappedComponent) => {
5 | const Component = memo(WrappedComponent);
6 | const WithHovering = forwardRef((props, ref) => {
7 | const itemRef = useRef(null);
8 | return (
9 |
15 | );
16 | });
17 |
18 | WithHovering.displayName = `WithHovering(${name})`;
19 |
20 | return WithHovering;
21 | };
22 |
--------------------------------------------------------------------------------
/dist/esm/components/MenuDivider.mjs:
--------------------------------------------------------------------------------
1 | import { memo, forwardRef } from 'react';
2 | import { jsx } from 'react/jsx-runtime';
3 | import { useBEM } from '../hooks/useBEM.mjs';
4 | import { menuClass, menuDividerClass } from '../utils/constants.mjs';
5 |
6 | const MenuDivider = /*#__PURE__*/memo(/*#__PURE__*/forwardRef(function MenuDivider({
7 | className,
8 | ...restProps
9 | }, externalRef) {
10 | return /*#__PURE__*/jsx("li", {
11 | role: "separator",
12 | ...restProps,
13 | ref: externalRef,
14 | className: useBEM({
15 | block: menuClass,
16 | element: menuDividerClass,
17 | className
18 | })
19 | });
20 | }));
21 |
22 | export { MenuDivider };
23 |
--------------------------------------------------------------------------------
/dist/esm/components/MenuHeader.mjs:
--------------------------------------------------------------------------------
1 | import { memo, forwardRef } from 'react';
2 | import { jsx } from 'react/jsx-runtime';
3 | import { useBEM } from '../hooks/useBEM.mjs';
4 | import { roleNone, menuClass, menuHeaderClass } from '../utils/constants.mjs';
5 |
6 | const MenuHeader = /*#__PURE__*/memo(/*#__PURE__*/forwardRef(function MenuHeader({
7 | className,
8 | ...restProps
9 | }, externalRef) {
10 | return /*#__PURE__*/jsx("li", {
11 | role: roleNone,
12 | ...restProps,
13 | ref: externalRef,
14 | className: useBEM({
15 | block: menuClass,
16 | element: menuHeaderClass,
17 | className
18 | })
19 | });
20 | }));
21 |
22 | export { MenuHeader };
23 |
--------------------------------------------------------------------------------
/src/utils/submenuCtx.js:
--------------------------------------------------------------------------------
1 | const createSubmenuCtx = () => {
2 | let timer,
3 | count = 0;
4 | return {
5 | toggle: (isOpen) => {
6 | isOpen ? count++ : count--;
7 | count = Math.max(count, 0);
8 | },
9 | on: (closeDelay, pending, settled) => {
10 | if (count) {
11 | if (!timer)
12 | timer = setTimeout(() => {
13 | timer = 0;
14 | pending();
15 | }, closeDelay);
16 | } else {
17 | settled?.();
18 | }
19 | },
20 | off: () => {
21 | if (timer) {
22 | clearTimeout(timer);
23 | timer = 0;
24 | }
25 | }
26 | };
27 | };
28 |
29 | export { createSubmenuCtx };
30 |
--------------------------------------------------------------------------------
/dist/esm/hooks/useItemEffect.mjs:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect as useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect.mjs';
2 |
3 | const useItemEffect = (isDisabled, itemRef, updateItems) => {
4 | useIsomorphicLayoutEffect(() => {
5 | if (process.env.NODE_ENV !== 'production' && !updateItems) {
6 | throw new Error(`[React-Menu] This menu item or submenu should be rendered under a menu: ${itemRef.current.outerHTML}`);
7 | }
8 | if (isDisabled) return;
9 | const item = itemRef.current;
10 | updateItems(item, true);
11 | return () => {
12 | updateItems(item);
13 | };
14 | }, [isDisabled, itemRef, updateItems]);
15 | };
16 |
17 | export { useItemEffect };
18 |
--------------------------------------------------------------------------------
/example/src/components/HeaderBanner.js:
--------------------------------------------------------------------------------
1 | import { useDomInfo } from '../store';
2 | import { version } from '../utils';
3 |
4 | export function HeaderBanner({ onClose }) {
5 | const isFullSize = useDomInfo().vWidth > 700;
6 |
7 | return (
8 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/example/src/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '@szhsin/react-menu/dist/index.css';
2 | import '@szhsin/react-menu/dist/theme-dark.css';
3 | import '@szhsin/react-menu/dist/transitions/zoom.css';
4 | import '../styles/index.scss';
5 | import Head from 'next/head';
6 | import App from '../components/App';
7 |
8 | function MyApp({ Component, pageProps }) {
9 | return (
10 | <>
11 |
12 | React menu library - szhsin/react-menu
13 |
14 |
15 |
16 |
17 |
18 | >
19 | );
20 | }
21 |
22 | export default MyApp;
23 |
--------------------------------------------------------------------------------
/example/src/components/PageView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TableContents } from './TableContents';
3 | import { CascadingContents } from './CascadingContents';
4 | import { RightSection } from './RightSection';
5 |
6 | export const PageView = React.memo(function PageView({ id, data, showRightSection }) {
7 | return (
8 | <>
9 | {data}
10 |
11 |
12 |
13 | {data.map((c) => (
14 |
15 | ))}
16 |
17 |
18 |
19 | {showRightSection && }
20 | >
21 | );
22 | });
23 |
--------------------------------------------------------------------------------
/example/src/styles/_prism.scss:
--------------------------------------------------------------------------------
1 | @use 'var';
2 | @use 'mixins';
3 |
4 | .prism-code {
5 | @include mixins.scrollbar(10px, height);
6 | background-color: #f2f2f3 !important;
7 | display: block;
8 | overflow-x: auto;
9 | font-size: 90%;
10 | color: inherit;
11 | border-radius: var.$border-radius-medium;
12 | padding: 1rem 1.25rem;
13 | }
14 |
15 | .szh-app--theme-dark .prism-code {
16 | @include mixins.scrollbar-dark;
17 | background-color: var.$block-dark !important;
18 | }
19 |
20 | .szh-app--theme-light .prism-code .token {
21 | &.comment {
22 | color: #028000 !important;
23 | font-style: normal !important;
24 | }
25 | &.attr-name {
26 | color: #235dbe !important;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/dist/cjs/utils/submenuCtx.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const createSubmenuCtx = () => {
4 | let timer,
5 | count = 0;
6 | return {
7 | toggle: isOpen => {
8 | isOpen ? count++ : count--;
9 | count = Math.max(count, 0);
10 | },
11 | on: (closeDelay, pending, settled) => {
12 | if (count) {
13 | if (!timer) timer = setTimeout(() => {
14 | timer = 0;
15 | pending();
16 | }, closeDelay);
17 | } else {
18 | settled?.();
19 | }
20 | },
21 | off: () => {
22 | if (timer) {
23 | clearTimeout(timer);
24 | timer = 0;
25 | }
26 | }
27 | };
28 | };
29 |
30 | exports.createSubmenuCtx = createSubmenuCtx;
31 |
--------------------------------------------------------------------------------
/dist/cjs/hooks/useItemEffect.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var useIsomorphicLayoutEffect = require('./useIsomorphicLayoutEffect.cjs');
4 |
5 | const useItemEffect = (isDisabled, itemRef, updateItems) => {
6 | useIsomorphicLayoutEffect.useLayoutEffect(() => {
7 | if (process.env.NODE_ENV !== 'production' && !updateItems) {
8 | throw new Error(`[React-Menu] This menu item or submenu should be rendered under a menu: ${itemRef.current.outerHTML}`);
9 | }
10 | if (isDisabled) return;
11 | const item = itemRef.current;
12 | updateItems(item, true);
13 | return () => {
14 | updateItems(item);
15 | };
16 | }, [isDisabled, itemRef, updateItems]);
17 | };
18 |
19 | exports.useItemEffect = useItemEffect;
20 |
--------------------------------------------------------------------------------
/src/hooks/useIsomorphicLayoutEffect.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useLayoutEffect } from 'react';
2 |
3 | // Get around a warning when using useLayoutEffect on the server.
4 | // https://github.com/reduxjs/react-redux/blob/b48d087d76f666e1c6c5a9713bbec112a1631841/src/utils/useIsomorphicLayoutEffect.js#L12
5 | // https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
6 | // https://github.com/facebook/react/issues/14927#issuecomment-549457471
7 |
8 | const useIsomorphicLayoutEffect =
9 | typeof window !== 'undefined' &&
10 | typeof window.document !== 'undefined' &&
11 | typeof window.document.createElement !== 'undefined'
12 | ? useLayoutEffect
13 | : useEffect;
14 |
15 | export { useIsomorphicLayoutEffect as useLayoutEffect };
16 |
--------------------------------------------------------------------------------
/dist/cjs/components/MenuDivider.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var react = require('react');
4 | var jsxRuntime = require('react/jsx-runtime');
5 | var useBEM = require('../hooks/useBEM.cjs');
6 | var constants = require('../utils/constants.cjs');
7 |
8 | const MenuDivider = /*#__PURE__*/react.memo(/*#__PURE__*/react.forwardRef(function MenuDivider({
9 | className,
10 | ...restProps
11 | }, externalRef) {
12 | return /*#__PURE__*/jsxRuntime.jsx("li", {
13 | role: "separator",
14 | ...restProps,
15 | ref: externalRef,
16 | className: useBEM.useBEM({
17 | block: constants.menuClass,
18 | element: constants.menuDividerClass,
19 | className
20 | })
21 | });
22 | }));
23 |
24 | exports.MenuDivider = MenuDivider;
25 |
--------------------------------------------------------------------------------
/dist/cjs/components/MenuHeader.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var react = require('react');
4 | var jsxRuntime = require('react/jsx-runtime');
5 | var useBEM = require('../hooks/useBEM.cjs');
6 | var constants = require('../utils/constants.cjs');
7 |
8 | const MenuHeader = /*#__PURE__*/react.memo(/*#__PURE__*/react.forwardRef(function MenuHeader({
9 | className,
10 | ...restProps
11 | }, externalRef) {
12 | return /*#__PURE__*/jsxRuntime.jsx("li", {
13 | role: constants.roleNone,
14 | ...restProps,
15 | ref: externalRef,
16 | className: useBEM.useBEM({
17 | block: constants.menuClass,
18 | element: constants.menuHeaderClass,
19 | className
20 | })
21 | });
22 | }));
23 |
24 | exports.MenuHeader = MenuHeader;
25 |
--------------------------------------------------------------------------------
/dist/theme-dark.css:
--------------------------------------------------------------------------------
1 | .szh-menu-container--theme-dark .szh-menu {
2 | color: #cad1d8;
3 | background-color: #22262c;
4 | border: 1px solid #333;
5 | box-shadow: 0 2px 9px 3px rgba(0, 0, 0, 0.25);
6 | }
7 | .szh-menu-container--theme-dark .szh-menu__arrow {
8 | background-color: #22262c;
9 | border-left-color: #333;
10 | border-top-color: #333;
11 | }
12 | .szh-menu-container--theme-dark .szh-menu__item--hover {
13 | background-color: #31363c;
14 | }
15 | .szh-menu-container--theme-dark .szh-menu__item--focusable {
16 | background-color: inherit;
17 | }
18 | .szh-menu-container--theme-dark .szh-menu__item--disabled {
19 | color: #666;
20 | }
21 | .szh-menu-container--theme-dark .szh-menu__divider {
22 | background-color: #3a3a3a;
23 | }
24 |
--------------------------------------------------------------------------------
/dist/esm/utils/withHovering.mjs:
--------------------------------------------------------------------------------
1 | import { forwardRef, useRef, memo, useContext } from 'react';
2 | import { HoverItemContext } from './constants.mjs';
3 | import { jsx } from 'react/jsx-runtime';
4 |
5 | const withHovering = (name, WrappedComponent) => {
6 | const Component = /*#__PURE__*/memo(WrappedComponent);
7 | const WithHovering = /*#__PURE__*/forwardRef((props, ref) => {
8 | const itemRef = useRef(null);
9 | return /*#__PURE__*/jsx(Component, {
10 | ...props,
11 | itemRef: itemRef,
12 | externalRef: ref,
13 | isHovering: useContext(HoverItemContext) === itemRef.current
14 | });
15 | });
16 | WithHovering.displayName = `WithHovering(${name})`;
17 | return WithHovering;
18 | };
19 |
20 | export { withHovering };
21 |
--------------------------------------------------------------------------------
/src/hooks/useHover.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useClick } from './useClick';
3 |
4 | const useHover = (isOpen, onToggle, { openDelay = 100, closeDelay = 300 } = {}) => {
5 | const [config] = useState({});
6 |
7 | const clearTimer = () => clearTimeout(config.t);
8 | const delayAction = (toOpen) => (e) => {
9 | clearTimer();
10 | config.t = setTimeout(() => onToggle(toOpen, e), toOpen ? openDelay : closeDelay);
11 | };
12 | const props = {
13 | onMouseEnter: delayAction(true),
14 | onMouseLeave: delayAction(false)
15 | };
16 |
17 | return {
18 | anchorProps: { ...props, ...useClick(isOpen, onToggle) },
19 | hoverProps: { ...props, onMouseEnter: clearTimer }
20 | };
21 | };
22 |
23 | export { useHover };
24 |
--------------------------------------------------------------------------------
/dist/cjs/utils/withHovering.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var react = require('react');
4 | var constants = require('./constants.cjs');
5 | var jsxRuntime = require('react/jsx-runtime');
6 |
7 | const withHovering = (name, WrappedComponent) => {
8 | const Component = /*#__PURE__*/react.memo(WrappedComponent);
9 | const WithHovering = /*#__PURE__*/react.forwardRef((props, ref) => {
10 | const itemRef = react.useRef(null);
11 | return /*#__PURE__*/jsxRuntime.jsx(Component, {
12 | ...props,
13 | itemRef: itemRef,
14 | externalRef: ref,
15 | isHovering: react.useContext(constants.HoverItemContext) === itemRef.current
16 | });
17 | });
18 | WithHovering.displayName = `WithHovering(${name})`;
19 | return WithHovering;
20 | };
21 |
22 | exports.withHovering = withHovering;
23 |
--------------------------------------------------------------------------------
/dist/esm/index.mjs:
--------------------------------------------------------------------------------
1 |
2 | 'use client';
3 | export { MenuButton } from './components/MenuButton.mjs';
4 | export { Menu } from './components/Menu.mjs';
5 | export { ControlledMenu } from './components/ControlledMenu.mjs';
6 | export { SubMenu } from './components/SubMenu.mjs';
7 | export { MenuItem } from './components/MenuItem.mjs';
8 | export { FocusableItem } from './components/FocusableItem.mjs';
9 | export { MenuDivider } from './components/MenuDivider.mjs';
10 | export { MenuHeader } from './components/MenuHeader.mjs';
11 | export { MenuGroup } from './components/MenuGroup.mjs';
12 | export { MenuRadioGroup } from './components/MenuRadioGroup.mjs';
13 | export { useClick } from './hooks/useClick.mjs';
14 | export { useHover } from './hooks/useHover.mjs';
15 | export { useMenuState } from './hooks/useMenuState.mjs';
16 |
--------------------------------------------------------------------------------
/src/styles/theme-dark.scss:
--------------------------------------------------------------------------------
1 | @use 'var';
2 |
3 | .szh-menu-container--theme-dark .szh-menu {
4 | color: var.$color-dark;
5 | background-color: var.$background-color-dark;
6 | border: 1px solid var.$border-color-dark;
7 | box-shadow: 0 2px 9px 3px rgba(0, 0, 0, 0.25);
8 |
9 | &__arrow {
10 | background-color: var.$background-color-dark;
11 | border-left-color: var.$border-color-dark;
12 | border-top-color: var.$border-color-dark;
13 | }
14 |
15 | &__item {
16 | &--hover {
17 | background-color: var.$background-color-hover-dark;
18 | }
19 | &--focusable {
20 | background-color: inherit;
21 | }
22 | &--disabled {
23 | color: var.$color-disabled-dark;
24 | }
25 | }
26 |
27 | &__divider {
28 | background-color: var.$divider-color-dark;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: 'npm'
9 | directory: '/'
10 | versioning-strategy: increase
11 | schedule:
12 | interval: 'daily'
13 | groups:
14 | all-dependencies:
15 | patterns: ['*']
16 | - package-ecosystem: 'npm'
17 | directory: '/example'
18 | versioning-strategy: increase
19 | schedule:
20 | interval: 'weekly'
21 | groups:
22 | all-dependencies:
23 | patterns: ['*']
24 |
--------------------------------------------------------------------------------
/src/components/MenuButton.js:
--------------------------------------------------------------------------------
1 | import { forwardRef, useMemo } from 'react';
2 | import { useBEM } from '../hooks';
3 | import { defineName, menuButtonClass } from '../utils';
4 |
5 | export const MenuButton = defineName(
6 | 'MenuButton',
7 | forwardRef(function MenuButton({ className, isOpen, disabled, children, ...restProps }, ref) {
8 | const modifiers = useMemo(() => ({ open: isOpen }), [isOpen]);
9 |
10 | return (
11 |
23 | );
24 | })
25 | );
26 |
--------------------------------------------------------------------------------
/dist/esm/hooks/useHover.mjs:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useClick } from './useClick.mjs';
3 |
4 | const useHover = (isOpen, onToggle, {
5 | openDelay = 100,
6 | closeDelay = 300
7 | } = {}) => {
8 | const [config] = useState({});
9 | const clearTimer = () => clearTimeout(config.t);
10 | const delayAction = toOpen => e => {
11 | clearTimer();
12 | config.t = setTimeout(() => onToggle(toOpen, e), toOpen ? openDelay : closeDelay);
13 | };
14 | const props = {
15 | onMouseEnter: delayAction(true),
16 | onMouseLeave: delayAction(false)
17 | };
18 | return {
19 | anchorProps: {
20 | ...props,
21 | ...useClick(isOpen, onToggle)
22 | },
23 | hoverProps: {
24 | ...props,
25 | onMouseEnter: clearTimer
26 | }
27 | };
28 | };
29 |
30 | export { useHover };
31 |
--------------------------------------------------------------------------------
/dist/esm/hooks/useBEM.mjs:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | const useBEM = ({
4 | block,
5 | element,
6 | modifiers,
7 | className
8 | }) => useMemo(() => {
9 | const blockElement = element ? `${block}__${element}` : block;
10 | let classString = blockElement;
11 | modifiers && Object.keys(modifiers).forEach(name => {
12 | const value = modifiers[name];
13 | if (value) classString += ` ${blockElement}--${value === true ? name : `${name}-${value}`}`;
14 | });
15 | let expandedClassName = typeof className === 'function' ? className(modifiers) : className;
16 | if (typeof expandedClassName === 'string') {
17 | expandedClassName = expandedClassName.trim();
18 | if (expandedClassName) classString += ` ${expandedClassName}`;
19 | }
20 | return classString;
21 | }, [block, element, modifiers, className]);
22 |
23 | export { useBEM };
24 |
--------------------------------------------------------------------------------
/dist/cjs/hooks/useHover.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var react = require('react');
4 | var useClick = require('./useClick.cjs');
5 |
6 | const useHover = (isOpen, onToggle, {
7 | openDelay = 100,
8 | closeDelay = 300
9 | } = {}) => {
10 | const [config] = react.useState({});
11 | const clearTimer = () => clearTimeout(config.t);
12 | const delayAction = toOpen => e => {
13 | clearTimer();
14 | config.t = setTimeout(() => onToggle(toOpen, e), toOpen ? openDelay : closeDelay);
15 | };
16 | const props = {
17 | onMouseEnter: delayAction(true),
18 | onMouseLeave: delayAction(false)
19 | };
20 | return {
21 | anchorProps: {
22 | ...props,
23 | ...useClick.useClick(isOpen, onToggle)
24 | },
25 | hoverProps: {
26 | ...props,
27 | onMouseEnter: clearTimer
28 | }
29 | };
30 | };
31 |
32 | exports.useHover = useHover;
33 |
--------------------------------------------------------------------------------
/example/src/components/CascadingContents.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { HashHeading } from './HashHeading';
3 | import { Table } from './Table';
4 |
5 | export const CascadingContents = React.memo(function CascadingContents({
6 | id,
7 | title,
8 | contents,
9 | list,
10 | level = 1,
11 | sectioning = true
12 | }) {
13 | return React.createElement(
14 | sectioning ? 'section' : React.Fragment,
15 | sectioning ? { 'aria-labelledby': id } : undefined,
16 | ,
17 | contents &&
18 | contents.map((c, i) =>
19 | c.contentType === 'table' ? (
20 |
21 | ) : (
22 | {c}
23 | )
24 | ),
25 | list && list.map((c) => )
26 | );
27 | });
28 |
--------------------------------------------------------------------------------
/dist/cjs/hooks/useBEM.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var react = require('react');
4 |
5 | const useBEM = ({
6 | block,
7 | element,
8 | modifiers,
9 | className
10 | }) => react.useMemo(() => {
11 | const blockElement = element ? `${block}__${element}` : block;
12 | let classString = blockElement;
13 | modifiers && Object.keys(modifiers).forEach(name => {
14 | const value = modifiers[name];
15 | if (value) classString += ` ${blockElement}--${value === true ? name : `${name}-${value}`}`;
16 | });
17 | let expandedClassName = typeof className === 'function' ? className(modifiers) : className;
18 | if (typeof expandedClassName === 'string') {
19 | expandedClassName = expandedClassName.trim();
20 | if (expandedClassName) classString += ` ${expandedClassName}`;
21 | }
22 | return classString;
23 | }, [block, element, modifiers, className]);
24 |
25 | exports.useBEM = useBEM;
26 |
--------------------------------------------------------------------------------
/example/src/components/TableContentsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Link from 'next/link';
4 | import { Promo } from './Promo';
5 |
6 | export const TableContentsList = React.memo(function TableContentsList({
7 | list,
8 | level = 1,
9 | maxHeight
10 | }) {
11 | const listElt = list.map((item) => {
12 | let nested = null;
13 | if (item.list) {
14 | nested = ;
15 | }
16 |
17 | return (
18 |
19 | {item.title}
20 | {nested}
21 |
22 | );
23 | });
24 |
25 | return (
26 |
27 | {listElt}
28 | {level === 1 && }
29 |
30 | );
31 | });
32 |
33 | TableContentsList.propTypes = {
34 | list: PropTypes.array.isRequired,
35 | level: PropTypes.number
36 | };
37 |
--------------------------------------------------------------------------------
/src/components/MenuRadioGroup.js:
--------------------------------------------------------------------------------
1 | import { forwardRef, useMemo } from 'react';
2 | import { useBEM } from '../hooks';
3 | import { menuClass, radioGroupClass, RadioGroupContext, roleNone } from '../utils';
4 |
5 | export const MenuRadioGroup = forwardRef(function MenuRadioGroup(
6 | { 'aria-label': ariaLabel, className, name, value, onRadioChange, ...restProps },
7 | externalRef
8 | ) {
9 | const contextValue = useMemo(
10 | () => ({ name, value, onRadioChange }),
11 | [name, value, onRadioChange]
12 | );
13 |
14 | return (
15 |
16 |
17 |
24 |
25 |
26 | );
27 | });
28 |
--------------------------------------------------------------------------------
/example/src/components/Promo.js:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { PromoSpot } from './PromoSpot';
3 |
4 | export const Promo = memo(function Promo() {
5 | return (
6 |
28 | );
29 | });
30 |
--------------------------------------------------------------------------------
/dist/esm/components/MenuButton.mjs:
--------------------------------------------------------------------------------
1 | import { forwardRef, useMemo } from 'react';
2 | import { jsx } from 'react/jsx-runtime';
3 | import { defineName } from '../utils/utils.mjs';
4 | import { useBEM } from '../hooks/useBEM.mjs';
5 | import { menuButtonClass } from '../utils/constants.mjs';
6 |
7 | const MenuButton = /*#__PURE__*/defineName('MenuButton', /*#__PURE__*/forwardRef(function MenuButton({
8 | className,
9 | isOpen,
10 | disabled,
11 | children,
12 | ...restProps
13 | }, ref) {
14 | const modifiers = useMemo(() => ({
15 | open: isOpen
16 | }), [isOpen]);
17 | return /*#__PURE__*/jsx("button", {
18 | "aria-haspopup": true,
19 | "aria-expanded": isOpen,
20 | "aria-disabled": disabled || undefined,
21 | type: "button",
22 | disabled: disabled,
23 | ...restProps,
24 | ref: ref,
25 | className: useBEM({
26 | block: menuButtonClass,
27 | modifiers,
28 | className
29 | }),
30 | children: children
31 | });
32 | }));
33 |
34 | export { MenuButton };
35 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | targets: 'defaults',
3 | assumptions: {
4 | constantReexports: true,
5 | ignoreFunctionLength: true,
6 | ignoreToPrimitiveHint: true,
7 | iterableIsArray: true,
8 | noDocumentAll: true,
9 | noIncompleteNsImportDetection: true,
10 | noNewArrows: true,
11 | objectRestNoSymbols: true,
12 | pureGetters: true,
13 | setComputedProperties: true,
14 | setSpreadProperties: true,
15 | skipForOfIteratorClosing: true
16 | },
17 | shouldPrintComment: (val) => /[@#]__PURE__/.test(val),
18 | plugins: ['pure-annotations'],
19 | presets: [
20 | [
21 | '@babel/preset-env',
22 | {
23 | bugfixes: true,
24 | include: [
25 | '@babel/plugin-transform-nullish-coalescing-operator',
26 | '@babel/plugin-transform-optional-catch-binding'
27 | ],
28 | exclude: ['@babel/plugin-transform-typeof-symbol']
29 | }
30 | ],
31 | ['@babel/preset-react', { runtime: 'automatic' }]
32 | ]
33 | };
34 |
--------------------------------------------------------------------------------
/src/hooks/useBEM.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | // Generate className following BEM methodology: http://getbem.com/naming/
4 | // Modifier value can be one of the following types: boolean, string, undefined
5 | export const useBEM = ({ block, element, modifiers, className }) =>
6 | useMemo(() => {
7 | const blockElement = element ? `${block}__${element}` : block;
8 |
9 | let classString = blockElement;
10 | modifiers &&
11 | Object.keys(modifiers).forEach((name) => {
12 | const value = modifiers[name];
13 | if (value) classString += ` ${blockElement}--${value === true ? name : `${name}-${value}`}`;
14 | });
15 |
16 | let expandedClassName = typeof className === 'function' ? className(modifiers) : className;
17 | if (typeof expandedClassName === 'string') {
18 | expandedClassName = expandedClassName.trim();
19 | if (expandedClassName) classString += ` ${expandedClassName}`;
20 | }
21 |
22 | return classString;
23 | }, [block, element, modifiers, className]);
24 |
--------------------------------------------------------------------------------
/example/src/components/Table.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const Table = React.memo(function Table({ heading, sorting, head, rows }) {
4 | if (sorting) {
5 | rows.sort(({ [sorting.key]: r1 }, { [sorting.key]: r2 }) => {
6 | if (r1 < r2) {
7 | return -1;
8 | }
9 | if (r1 > r2) {
10 | return 1;
11 | }
12 | return 0;
13 | });
14 | }
15 |
16 | return (
17 | <>
18 | {heading}
19 |
20 |
21 |
22 | {head.map(({ key, value }) => (
23 | |
24 | {value}
25 | |
26 | ))}
27 |
28 |
29 |
30 | {rows.map((row, i) => (
31 |
32 | {head.map((th, i) => (
33 | | {row[th.key]} |
34 | ))}
35 |
36 | ))}
37 |
38 |
39 | >
40 | );
41 | });
42 |
--------------------------------------------------------------------------------
/example/src/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from 'next/document';
2 | import { basePath } from '../../next.config';
3 | import { bem } from '../utils';
4 |
5 | class MyDocument extends Document {
6 | render() {
7 | return (
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | export default MyDocument;
29 |
--------------------------------------------------------------------------------
/example/src/store.js:
--------------------------------------------------------------------------------
1 | import { state, useSnapshot } from 'reactish-state';
2 | import { bem } from './utils';
3 |
4 | export const showBannerState = state(false);
5 | export const isTocOpenState = state(false);
6 | export const toastState = state(null);
7 |
8 | export const domInfoState = state({});
9 | export const useDomInfo = () => useSnapshot(domInfoState);
10 |
11 | export const themeState = state('dark');
12 | export const useTheme = () => {
13 | const theme = useSnapshot(themeState);
14 | return {
15 | isDark: theme === 'dark',
16 | theme
17 | };
18 | };
19 | themeState.subscribe(() => {
20 | const theme = themeState.get();
21 | document.body.className = bem('szh-app', null, { theme });
22 | try {
23 | localStorage.setItem('theme', theme);
24 | } catch {
25 | // continue regardless of error
26 | }
27 | });
28 | export const hydrate = () => {
29 | try {
30 | const theme = localStorage.getItem('theme');
31 | if (theme === 'light') themeState.set(theme);
32 | } catch {
33 | // continue regardless of error
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/example/src/styles/_menu.scss:
--------------------------------------------------------------------------------
1 | @use 'var';
2 | @use 'mixins';
3 |
4 | .szh-menu__item {
5 | i {
6 | font-size: 1.25rem;
7 | margin-right: 0.5rem;
8 | }
9 |
10 | img {
11 | height: 3rem;
12 | margin-left: -0.5rem;
13 | margin-right: 0.8rem;
14 | }
15 |
16 | .md-48 {
17 | font-size: 3rem;
18 | }
19 |
20 | &:active:not(.szh-menu__item--focusable):not(.szh-menu__item--disabled) {
21 | color: #fff !important;
22 | background-color: var.$primary-light;
23 | }
24 | }
25 |
26 | .szh-menu {
27 | white-space: nowrap;
28 | }
29 |
30 | .szh-menu,
31 | .szh-menu__group {
32 | @include mixins.scrollbar;
33 | @media (max-width: var.$bp-sm) {
34 | &::-webkit-scrollbar {
35 | width: 3px;
36 | }
37 | }
38 | .szh-app--theme-dark & {
39 | @include mixins.scrollbar-dark;
40 | }
41 | }
42 |
43 | .szh-menu__group {
44 | margin: 0.5rem 0;
45 | @include mixins.border(border-top);
46 | @include mixins.border(border-bottom);
47 | .szh-app--theme-dark & {
48 | border-color: var.$border-dark;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/dist/cjs/components/MenuButton.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var react = require('react');
4 | var jsxRuntime = require('react/jsx-runtime');
5 | var utils = require('../utils/utils.cjs');
6 | var useBEM = require('../hooks/useBEM.cjs');
7 | var constants = require('../utils/constants.cjs');
8 |
9 | const MenuButton = /*#__PURE__*/utils.defineName('MenuButton', /*#__PURE__*/react.forwardRef(function MenuButton({
10 | className,
11 | isOpen,
12 | disabled,
13 | children,
14 | ...restProps
15 | }, ref) {
16 | const modifiers = react.useMemo(() => ({
17 | open: isOpen
18 | }), [isOpen]);
19 | return /*#__PURE__*/jsxRuntime.jsx("button", {
20 | "aria-haspopup": true,
21 | "aria-expanded": isOpen,
22 | "aria-disabled": disabled || undefined,
23 | type: "button",
24 | disabled: disabled,
25 | ...restProps,
26 | ref: ref,
27 | className: useBEM.useBEM({
28 | block: constants.menuButtonClass,
29 | modifiers,
30 | className
31 | }),
32 | children: children
33 | });
34 | }));
35 |
36 | exports.MenuButton = MenuButton;
37 |
--------------------------------------------------------------------------------
/example/src/components/HashHeading.js:
--------------------------------------------------------------------------------
1 | import { createElement, memo, useState, useRef } from 'react';
2 | import Link from 'next/link';
3 | import { bem, useLayoutEffect } from '../utils';
4 |
5 | const blockName = 'hash-heading';
6 |
7 | export const HashHeading = memo(function HashHeading({ id, title, heading = 'h1' }) {
8 | const ref = useRef(null);
9 | const [hover, setHover] = useState(false);
10 | const [fontSize, setFontSize] = useState();
11 |
12 | useLayoutEffect(() => {
13 | setFontSize(getComputedStyle(ref.current).getPropertyValue('font-size'));
14 | }, []);
15 |
16 | return (
17 | setHover(true)}
20 | onMouseLeave={() => setHover(false)}
21 | >
22 | {createElement(
23 | heading,
24 | {
25 | id,
26 | ref,
27 | className: bem(blockName, 'heading')
28 | },
29 | title
30 | )}
31 |
32 |
33 | #
34 |
35 |
36 | );
37 | });
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Zheng Song
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/styles/transitions/slide.scss:
--------------------------------------------------------------------------------
1 | @use 'sass:list';
2 | @use 'sass:map';
3 |
4 | $menu: 'szh-menu';
5 | $duration: 0.15s;
6 | $directions: (
7 | 'left': 'X' 1,
8 | 'right': 'X' -1,
9 | 'top': 'Y' 1,
10 | 'bottom': 'Y' -1
11 | );
12 |
13 | @mixin animation($name, $dir) {
14 | .#{$menu}--state-opening.#{$menu}--dir-#{$dir} {
15 | animation: #{$menu}-show-#{$name}-#{$dir} $duration ease-out;
16 | }
17 |
18 | .#{$menu}--state-closing.#{$menu}--dir-#{$dir} {
19 | animation: #{$menu}-hide-#{$name}-#{$dir} $duration ease-in forwards;
20 | }
21 | }
22 |
23 | @mixin slide-start($axis, $sign) {
24 | opacity: 0;
25 | transform: translate#{$axis }($sign * 0.75rem);
26 | }
27 |
28 | @each $dir, $value in $directions {
29 | $axis: list.nth($value, 1);
30 | $sign: list.nth($value, 2);
31 |
32 | @keyframes #{$menu}-show-slide-#{$dir} {
33 | from {
34 | @include slide-start($axis, $sign);
35 | }
36 | }
37 |
38 | @keyframes #{$menu}-hide-slide-#{$dir} {
39 | to {
40 | @include slide-start($axis, $sign);
41 | }
42 | }
43 | }
44 |
45 | @each $dir in map.keys($directions) {
46 | @include animation('slide', $dir);
47 | }
48 |
--------------------------------------------------------------------------------
/src/hooks/useMenuState.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import { useTransitionState } from 'react-transition-state';
3 | import { MenuStateMap, getTransition } from '../utils';
4 |
5 | export const useMenuState = ({
6 | initialOpen,
7 | initialMounted,
8 | unmountOnClose,
9 | transition,
10 | transitionTimeout = 500,
11 | onMenuChange
12 | } = {}) => {
13 | const enter = getTransition(transition, 'open');
14 | const exit = getTransition(transition, 'close');
15 | const [{ status }, toggleMenu, endTransition] = useTransitionState({
16 | initialEntered: initialOpen,
17 | mountOnEnter: !initialMounted,
18 | unmountOnExit: unmountOnClose,
19 | timeout: transitionTimeout,
20 | enter,
21 | exit,
22 | onStateChange: useCallback(
23 | ({ current: { isEnter, isResolved } }) => {
24 | if (!onMenuChange || (isEnter && enter && isResolved) || (!isEnter && exit && isResolved)) {
25 | return;
26 | }
27 | onMenuChange({ open: isEnter });
28 | },
29 | [onMenuChange, enter, exit]
30 | )
31 | });
32 |
33 | return [{ state: MenuStateMap[status], endTransition }, toggleMenu];
34 | };
35 |
--------------------------------------------------------------------------------
/example/src/styles/_var.scss:
--------------------------------------------------------------------------------
1 | $border-radius-small: 0.125rem;
2 | $border-radius-medium: 0.25rem;
3 | $border-radius-large: 0.375rem;
4 |
5 | $focus-light: rgba(17, 113, 240, 0.5);
6 | $focus-dark: #69a6f8;
7 |
8 | $code-light: #ebebeb;
9 | $code-dark: #343a40;
10 |
11 | $btn-light: #fafbfc;
12 | $btn-light-lv2: #f3f4f5;
13 | $btn-light-lv3: #edeff1;
14 | $btn-dark: #22262c;
15 | $btn-dark-lv2: #2b3036;
16 |
17 | $primary-light: #007bff;
18 | $primary-light-lv2: #2767cf;
19 | $primary-light-lv3: #1d4d9b;
20 | $primary-dark: #69a6f8;
21 | $primary-dark-lv2: #3688f6;
22 |
23 | $heading-light: #505050;
24 | $heading-dark: #b0b0b0;
25 |
26 | $navbar-light: #f8f9fa;
27 | $navbar-dark: #1f2021;
28 |
29 | $border-light: #ddd;
30 | $border-dark: #333;
31 |
32 | $border-hover-light: #b1b1b1;
33 | $border-hover-dark: #858585;
34 |
35 | $secondary-light: #808080;
36 | $secondary-dark: #aaa;
37 |
38 | $background-dark: #18191a;
39 | $block-dark: #1d1e1f;
40 | $text-dark: #cad1d8;
41 |
42 | $alert-light: #fef7d6;
43 | $alert-dark: #605a3a;
44 |
45 | $highlight-color: rgba(255, 229, 100, 0.3);
46 | $mask-color: rgba(0, 0, 0, 0.5);
47 |
48 | $bp-lg: 1150px;
49 | $bp-md: 950px;
50 | $bp-sm: 600px;
51 |
--------------------------------------------------------------------------------
/dist/esm/components/MenuRadioGroup.mjs:
--------------------------------------------------------------------------------
1 | import { forwardRef, useMemo } from 'react';
2 | import { jsx } from 'react/jsx-runtime';
3 | import { useBEM } from '../hooks/useBEM.mjs';
4 | import { RadioGroupContext, roleNone, menuClass, radioGroupClass } from '../utils/constants.mjs';
5 |
6 | const MenuRadioGroup = /*#__PURE__*/forwardRef(function MenuRadioGroup({
7 | 'aria-label': ariaLabel,
8 | className,
9 | name,
10 | value,
11 | onRadioChange,
12 | ...restProps
13 | }, externalRef) {
14 | const contextValue = useMemo(() => ({
15 | name,
16 | value,
17 | onRadioChange
18 | }), [name, value, onRadioChange]);
19 | return /*#__PURE__*/jsx(RadioGroupContext.Provider, {
20 | value: contextValue,
21 | children: /*#__PURE__*/jsx("li", {
22 | role: roleNone,
23 | children: /*#__PURE__*/jsx("ul", {
24 | role: "group",
25 | "aria-label": ariaLabel || name || 'Radio group',
26 | ...restProps,
27 | ref: externalRef,
28 | className: useBEM({
29 | block: menuClass,
30 | element: radioGroupClass,
31 | className
32 | })
33 | })
34 | })
35 | });
36 | });
37 |
38 | export { MenuRadioGroup };
39 |
--------------------------------------------------------------------------------
/src/__tests__/utils.test.js:
--------------------------------------------------------------------------------
1 | import { parsePadding, getTransition } from '../utils';
2 |
3 | test('parsePadding', () => {
4 | const defaultPadding = { top: 0, right: 0, bottom: 0, left: 0 };
5 | expect(parsePadding()).toEqual(defaultPadding);
6 | expect(parsePadding('')).toEqual(defaultPadding);
7 | expect(parsePadding('abcd')).toEqual(defaultPadding);
8 | expect(parsePadding('10')).toEqual({ top: 10, right: 10, bottom: 10, left: 10 });
9 | expect(parsePadding('10 20')).toEqual({ top: 10, right: 20, bottom: 10, left: 20 });
10 | expect(parsePadding('10 20 30')).toEqual({ top: 10, right: 20, bottom: 30, left: 20 });
11 | expect(parsePadding('10 20 30 40')).toEqual({ top: 10, right: 20, bottom: 30, left: 40 });
12 | expect(parsePadding(' 10px 20 30 40px 50 ')).toEqual({
13 | top: 10,
14 | right: 20,
15 | bottom: 30,
16 | left: 40
17 | });
18 | });
19 |
20 | test('getTransition', () => {
21 | expect(getTransition(true, 'open')).toBe(true);
22 | expect(getTransition(undefined, 'open')).toBe(false);
23 | expect(getTransition({}, 'open')).toBe(false);
24 | expect(getTransition({ open: true }, 'open')).toBe(true);
25 | expect(getTransition({ open: false }, 'open')).toBe(false);
26 | });
27 |
--------------------------------------------------------------------------------
/dist/cjs/components/MenuRadioGroup.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var react = require('react');
4 | var jsxRuntime = require('react/jsx-runtime');
5 | var useBEM = require('../hooks/useBEM.cjs');
6 | var constants = require('../utils/constants.cjs');
7 |
8 | const MenuRadioGroup = /*#__PURE__*/react.forwardRef(function MenuRadioGroup({
9 | 'aria-label': ariaLabel,
10 | className,
11 | name,
12 | value,
13 | onRadioChange,
14 | ...restProps
15 | }, externalRef) {
16 | const contextValue = react.useMemo(() => ({
17 | name,
18 | value,
19 | onRadioChange
20 | }), [name, value, onRadioChange]);
21 | return /*#__PURE__*/jsxRuntime.jsx(constants.RadioGroupContext.Provider, {
22 | value: contextValue,
23 | children: /*#__PURE__*/jsxRuntime.jsx("li", {
24 | role: constants.roleNone,
25 | children: /*#__PURE__*/jsxRuntime.jsx("ul", {
26 | role: "group",
27 | "aria-label": ariaLabel || name || 'Radio group',
28 | ...restProps,
29 | ref: externalRef,
30 | className: useBEM.useBEM({
31 | block: constants.menuClass,
32 | element: constants.radioGroupClass,
33 | className
34 | })
35 | })
36 | })
37 | });
38 | });
39 |
40 | exports.MenuRadioGroup = MenuRadioGroup;
41 |
--------------------------------------------------------------------------------
/docs/FAQs.md:
--------------------------------------------------------------------------------
1 | # FAQs
2 |
3 | ## How do I server-side render a menu and its items?
4 |
5 | By default a menu and its items don't get mounted into DOM until the menu has been opened for the first time. This means if you render a menu on the server side, there won't be any HTML string output. You can set the `initialMounted` prop to render a menu and all of its descendants before being opened.
6 |
7 | ```jsx
8 |
9 | ```
10 |
11 | Or, when using `ControlledMenu` along with `useMenuState`
12 |
13 | ```js
14 | useMenuState({ initialMounted: true });
15 | ```
16 |
17 | ## How do I add icons/images to a submenu label?
18 |
19 | The `label` prop of a submenu accepts not only a plain string but any valid JSX element as well.
20 |
21 | ```jsx
22 |
25 |
26 | Edit
27 |
28 | >
29 | }
30 | />
31 | ```
32 |
33 | ## How do I set additional attributes or event handlers to a submenu label?
34 |
35 | You can use the `itemProps` to pass additional props to a submenu label.
36 |
37 | ```jsx
38 | console.log('Clicked!')
43 | }}
44 | />
45 | ```
46 |
--------------------------------------------------------------------------------
/dist/esm/hooks/useMenuState.mjs:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import { useTransitionState } from 'react-transition-state';
3 | import { getTransition } from '../utils/utils.mjs';
4 | import { MenuStateMap } from '../utils/constants.mjs';
5 |
6 | const useMenuState = ({
7 | initialOpen,
8 | initialMounted,
9 | unmountOnClose,
10 | transition,
11 | transitionTimeout = 500,
12 | onMenuChange
13 | } = {}) => {
14 | const enter = getTransition(transition, 'open');
15 | const exit = getTransition(transition, 'close');
16 | const [{
17 | status
18 | }, toggleMenu, endTransition] = useTransitionState({
19 | initialEntered: initialOpen,
20 | mountOnEnter: !initialMounted,
21 | unmountOnExit: unmountOnClose,
22 | timeout: transitionTimeout,
23 | enter,
24 | exit,
25 | onStateChange: useCallback(({
26 | current: {
27 | isEnter,
28 | isResolved
29 | }
30 | }) => {
31 | if (!onMenuChange || isEnter && enter && isResolved || !isEnter && exit && isResolved) {
32 | return;
33 | }
34 | onMenuChange({
35 | open: isEnter
36 | });
37 | }, [onMenuChange, enter, exit])
38 | });
39 | return [{
40 | state: MenuStateMap[status],
41 | endTransition
42 | }, toggleMenu];
43 | };
44 |
45 | export { useMenuState };
46 |
--------------------------------------------------------------------------------
/src/components/MenuContainer.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { useBEM } from '../hooks';
3 | import {
4 | menuContainerClass,
5 | mergeProps,
6 | safeCall,
7 | getTransition,
8 | CloseReason,
9 | Keys
10 | } from '../utils';
11 |
12 | export const MenuContainer = ({
13 | className,
14 | containerRef,
15 | containerProps,
16 | children,
17 | isOpen,
18 | theming,
19 | transition,
20 | onClose
21 | }) => {
22 | const itemTransition = getTransition(transition, 'item');
23 |
24 | const onKeyDown = ({ key }) => {
25 | switch (key) {
26 | case Keys.ESC:
27 | safeCall(onClose, { key, reason: CloseReason.CANCEL });
28 | break;
29 | }
30 | };
31 |
32 | const onBlur = (e) => {
33 | if (isOpen && !e.currentTarget.contains(e.relatedTarget)) {
34 | safeCall(onClose, { reason: CloseReason.BLUR });
35 | }
36 | };
37 |
38 | return (
39 | ({ theme: theming, itemTransition }), [theming, itemTransition]),
44 | className
45 | })}
46 | style={{ position: 'absolute', ...containerProps?.style }}
47 | ref={containerRef}
48 | >
49 | {children}
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/components/MenuGroup.js:
--------------------------------------------------------------------------------
1 | import { forwardRef, useContext, useRef, useState } from 'react';
2 | import { useBEM, useLayoutEffect, useCombinedRef } from '../hooks';
3 | import { menuClass, menuGroupClass, MenuListContext } from '../utils';
4 | import { getNormalizedClientRect } from '../positionUtils';
5 |
6 | export const MenuGroup = forwardRef(function MenuGroup(
7 | { className, style, takeOverflow, ...restProps },
8 | externalRef
9 | ) {
10 | const ref = useRef(null);
11 | const [overflowStyle, setOverflowStyle] = useState();
12 | const { overflow, overflowAmt } = useContext(MenuListContext);
13 |
14 | useLayoutEffect(() => {
15 | let maxHeight;
16 | if (takeOverflow && overflowAmt >= 0) {
17 | maxHeight = getNormalizedClientRect(ref.current).height - overflowAmt;
18 | if (maxHeight < 0) maxHeight = 0;
19 | }
20 | setOverflowStyle(maxHeight >= 0 ? { maxHeight, overflow } : undefined);
21 | }, [takeOverflow, overflow, overflowAmt]);
22 |
23 | useLayoutEffect(() => {
24 | if (overflowStyle) ref.current.scrollTop = 0;
25 | }, [overflowStyle]);
26 |
27 | return (
28 |
34 | );
35 | });
36 |
--------------------------------------------------------------------------------
/src/styles/transitions/zoom.scss:
--------------------------------------------------------------------------------
1 | @use 'sass:list';
2 | @use 'sass:string';
3 |
4 | $menu: 'szh-menu';
5 | $duration: 0.125s;
6 | $scale: 0.95;
7 |
8 | $origins: (
9 | ('left', 'start', 'right top'),
10 | ('left', 'center', 'right center'),
11 | ('left', 'end', 'right bottom'),
12 | ('right', 'start', 'left top'),
13 | ('right', 'center', 'left center'),
14 | ('right', 'end', 'left bottom'),
15 | ('top', 'start', 'left bottom'),
16 | ('top', 'center', 'center bottom'),
17 | ('top', 'end', 'right bottom'),
18 | ('bottom', 'start', 'left top'),
19 | ('bottom', 'center', 'center top'),
20 | ('bottom', 'end', 'right top')
21 | );
22 |
23 | @mixin zoom {
24 | opacity: 0.1;
25 | transform: scale($scale);
26 | }
27 |
28 | @keyframes #{$menu}-show-zoom {
29 | from {
30 | @include zoom;
31 | }
32 | }
33 |
34 | @keyframes #{$menu}-hide-zoom {
35 | to {
36 | @include zoom;
37 | }
38 | }
39 |
40 | .#{$menu}--state-opening {
41 | animation: #{$menu}-show-zoom $duration ease-out;
42 | }
43 |
44 | .#{$menu}--state-closing {
45 | animation: #{$menu}-hide-zoom $duration ease-in forwards;
46 | }
47 |
48 | @each $item in $origins {
49 | $dir: list.nth($item, 1);
50 | $align: list.nth($item, 2);
51 | $origin: list.nth($item, 3);
52 |
53 | .#{$menu}--dir-#{$dir}.#{$menu}--align-#{$align} {
54 | transform-origin: string.unquote($origin);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/dist/cjs/hooks/useMenuState.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var react = require('react');
4 | var reactTransitionState = require('react-transition-state');
5 | var utils = require('../utils/utils.cjs');
6 | var constants = require('../utils/constants.cjs');
7 |
8 | const useMenuState = ({
9 | initialOpen,
10 | initialMounted,
11 | unmountOnClose,
12 | transition,
13 | transitionTimeout = 500,
14 | onMenuChange
15 | } = {}) => {
16 | const enter = utils.getTransition(transition, 'open');
17 | const exit = utils.getTransition(transition, 'close');
18 | const [{
19 | status
20 | }, toggleMenu, endTransition] = reactTransitionState.useTransitionState({
21 | initialEntered: initialOpen,
22 | mountOnEnter: !initialMounted,
23 | unmountOnExit: unmountOnClose,
24 | timeout: transitionTimeout,
25 | enter,
26 | exit,
27 | onStateChange: react.useCallback(({
28 | current: {
29 | isEnter,
30 | isResolved
31 | }
32 | }) => {
33 | if (!onMenuChange || isEnter && enter && isResolved || !isEnter && exit && isResolved) {
34 | return;
35 | }
36 | onMenuChange({
37 | open: isEnter
38 | });
39 | }, [onMenuChange, enter, exit])
40 | });
41 | return [{
42 | state: constants.MenuStateMap[status],
43 | endTransition
44 | }, toggleMenu];
45 | };
46 |
47 | exports.useMenuState = useMenuState;
48 |
--------------------------------------------------------------------------------
/example/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useLayoutEffect } from 'react';
2 | import { useTheme } from '../store';
3 |
4 | export const version = '4.5.1';
5 | export const build = '146';
6 |
7 | export const bem = (block, element, modifiers = {}) => {
8 | let blockElement = element ? `${block}__${element}` : block;
9 | let className = blockElement;
10 | for (const name of Object.keys(modifiers)) {
11 | const value = modifiers[name];
12 | if (value) {
13 | className += ` ${blockElement}--`;
14 | className += value === true ? name : `${name}-${value}`;
15 | }
16 | }
17 |
18 | return className;
19 | };
20 |
21 | export const withPresetProps = (MenuComponent) => (props) => (
22 |
23 | );
24 |
25 | // Get around a warning when using useLayoutEffect on the server.
26 | // https://github.com/reduxjs/react-redux/blob/b48d087d76f666e1c6c5a9713bbec112a1631841/src/utils/useIsomorphicLayoutEffect.js#L12
27 | // https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
28 | // https://github.com/facebook/react/issues/14927#issuecomment-549457471
29 | const useIsomorphicLayoutEffect =
30 | typeof window !== 'undefined' &&
31 | typeof window.document !== 'undefined' &&
32 | typeof window.document.createElement !== 'undefined'
33 | ? useLayoutEffect
34 | : useEffect;
35 |
36 | export { useIsomorphicLayoutEffect as useLayoutEffect };
37 |
--------------------------------------------------------------------------------
/dist/cjs/index.cjs:
--------------------------------------------------------------------------------
1 |
2 | 'use client';
3 | 'use strict';
4 |
5 | var MenuButton = require('./components/MenuButton.cjs');
6 | var Menu = require('./components/Menu.cjs');
7 | var ControlledMenu = require('./components/ControlledMenu.cjs');
8 | var SubMenu = require('./components/SubMenu.cjs');
9 | var MenuItem = require('./components/MenuItem.cjs');
10 | var FocusableItem = require('./components/FocusableItem.cjs');
11 | var MenuDivider = require('./components/MenuDivider.cjs');
12 | var MenuHeader = require('./components/MenuHeader.cjs');
13 | var MenuGroup = require('./components/MenuGroup.cjs');
14 | var MenuRadioGroup = require('./components/MenuRadioGroup.cjs');
15 | var useClick = require('./hooks/useClick.cjs');
16 | var useHover = require('./hooks/useHover.cjs');
17 | var useMenuState = require('./hooks/useMenuState.cjs');
18 |
19 |
20 |
21 | exports.MenuButton = MenuButton.MenuButton;
22 | exports.Menu = Menu.Menu;
23 | exports.ControlledMenu = ControlledMenu.ControlledMenu;
24 | exports.SubMenu = SubMenu.SubMenu;
25 | exports.MenuItem = MenuItem.MenuItem;
26 | exports.FocusableItem = FocusableItem.FocusableItem;
27 | exports.MenuDivider = MenuDivider.MenuDivider;
28 | exports.MenuHeader = MenuHeader.MenuHeader;
29 | exports.MenuGroup = MenuGroup.MenuGroup;
30 | exports.MenuRadioGroup = MenuRadioGroup.MenuRadioGroup;
31 | exports.useClick = useClick.useClick;
32 | exports.useHover = useHover.useHover;
33 | exports.useMenuState = useMenuState.useMenuState;
34 |
--------------------------------------------------------------------------------
/src/__tests__/SSR.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment node
3 | */
4 |
5 | import { renderToString } from 'react-dom/server';
6 | import { MenuItem, Menu, MenuButton } from './entry';
7 |
8 | const getMenu = (props) => (
9 |
10 |
13 |
14 | );
15 |
16 | describe('Server rendering', () => {
17 | test('portal is not provided', () => {
18 | expect(renderToString(getMenu())).toContain(
19 | '