1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-04-07 21:18:42 +02:00

Board switcher keyboard shortcut navigation (#3242)

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Paul Esch-Laurent 2022-07-28 12:10:18 -05:00 committed by GitHub
parent cd756a97f4
commit f241fc19da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 86 additions and 7 deletions

View File

@ -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)
}
}, [])

View File

@ -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'
@ -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)
@ -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/>}
@ -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)}
/>
)
}

View File

@ -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);
}
}

View File

@ -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'
@ -10,6 +10,7 @@ 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
@ -17,9 +18,11 @@ type Props = {
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/>
@ -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/>
@ -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)
@ -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'
@ -101,6 +129,7 @@ const SearchDialog = (props: Props): JSX.Element => {
<div
key={Utils.uuid()}
className='searchResult'
tabIndex={-1}
>
{result}
</div>

View File

@ -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],