invite-search: allow keyboard nav + selection

Allows the user to navigate potential invitees with the arrow keys and
with shift+tab and tab. Enter adds the currently selected invitee.
This commit is contained in:
Liam Fitzgerald 2020-04-09 08:03:21 +10:00
parent 2bfcf20bf7
commit 3801056d3c
4 changed files with 360 additions and 44 deletions

View File

@ -1,4 +1,6 @@
import React, { Component } from "react"; import React, { Component } from "react";
import _ from 'lodash';
import Mousetrap from 'mousetrap';
import urbitOb from "urbit-ob"; import urbitOb from "urbit-ob";
import { Sigil } from "../lib/icons/sigil"; import { Sigil } from "../lib/icons/sigil";
@ -14,13 +16,17 @@ export class InviteSearch extends Component {
groups: [], groups: [],
ships: [] ships: []
}, },
selected: null,
inviteError: false inviteError: false
}; };
this.search = this.search.bind(this); this.search = this.search.bind(this);
this.textarea = React.createRef();
} }
componentDidMount() { componentDidMount() {
this.peerUpdate(); this.peerUpdate();
this.bindShortcuts();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@ -126,12 +132,91 @@ export class InviteSearch extends Component {
} }
} }
let { selected } = this.state;
let groupIdx = groupMatches.findIndex(([path]) => path === selected);
let shipIdx = shipMatches.findIndex(ship => ship === selected);
let staleSelection = groupIdx < 0 && shipIdx < 0;
if(!selected || staleSelection) {
const newSelection = _.get(groupMatches, '[0][0]') || shipMatches[0];
this.setState({ selected: newSelection })
}
this.setState({ this.setState({
searchResults: { groups: groupMatches, ships: shipMatches } searchResults: { groups: groupMatches, ships: shipMatches }
}); });
} }
} }
bindShortcuts() {
let mousetrap = Mousetrap(this.textarea.current);
mousetrap.bind(['down', 'tab'], e => {
e.preventDefault();
e.stopPropagation();
this.nextSelection();
});
mousetrap.bind(['up', 'shift+tab'], e => {
e.preventDefault();
e.stopPropagation();
this.nextSelection('backward');
});
mousetrap.bind('enter', e => {
e.preventDefault();
e.stopPropagation();
const { selected } = this.state;
if(selected.startsWith('/')) {
this.addGroup(selected)
} else {
this.addShip(selected);
}
this.setState({ selected: null })
})
}
nextSelection(backward = false) {
let { selected, searchResults } = this.state;
const { ships, groups } = searchResults;
if(!selected) {
return;
}
let groupIdx = groups.findIndex(([path]) => path === selected);
let shipIdx = ships.findIndex(ship => ship === selected);
if(groupIdx >= 0) {
backward ? groupIdx-- : groupIdx++;
let selected = _.get(groups,[groupIdx], '[0]');
if(groupIdx === groups.length) {
selected = ships.length === 0
? groups[0][0]
: ships[0];
}
if(groupIdx < 0) {
selected = ships.length === 0
? groups[groups.length - 1][0]
: ships[ships.length - 1];
}
this.setState({ selected });
return;
}
if(shipIdx >= 0) {
backward ? shipIdx-- : shipIdx++;
let selected = ships[shipIdx];
if(shipIdx === ships.length) {
selected = groups.length === 0
? ships[0]
: groups[0][0];
}
if(shipIdx < 0) {
selected = groups.length === 0
? ships[ships.length - 1]
: groups[groups.length - 1][0];
}
this.setState({ selected });
}
}
deleteGroup() { deleteGroup() {
let { ships } = this.props.invites; let { ships } = this.props.invites;
this.setState({ this.setState({
@ -251,7 +336,8 @@ export class InviteSearch extends Component {
className={ className={
"list white-d f8 pv2 ph3 pointer" + "list white-d f8 pv2 ph3 pointer" +
" hover-bg-gray4 hover-bg-gray1-d " + " hover-bg-gray4 hover-bg-gray1-d " +
((group[1]) ? "inter" : "mono") ((group[1]) ? "inter" : "mono") +
( group[0] === state.selected ? ' bg-gray1-d bg-gray4' : '')
} }
onClick={() => this.addGroup(group[0])}> onClick={() => this.addGroup(group[0])}>
<span className="mix-blend-diff white">{(group[1]) ? group[1] : group[0]}</span> <span className="mix-blend-diff white">{(group[1]) ? group[1] : group[0]}</span>
@ -270,7 +356,8 @@ export class InviteSearch extends Component {
key={ship} key={ship}
className={ className={
"list mono white-d f8 pv1 ph3 pointer" + "list mono white-d f8 pv1 ph3 pointer" +
" hover-bg-gray4 hover-bg-gray1-d relative" " hover-bg-gray4 hover-bg-gray1-d relative" +
( ship === state.selected ? ' bg-gray1-d bg-gray4' : '')
} }
onClick={e => this.addShip(ship)}> onClick={e => this.addShip(ship)}>
<Sigil <Sigil
@ -364,9 +451,7 @@ export class InviteSearch extends Component {
}} }}
/> />
<textarea <textarea
ref={e => { ref={this.textarea}
this.textarea = e;
}}
className={ className={
"f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100" + "f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100" +
" db focus-b--black focus-b--white-d" " db focus-b--black focus-b--white-d"
@ -379,12 +464,6 @@ export class InviteSearch extends Component {
resize: "none", resize: "none",
paddingLeft: 36 paddingLeft: 36
}} }}
onKeyPress={e => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
this.submitShipToAdd(this.state.searchValue);
}
}}
onChange={this.search} onChange={this.search}
value={state.searchValue} value={state.searchValue}
/> />

View File

@ -1,4 +1,6 @@
import React, { Component } from "react"; import React, { Component } from "react";
import _ from 'lodash';
import Mousetrap from 'mousetrap';
import urbitOb from "urbit-ob"; import urbitOb from "urbit-ob";
import { Sigil } from "../lib/icons/sigil"; import { Sigil } from "../lib/icons/sigil";
@ -14,13 +16,17 @@ export class InviteSearch extends Component {
groups: [], groups: [],
ships: [] ships: []
}, },
selected: null,
inviteError: false inviteError: false
}; };
this.search = this.search.bind(this); this.search = this.search.bind(this);
this.textarea = React.createRef();
} }
componentDidMount() { componentDidMount() {
this.peerUpdate(); this.peerUpdate();
this.bindShortcuts();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@ -127,12 +133,91 @@ export class InviteSearch extends Component {
} }
} }
let { selected } = this.state;
let groupIdx = groupMatches.findIndex(([path]) => path === selected);
let shipIdx = shipMatches.findIndex(ship => ship === selected);
let staleSelection = groupIdx < 0 && shipIdx < 0;
if(!selected || staleSelection) {
const newSelection = _.get(groupMatches, '[0][0]') || shipMatches[0];
this.setState({ selected: newSelection })
}
this.setState({ this.setState({
searchResults: { groups: groupMatches, ships: shipMatches } searchResults: { groups: groupMatches, ships: shipMatches }
}); });
} }
} }
bindShortcuts() {
let mousetrap = Mousetrap(this.textarea.current);
mousetrap.bind(['down', 'tab'], e => {
e.preventDefault();
e.stopPropagation();
this.nextSelection();
});
mousetrap.bind(['up', 'shift+tab'], e => {
e.preventDefault();
e.stopPropagation();
this.nextSelection('backward');
});
mousetrap.bind('enter', e => {
e.preventDefault();
e.stopPropagation();
const { selected } = this.state;
if(selected.startsWith('/')) {
this.addGroup(selected)
} else {
this.addShip(selected);
}
this.setState({ selected: null })
})
}
nextSelection(backward = false) {
let { selected, searchResults } = this.state;
const { ships, groups } = searchResults;
if(!selected) {
return;
}
let groupIdx = groups.findIndex(([path]) => path === selected);
let shipIdx = ships.findIndex(ship => ship === selected);
if(groupIdx >= 0) {
backward ? groupIdx-- : groupIdx++;
let selected = _.get(groups,[groupIdx], '[0]');
if(groupIdx === groups.length) {
selected = ships.length === 0
? groups[0][0]
: ships[0];
}
if(groupIdx < 0) {
selected = ships.length === 0
? groups[groups.length - 1][0]
: ships[ships.length - 1];
}
this.setState({ selected });
return;
}
if(shipIdx >= 0) {
backward ? shipIdx-- : shipIdx++;
let selected = ships[shipIdx];
if(shipIdx === ships.length) {
selected = groups.length === 0
? ships[0]
: groups[0][0];
}
if(shipIdx < 0) {
selected = groups.length === 0
? ships[ships.length - 1]
: groups[groups.length - 1][0];
}
this.setState({ selected });
}
}
deleteGroup() { deleteGroup() {
let { ships } = this.props.invites; let { ships } = this.props.invites;
this.setState({ this.setState({
@ -252,7 +337,8 @@ export class InviteSearch extends Component {
className={ className={
"list white-d f8 pv2 ph3 pointer" + "list white-d f8 pv2 ph3 pointer" +
" hover-bg-gray4 hover-bg-gray1-d " + " hover-bg-gray4 hover-bg-gray1-d " +
(group[1] ? "inter" : "mono") (group[1] ? "inter" : "mono") +
( group[0] === state.selected ? ' bg-gray1-d bg-gray4' : '')
} }
onClick={() => this.addGroup(group[0])}> onClick={() => this.addGroup(group[0])}>
<span className="mix-blend-diff white"> <span className="mix-blend-diff white">
@ -276,7 +362,8 @@ export class InviteSearch extends Component {
key={ship} key={ship}
className={ className={
"list mono white-d f8 pv1 ph3 pointer" + "list mono white-d f8 pv1 ph3 pointer" +
" hover-bg-gray4 hover-bg-gray1-d relative" " hover-bg-gray4 hover-bg-gray1-d relative" +
( ship === state.selected ? ' bg-gray1-d bg-gray4' : '')
} }
onClick={e => this.addShip(ship)}> onClick={e => this.addShip(ship)}>
<Sigil <Sigil
@ -374,9 +461,7 @@ export class InviteSearch extends Component {
}} }}
/> />
<textarea <textarea
ref={e => { ref={this.textarea}
this.textarea = e;
}}
className={ className={
"f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100" + "f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100" +
" db focus-b--black focus-b--white-d" " db focus-b--black focus-b--white-d"
@ -389,12 +474,6 @@ export class InviteSearch extends Component {
resize: "none", resize: "none",
paddingLeft: 36 paddingLeft: 36
}} }}
onKeyPress={e => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
this.submitShipToAdd(this.state.searchValue);
}
}}
onChange={this.search} onChange={this.search}
value={state.searchValue} value={state.searchValue}
/> />

View File

@ -1,4 +1,6 @@
import React, { Component } from "react"; import React, { Component } from "react";
import _ from 'lodash';
import Mousetrap from 'mousetrap';
import urbitOb from "urbit-ob"; import urbitOb from "urbit-ob";
import { Sigil } from "../lib/icons/sigil"; import { Sigil } from "../lib/icons/sigil";
@ -14,13 +16,17 @@ export class InviteSearch extends Component {
groups: [], groups: [],
ships: [] ships: []
}, },
selected: null,
inviteError: false inviteError: false
}; };
this.search = this.search.bind(this); this.search = this.search.bind(this);
this.textarea = React.createRef();
} }
componentDidMount() { componentDidMount() {
this.peerUpdate(); this.peerUpdate();
this.bindShortcuts();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@ -127,12 +133,91 @@ export class InviteSearch extends Component {
} }
} }
let { selected } = this.state;
let groupIdx = groupMatches.findIndex(([path]) => path === selected);
let shipIdx = shipMatches.findIndex(ship => ship === selected);
let staleSelection = groupIdx < 0 && shipIdx < 0;
if(!selected || staleSelection) {
const newSelection = _.get(groupMatches, '[0][0]') || shipMatches[0];
this.setState({ selected: newSelection })
}
this.setState({ this.setState({
searchResults: { groups: groupMatches, ships: shipMatches } searchResults: { groups: groupMatches, ships: shipMatches }
}); });
} }
} }
bindShortcuts() {
let mousetrap = Mousetrap(this.textarea.current);
mousetrap.bind(['down', 'tab'], e => {
e.preventDefault();
e.stopPropagation();
this.nextSelection();
});
mousetrap.bind(['up', 'shift+tab'], e => {
e.preventDefault();
e.stopPropagation();
this.nextSelection('backward');
});
mousetrap.bind('enter', e => {
e.preventDefault();
e.stopPropagation();
const { selected } = this.state;
if(selected.startsWith('/')) {
this.addGroup(selected)
} else {
this.addShip(selected);
}
this.setState({ selected: null })
})
}
nextSelection(backward = false) {
let { selected, searchResults } = this.state;
const { ships, groups } = searchResults;
if(!selected) {
return;
}
let groupIdx = groups.findIndex(([path]) => path === selected);
let shipIdx = ships.findIndex(ship => ship === selected);
if(groupIdx >= 0) {
backward ? groupIdx-- : groupIdx++;
let selected = _.get(groups,[groupIdx], '[0]');
if(groupIdx === groups.length) {
selected = ships.length === 0
? groups[0][0]
: ships[0];
}
if(groupIdx < 0) {
selected = ships.length === 0
? groups[groups.length - 1][0]
: ships[ships.length - 1];
}
this.setState({ selected });
return;
}
if(shipIdx >= 0) {
backward ? shipIdx-- : shipIdx++;
let selected = ships[shipIdx];
if(shipIdx === ships.length) {
selected = groups.length === 0
? ships[0]
: groups[0][0];
}
if(shipIdx < 0) {
selected = groups.length === 0
? ships[ships.length - 1]
: groups[groups.length - 1][0];
}
this.setState({ selected });
}
}
deleteGroup() { deleteGroup() {
let { ships } = this.props.invites; let { ships } = this.props.invites;
this.setState({ this.setState({
@ -252,7 +337,8 @@ export class InviteSearch extends Component {
className={ className={
"list white-d f8 pv2 ph3 pointer" + "list white-d f8 pv2 ph3 pointer" +
" hover-bg-gray4 hover-bg-gray1-d " + " hover-bg-gray4 hover-bg-gray1-d " +
(group[1] ? "inter" : "mono") (group[1] ? "inter" : "mono") +
( group[0] === state.selected ? ' bg-gray1-d bg-gray4' : '')
} }
onClick={() => this.addGroup(group[0])}> onClick={() => this.addGroup(group[0])}>
<span className="mix-blend-diff white"> <span className="mix-blend-diff white">
@ -276,7 +362,8 @@ export class InviteSearch extends Component {
key={ship} key={ship}
className={ className={
"list mono white-d f8 pv1 ph3 pointer" + "list mono white-d f8 pv1 ph3 pointer" +
" hover-bg-gray4 hover-bg-gray1-d relative" " hover-bg-gray4 hover-bg-gray1-d relative" +
( ship === state.selected ? ' bg-gray1-d bg-gray4' : '')
} }
onClick={e => this.addShip(ship)}> onClick={e => this.addShip(ship)}>
<Sigil <Sigil
@ -374,9 +461,7 @@ export class InviteSearch extends Component {
}} }}
/> />
<textarea <textarea
ref={e => { ref={this.textarea}
this.textarea = e;
}}
className={ className={
"f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100" + "f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100" +
" db focus-b--black focus-b--white-d" " db focus-b--black focus-b--white-d"
@ -389,12 +474,6 @@ export class InviteSearch extends Component {
resize: "none", resize: "none",
paddingLeft: 36 paddingLeft: 36
}} }}
onKeyPress={e => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
this.submitShipToAdd(this.state.searchValue);
}
}}
onChange={this.search} onChange={this.search}
value={state.searchValue} value={state.searchValue}
/> />

View File

@ -1,4 +1,6 @@
import React, { Component } from "react"; import React, { Component } from "react";
import _ from 'lodash';
import Mousetrap from 'mousetrap';
import urbitOb from "urbit-ob"; import urbitOb from "urbit-ob";
import { Sigil } from "../lib/icons/sigil"; import { Sigil } from "../lib/icons/sigil";
@ -14,13 +16,17 @@ export class InviteSearch extends Component {
groups: [], groups: [],
ships: [] ships: []
}, },
selected: null,
inviteError: false inviteError: false
}; };
this.search = this.search.bind(this); this.search = this.search.bind(this);
this.textarea = React.createRef();
} }
componentDidMount() { componentDidMount() {
this.peerUpdate(); this.peerUpdate();
this.bindShortcuts();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@ -127,12 +133,91 @@ export class InviteSearch extends Component {
} }
} }
let { selected } = this.state;
let groupIdx = groupMatches.findIndex(([path]) => path === selected);
let shipIdx = shipMatches.findIndex(ship => ship === selected);
let staleSelection = groupIdx < 0 && shipIdx < 0;
if(!selected || staleSelection) {
const newSelection = _.get(groupMatches, '[0][0]') || shipMatches[0];
this.setState({ selected: newSelection })
}
this.setState({ this.setState({
searchResults: { groups: groupMatches, ships: shipMatches } searchResults: { groups: groupMatches, ships: shipMatches }
}); });
} }
} }
bindShortcuts() {
let mousetrap = Mousetrap(this.textarea.current);
mousetrap.bind(['down', 'tab'], e => {
e.preventDefault();
e.stopPropagation();
this.nextSelection();
});
mousetrap.bind(['up', 'shift+tab'], e => {
e.preventDefault();
e.stopPropagation();
this.nextSelection('backward');
});
mousetrap.bind('enter', e => {
e.preventDefault();
e.stopPropagation();
const { selected } = this.state;
if(selected.startsWith('/')) {
this.addGroup(selected)
} else {
this.addShip(selected);
}
this.setState({ selected: null })
})
}
nextSelection(backward = false) {
let { selected, searchResults } = this.state;
const { ships, groups } = searchResults;
if(!selected) {
return;
}
let groupIdx = groups.findIndex(([path]) => path === selected);
let shipIdx = ships.findIndex(ship => ship === selected);
if(groupIdx >= 0) {
backward ? groupIdx-- : groupIdx++;
let selected = _.get(groups,[groupIdx], '[0]');
if(groupIdx === groups.length) {
selected = ships.length === 0
? groups[0][0]
: ships[0];
}
if(groupIdx < 0) {
selected = ships.length === 0
? groups[groups.length - 1][0]
: ships[ships.length - 1];
}
this.setState({ selected });
return;
}
if(shipIdx >= 0) {
backward ? shipIdx-- : shipIdx++;
let selected = ships[shipIdx];
if(shipIdx === ships.length) {
selected = groups.length === 0
? ships[0]
: groups[0][0];
}
if(shipIdx < 0) {
selected = groups.length === 0
? ships[ships.length - 1]
: groups[groups.length - 1][0];
}
this.setState({ selected });
}
}
deleteGroup() { deleteGroup() {
let { ships } = this.props.invites; let { ships } = this.props.invites;
this.setState({ this.setState({
@ -252,7 +337,8 @@ export class InviteSearch extends Component {
className={ className={
"list white-d f8 pv2 ph3 pointer" + "list white-d f8 pv2 ph3 pointer" +
" hover-bg-gray4 hover-bg-gray1-d " + " hover-bg-gray4 hover-bg-gray1-d " +
(group[1] ? "inter" : "mono") (group[1] ? "inter" : "mono") +
( group[0] === state.selected ? ' bg-gray1-d bg-gray4' : '')
} }
onClick={() => this.addGroup(group[0])}> onClick={() => this.addGroup(group[0])}>
<span className="mix-blend-diff white"> <span className="mix-blend-diff white">
@ -276,7 +362,8 @@ export class InviteSearch extends Component {
key={ship} key={ship}
className={ className={
"list mono white-d f8 pv1 ph3 pointer" + "list mono white-d f8 pv1 ph3 pointer" +
" hover-bg-gray4 hover-bg-gray1-d relative" " hover-bg-gray4 hover-bg-gray1-d relative" +
( ship === state.selected ? ' bg-gray1-d bg-gray4' : '')
} }
onClick={e => this.addShip(ship)}> onClick={e => this.addShip(ship)}>
<Sigil <Sigil
@ -374,9 +461,7 @@ export class InviteSearch extends Component {
}} }}
/> />
<textarea <textarea
ref={e => { ref={this.textarea}
this.textarea = e;
}}
className={ className={
"f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100" + "f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100" +
" db focus-b--black focus-b--white-d" " db focus-b--black focus-b--white-d"
@ -389,12 +474,6 @@ export class InviteSearch extends Component {
resize: "none", resize: "none",
paddingLeft: 36 paddingLeft: 36
}} }}
onKeyPress={e => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
this.submitShipToAdd(this.state.searchValue);
}
}}
onChange={this.search} onChange={this.search}
value={state.searchValue} value={state.searchValue}
/> />