New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@puppet/react-components

Package Overview
Dependencies
Maintainers
5
Versions
134
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@puppet/react-components - npm Package Compare versions

Comparing version 5.28.0 to 5.29.0

test/option-menu-list/OptionMenuList.js

4

package.json
{
"name": "@puppet/react-components",
"version": "5.28.0",
"version": "5.29.0",
"author": "Puppet, Inc.",

@@ -96,3 +96,3 @@ "license": "Apache-2.0",

},
"gitHead": "c0dfba39b0bdfd9da896c924e94c5c250c8830a7"
"gitHead": "679f3c013b3d114d20d84324c510fc75cb58d619"
}
import PropTypes from 'prop-types';
import Icon from '../library/icon';

@@ -84,1 +85,14 @@ /**

};
export const optionMenuItemShape = {
/** Value of the option */
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/** The label to show */
label: PropTypes.node.isRequired,
/** Optional icon associated with this option */
icon: PropTypes.oneOf(Icon.AVAILABLE_ICONS),
/** Optional custom icon associated with this option */
svg: PropTypes.element,
/** Whether this option is disabled */
disabled: PropTypes.bool,
};

@@ -6,2 +6,3 @@ import React, { Component } from 'react';

import { isNil, focus, cancelEvent } from '../../helpers/statics';
import { optionMenuItemShape } from '../../helpers/customPropTypes';

@@ -19,3 +20,2 @@ import {

import OptionMenuListItem from './OptionMenuListItem';
import Icon from '../../library/icon';

@@ -28,9 +28,9 @@ const propTypes = {

options: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
label: PropTypes.node.isRequired,
icon: PropTypes.oneOf(Icon.AVAILABLE_ICONS),
disabled: PropTypes.bool,
}),
PropTypes.oneOfType([
PropTypes.shape(optionMenuItemShape),
PropTypes.shape({
...optionMenuItemShape,
value: PropTypes.arrayOf(PropTypes.shape(optionMenuItemShape)),
}),
]),
),

@@ -78,9 +78,17 @@ selected: PropTypes.oneOfType([

const getFocusableOptions = options =>
options.map(opt => (Array.isArray(opt.value) ? opt.value : opt)).flat();
const getFocusedId = (focusedIndex, id, options) =>
isNil(focusedIndex) || focusedIndex >= options.length
typeof focusedIndex !== 'number' || focusedIndex >= options.length
? undefined
: getOptionId(id, options[focusedIndex].value);
: getOptionId(
id,
getFocusableOptions(options)[Math.max(focusedIndex, 0)].value,
);
const getSelectionSet = selection =>
new Set(Array.isArray(selection) ? selection : [selection]);
new Set(
(Array.isArray(selection) ? selection : [selection]).filter(el => !!el),
);

@@ -139,3 +147,7 @@ class OptionMenuList extends Component {

onMouseEnterItem(focusedIndex) {
this.focusItem(focusedIndex);
if (typeof focusedIndex === 'number') {
this.focusItem(focusedIndex);
} else {
this.setState({ focusedIndex: null });
}
}

@@ -167,3 +179,5 @@

} else {
this.focusItem(Math.min(options.length - 1, focusedIndex + 1));
this.focusItem(
Math.min(getFocusableOptions(options).length - 1, focusedIndex + 1),
);
}

@@ -173,3 +187,4 @@ }

onKeyDown(e) {
const { onEscape, onClickItem } = this.props;
const { onEscape, onClickItem, options } = this.props;
const { focusedIndex } = this.state;

@@ -199,4 +214,7 @@ switch (e.keyCode) {

case ENTER_KEY_CODE: {
this.selectFocusedItem();
onClickItem();
const focused = getFocusableOptions(options)[focusedIndex];
if (focused && !focused.disabled) {
this.selectFocusedItem();
onClickItem();
}
cancelEvent(e);

@@ -256,3 +274,3 @@ break;

this.focusItem(options.length - 1);
this.focusItem(getFocusableOptions(options).length - 1);
}

@@ -302,3 +320,3 @@

if (!isNil(focusedIndex)) {
const { value } = options[focusedIndex];
const { value } = getFocusableOptions(options)[focusedIndex];

@@ -312,13 +330,2 @@ this.select(value);

const {
onClickItem,
onMouseEnterItem,
onCancel,
onKeyDown,
onKeyDownInAction,
onFocus,
onMenuBlur,
onActionBlur,
} = this;
const { focusedIndex } = this.state;
const {
id,

@@ -337,3 +344,2 @@ options,

onBlur,
focusedIndex: focussed,
onFocusItem,

@@ -349,5 +355,92 @@ footer,

const {
onClickItem,
onMouseEnterItem,
onCancel,
onKeyDown,
onKeyDownInAction,
onFocus,
onMenuBlur,
onActionBlur,
} = this;
const selectionSet = getSelectionSet(selected);
const focusedId = getFocusedId(focusedIndex, id, options);
delete rest.focusedIndex;
const { focusedIndex } = this.state;
const focusedId = getFocusedId(
focusedIndex,
id,
getFocusableOptions(options),
);
const renderListItems = (items, offset = 0) => {
const list = [];
items.forEach(item => {
if (Array.isArray(item.value)) {
const groupId = `group-${item.value
.map(child => child.value)
.join('-')}`;
const labelId = `${groupId}-label`;
list.push(
<ul
role="group"
aria-labelledby={labelId}
className="rc-menu-list-group"
id={groupId}
key={groupId}
>
{item.label && (
<OptionMenuListItem
type="heading"
disabled={item.disabled}
id={labelId}
key={labelId}
onMouseEnter={() => onMouseEnterItem(null)}
>
{item.label}
</OptionMenuListItem>
)}
{renderListItems(
item.value.map(child =>
Object.assign(child, {
disabled: item.disabled || child.disabled,
}),
),
list.length + offset,
)}
</ul>,
);
// eslint-disable-next-line no-param-reassign
offset += item.value.length - 1;
} else {
const index = list.length + offset;
list.push(
<OptionMenuListItem
id={getOptionId(id, item.value)}
key={item.value}
focused={index === focusedIndex}
selected={selectionSet.has(item.value)}
icon={item.icon}
svg={item.svg}
disabled={item.disabled}
onClick={() =>
item.disabled ? undefined : onClickItem(item.value)
}
onMouseEnter={() => onMouseEnterItem(index)}
ref={option => {
this.optionRefs[index] = option;
}}
>
{item.label}
</OptionMenuListItem>,
);
}
});
return list;
};
const list = (

@@ -369,20 +462,3 @@ <ul

>
{options.map(({ value, label, icon, svg, disabled }, index) => (
<OptionMenuListItem
id={getOptionId(id, value)}
key={value}
focused={index === focusedIndex}
selected={selectionSet.has(value)}
icon={icon}
svg={svg}
disabled={disabled}
onClick={disabled ? undefined : () => onClickItem(value)}
onMouseEnter={() => onMouseEnterItem(index)}
ref={option => {
this.optionRefs[index] = option;
}}
>
{label}
</OptionMenuListItem>
))}
{renderListItems(options)}
</ul>

@@ -389,0 +465,0 @@ );

@@ -7,7 +7,12 @@ import React, { forwardRef } from 'react';

const types = {
OPTION: 'option',
HEADING: 'heading',
};
const propTypes = {
id: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
focused: PropTypes.bool.isRequired,
selected: PropTypes.bool.isRequired,
focused: PropTypes.bool,
selected: PropTypes.bool,
/** Optional: choose an icon */

@@ -17,5 +22,6 @@ icon: PropTypes.oneOf(Icon.AVAILABLE_ICONS),

svg: PropTypes.element,
onClick: PropTypes.func.isRequired,
onMouseEnter: PropTypes.func.isRequired,
onClick: PropTypes.func,
onMouseEnter: PropTypes.func,
disabled: PropTypes.bool,
type: PropTypes.oneOf(Object.values(types)),
};

@@ -26,3 +32,8 @@

svg: null,
focused: false,
disabled: false,
selected: false,
onClick: null,
onMouseEnter: null,
type: types.OPTION,
};

@@ -43,30 +54,48 @@

disabled,
type,
},
ref,
) => (
<li
role="option"
id={id}
className={classNames('rc-menu-list-item', {
'rc-menu-list-item-focused': focused,
'rc-menu-list-item-selected': selected,
'rc-menu-list-item-disabled': disabled,
})}
aria-selected={selected}
onClick={onClick}
onMouseEnter={onMouseEnter}
ref={ref}
>
{icon && <Icon className="rc-menu-list-item-icon" type={icon} />}
{svg && !icon && <Icon className="rc-menu-list-item-icon" svg={svg} />}
<span className="rc-menu-list-item-content">{children}</span>
{selected && (
<Icon
className="rc-menu-list-item-checkmark"
type="check"
size="small"
/>
)}
</li>
),
) => {
const isHeading = type === types.HEADING;
const itemProps = {
id,
ref,
onMouseEnter,
onClick,
role: isHeading ? 'presentation' : 'option',
};
if (!isHeading) {
itemProps['aria-selected'] = selected;
}
return (
<li
{...itemProps}
className={classNames(
'rc-menu-list-item',
{
'rc-menu-list-item-disabled': disabled,
},
isHeading
? 'rc-menu-list-group-heading'
: {
'rc-menu-list-item-focused': focused,
'rc-menu-list-item-selected': selected,
},
)}
>
{icon && <Icon className="rc-menu-list-item-icon" type={icon} />}
{svg && !icon && <Icon className="rc-menu-list-item-icon" svg={svg} />}
<span className="rc-menu-list-item-content">{children}</span>
{!isHeading && selected && (
<Icon
className="rc-menu-list-item-checkmark"
type="check"
size="small"
/>
)}
</li>
);
},
);

@@ -73,0 +102,0 @@ /* eslint-enable */

@@ -5,4 +5,6 @@ import React, { Component } from 'react';

import OptionMenuList from '../../internal/option-menu-list';
import { anchorOrientation } from '../../helpers/customPropTypes';
import Icon from '../icon';
import {
anchorOrientation,
optionMenuItemShape,
} from '../../helpers/customPropTypes';
import Input from '../input';

@@ -27,12 +29,9 @@ import SelectTarget from './SelectTarget';

options: PropTypes.arrayOf(
PropTypes.shape({
/** Select option value */
value: PropTypes.string.isRequired,
/** Select option label */
label: PropTypes.string.isRequired,
/** Optional icon associated with this option */
icon: PropTypes.oneOf(Icon.AVAILABLE_ICONS),
/** Optional custom icon associated with this option */
svg: PropTypes.element,
}),
PropTypes.oneOfType([
PropTypes.shape(optionMenuItemShape),
PropTypes.shape({
...optionMenuItemShape,
value: PropTypes.arrayOf(PropTypes.shape(optionMenuItemShape)),
}),
]),
),

@@ -135,4 +134,4 @@ /** Currently selected value or values */

static getDerivedStateFromProps(props, state) {
if (isControlled(props) || !state.open) {
static getDerivedStateFromProps(props) {
if (isControlled(props)) {
return {

@@ -289,3 +288,3 @@ listValue: props.value,

getButtonLabel() {
const { type, options, value, placeholder } = this.props;
const { type, value, placeholder } = this.props;
if (!value || value.length === 0) {

@@ -296,3 +295,3 @@ return placeholder;

if (type === MULTISELECT) {
const selectedOptions = options
const selectedOptions = this.getOptions()
.filter(option => value.includes(option.value))

@@ -304,3 +303,5 @@ .map(option => option.selectedLabel || option.label);

const selectedOption = options.find(option => option.value === value);
const selectedOption = this.getOptions().find(
option => option.value === value,
);

@@ -312,3 +313,5 @@ return selectedOption.label;

const { options, value, type, onFilter } = this.props;
let filteredOptions = options;
let opts = options
.map(opt => (Array.isArray(opt.value) ? opt.value : opt))
.flat();

@@ -318,3 +321,3 @@ // If the ingesting app uses the onFilter event handler, it should provide the filtered options

if (value && type === AUTOCOMPLETE && !onFilter) {
filteredOptions = options.filter(
opts = opts.filter(
option => option.value.toLowerCase().indexOf(value.toLowerCase()) > -1,

@@ -324,3 +327,3 @@ );

return filteredOptions;
return opts;
}

@@ -368,3 +371,2 @@

onFocusItem,
getOptions,
open: onOpen,

@@ -374,13 +376,14 @@ } = this;

const {
name,
type,
applyImmediately,
className,
disabled,
className,
style,
error,
value,
footer,
name,
options,
placeholder,
applyImmediately,
required,
footer,
style,
type,
value,
} = this.props;

@@ -471,3 +474,3 @@

showCancel={type === MULTISELECT && !applyImmediately}
options={getOptions()}
options={options}
selected={listValue}

@@ -474,0 +477,0 @@ focusedIndex={focusedIndex}

@@ -84,2 +84,41 @@ ## Overview

### Option groups
To render an option group, provide an array of child options as the value for a regular option. Parent options should
still have labels, and if a parent is disabled, all its child options will be disabled, too.
```jsx
const optionsWithGroups = [{
label: "Spices",
value: [
{label: "Cinnamon", value: "cinnamon"},
{label: "Coriander", value: "coriander"},
{label: "Cumin", value: "cumin"},
]
}, {
label: "Oil",
value: "oil"
}, {
label: "Vinegar",
value: "vinegar"
}, {
label: "Herbs",
disabled: true,
value: [
{label: "Parsley", value: "parsley"},
{label: "Sage", value: "sage"},
{label: "Rosemary", value: "rosemary"},
]
}];
<Select
name="select-option-group-example"
options={optionsWithGroups}
value={state.value}
onChange={value => {
setState({value});
}}
/>;
```
### MultiSelect

@@ -86,0 +125,0 @@

@@ -1,6 +0,11 @@

import chai, { expect } from 'chai';
import '../setup';
import { expect } from 'chai';
import sinon from 'sinon';
import { shallow, mount } from 'enzyme';
import React from 'react';
import React, { useState } from 'react';
import Input from '../../source/react/library/input/Input';
import Select from '../../source/react/library/select/Select';
import SelectTarget from '../../source/react/library/select/SelectTarget';
import OptionMenuList from '../../source/react/internal/option-menu-list';
import OptionMenuListItem from '../../source/react/internal/option-menu-list/OptionMenuListItem';

@@ -17,4 +22,35 @@ const options = [

{ value: 'raspberry', label: 'raspberry' },
{
label: 'herbs',
value: [
{ label: 'basil', value: 'basil' },
{ label: 'parsley', value: 'parsley' },
{ label: 'thyme', value: 'thyme' },
],
},
];
// eslint-disable-next-line react/prop-types
const ExampleSelect = ({ value: initialValue, ...props }) => {
const [value, setValue] = useState(initialValue || null);
return (
<Select
name="test"
value={value}
onChange={val => {
setValue(val);
}}
open
{...props}
/>
);
};
const getChoiceByText = (wrapper, name) =>
wrapper
.find(OptionMenuListItem)
.filterWhere(n => n.text() === name)
.first();
describe('<Select />', () => {

@@ -65,2 +101,128 @@ it('should render without falling over', () => {

});
it('starts with the listbox closed and shows it when the target button is clicked', () => {
const wrapper = mount(<Select name="test" options={options} />);
expect(wrapper.find(Select).state('open')).to.equal(false);
const menu = wrapper.find(OptionMenuList);
expect(menu.prop('aria-labelledby')).to.equal('test-label');
expect(menu.prop('role')).to.equal('listbox');
const target = wrapper.find(SelectTarget);
expect(target.prop('aria-controls')).to.equal('test-menu');
expect(target.prop('aria-haspopup')).to.equal('listbox');
expect(target.prop('aria-expanded')).to.equal(false);
target.find('button').simulate('click');
expect(wrapper.find(SelectTarget).prop('aria-expanded')).to.equal(true);
expect(wrapper.find(Select).state('open')).to.equal(true);
});
it('selects an option on click', () => {
const wrapper = mount(<ExampleSelect name="test" options={options} open />);
getChoiceByText(wrapper, 'kiwi').simulate('click');
expect(wrapper.find(Select).state('open')).to.equal(false);
expect(wrapper.find(OptionMenuList).prop('selected')).to.equal('kiwi');
expect(wrapper.find('input').prop('value')).to.equal('kiwi');
expect(wrapper.find(SelectTarget).text()).to.equal('kiwi');
wrapper.find(SelectTarget).simulate('click');
getChoiceByText(wrapper, 'parsley').simulate('click');
expect(wrapper.find(OptionMenuList).prop('selected')).to.equal('parsley');
expect(wrapper.find('input').prop('value')).to.equal('parsley');
expect(wrapper.find(SelectTarget).text()).to.equal('parsley');
});
it('does _not_ deselect the option on second click', () => {
const wrapper = mount(<ExampleSelect name="test" options={options} open />);
getChoiceByText(wrapper, 'kiwi').simulate('click');
wrapper.find(SelectTarget).simulate('click');
getChoiceByText(wrapper, 'kiwi').simulate('click');
expect(wrapper.find(OptionMenuList).prop('selected')).to.equal('kiwi');
expect(wrapper.find('input').prop('value')).to.equal('kiwi');
expect(wrapper.find(SelectTarget).text()).to.equal('kiwi');
});
it('displays option groups with headings correctly in the rendered listbox', () => {
const wrapper = mount(<Select name="test" options={options} />);
const list = wrapper.find('ul ul');
const headingItem = list.find(OptionMenuListItem).first();
expect(headingItem.prop('type')).to.equal('heading');
});
describe('with multiselect', () => {
const getApplyButton = wrapper =>
wrapper
.find('button')
.filterWhere(n => n.text() === 'Apply')
.first();
it('allows selection of multiple items on click + Apply', () => {
const wrapper = mount(
<ExampleSelect name="test" options={options} type="multiselect" open />,
);
const selections = ['banana', 'parsley', 'kiwi', 'thyme'];
selections.forEach(item => {
getChoiceByText(wrapper, item).simulate('click');
});
getApplyButton(wrapper).simulate('click');
expect(wrapper.find(OptionMenuList).prop('selected')).to.eql(selections);
expect(wrapper.find('input').prop('value')).to.eql(selections);
expect(wrapper.find(SelectTarget).text()).to.equal(
selections.sort().join(', '),
);
});
it('deselects a selected item on second click + Apply', () => {
const wrapper = mount(
<ExampleSelect name="test" options={options} type="multiselect" open />,
);
const selections = ['banana', 'parsley', 'kiwi', 'thyme'];
selections.forEach(choice => {
getChoiceByText(wrapper, choice).simulate('click');
});
getApplyButton(wrapper).simulate('click');
wrapper.find(SelectTarget).simulate('click');
[selections.shift(), selections.shift()].forEach(choice => {
getChoiceByText(wrapper, choice).simulate('click');
});
getApplyButton(wrapper).simulate('click');
expect(wrapper.find(OptionMenuList).prop('selected')).to.eql(selections);
expect(wrapper.find(SelectTarget).text()).to.equal(
selections.sort().join(', '),
);
expect(
wrapper
.find('input')
.prop('value')
.sort(),
).to.eql(selections.sort());
});
it('applies selections without requiring the Apply button when applyImmediately is set', () => {
const wrapper = mount(
<ExampleSelect
name="test"
options={options}
type="multiselect"
applyImmediately
open
/>,
);
getChoiceByText(wrapper, 'kiwi').simulate('click');
expect(wrapper.find(OptionMenuList).prop('selected')).to.eql(['kiwi']);
expect(wrapper.find(SelectTarget).text()).to.equal('kiwi');
expect(wrapper.find('input').prop('value')).to.eql(['kiwi']);
});
});
});

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

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

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc