UNPKG

@salesforce/design-system-react

Version:

Salesforce Lightning Design System for React

384 lines (337 loc) 9.18 kB
/* 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;