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:
parent
cd756a97f4
commit
f241fc19da
@ -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)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
@ -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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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],
|
||||
|
Loading…
x
Reference in New Issue
Block a user