@salesforce/design-system-react
Version:
Salesforce Lightning Design System for React
384 lines (337 loc) • 9.18 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 class-methods-use-this */
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import eventUtil from '../../utilities/event';
import { SPLIT_VIEW_LISTBOX } from '../../utilities/constants';
import Icon from '../icon';
import SplitViewListItemContent from './private/list-item-content';
import listItemWithContent from './private/list-item-with-content';
export const SORT_OPTIONS = Object.freeze({
UP: 'up',
DOWN: 'down'
});
const propTypes = {
/**
* **Assistive text for accessibility**
* * `list`: aria label for the list
* * `sort`
* * `sortedBy`: Clickable sort header for the list.
* * `descending`: Descending sorting.
* * `ascending`: Ascending sorting.
*/
assistiveText: PropTypes.shape({
list: PropTypes.string,
sort: PropTypes.shape({
sortedBy: PropTypes.string,
descending: PropTypes.string,
ascending: PropTypes.string
}),
unreadItem: PropTypes.string
}),
/**
* CSS classes to be added to the parent `div` tag.
*/
className: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
PropTypes.string
]),
/**
* Event Callbacks
* * `onSelect`: Called when a list item is selected.
* * event {object} List item click event
* * Meta {object}
* * selectedItems {array} List of selected items.
* * item {object} Last selected item.
* * `onSort`: Called when the list is sorted.
* * event {object} Sort click event
*/
events: PropTypes.shape({
onSelect: PropTypes.func.isRequired,
onSort: PropTypes.func
}),
/**
* HTML id for component.
*/
id: PropTypes.string,
/**
* **Text labels for internationalization**
* * `header`: This is the header of the list.
*/
labels: PropTypes.shape({
header: PropTypes.string
}),
/**
* The direction of the sort arrow. Option are:
* * SORT_OPTIONS.UP: `up`
* * SORT_OPTIONS.DOWN: `down`
*/
sortDirection: PropTypes.oneOf([SORT_OPTIONS.UP, SORT_OPTIONS.DOWN]),
/**
* Allows multiple item to be selection
*/
multiple: PropTypes.bool,
/**
* The list of items.
* It is recommended that you have a unique `id` for each item.
*/
options: PropTypes.array.isRequired,
/**
* Accepts an array of item objects. For single selection, pass in an array of one object.
*/
selection: PropTypes.array,
/**
* Accepts an array of item objects. For single unread, pass in an array of one object.
*/
unread: PropTypes.array,
/**
* Custom list item template for the list item content. The select and unread functionality wraps the custom list item.
* This should be a React component that accepts props.
*/
listItem: PropTypes.func
};
const defaultProps = {
assistiveText: {
list: 'Select an item to open it in a new workspace tab.',
sort: {
sortedBy: 'Sorted by',
descending: 'Descending',
ascending: 'Ascending'
}
},
events: {},
labels: {},
selection: [],
unread: []
};
class SplitViewListbox extends React.Component {
static displayName = SPLIT_VIEW_LISTBOX;
static propTypes = propTypes;
static defaultProps = defaultProps;
constructor (props) {
super(props);
this.listItemComponents = {};
this.state = {
currentSelectedItem: null,
currentFocusedListItem: {
index: 0,
item: null
}
};
}
componentWillMount () {
// Generates the list item template
this.ListItemWithContent = listItemWithContent(
this.props.listItem || SplitViewListItemContent
);
}
componentDidMount () {
this.focusFirstItem();
}
isListItemFocused (item) {
return this.state.currentFocusedListItem.item === item;
}
isSelected (item) {
return this.props.selection.includes(item);
}
isUnread (item) {
return this.props.unread.includes(item);
}
handleKeyDown (event) {
if (this.props.multiple && event.key === 'a' && event.ctrlKey) {
// select / deselect all
eventUtil.trap(event);
this.props.options === this.props.selection
? this.deselectAllListItems(event)
: this.selectAllListItems(event);
} else if (event.key === 'ArrowUp') {
eventUtil.trap(event);
this.moveToPreviousItem(event);
} else if (event.key === 'ArrowDown') {
eventUtil.trap(event);
this.moveToNextItem(event);
}
}
moveToNextItem (event) {
const nextFocusIndex =
this.state.currentFocusedListItem.index === this.props.options.length - 1
? 0
: this.state.currentFocusedListItem.index + 1;
this.moveToIndex(event, nextFocusIndex);
}
moveToPreviousItem (event) {
const previousFocusIndex =
this.state.currentFocusedListItem.index === 0
? this.props.options.length - 1
: this.state.currentFocusedListItem.index - 1;
this.moveToIndex(event, previousFocusIndex);
}
moveToIndex (event, index) {
const item = this.props.options[index];
!event.metaKey && !event.ctrlKey && this.selectListItem(item, event);
this.focusItem(item);
}
focusFirstItem () {
const firstSelectedItem =
this.props.options.find((item) => this.props.selection.includes(item)) ||
this.props.options[0];
if (firstSelectedItem) {
this.focusItem(firstSelectedItem, true);
}
}
focusItem (item, setDataOnly) {
const index = this.props.options.indexOf(item);
!setDataOnly && this.listItemComponents[index].focus();
this.setState({
currentFocusedListItem: {
index,
item
}
});
}
deselectAllListItems (event) {
this.setState({ currentSelectedItem: null });
this.props.events.onSelect(event, {
selectedItems: [],
item: null
});
}
selectAllListItems (event) {
this.props.events.onSelect(event, {
selectedItems: this.props.options,
item: this.state.currentSelectedItem
});
}
selectListItem (item, event) {
let selectedItems = [item];
if (this.props.multiple) {
if (event.metaKey) {
selectedItems = this.props.selection.includes(item)
? this.props.selection.filter((i) => i !== item)
: [item, ...this.props.selection];
} else if (event.shiftKey) {
const [begin, end] = [
this.props.options.indexOf(this.state.currentSelectedItem),
this.props.options.indexOf(item)
].sort();
const addToSelection = this.props.options.slice(begin, end + 1);
selectedItems = [
...addToSelection,
...this.props.selection.filter((i) => !addToSelection.includes(i))
];
}
}
this.setState({ currentSelectedItem: item });
this.props.events.onSelect(event, { selectedItems, item });
}
handleOnSelect (event, { item }) {
this.selectListItem(item, event);
this.focusItem(item);
}
sortDirection () {
return this.props.sortDirection ? (
<Icon
category={'utility'}
name={
this.props.sortDirection === SORT_OPTIONS.DOWN
? 'arrowdown'
: 'arrowup'
}
size={'xx-small'}
className={'slds-align-top'}
/>
) : null;
}
headerWrapper (children) {
return this.props.events.onSort ? (
<a
style={{ borderTop: '0' }}
href="javascript:void(0);"
role="button"
className="slds-split-view__list-header slds-grid slds-text-title_caps slds-text-link_reset"
onClick={this.props.events.onSort}
>
{children}
</a>
) : (
<div
style={{ borderTop: '0' }}
className="slds-split-view__list-header slds-grid slds-text-title_caps"
>
{children}
</div>
);
}
header () {
return this.props.labels.header
? this.headerWrapper(
<span>
<span className="slds-assistive-text">
{this.props.assistiveText.sort.sortedBy}:
</span>
<span>
{this.props.labels.header}
{this.sortDirection()}
</span>
<span className="slds-assistive-text">
-{' '}
{this.props.sortDirection === SORT_OPTIONS.DOWN
? this.props.assistiveText.sort.descending
: this.props.assistiveText.sort.ascending}
</span>
</span>
)
: null;
}
addListItemComponent (component, index) {
this.listItemComponents[index] = component;
}
listItems () {
const ListItemWithContent = this.ListItemWithContent;
return this.props.options.map((item, index) => (
<ListItemWithContent
key={item.id || index}
assistiveText={{
unreadItem: this.props.assistiveText.unreadItem
}}
listItemRef={(component) => {
this.addListItemComponent(component, index);
}}
item={item}
isFocused={this.isListItemFocused(item)}
isSelected={this.isSelected(item)}
isUnread={this.isUnread(item)}
events={{
onClick: (event, meta) => this.handleOnSelect(event, meta)
}}
multiple={this.props.multiple}
/>
));
}
render () {
return (
<div
id={this.props.id}
className={classNames(
'slds-grid slds-grid_vertical slds-scrollable_none',
this.props.className
)}
>
{this.header()}
<ul
className="slds-scrollable_y"
aria-label={this.props.assistiveText.list}
aria-multiselectable={this.props.multiple}
role="listbox"
onKeyDown={(event) => this.handleKeyDown(event)}
>
{this.listItems()}
</ul>
</div>
);
}
}
export default SplitViewListbox;