@salesforce/design-system-react
Version:
Salesforce Lightning Design System for React
234 lines (222 loc) • 6.55 kB
JSX
/* Copyright (c) 2015-present, salesforce.com, inc. All rights reserved */
/* Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license */
/* eslint-disable jsx-a11y/interactive-supports-focus */
import React from 'react';
import PropTypes from 'prop-types';
import isEqual from 'lodash.isequal';
import classNames from 'classnames';
import { shape } from 'airbnb-prop-types';
import Icon from '../../icon';
const propTypes = {
/*
* Active descendant in menu
*/
activeOption: PropTypes.object,
/*
* Index of active descendant in menu
*/
activeOptionIndex: PropTypes.number,
/**
* CSS classes to be added to container `div` tag. Uses `classNames` [API](https://github.com/JedWatson/classnames).
*/
className: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
PropTypes.string
]),
/**
* CSS classes to be added to tag with `.slds-dropdown`. Uses `classNames` [API](https://github.com/JedWatson/classnames).
*/
classNameMenu: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
PropTypes.string
]),
/*
* Id used for assistive technology
*/
inputId: PropTypes.string,
/**
* Determines the height of the menu based on SLDS CSS classes.
*/
itemVisibleLength: PropTypes.oneOf([5, 7, 10]),
/**
* **Text labels for internationalization**
* This object is merged with the default props object on every render.
* * `noOptionsFound`: Custom message that renders when no matches found. The default empty state is just text that says, 'No matches found.'.
*/
labels: shape({
noOptionsFound: PropTypes.string.isRequired
}),
/**
* Accepts a custom menu item rendering function that becomes a custom component and is passed in the following props:
* * `assistiveText`: Object, `assistiveText` prop that is passed into Combobox
* * `option`: Object, option data for item being rendered that is passed into Combobox
* * `selected`: Boolean, allows rendering of `assistiveText.optionSelectedInMenu` in Readonly Combobox
*
* _Tested with snapshot testing._
*/
menuItem: PropTypes.func,
/*
* Menu options
*/
options: PropTypes.array,
/*
* Callback to remove active descendent
*/
resetActiveOption: PropTypes.func,
/*
* Callback when option is selected with keyboard or mouse
*/
onSelect: PropTypes.func,
/*
* Selected options
*/
selection: PropTypes.array,
/**
* Changes styles of the menu option
*/
variant: PropTypes.oneOf(['icon-title-subtitle', 'checkbox']),
isSelected: PropTypes.func,
assistiveText: PropTypes.object
};
const defaultProps = {};
const Menu = (props) => {
const menuOptions = props.options.map((optionData, index) => {
const active =
index === props.activeOptionIndex &&
isEqual(optionData, props.activeOption);
const selected = props.isSelected({
selection: props.selection,
option: optionData
});
const MenuItem = props.menuItem;
return (
<li
className="slds-listbox__item"
key={`menu-option-${optionData.id}`}
role="presentation"
>
{
{
'icon-title-subtitle': (
<span // eslint-disable-line jsx-a11y/no-static-element-interactions
aria-selected={active}
id={`${props.inputId}-listbox-option-${optionData.id}`}
className={classNames(
'slds-media slds-listbox__option',
'slds-listbox__option_entity slds-listbox__option_has-meta',
{ 'slds-has-focus': active }
)}
onClick={(event) => {
props.onSelect(event, { option: optionData });
}}
role="option"
>
{optionData.icon && !props.menuItem ? (
<span className="slds-media__figure">{optionData.icon}</span>
) : null}
{props.menuItem ? (
<MenuItem
assistiveText={props.assistiveText}
selected={selected}
option={optionData}
/>
) : (
<span className="slds-media__body">
<span className="slds-listbox__option-text slds-listbox__option-text_entity">
{optionData.label}
</span>
<span className="slds-listbox__option-meta slds-listbox__option-meta_entity">
{optionData.subTitle}
</span>
</span>
)}
</span>
),
checkbox: (
<span // eslint-disable-line jsx-a11y/no-static-element-interactions
aria-selected={selected}
id={`${props.inputId}-listbox-option-${optionData.id}`}
className={classNames(
'slds-media slds-listbox__option',
' slds-listbox__option_plain slds-media_small slds-media_center',
{
'slds-has-focus': active,
'slds-is-selected': selected
}
)}
onClick={(event) => {
props.onSelect(event, {
selection: props.selection,
option: optionData
});
}}
role="option"
>
<span className="slds-media__figure">
<Icon
className="slds-listbox__icon-selected"
category="utility"
name="check"
size="x-small"
/>
</span>
<span className="slds-media__body">
{props.menuItem ? (
<MenuItem
assistiveText={props.assistiveText}
selected={selected}
option={optionData}
/>
) : (
<span className="slds-truncate" title={optionData.label}>
{selected ? (
<span className="slds-assistive-text">
{props.assistiveText.optionSelectedInMenu}
</span>
) : null}{' '}
{optionData.label}
</span>
)}
</span>
</span>
)
}[props.variant]
}
</li>
);
});
return (
<ul
className={classNames(
'slds-listbox slds-listbox_vertical slds-dropdown slds-dropdown_fluid',
{
'slds-dropdown_length-with-icon-5': props.itemVisibleLength === 5,
'slds-dropdown_length-with-icon-7': props.itemVisibleLength === 7,
'slds-dropdown_length-with-icon-10': props.itemVisibleLength === 10
},
props.classNameMenu
)}
role="presentation"
>
{menuOptions.length ? (
menuOptions
) : (
<li
className="slds-listbox__item slds-listbox__status"
role="status"
aria-live="polite"
>
<span className="slds-m-left--x-large slds-p-vertical--medium">
{props.labels.noOptionsFound}
</span>
</li>
)}
</ul>
);
};
Menu.displayName = 'Menu';
Menu.propTypes = propTypes;
Menu.defaultProps = defaultProps;
export default Menu;