Skip to content

Commit

Permalink
Board switcher keyboard shortcut navigation (mattermost-community#3242)
Browse files Browse the repository at this point in the history
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
(cherry picked from commit f241fc1)
  • Loading branch information
Pinjasaur authored and mattermost-build committed Jul 28, 2022
1 parent c03d096 commit 9e3ee34
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 7 deletions.
9 changes: 9 additions & 0 deletions webapp/src/components/boardsSwitcher/boardsSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,21 @@ const BoardsSwitcher = (props: Props): JSX.Element => {
}
}

const handleEscKeyPress = (e: KeyboardEvent) => {
if (Utils.isKeyPressed(e, Constants.keyCodes.ESC)) {
e.preventDefault()
setShowSwitcher(false)
}
}

useEffect(() => {
document.addEventListener('keydown', handleQuickSwitchKeyPress)
document.addEventListener('keydown', handleEscKeyPress)

// cleanup function
return () => {
document.removeEventListener('keydown', handleQuickSwitchKeyPress)
document.removeEventListener('keydown', handleEscKeyPress)
}
}, [])

Expand Down
40 changes: 38 additions & 2 deletions webapp/src/components/boardsSwitcherDialog/boardSwitcherDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ReactNode} from 'react'
import React, {ReactNode, useRef, createRef, useState, useEffect, MutableRefObject} from 'react'

import './boardSwitcherDialog.scss'
import {useIntl} from 'react-intl'
Expand All @@ -16,12 +16,18 @@ import {getAllTeams, getCurrentTeam, Team} from '../../store/teams'
import {getMe} from '../../store/users'
import {Utils} from '../../utils'
import {BoardTypeOpen, BoardTypePrivate} from '../../blocks/board'
import { Constants } from '../../constants'

type Props = {
onClose: () => void
}

const BoardSwitcherDialog = (props: Props): JSX.Element => {
const [selected, setSelected] = useState<number>(-1)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [refs, setRefs] = useState<MutableRefObject<any>>(useRef([]))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [IDs, setIDs] = useState<any>({})
const intl = useIntl()
const team = useAppSelector(getCurrentTeam)
const me = useAppSelector(getMe)
Expand Down Expand Up @@ -58,14 +64,22 @@ const BoardSwitcherDialog = (props: Props): JSX.Element => {

const items = await octoClient.searchAll(query)
const untitledBoardTitle = intl.formatMessage({id: 'ViewTitle.untitled-board', defaultMessage: 'Untitled board'})
return items.map((item) => {
refs.current = items.map((_, i) => refs.current[i] ?? createRef())
setRefs(refs)
return items.map((item, i) => {
const resultTitle = item.title || untitledBoardTitle
const teamTitle = teamsById[item.teamId].title
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setIDs((prevIDs: any) => ({
...prevIDs,
[i]: [item.teamId, item.id]
}))
return (
<div
key={item.id}
className='blockSearchResult'
onClick={() => selectBoard(item.teamId, item.id)}
ref={refs.current[i]}
>
{item.type === BoardTypeOpen && <Globe/>}
{item.type === BoardTypePrivate && <LockOutline/>}
Expand All @@ -76,12 +90,34 @@ const BoardSwitcherDialog = (props: Props): JSX.Element => {
})
}

const handleEnterKeyPress = (e: KeyboardEvent) => {
if (Utils.isKeyPressed(e, Constants.keyCodes.ENTER) && selected > -1) {
e.preventDefault()
const [teamId, id] = IDs[selected]
selectBoard(teamId, id)
}
}

useEffect(() => {
if (selected >= 0)
refs.current[selected].current.parentElement.focus()

document.addEventListener('keydown', handleEnterKeyPress)

// cleanup function
return () => {
document.removeEventListener('keydown', handleEnterKeyPress)
}
}, [selected, refs, IDs])

return (
<SearchDialog
onClose={props.onClose}
title={title}
subTitle={subTitle}
searchHandler={searchHandler}
selected={selected}
setSelected={(n: number) => setSelected(n)}
/>
)
}
Expand Down
5 changes: 3 additions & 2 deletions webapp/src/components/searchDialog/searchDialog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@
padding: 0 24px;
cursor: pointer;
overflow: hidden;

&.freesize {
height: unset;
}

&:hover {
&:hover,
&:focus {
background: rgba(var(--center-channel-color-rgb), 0.08);
}
}
Expand Down
35 changes: 32 additions & 3 deletions webapp/src/components/searchDialog/searchDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ReactNode, useMemo, useState} from 'react'
import React, {ReactNode, useEffect, useMemo, useState} from 'react'

import './searchDialog.scss'
import {FormattedMessage} from 'react-intl'
Expand All @@ -10,16 +10,19 @@ import {debounce} from 'lodash'
import Dialog from '../dialog'
import {Utils} from '../../utils'
import Search from '../../widgets/icons/search'
import { Constants } from '../../constants'

type Props = {
onClose: () => void
title: string
subTitle?: string | ReactNode
searchHandler: (query: string) => Promise<Array<ReactNode>>
initialData?: Array<ReactNode>
selected: number
setSelected: (n: number) => void
}

export const EmptySearch = () => (
export const EmptySearch = (): JSX.Element => (
<div className='noResults introScreen'>
<div className='iconWrapper'>
<Search/>
Expand All @@ -33,7 +36,7 @@ export const EmptySearch = () => (
</div>
)

export const EmptyResults = (props: {query: string}) => (
export const EmptyResults = (props: {query: string}): JSX.Element => (
<div className='noResults'>
<div className='iconWrapper'>
<Search/>
Expand All @@ -57,12 +60,14 @@ export const EmptyResults = (props: {query: string}) => (
)

const SearchDialog = (props: Props): JSX.Element => {
const {selected, setSelected} = props
const [results, setResults] = useState<Array<ReactNode>>(props.initialData || [])
const [isSearching, setIsSearching] = useState<boolean>(false)
const [searchQuery, setSearchQuery] = useState<string>('')

const searchHandler = async (query: string): Promise<void> => {
setIsSearching(true)
setSelected(-1)
setSearchQuery(query)
const searchResults = await props.searchHandler(query)
setResults(searchResults)
Expand All @@ -73,6 +78,29 @@ const SearchDialog = (props: Props): JSX.Element => {

const emptyResult = results.length === 0 && !isSearching && searchQuery

const handleUpDownKeyPress = (e: KeyboardEvent) => {
if (Utils.isKeyPressed(e, Constants.keyCodes.DOWN)) {
e.preventDefault()
if (results.length > 0)
setSelected(((selected + 1) < results.length) ? (selected + 1) : selected)
}

if (Utils.isKeyPressed(e, Constants.keyCodes.UP)) {
e.preventDefault()
if (results.length > 0)
setSelected(((selected - 1) > -1) ? (selected - 1) : selected)
}
}

useEffect(() => {
document.addEventListener('keydown', handleUpDownKeyPress)

// cleanup function
return () => {
document.removeEventListener('keydown', handleUpDownKeyPress)
}
}, [results, selected])

return (
<Dialog
className='BoardSwitcherDialog'
Expand Down Expand Up @@ -101,6 +129,7 @@ const SearchDialog = (props: Props): JSX.Element => {
<div
key={Utils.uuid()}
className='searchResult'
tabIndex={-1}
>
{result}
</div>
Expand Down
4 changes: 4 additions & 0 deletions webapp/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ class Constants {

static readonly keyCodes: {[key: string]: [string, number]} = {
COMPOSING: ['Composing', 229],
ESC: ['Esc', 27],
UP: ['Up', 38],
DOWN: ['Down', 40],
ENTER: ['Enter', 13],
A: ['a', 65],
B: ['b', 66],
C: ['c', 67],
Expand Down

0 comments on commit 9e3ee34

Please sign in to comment.