Skip to content

Commit

Permalink
Merge pull request #220 from fkhadra/next
Browse files Browse the repository at this point in the history
v6.0.0
  • Loading branch information
fkhadra committed Nov 13, 2022
2 parents 42e7d20 + 1d1ded7 commit 58a34bf
Show file tree
Hide file tree
Showing 53 changed files with 3,634 additions and 12,102 deletions.
8 changes: 3 additions & 5 deletions .github/workflows/build.yaml
Expand Up @@ -11,18 +11,16 @@ jobs:
- name: Install node
uses: actions/setup-node@v1
with:
node-version: '14.x'
node-version: '16.x'
- name: Install example dependencies
run: cd example && yarn
- name: Install dependencies & build
run: yarn
- name: Lint
run: yarn lint
- name: Run cypress
uses: cypress-io/github-action@v2
uses: cypress-io/github-action@v4
with:
browser: chrome
start: yarn start:example
start: yarn start
wait-on: 'http://localhost:1234'
- uses: actions/upload-artifact@v1
if: failure()
Expand Down
48 changes: 38 additions & 10 deletions README.md
Expand Up @@ -8,6 +8,22 @@

</div>

## Features

- Easy to set up for real, you can make it work in less than 10sec!
- Super easy to customize thanks to css variables 💅
- Custom position
- Sub menu support
- Does not go offscreen
- Dark mode 🌒
- Keyboard navigation + keyboard shortcut!
- Built-in animations
- Easy to test!
- Written in Typescript 💪
- Tiny! (3k gzipped)

Check the documentation for more!


## Documentation

Expand All @@ -30,9 +46,8 @@ $ npm install --save react-contexify
## The gist

```js
import React from 'react';
import { Menu, Item, Separator, Submenu, MenuProvider, useContextMenu } from 'react-contexify';
import 'react-contexify/dist/ReactContexify.css';
import { Menu, Item, Separator, Submenu, useContextMenu } from 'react-contexify';
import 'react-contexify/ReactContexify.css';

const MENU_ID = 'blahblah';

Expand All @@ -42,27 +57,40 @@ function App() {
});

function handleContextMenu(event){
event.preventDefault();
show(event, {
show({
event,
props: {
key: 'value'
}
})
}
const handleItemClick = ({ event, props }) => console.log(event,props);

// I'm using a single event handler for all items
// but you don't have too :)
const handleItemClick = ({ id, event, props }) => {
switch (id) {
case "copy":
console.log(event, props)
break;
case "cut";
console.log(event, props);
break;
//etc...
}
}

return (
<div>
<p onContextMenu={handleContextMenu}>lorem ipsum blabladhasi blaghs blah</p>
<Menu id={MENU_ID}>
<Item onClick={handleItemClick}>Item 1</Item>
<Item onClick={handleItemClick}>Item 2</Item>
<Item id="copy" onClick={handleItemClick}>Copy</Item>
<Item id="cut" onClick={handleItemClick}>Cut</Item>
<Separator />
<Item disabled>Disabled</Item>
<Separator />
<Submenu label="Foobar">
<Item onClick={handleItemClick}>Sub Item 1</Item>
<Item onClick={handleItemClick}>Sub Item 2</Item>
<Item id="reload" onClick={handleItemClick}>Reload</Item>
<Item id="something" onClick={handleItemClick}>Do something else</Item>
</Submenu>
</Menu>
</div>
Expand Down
14 changes: 14 additions & 0 deletions cypress.config.ts
@@ -0,0 +1,14 @@
import { defineConfig } from 'cypress'

export default defineConfig({
projectId: 'fwfrp4',
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
return require('./cypress/plugins/index.js')(on, config)
},
baseUrl: 'http://localhost:1234',
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
},
})
5 changes: 0 additions & 5 deletions cypress.json

This file was deleted.

@@ -1,10 +1,11 @@
import { DATA_TEST } from '../../example/constants';
import { STYLE, theme, animation } from '../../src/constants';
import { CssClass } from '../../src/constants';
import { animation, theme } from '../fixtures/constant';

const builtInAnimationClasses = Object.keys(animation).map(k => ({
name: k,
enter: `${STYLE.animationWillEnter}${animation[k]}`,
exit: `${STYLE.animationWillLeave}${animation[k]}`,
enter: `${CssClass.animationWillEnter}${animation[k]}`,
exit: `${CssClass.animationWillLeave}${animation[k]}`,
}));

describe('Context menu', () => {
Expand All @@ -13,7 +14,7 @@ describe('Context menu', () => {
});

it('Should not be mounted by default', () => {
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.be.visible');
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.exist');
});

it('Display the context menu', () => {
Expand All @@ -25,22 +26,22 @@ describe('Context menu', () => {
cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).rightclick();
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('be.visible');
cy.get('body').type('{esc}');
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.be.visible');
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.exist');
});

it('Close on Enter', () => {
cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).rightclick();
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('be.visible');
cy.get('body').type('{enter}');
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.be.visible');
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.exist');
});

it('Close on window resize', () => {
cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).rightclick();
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('be.visible');

cy.viewport(123, 456);
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.be.visible');
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.exist');
});

it('Prevent from rendering outside of the viewport if possible', () => {
Expand All @@ -53,13 +54,13 @@ describe('Context menu', () => {
it('Can change trigger event', () => {
cy.getByDataTest(DATA_TEST.EVENT_SELECTOR).select('onClick');
cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).rightclick();
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.be.visible');
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.exist');

cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).click();
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('be.visible');

cy.getByDataTest(DATA_TEST.EVENT_SELECTOR).select('onDoubleClick');
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.be.visible');
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.exist');

cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).dblclick();
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('be.visible');
Expand All @@ -71,7 +72,7 @@ describe('Context menu', () => {
// no theme selected
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should(
'have.class',
`${STYLE.theme}none`
`${CssClass.theme}none`
);

cy.getByDataTest(DATA_TEST.THEME_SELECTOR).select(theme.light);
Expand All @@ -80,7 +81,7 @@ describe('Context menu', () => {

cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should(
'have.class',
`${STYLE.theme}${theme.light}`
`${CssClass.theme}${theme.light}`
);

cy.getByDataTest(DATA_TEST.THEME_SELECTOR).select(theme.dark);
Expand All @@ -89,7 +90,7 @@ describe('Context menu', () => {

cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should(
'have.class',
`${STYLE.theme}${theme.dark}`
`${CssClass.theme}${theme.dark}`
);
});

Expand Down Expand Up @@ -118,22 +119,17 @@ describe('Context menu', () => {

it('Can disable animation', () => {
cy.getByDataTest(DATA_TEST.ANIMATION_SELECTOR).select('none');
cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).rightclick();

builtInAnimationClasses.forEach(builtInAnimation => {
cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).rightclick();
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should(
'not.have.class',
builtInAnimation.enter
);
});

cy.get('body').type('{esc}');
cy.get('body').type('{esc}');

builtInAnimationClasses.forEach(builtInAnimation => {
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should(
'not.have.class',
builtInAnimation.exit
);
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.exist');
});
});

Expand Down Expand Up @@ -177,10 +173,7 @@ describe('Context menu', () => {
// close the menu
cy.get('body').type('{esc}');

cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should(
'not.have.class',
builtInAnimation.exit
);
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.exist');

// wait for exit animation to complete
cy.wait(500);
Expand Down Expand Up @@ -224,6 +217,6 @@ describe('Context menu', () => {
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('be.visible');

cy.getByDataTest(DATA_TEST.MENU_FIRST_ITEM).click();
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.be.visible');
cy.getByDataTest(DATA_TEST.CONTEXT_MENU).should('not.exist');
});
});
4 changes: 2 additions & 2 deletions cypress/integration/item.spec.ts → cypress/e2e/item.spec.ts
Expand Up @@ -33,8 +33,8 @@ describe('Menu item', () => {
cy.getByDataTest(DATA_TEST.TOGGLE_HIDE_ITEMS).check();
showContextMenu();

cy.getByDataTest(DATA_TEST.MENU_FIRST_ITEM).should('not.be.visible');
cy.getByDataTest(DATA_TEST.MENU_SECOND_ITEM).should('not.be.visible');
cy.getByDataTest(DATA_TEST.MENU_FIRST_ITEM).should('not.exist');
cy.getByDataTest(DATA_TEST.MENU_SECOND_ITEM).should('not.exist');
});

it('Should pass payload when clicking on an Item', () => {
Expand Down
92 changes: 92 additions & 0 deletions cypress/e2e/keyboard.spec.ts
@@ -0,0 +1,92 @@
import { DATA_TEST } from '../../example/constants';

describe('Context menu keyboard interaction', () => {
beforeEach(() => {
cy.visit('/');
});

it('Select first menu item when pressing arrow down', () => {
cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).rightclick();
cy.get('body').type('{downarrow}');

cy.focused().should('have.attr', 'data-test', DATA_TEST.MENU_FIRST_ITEM);
});

it('Select the last item when pressing arrow up', () => {
cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).rightclick();
cy.get('body').type('{uparrow}');

cy.focused().should('have.attr', 'data-test', DATA_TEST.MENU_LAST_ITEM);
});

it('Should not select disabled items', () => {
cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).rightclick();

//🙅‍♀️ go to item 4 and skip disabled items
for (let i = 0; i < 4; i++) {
cy.get('body').type('{downarrow}');
cy.focused().should('have.attr', 'aria-disabled', 'false');
}
});

describe('Open submenu and focus first item', () => {
for (const key of ['{enter}', ' ', '{rightarrow}']) {
it(`When ${key} is pressed`, () => {
cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).rightclick();

//🙅‍♀️ go to submenu
cy.get('body').type('{uparrow}');
cy.get('body').type('{uparrow}');

cy.focused().should('have.attr', 'data-test', DATA_TEST.SUBMENU);

cy.getByDataTest(DATA_TEST.SUBMENU_FIRST_ITEM).should('not.be.visible');
cy.get('body').type(key);

// wait for transition
cy.wait(500);
cy.focused().should(
'have.attr',
'data-test',
DATA_TEST.SUBMENU_FIRST_ITEM
);
});
}
});

it('Close submenu on left arrow press', () => {
cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).rightclick();

//🙅‍♀️ go to submenu
cy.get('body').type('{uparrow}');
cy.get('body').type('{uparrow}');

cy.get('body').type('{rightarrow}');

// wait for transition
cy.wait(500);
cy.focused().should('have.attr', 'data-test', DATA_TEST.SUBMENU_FIRST_ITEM);

cy.get('body').type('{leftarrow}');
cy.focused().should('have.attr', 'data-test', DATA_TEST.SUBMENU);
});

it('Should handle keyboard shortcut', () => {
cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).rightclick();

cy.get('body').type('{ctrl}{u}');
cy.wait(500);
cy.getByDataTest(DATA_TEST.KDB_SHORTCUT).then((el) => {
expect(el.text()).eq('ctrl+u');
});

// nested shortcut
cy.getByDataTest(DATA_TEST.CONTEXT_MENU_TRIGGER).rightclick();

cy.get('body').type('{ctrl}{s}');
cy.wait(500);
cy.getByDataTest(DATA_TEST.KDB_SHORTCUT).then((el) => {
expect(el.text()).eq('ctrl+s');
});
});
});
11 changes: 11 additions & 0 deletions cypress/fixtures/constant.ts
@@ -0,0 +1,11 @@
export const animation = {
fade: 'fade',
flip: 'flip',
scale: 'scale',
slide: 'slide',
};

export const theme = {
light: 'light',
dark: 'dark',
};

0 comments on commit 58a34bf

Please sign in to comment.