Socket
Socket
Sign inDemoInstall

react-aria-menubutton

Package Overview
Dependencies
34
Maintainers
1
Versions
44
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.6.0 to 0.7.0

dist-modules/ariaMenuButton.js

5

CHANGELOG.md
# Changelog
## 0.7.0
- New, more flexible API: provided components are just wrappers around whatever elements they're given.
## 0.6.0
- Pass event object as second argument to `handleSelection` callback.
- Pass `event` to `handleSelection`.

@@ -6,0 +9,0 @@ ## 0.5.2

24

dist-modules/keys.js

@@ -1,2 +0,2 @@

// Lookey here
// Look here
// https://github.com/facebook/react/blob/0.13-stable/src/browser/ui/dom/getEventKey.js

@@ -7,15 +7,9 @@

exports.__esModule = true;
var ENTER = 'Enter';
exports.ENTER = ENTER;
var SPACE = ' ';
exports.SPACE = SPACE;
var ESCAPE = 'Escape';
exports.ESCAPE = ESCAPE;
var UP = 'ArrowUp';
exports.UP = UP;
var DOWN = 'ArrowDown';
exports.DOWN = DOWN;
var LOWEST_LETTER_CODE = 65;
exports.LOWEST_LETTER_CODE = LOWEST_LETTER_CODE;
var HIGHEST_LETTER_CODE = 91;
exports.HIGHEST_LETTER_CODE = HIGHEST_LETTER_CODE;
exports['default'] = {
ENTER: 'Enter',
SPACE: ' ',
ESCAPE: 'Escape',
UP: 'ArrowUp',
DOWN: 'ArrowDown'
};
module.exports = exports['default'];

@@ -5,4 +5,2 @@ 'use strict';

var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }

@@ -18,61 +16,49 @@

var _MenuItem = require('./MenuItem');
var Menu = (function (_React$Component) {
_inherits(Menu, _React$Component);
var _MenuItem2 = _interopRequireDefault(_MenuItem);
var _cssClassnamer = require('./cssClassnamer');
var _cssClassnamer2 = _interopRequireDefault(_cssClassnamer);
var Menu = (function (_Component) {
function Menu() {
function Menu(props) {
_classCallCheck(this, Menu);
_Component.apply(this, arguments);
_React$Component.call(this, props);
props.manager.menu = this;
}
_inherits(Menu, _Component);
Menu.prototype.componentWillUpdate = function componentWillUpdate() {
var manager = this.props.manager;
Menu.prototype.shouldComponentUpdate = function shouldComponentUpdate(newProps) {
return this.props.selectedValue !== newProps.selectedValue;
if (!manager.isOpen) {
// Clear the manager's items, so they
// can be reloaded next time this menu opens
manager.menuItems = [];
}
};
Menu.prototype.componentWillMount = function componentWillMount() {
this.props.focusManager.focusables = [];
};
Menu.prototype.componentDidMount = function componentDidMount() {
if (this.props.receiveFocus) this.props.focusManager.move(0);
};
Menu.prototype.render = function render() {
var props = this.props;
var selectedValue = props.selectedValue;
var _props = this.props;
var manager = _props.manager;
var children = _props.children;
var tag = _props.tag;
var className = _props.className;
var id = _props.id;
var items = props.items.map(function (item, i) {
return _react2['default'].createElement(
'li',
{ key: i,
className: _cssClassnamer2['default'].componentPart('menuItemWrapper'),
role: 'presentation' },
_react2['default'].createElement(_MenuItem2['default'], _extends({}, item, {
focusManager: props.focusManager,
handleSelection: props.handleSelection,
isSelected: item.value === selectedValue }))
);
});
var childrenToRender = (function () {
if (typeof children === 'function') {
return children({ isOpen: manager.isOpen });
}
if (manager.isOpen) return children;
return [];
})();
var menuClasses = [_cssClassnamer2['default'].componentPart('menu')];
if (props.flushRight) menuClasses.push(_cssClassnamer2['default'].componentPart('menu--flushRight'));
return _react2['default'].createElement(
'ol',
{ className: menuClasses.join(' '),
role: 'menu' },
items
);
return _react2['default'].createElement(tag, {
className: className,
id: id,
onKeyDown: manager.handleMenuKey,
role: 'menu',
onBlur: manager.handleBlur
}, childrenToRender);
};
return Menu;
})(_react.Component);
})(_react2['default'].Component);

@@ -82,9 +68,12 @@ exports['default'] = Menu;

Menu.propTypes = {
focusManager: _react.PropTypes.object.isRequired,
items: _react.PropTypes.arrayOf(_react.PropTypes.object).isRequired,
flushRight: _react.PropTypes.bool,
handleSelection: _react.PropTypes.func,
receiveFocus: _react.PropTypes.bool,
selectedValue: _react.PropTypes.oneOfType([_react.PropTypes.string, _react.PropTypes.number, _react.PropTypes.bool])
children: _react.PropTypes.oneOfType([_react.PropTypes.func, _react.PropTypes.element]).isRequired,
manager: _react.PropTypes.object.isRequired,
id: _react.PropTypes.string,
className: _react.PropTypes.string,
tag: _react.PropTypes.string
};
Menu.defaultProps = {
tag: 'div'
};
module.exports = exports['default'];

@@ -17,68 +17,55 @@ 'use strict';

var _cssClassnamer = require('./cssClassnamer');
var _keys2 = _interopRequireDefault(_keys);
var _cssClassnamer2 = _interopRequireDefault(_cssClassnamer);
var MenuItem = (function (_React$Component) {
_inherits(MenuItem, _React$Component);
var MenuItem = (function (_Component) {
function MenuItem() {
_classCallCheck(this, MenuItem);
_Component.apply(this, arguments);
_React$Component.apply(this, arguments);
}
_inherits(MenuItem, _Component);
MenuItem.prototype.shouldComponentUpdate = function shouldComponentUpdate(newProps) {
return this.props.isSelected !== newProps.isSelected;
MenuItem.prototype.componentDidMount = function componentDidMount() {
var props = this.props;
this.managedIndex = props.manager.menuItems.push({
node: _react2['default'].findDOMNode(this),
content: props.children,
text: props.text
}) - 1;
};
MenuItem.prototype.componentDidMount = function componentDidMount() {
this.props.focusManager.focusables.push({
content: this.props.content,
text: this.props.text,
node: _react2['default'].findDOMNode(this)
});
MenuItem.prototype.handleKeyDown = function handleKeyDown(event) {
if (event.key !== _keys2['default'].ENTER && event.key !== _keys2['default'].SPACE) return;
event.preventDefault();
this.selectItem(event);
};
MenuItem.prototype.handleClick = function handleClick(e) {
MenuItem.prototype.selectItem = function selectItem(event) {
var props = this.props;
if (props.isSelected) return;
// If there's no value, we'll send the label
var v = typeof props.value !== 'undefined' ? props.value : props.content;
props.handleSelection(v, e);
// If there's no value, we'll send the child
var value = typeof props.value !== 'undefined' ? props.value : props.children;
props.manager.handleSelection(value, event);
props.manager.currentFocus = this.managedIndex;
};
MenuItem.prototype.handleKey = function handleKey(e) {
if (e.key !== _keys.ENTER && e.key !== _keys.SPACE) return;
e.preventDefault();
this.handleClick(e);
};
MenuItem.prototype.render = function render() {
var props = this.props;
var itemClasses = [_cssClassnamer2['default'].componentPart('menuItem')];
if (props.isSelected) itemClasses.push(_cssClassnamer2['default'].applyNamespace('is-selected'));
var _props = this.props;
var tag = _props.tag;
var children = _props.children;
var className = _props.className;
var id = _props.id;
// tabindex -1 because: "With focus on the button pressing
// the Tab key will take the user to the next tab focusable item on the page.
// With focus on the drop-down menu, pressing the Tab key will take the user
// to the next tab focusable item on the page."
// "A menuitem within a menu or menubar may appear in the tab order
// only if it is not within a popup menu."
// ... so not in tab order, but programatically focusable
return _react2['default'].createElement(
'div',
{ id: props.id,
className: itemClasses.join(' '),
onClick: this.handleClick.bind(this),
onKeyDown: this.handleKey.bind(this),
role: 'menuitem',
tabIndex: '-1',
'data-value': props.value },
props.content
);
return _react2['default'].createElement(tag, {
className: className,
id: id,
onClick: this.selectItem.bind(this),
onKeyDown: this.handleKeyDown.bind(this),
role: 'menuitem',
tabIndex: '-1'
}, children);
};
return MenuItem;
})(_react.Component);
})(_react2['default'].Component);

@@ -88,10 +75,14 @@ exports['default'] = MenuItem;

MenuItem.propTypes = {
focusManager: _react.PropTypes.object.isRequired,
handleSelection: _react.PropTypes.func.isRequired,
content: _react.PropTypes.oneOfType([_react.PropTypes.string, _react.PropTypes.element]).isRequired,
children: _react.PropTypes.oneOfType([_react.PropTypes.element, _react.PropTypes.string, _react.PropTypes.arrayOf(_react.PropTypes.element)]).isRequired,
manager: _react.PropTypes.object.isRequired,
className: _react.PropTypes.string,
id: _react.PropTypes.string,
isSelected: _react.PropTypes.bool,
tag: _react.PropTypes.string,
text: _react.PropTypes.string,
value: _react.PropTypes.oneOfType([_react.PropTypes.string, _react.PropTypes.number, _react.PropTypes.bool])
value: _react.PropTypes.oneOfType([_react.PropTypes.bool, _react.PropTypes.number, _react.PropTypes.string])
};
MenuItem.defaultProps = {
tag: 'div'
};
module.exports = exports['default'];

@@ -1,1 +0,1 @@

module.exports = require('./dist-modules/createAriaMenuButton.js');
module.exports = require('./dist-modules/ariaMenuButton.js');
{
"name": "react-aria-menubutton",
"version": "0.6.0",
"version": "0.7.0",
"description": "A fully accessible, easily themeable, React-powered menu button",
"main": "index.js",
"scripts": {
"lint": "eslint .",
"minify": "uglifyjs ./dist/ariaMenuButton.js -c -m -o ./dist/ariaMenuButton.min.js",
"demo-bundle": "browserify demo/js/demo.js -t babelify --extension=.jsx -o demo/demo-bundle.js",
"demo-watch": "watchify demo/js/demo.js -t babelify --extension=.jsx -o demo/demo-bundle.js -v",
"demo-bs": "browser-sync start --server demo --files=\"demo/**/*.css,demo/index.html,demo/demo-bundle.js\"",
"demo-dev": "parallelshell \"npm run demo-watch\" \"npm run demo-bs\"",
"test-bundle": "browserify test -t babelify -o test/test-bundle.js --extension=.jsx",
"test-dev": "watchify test -t babelify -o test/test-bundle.js -v --extension=.jsx",
"test-single": "karma start --single-run",
"pretest": "npm run lint",
"test": "npm run test-bundle && npm run test-single",
"prebuild-bundle": "trash dist/* --force",
"build-bundle": " browserify src/ariaMenuButton.jsx -p bundle-collapser/plugin -x react -t [ babelify --loose ] --standalone ariaMenuButton -o dist/ariaMenuButton.js",
"prebuild-modules": "trash dist-modules/* --force",
"build-modules": "babel src --loose --out-dir dist-modules",
"build": "npm run build-bundle && parallelshell \"npm run build-modules\" \"npm run minify\"",
"prepublish": "npm run build"

@@ -37,38 +53,20 @@ },

"devDependencies": {
"babel": "5.5.8",
"babel-loader": "5.1.4",
"css-loader": "0.14.5",
"es5-shim": "4.1.6",
"eslint": "0.23.0",
"eslint-plugin-react": "2.5.2",
"imports-loader": "0.6.4",
"karma": "0.12.36",
"karma-cli": "0.0.4",
"babel": "5.6.23",
"babelify": "6.1.3",
"browser-sync": "2.7.13",
"browserify": "11.0.0",
"bundle-collapser": "1.2.0",
"es5-shim": "4.1.9",
"eslint": "0.24.1",
"eslint-plugin-react": "2.7.1",
"karma": "0.13.2",
"karma-phantomjs-launcher": "0.2.0",
"karma-tap": "1.0.3",
"parallelshell": "1.1.1",
"sinon": "git://github.com/cjohansen/Sinon.JS#b672042043517b9f84e14ed0fb8265126168778a",
"parallelshell": "1.2.0",
"sinon": "1.15.4",
"tape": "4.0.0",
"trash": "1.4.1",
"uglify-js": "2.4.23",
"watchify": "3.2.2",
"webpack": "1.9.11",
"webpack-dev-server": "1.9.0"
},
"scripts": {
"lint": "eslint .",
"build-modules": "trash dist-modules/* --force && babel src --loose --out-dir dist-modules",
"minify": "uglifyjs ./dist/createAriaMenuButton.js -c -m -o ./dist/createAriaMenuButton.min.js",
"bundle-demo": "webpack --config webpack.config.demo.js",
"watch-demo": "webpack --config webpack.config.demo.js --watch",
"demo-dev": "webpack-dev-server --config webpack.config.demo.js --watch",
"watch-karma": "karma start",
"bundle-test": "webpack --config webpack.config.test.js",
"watch-test": "webpack --config webpack.config.test.js --watch",
"test-dev": "parallelshell 'npm run watch-test' 'npm run watch-karma'",
"test-single": "npm run bundle-test && karma start --single-run",
"test": "npm run lint && npm run test-single",
"bundle-dist": "webpack --config webpack.config.dist.js",
"build": "trash dist/* --force && npm run bundle-dist && parallelshell 'npm run build-modules' 'npm run minify'"
"watchify": "3.3.0"
}
}
# react-aria-menubutton [![Build Status](https://travis-ci.org/davidtheclark/react-aria-menubutton.svg?branch=master)](https://travis-ci.org/davidtheclark/react-aria-menubutton)
A menu button that
---
- is a React component;
- follows [the WAI-ARIA Menu Button Design Pattern](http://www.w3.org/TR/wai-aria-practices/#menubutton) for maximal accessibility (including *ARIA attributes* and *keyboard interaction*);
- uses [SUIT CSS conventions](https://github.com/suitcss/suit/blob/master/doc/README.md) for maximal themeability;
- is very thoroughly tested (have a look in `test/`);
- is flexible & customizable enough that it's worth passing around.
**v0.7.0 provides a new, better, more flexible API.** It shouldn't be too hard to upgrade.
**Please check out [the demo](http://davidtheclark.github.io/react-aria-menubutton/).**
---
## Accessibility
A React component that helps you build accessible menu buttons, by providing keyboard interactions and ARIA attributes aligned with [the WAI-ARIA Menu Button Design Pattern](http://www.w3.org/TR/wai-aria-practices/#menubutton).
The primary goal of this project is to build a React component that follows every detail of [the WAI-ARIA Menu Button Design Pattern](http://www.w3.org/TR/wai-aria-practices/#menubutton).
This is kind of hard (because of keyboard interaction and focus management), so I wanted to share what I'd learned and (hopefully) learn more from others.
Follow the link and read about the keyboard interactions and ARIA attributes. Quotations from this spec are scattered in comments throughout the source code, so it's clear which code addresses which requirements.
Please check out [the demo](http://davidtheclark.github.io/react-aria-menubutton/).
The [demo](http://davidtheclark.github.io/react-aria-menubutton/) also lists all of the interactions that are built in.
## Project Goal
### Accessibility
The project started as an effort to build a React component that follows every detail of [the WAI-ARIA Menu Button Design Pattern](http://www.w3.org/TR/wai-aria-practices/#menubutton) for **maximum accessibility**.
Just hiding and showing a menu is easy; but the required **keyboard interactions** are kind of tricky, and the required **ARIA attributes** are easy to forget.
So I decided to try to abstract the component enough that it would be **worth sharing with others**.
Follow [the link](http://www.w3.org/TR/wai-aria-practices/#menubutton) and read about the keyboard interactions and ARIA attributes. [The demo](http://davidtheclark.github.io/react-aria-menubutton/) also lists all of the interactions that are built in.
*If you think that this component does not satisfy the spec or if you know of other ways to make it even more accessible, please file an issue.*
### Flexibility
Instead of providing a pre-fabricated, fully styled component, this module's goal is to provide a component that others can build their own stuff on top of.
The first draft of the API (<0.7.0) tried to achieve this flexibility by following [SUIT CSS conventions](https://github.com/suitcss/suit/blob/master/doc/README.md) and allowing users to customize the class names to their liking. It also had a weird, non-optimal way to allow users to take advantage of [React's CSSTransitionGroup](https://facebook.github.io/react/docs/animation.html).
The current API is more flexible: it **does not provide any classes**, only **provides smart components to wrap *your* components**. These components provide the keyboard interaction and ARIA attributes, while your components do whatever you want your components to do.
## Installation

@@ -33,23 +44,14 @@

There are two ways to use this module:
There are two ways to consume this module:
- with CommonJS
- as a global UMD library
Either way, what is exposed is the function `createAriaMenuButton([options])`, which returns the component you want, tailored with your options.
Using CommonJS, for example, you can simply `require()` the module to get the function `ariaMenuButton([options])`, which:
Using CommonJS, for example, you can simply `require()` the module to get the factory:
```js
var createAriaMenuButton = require('react-aria-menubutton');
var ariaMenuButton = require('react-aria-menubutton');
var AriaMenuButton = createAriaMenuButton();
React.render(
<AriaMenuButton id='myMenuButton'
handleSelection={mySelectionHandler}
items={myItems}
triggerContent='Click me'
closeOnSelection={true} />,
document.getElementById('container')
);
var myAmb = ariaMenuButton({
onSelection: mySelectionHandler
});
```

@@ -61,158 +63,305 @@

For example:
```html
<script src="react.min.js"></script>
<script src="node_modules/react-aria-menu-button/dist/createAriaMenuButton.min.js"></script>
<script src="node_modules/react-aria-menu-button/dist/ariaMenuButton.min.js"></script>
<script>
var AriaMenuButton = createAriaMenuButton();
// ...
var myAmb = ariaMenuButton({
onSelection: mySelectionHandler
});
</script>
```
## Styling
**You *get to* (have to) write your own CSS, your own way!**
It is not a goal of this module to share some new neat dropdown style.
This module's focus is on functionality — especially accessibility.
However, in order for this thing to look like a menu button, it will need to be styled like a menu button.
Therefore, I'm trying to *give you, the user, the means to write your own styles*, rather than forcing you into anything.
### ariaMenuButton([options])
Towards that end, the elements of `AriaMenuButton` are marked with classes that follow [SUIT CSS conventions](https://github.com/suitcss/suit/blob/master/doc/README.md); so the whole thing is very easily themeable.
Returns an object with three components: `Button`, `Menu`, and `MenuItem`. Each of these is documented below.
Within `css/`, there is a `base.css` stylesheet that provides some minimal rule sets that can get you started.
There are also a few ready-made themes that match the styles of popular frameworks.
All of them are on display in the [demo](http://davidtheclark.github.io/react-aria-menubutton/), so have a look.
```js
var myAmb = ariaMenuButton({
onSelection: mySelectionHandler
});
var MyAmbButton = myAmb.Button;
var MyAmbMenu = myAmb.Menu;
var MyAmbMenuItem = myAmb.MenuItem;
```
The following classes are used:
#### options
```css
.AriaMenuButton {}
.AriaMenuButton-trigger {}
.AriaMenuButton-trigger.is-open {}
.AriaMenuButton-menuWrapper {}
.AriaMenuButton-menuWrapper--transition {}
.AriaMenuButton-menu {}
.AriaMenuButton-menu--flushRight {}
.AriaMenuButton-menuItemWrapper {}
.AriaMenuButton-menuItem {}
.AriaMenuButton-menuItem.is-selected {}
##### onSelection
Type: `Function` *Required*
A callback to run when the user makes a selection (i.e. clicks or presses Enter or Space on a `MenuItem`). It will be passed the value of the selected `MenuItem` and the React SyntheticEvent.
```js
var myAmb = ariaMenuButton({
onSelection: function(value, event) {
event.stopPropagation;
console.log(value);
}
});
```
### Customizing class names
##### closeOnSelection
You can customize these class names in SUIT-compliant ways by passing `componentName` and `namespace` options to `createAriaMenuButton([options]).
Type: `Boolean` Default: `true`
#### Specify your own component name
If `false`, the menu will *not* automatically close when a selection is made. (By default, it *does* automatically close.)
This will replace `AriaMenuButton` in the class name with the component name of your choice. For example, if you pass `{ componentName: 'Dropdown' }`, your classes will be
## Examples
```css
.Dropdown {}
.Dropdown-trigger {}
.Dropdown-trigger.is-open {}
.Dropdown-menuWrapper {}
/* ... and so on */
```
For details about why the examples work, read the component API documentation below.
#### Specify your own namespace
You can see more examples by looking in `demo/`.
This will add you namespace to the front of every class name, including the state classes. For example, if you pass `{ namespace: 'up' }`, you classes will be
```js
// Very simple ES6 example
```css
.up-AriaMenuButton {}
.up-AriaMenuButton-trigger {}
.up-AriaMenuButton-trigger.up-is-open {}
.up-AriaMenuButton-menuWrapper {}
```
import React from 'react';
import ariaMenuButton from 'react-aria-menubutton';
#### Specify both component name and namespace
const menuItemWords = ['foo', 'bar', 'baz'];
And you can, of course, specify both a component name and a namespace. Passing `{ componentName: 'Down', namespace: 'lo' }`, you'll get
class MyMenuButton extends React.Component {
componentWillMount() {
this.amb = ariaMenuButton({
onSelection: handleSelection
});
}
render() {
const { Button, Menu, MenuItem } = this.amb;
```css
.lo-Down {}
.lo-Down-trigger {}
.lo-Down-trigger.lo-is-open {}
.lo-Down-menuWrapper {}
const menuItems = menuItemWords.map((word, i) => {
return (
<li key={i}>
<MenuItem className='MyMenuButton-menuItem'>
{word}
</MenuItem>
</li>
);
});
return (
<div className='MyMenuButton'>
<Button className='MyMenuButton-button'>
click me
</Button>
<Menu className='MyMenuButton-menu'>
<ul>{menuItems}</ul>
</Menu>
</div>
);
}
}
function handleSelection(value, event) { .. }
```
## API
```js
// Slightly more complex, ES5 example:
// - MenuItems have hidden values that are passed
// to the selection handler
// - User can navigate the MenuItems by typing the
// first letter of a person's name, even though
// each MenuItem's child is not simple text
// - Menu has a function for a child
// - React's CSSTransitionGroup is used for open-close animation
If you `require()` this module with CommonJS, you will receive the the function `createAriaMenuButton()`.
var React = require('react/addons');
var ariaMenuButton = require('react-aria-menubutton');
var CSSTransitionGroup = React.addons.CSSTransitionGroup;
If you are not using CommonJS, the same function will be globally exposed.
var people = [{
name: 'Charles Choo-Choo',
id: 1242
}, {
name: 'Mina Meowmers',
id: 8372
}, {
name: 'Susan Sailor',
id: 2435
}];
### createAriaMenuButton([options])
var MyMenuButton = React.createClass({
componentWillMount: function() {
this.amb = ariaMenuButton({
onSelection: handleSelection
});
},
Returns a React component, an `AriaMenuButton`, as described below.
render: function() {
var MyButton = this.amb.Button;
var MyMenu = this.amb.Menu;
var MyMenuItem = this.amb.MenuItem;
```js
/* little example */
var React = require('react/addons');
var createAriaMenuButton = require('react-aria-menubutton');
var MySpecialButton = createAriaMenuButton({
componentName: 'MySpecialButton',
namespace: 'me',
transition: React.addons.CSSTransitionGroup
var peopleMenuItems = people.map(function(person, i) {
return (
<MyMenuItem
key={i}
tag='li'
value={person.id}
text={person.name}
className='PeopleMenu-person'
>
<div className='PeopleMenu-personPhoto'>
<img src={'/people/pictures/' + person.id + '.jpg'}/ >
</div>
<div className='PeopleMenu-personName'>
{person.name}
</div>
</MyMenuItem>
);
});
var peopleMenuInnards = function(menuState) {
var menu = (!menuState.isOpen) ? false : (
<div
className='PeopleMenu-menu'
key='menu'
>
{peopleMenuItems}
</div>
);
return (
<CSSTransitionGroup transitionName='people'>
{menu}
</CSSTransitionGroup>
);
};
return (
<div className='PeopleMenu'>
<MyButton className='PeopleMenu-trigger'>
<span className='PeopleMenu-triggerText'>
Select a person
</span>
<span className='PeopleMenu-triggerIcon' />
</MyButton>
<MyMenu>
{peopleMenuInnards}
</MyMenu>
</div>
);
}
});
function handleSelection(value, event) { .. }
```
#### options
## Component API
- **componentName**: Specify a component name for css classes. [See above](#specify-your-own-component-name).
- **namespace**: Specify a namespace for css classes. [See above](#specify-your-own-namespace).
- **transition**: If you want to animate the opening & closing of the menu, pass in [React's CSSTransitionGroup](https://facebook.github.io/react/docs/animation.html) here. (See example above.) Make sure you read React's docs on the component and setup your CSS to properly work with it.
### `Button`
### AriaMenuButton
A React component to wrap the content of your menu-button-pattern's button.
A React component that takes the following props:
A `Button`'s child can be a string or a React element.
#### handleSelection: Function, required
#### props
A callback to run when the user makes a selection (i.e. clicks or presses Enter or Space on a menu item).
*All props are optional.*
It will be passed the value of the selected menu item (as defined in the `items` prop) as the first argument, and the React SyntheticEvent that triggered the selection as the second argument.
##### tag
While the menu's open/closed-state is handled internally, the selection-state is up to you: do with it what you will.
Type: `String` Default: `'span'`
#### items: Array of Objects, required
The HTML tag for this element.
Each item has the following properties:
- **content: String|ReactElement, required** — The content to be rendered inside the menu item. A simple string or a super fancy React element: it's up to you.
- **id: String, optional** — An id for the element (maybe useful for testing).
- **text: String, optional** — If `content` is an element, include a `text` property to be used when letter keys are pressed. For example, if your item essentially says "horse" but the `content` prop is a complicated element with icons and sub-elements and whatnot, provide `test: 'horse'` and the component will know that when the user types "h" this item should be focused.
- **value: String|Number|Boolean, optional** — The value to be passed to the `handleSelection()` function when this item is selected. If no `value` is provided, the item's `content` will be passed to the selection handler.
##### id
#### triggerContent: String|ReactElement, required
Type: `String`
The content to be displayed in the menu button — the "trigger".
This is inserted directly into the button via JSX, so it can be a string or a React element with mind-blowing innards, whatever you need.
An id value.
#### closeOnSelection: Boolean, optional
##### className
If `true`, the menu will close when a selection is made.
Type: `String`
#### flushRight: Boolean, optional
A class value.
If `true`, the menu will receive the modifier class
`AriaMenuButton-menu--flushRight`.
### Menu
All of the provided styles (in `css/`) by default position the menu flush with the left side of the button.
But if the button were at the *right* edge of the screen, you'd want the menu flushed with its right side, instead, so that the menu didn't extend offscreen to the right.
That's why we have this option.
A React component to wrap the content of your menu-button-pattern's menu.
#### id: String, optional
A `Menu`'s child may be one of the following:
A base id for the component.
This will be used to generate ids for all clickable elements besides menu items (which each get their own id).
The ids might be useful for testing.
- a React element, which will mount when the menu is open and unmount when the menu closes
- a function accepting the following menu-state object
#### startOpen: Boolean, optional
```js
{
isOpen: Boolean // whether or not the menu is open
}
```
If `true`, the menu will be start open when `AriaMenuButton` mounts.
This is useful for testing, but probably not useful for you (unless you are contributing).
#### props
#### selectedValue: String|Number|Boolean, optional
*All props are optional.*
The currently selected value.
The item that has this value will receive the state class `is-selected`, which CSS can use for special standout styling.
##### tag
Type: `String` Default: `'span'`
The HTML tag for this element.
##### id
Type: `String`
An id value.
##### className
Type: `String`
A class value.
### MenuItem
A React component to wrap the content of one of your menu-button-pattern's menu items.
When a `MenuItem` is *selected* (by clicking or focusing and hitting Enter or Space), it calls the `onSelection` handler you passed `ariaMenuButton` when creating this set of components.
It passes that handler a *value* and the *event*. The value it passes is determined as follows:
- If the `MenuItem` has a `value` prop, that is passed.
- If the `MenuItem` has no `value` prop, the component's child is passed (so it better be simple text!).
When the menu is open and the user hits a letter key, focus moves to the next `MenuItem` whose "text" starts with that letter. The `MenuItem`'s relevant "text" is determined as follows:
- If the `MenuItem` has a `text` prop, that is used.
- If the `MenuItem` has no `text` prop, the component's child is used (so it better be simple text!).
#### props
*All props are optional.*
##### text
Type: `String` *Required if child is an element*
If `text` has a value, its first letter will be the letter a user can type to navigate to that item.
##### value
Type: `String|Boolean|Number` *Required if child is an element*
If `value` has a value, it will be passed to the `onSelection` handler when the `MenuItem` is selected.
##### tag
Type: `String` Default: `'span'`
The HTML tag for this element.
##### id
Type: `String`
An id value.
##### className
Type: `String`
A class value.

@@ -1,10 +0,10 @@

// Lookey here
// Look here
// https://github.com/facebook/react/blob/0.13-stable/src/browser/ui/dom/getEventKey.js
export const ENTER = 'Enter';
export const SPACE = ' ';
export const ESCAPE = 'Escape';
export const UP = 'ArrowUp';
export const DOWN = 'ArrowDown';
export const LOWEST_LETTER_CODE = 65;
export const HIGHEST_LETTER_CODE = 91;
export default {
ENTER: 'Enter',
SPACE: ' ',
ESCAPE: 'Escape',
UP: 'ArrowUp',
DOWN: 'ArrowDown',
};

@@ -1,45 +0,36 @@

import React, { Component, PropTypes } from 'react';
import MenuItem from './MenuItem';
import cssClassnamer from './cssClassnamer';
import React, { PropTypes } from 'react';
export default class Menu extends Component {
shouldComponentUpdate(newProps) {
return this.props.selectedValue !== newProps.selectedValue;
export default class Menu extends React.Component {
constructor(props) {
super(props);
props.manager.menu = this;
}
componentWillMount() {
this.props.focusManager.focusables = [];
componentWillUpdate() {
const { manager } = this.props;
if (!manager.isOpen) {
// Clear the manager's items, so they
// can be reloaded next time this menu opens
manager.menuItems = [];
}
}
componentDidMount() {
if (this.props.receiveFocus) this.props.focusManager.move(0);
}
render() {
const props = this.props;
const selectedValue = props.selectedValue;
const { manager, children, tag, className, id } = this.props;
const items = props.items.map((item, i) => {
return (
<li key={i}
className={cssClassnamer.componentPart('menuItemWrapper')}
role='presentation'>
<MenuItem {...item}
focusManager={props.focusManager}
handleSelection={props.handleSelection}
isSelected={item.value === selectedValue} />
</li>
);
});
const childrenToRender = (() => {
if (typeof children === 'function') {
return children({ isOpen: manager.isOpen });
}
if (manager.isOpen) return children;
return [];
})();
const menuClasses = [cssClassnamer.componentPart('menu')];
if (props.flushRight) menuClasses.push(cssClassnamer.componentPart('menu--flushRight'));
return (
<ol className={menuClasses.join(' ')}
role='menu'>
{items}
</ol>
);
return React.createElement(tag, {
className,
id,
onKeyDown: manager.handleMenuKey,
role: 'menu',
onBlur: manager.handleBlur,
}, childrenToRender);
}

@@ -49,8 +40,11 @@ }

Menu.propTypes = {
focusManager: PropTypes.object.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
flushRight: PropTypes.bool,
handleSelection: PropTypes.func,
receiveFocus: PropTypes.bool,
selectedValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
children: PropTypes.oneOfType([PropTypes.func, PropTypes.element]).isRequired,
manager: PropTypes.object.isRequired,
id: PropTypes.string,
className: PropTypes.string,
tag: PropTypes.string,
};
Menu.defaultProps = {
tag: 'div',
};

@@ -1,56 +0,41 @@

import React, { PropTypes, Component } from 'react';
import { ENTER, SPACE } from './keys';
import cssClassnamer from './cssClassnamer';
import React, { PropTypes } from 'react';
import keys from './keys';
export default class MenuItem extends Component {
shouldComponentUpdate(newProps) {
return this.props.isSelected !== newProps.isSelected;
export default class MenuItem extends React.Component {
componentDidMount() {
const props = this.props;
this.managedIndex = props.manager.menuItems.push({
node: React.findDOMNode(this),
content: props.children,
text: props.text,
}) - 1;
}
componentDidMount() {
this.props.focusManager.focusables.push({
content: this.props.content,
text: this.props.text,
node: React.findDOMNode(this)
});
handleKeyDown(event) {
if (event.key !== keys.ENTER && event.key !== keys.SPACE) return;
event.preventDefault();
this.selectItem(event);
}
handleClick(e) {
selectItem(event) {
const props = this.props;
if (props.isSelected) return;
// If there's no value, we'll send the label
const v = (typeof props.value !== 'undefined') ? props.value : props.content;
props.handleSelection(v, e);
// If there's no value, we'll send the child
const value = (typeof props.value !== 'undefined')
? props.value
: props.children;
props.manager.handleSelection(value, event);
props.manager.currentFocus = this.managedIndex;
}
handleKey(e) {
if (e.key !== ENTER && e.key !== SPACE) return;
e.preventDefault();
this.handleClick(e);
}
render() {
const props = this.props;
const itemClasses = [cssClassnamer.componentPart('menuItem')];
if (props.isSelected) itemClasses.push(cssClassnamer.applyNamespace('is-selected'));
const { tag, children, className, id } = this.props;
// tabindex -1 because: "With focus on the button pressing
// the Tab key will take the user to the next tab focusable item on the page.
// With focus on the drop-down menu, pressing the Tab key will take the user
// to the next tab focusable item on the page."
// "A menuitem within a menu or menubar may appear in the tab order
// only if it is not within a popup menu."
// ... so not in tab order, but programatically focusable
return (
<div id={props.id}
className={itemClasses.join(' ')}
onClick={this.handleClick.bind(this)}
onKeyDown={this.handleKey.bind(this)}
role='menuitem'
tabIndex='-1'
data-value={props.value}>
{props.content}
</div>
);
return React.createElement(tag, {
className,
id,
onClick: this.selectItem.bind(this),
onKeyDown: this.handleKeyDown.bind(this),
role: 'menuitem',
tabIndex: '-1',
}, children);
}

@@ -60,9 +45,21 @@ }

MenuItem.propTypes = {
focusManager: PropTypes.object.isRequired,
handleSelection: PropTypes.func.isRequired,
content: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.string,
PropTypes.arrayOf(PropTypes.element),
]).isRequired,
manager: PropTypes.object.isRequired,
className: PropTypes.string,
id: PropTypes.string,
isSelected: PropTypes.bool,
tag: PropTypes.string,
text: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
value: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.number,
PropTypes.string,
]),
};
MenuItem.defaultProps = {
tag: 'div',
};
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc