Skip to content

Commit

Permalink
feat(SortedTable): keyboard shortcuts support
Browse files Browse the repository at this point in the history
Fixes #2330
  • Loading branch information
pdonias committed Sep 29, 2017
1 parent f5e3aef commit 12aebe9
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 40 deletions.
30 changes: 18 additions & 12 deletions src/common/intl/messages.js
Expand Up @@ -1374,19 +1374,25 @@ var messages = {
// ----- Shortcuts -----
shortcutModalTitle: 'Keyboard shortcuts',
shortcut_XoApp: 'Global',
shortcut_GO_TO_HOSTS: 'Go to hosts list',
shortcut_GO_TO_POOLS: 'Go to pools list',
shortcut_GO_TO_VMS: 'Go to VMs list',
shortcut_GO_TO_SRS: 'Go to SRs list',
shortcut_CREATE_VM: 'Create a new VM',
shortcut_UNFOCUS: 'Unfocus field',
shortcut_HELP: 'Show shortcuts key bindings',
shortcut_XoApp_GO_TO_HOSTS: 'Go to hosts list',
shortcut_XoApp_GO_TO_POOLS: 'Go to pools list',
shortcut_XoApp_GO_TO_VMS: 'Go to VMs list',
shortcut_XoApp_GO_TO_SRS: 'Go to SRs list',
shortcut_XoApp_CREATE_VM: 'Create a new VM',
shortcut_XoApp_UNFOCUS: 'Unfocus field',
shortcut_XoApp_HELP: 'Show shortcuts key bindings',
shortcut_Home: 'Home',
shortcut_SEARCH: 'Focus search bar',
shortcut_NAV_DOWN: 'Next item',
shortcut_NAV_UP: 'Previous item',
shortcut_SELECT: 'Select item',
shortcut_JUMP_INTO: 'Open',
shortcut_Home_SEARCH: 'Focus search bar',
shortcut_Home_NAV_DOWN: 'Next item',
shortcut_Home_NAV_UP: 'Previous item',
shortcut_Home_SELECT: 'Select item',
shortcut_Home_JUMP_INTO: 'Open',
shortcut_SortedTable: 'Supported tables',
shortcut_SortedTable_SEARCH: 'Focus the table search bar',
shortcut_SortedTable_NAV_DOWN: 'Next item',
shortcut_SortedTable_NAV_UP: 'Previous item',
shortcut_SortedTable_SELECT: 'Select item',
shortcut_SortedTable_ROW_ACTION: 'Action',

// ----- Settings/ACLs -----
settingsAclsButtonTooltipVM: 'VM',
Expand Down
4 changes: 3 additions & 1 deletion src/common/link.js
Expand Up @@ -19,6 +19,7 @@ const _IGNORED_TAGNAMES = {
}

@propTypes({
className: propTypes.string,
tagName: propTypes.string
})
export class BlockLink extends Component {
Expand Down Expand Up @@ -54,10 +55,11 @@ export class BlockLink extends Component {
}

render () {
const { children, tagName = 'div' } = this.props
const { children, tagName = 'div', className } = this.props
const Component = tagName
return (
<Component
className={className}
ref={this._addAuxClickListener}
style={this._style}
onClickCapture={this._onClickCapture}
Expand Down
5 changes: 5 additions & 0 deletions src/common/sorted-table/index.css
Expand Up @@ -10,3 +10,8 @@
.clickableRow {
cursor: pointer;
}

.highlight {
outline: 2px solid #366e98;
outline-offset: -2px;
}
124 changes: 102 additions & 22 deletions src/common/sorted-table/index.js
Expand Up @@ -3,6 +3,7 @@ import classNames from 'classnames'
import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
import React from 'react'
import Shortcuts from 'shortcuts'
import { Portal } from 'react-overlays'
import { routerShape } from 'react-router/lib/PropTypes'
import { Set } from 'immutable'
Expand Down Expand Up @@ -219,6 +220,9 @@ const actionsShape = propTypes.arrayOf(propTypes.shape({
propTypes.func,
propTypes.string
]),
// DOM node selector like body or .my-class
// The shortcuts will be enabled when the node is focused
shortcutsTarget: propTypes.string,
userData: propTypes.any
}, {
router: routerShape
Expand Down Expand Up @@ -287,6 +291,69 @@ export default class SortedTable extends Component {
)

this.state.selectedItemsIds = new Set()

this._getShortcutsHandler = createSelector(
() => {
const items = this._getVisibleItems()
return items && items.length
},
() => {
const items = this._getVisibleItems()
const { highlighted } = this.state
return items && highlighted != null && items[highlighted]
},
() => this.state.highlighted,
() => this.props.groupedActions,
() => this.props.rowLink,
() => this.props.rowAction,
() => this.props.userData,
(nItems, item, itemIndex, groupedActions, rowLink, rowAction, userData) => (command, event) => {
event.preventDefault()
switch (command) {
case 'SEARCH':
this.refs.filterInput.refs.filter.focus()
break
case 'NAV_DOWN':
if (groupedActions == null && rowAction == null && rowLink == null) {
break
}
this.setState({ highlighted: (itemIndex + nItems + 1) % nItems || 0 })
break
case 'NAV_UP':
if (groupedActions == null && rowAction == null && rowLink == null) {
break
}
this.setState({ highlighted: (itemIndex + nItems - 1) % nItems || 0 })
break
case 'SELECT':
if (groupedActions == null) {
break
}
const { selectedItemsIds } = this.state
const method = selectedItemsIds.has(item.id)
? 'delete'
: 'add'
this.setState({
selectedItemsIds: selectedItemsIds[method](item.id)
})
break
case 'ROW_ACTION':
if (item == null) {
break
}
if (rowLink != null) {
this.context.router.push(isFunction(rowLink)
? rowLink(item, userData)
: rowLink
)
break
}
if (rowAction != null) {
rowAction(item, userData)
}
}
}
)
}

componentDidMount () {
Expand Down Expand Up @@ -330,7 +397,8 @@ export default class SortedTable extends Component {
}

_onPageSelection = (_, event) => this.setState({
activePage: event.eventKey
activePage: event.eventKey,
highlighted: undefined
})

_selectAllVisibleItems = event => {
Expand Down Expand Up @@ -418,8 +486,9 @@ export default class SortedTable extends Component {
})
}
this.setState({
activePage: 1,
filter,
activePage: 1
highlighted: undefined
})
}, 500)

Expand Down Expand Up @@ -489,25 +558,28 @@ export default class SortedTable extends Component {
</div></td>

return rowLink != null
? <BlockLink
key={id}
tagName='tr'
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
>
{selectionColumn}
{columns}
{actionsColumn}
</BlockLink>
: <tr
className={rowAction && styles.clickableRow}
data-index={i}
key={id}
onClick={rowAction && this._executeRowAction}
>
{selectionColumn}
{columns}
{actionsColumn}
</tr>
? <BlockLink
className={state.highlighted === i ? styles.highlight : undefined}
key={id}
tagName='tr'
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
>
{selectionColumn}
{columns}
{actionsColumn}
</BlockLink>
: <tr
className={classNames(
rowAction && styles.clickableRow,
state.highlighted === i && styles.highlight
)}
key={id}
onClick={rowAction && (() => rowAction(item, userData))}
>
{selectionColumn}
{columns}
{actionsColumn}
</tr>
}

render () {
Expand All @@ -516,7 +588,8 @@ export default class SortedTable extends Component {
filterContainer,
groupedActions,
itemsPerPage,
paginationContainer
paginationContainer,
shortcutsTarget
} = props
const { all } = state

Expand Down Expand Up @@ -557,11 +630,18 @@ export default class SortedTable extends Component {
defaultFilter={state.filter}
filters={props.filters}
onChange={this._onFilterChange}
ref='filterInput'
/>
)

return (
<div>
{shortcutsTarget !== undefined && <Shortcuts
handler={this._getShortcutsHandler()}
name='SortedTable'
stopPropagation
targetNodeSelector={shortcutsTarget}
/>}
<table className='table'>
<thead className='thead-default'>
<tr>
Expand Down
9 changes: 8 additions & 1 deletion src/keymap.js
Expand Up @@ -17,6 +17,13 @@ const keymap = {
NAV_UP: 'k',
SELECT: 'x',
JUMP_INTO: 'enter'
},
SortedTable: {
SEARCH: '/',
NAV_DOWN: 'j',
NAV_UP: 'k',
SELECT: 'x',
ROW_ACTION: 'enter'
}
}
export { keymap as default }
Expand All @@ -25,6 +32,6 @@ export const help = mapValues(keymap, (shortcuts, contextLabel) => ({
name: _(`shortcut_${contextLabel}`),
shortcuts: mapValues(shortcuts, (shortcut, label) => ({
keys: shortcuts[label],
message: _(`shortcut_${label}`)
message: _(`shortcut_${contextLabel}_${label}`)
}))
}))
5 changes: 3 additions & 2 deletions src/xo-app/dashboard/health/index.js
Expand Up @@ -451,6 +451,7 @@ export default class Health extends Component {
collection={props.userSrs}
columns={SR_COLUMNS}
rowLink={this._getSrUrl}
shortcutsTarget='body'
/>
</Col>
</Row>
Expand Down Expand Up @@ -509,7 +510,7 @@ export default class Health extends Component {
</Card>
</Col>
</Row>
<Row>
<Row className='orphaned-vms'>
<Col>
<Card>
<CardHeader>
Expand All @@ -520,7 +521,7 @@ export default class Health extends Component {
collection={props.areObjectsFetched ? props.vmOrphaned : null}
emptyMessage={_('noOrphanedObject')}
>
<SortedTable collection={props.vmOrphaned} columns={VM_COLUMNS} />
<SortedTable collection={props.vmOrphaned} columns={VM_COLUMNS} shortcutsTarget='.orphaned-vms' />
</NoObjects>
</CardBlock>
</Card>
Expand Down
4 changes: 2 additions & 2 deletions src/xo-app/home/index.js
Expand Up @@ -618,10 +618,10 @@ export default class Home extends Component {
this.refs.filterInput.focus()
break
case 'NAV_DOWN':
this.setState({ highlighted: (this.state.highlighted + items.length + 1) % items.length || 0 })
this.setState({ highlighted: (this.state.highlighted + 1) % items.length || 0 })
break
case 'NAV_UP':
this.setState({ highlighted: (this.state.highlighted + items.length - 1) % items.length || 0 })
this.setState({ highlighted: (this.state.highlighted - 1) % items.length || 0 })
break
case 'SELECT':
const itemId = items[this.state.highlighted].id
Expand Down
1 change: 1 addition & 0 deletions src/xo-app/sr/tab-disks.js
Expand Up @@ -139,6 +139,7 @@ export default class SrDisks extends Component {
filterUrlParam='s'
groupedActions={GROUPED_ACTIONS}
individualActions={INDIVIDUAL_ACTIONS}
shortcutsTarget='body'
/>
: <h4 className='text-xs-center'>{_('srNoVdis')}</h4>
}
Expand Down

0 comments on commit 12aebe9

Please sign in to comment.