Skip to content

Commit 07393aa

Browse files
authored
feat: hotkey, close #27 (#56)
1 parent 464867d commit 07393aa

File tree

14 files changed

+232
-38
lines changed

14 files changed

+232
-38
lines changed

components/hotkey-tooltip.tsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Tooltip, TooltipProps } from '@material-ui/core'
2+
import UIState from 'libs/web/state/ui'
3+
import { noop } from 'lodash'
24
import { FC } from 'react'
5+
import { useHotkeys } from 'react-hotkeys-hook'
36

4-
// todo: tooltip style
57
const Title: FC<{
68
text: string
79
keys: string[]
@@ -16,13 +18,53 @@ const Title: FC<{
1618
const HotkeyTooltip: FC<{
1719
text: string
1820
keys?: string[]
21+
/**
22+
* first key
23+
*/
24+
commandKey?: boolean
25+
optionKey?: boolean
1926
onClose?: TooltipProps['onClose']
20-
}> = ({ text, keys = [], children, onClose }) => {
27+
onHotkey?: () => void
28+
}> = ({
29+
text,
30+
keys = [],
31+
children,
32+
onClose,
33+
commandKey,
34+
optionKey,
35+
onHotkey = noop,
36+
}) => {
37+
const {
38+
ua: { isMac },
39+
} = UIState.useContainer()
40+
const keyMap = [...keys]
41+
42+
if (commandKey) {
43+
keyMap.unshift(isMac ? '⌘' : 'ctrl')
44+
}
45+
46+
if (optionKey) {
47+
keyMap.unshift(isMac ? '⌥' : 'alt')
48+
}
49+
50+
useHotkeys(
51+
keyMap.join('+'),
52+
(event) => {
53+
event.preventDefault()
54+
onHotkey()
55+
},
56+
{
57+
enabled: !!keys.length,
58+
enableOnTags: ['INPUT', 'TEXTAREA'],
59+
enableOnContentEditable: true,
60+
}
61+
)
62+
2163
return (
2264
<Tooltip
2365
enterDelay={200}
2466
TransitionProps={{ timeout: 0 }}
25-
title={<Title text={text} keys={keys} />}
67+
title={<Title text={text} keys={keyMap} />}
2668
onClose={onClose}
2769
placement="bottom-start"
2870
>

components/portal/filter-modal/filter-modal-list.tsx

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,69 @@
11
import UIState from 'libs/web/state/ui'
2-
import { FC, ReactNode } from 'react'
2+
import { ReactNode, useState } from 'react'
33
import { use100vh } from 'react-div-100vh'
4+
import { useHotkeys } from 'react-hotkeys-hook'
45

5-
const FilterModalList: FC<{
6-
ItemComponent: (item: any) => ReactNode
7-
items?: any[]
8-
}> = ({ ItemComponent, items }) => {
6+
interface ItemProps {
7+
selected: boolean
8+
}
9+
interface Props<T> {
10+
ItemComponent: (item: T, props: ItemProps) => ReactNode
11+
items: T[]
12+
onEnter?: (item: T) => void
13+
}
14+
15+
export default function FilterModalList<T>({
16+
ItemComponent,
17+
items,
18+
onEnter,
19+
}: Props<T>) {
920
const {
1021
ua: { isMobileOnly },
1122
} = UIState.useContainer()
1223
const height = use100vh() || 0
1324
const calcHeight = isMobileOnly ? height : (height * 2) / 3
25+
const [selectedIndex, setSelectedIndex] = useState(0)
26+
27+
useHotkeys(
28+
'down',
29+
(event) => {
30+
event.preventDefault()
31+
setSelectedIndex((prev) => Math.min(items?.length ?? 0, prev + 1))
32+
},
33+
{
34+
enableOnTags: ['INPUT'],
35+
}
36+
)
37+
useHotkeys(
38+
'up',
39+
(event) => {
40+
event.preventDefault()
41+
setSelectedIndex((prev) => Math.max(0, prev - 1))
42+
},
43+
{
44+
enableOnTags: ['INPUT'],
45+
}
46+
)
47+
useHotkeys(
48+
'enter',
49+
(event) => {
50+
event.preventDefault()
51+
onEnter?.(items[selectedIndex])
52+
},
53+
{
54+
enableOnTags: ['INPUT'],
55+
}
56+
)
1457

1558
return (
1659
<>
1760
{items?.length ? (
1861
<ul className="list border-t border-gray-100 overflow-auto divide-y divide-gray-100">
19-
{items?.map((item) => ItemComponent(item))}
62+
{items?.map((item, index) =>
63+
ItemComponent(item, {
64+
selected: selectedIndex === index,
65+
})
66+
)}
2067
</ul>
2168
) : null}
2269
<style jsx>{`
@@ -27,5 +74,3 @@ const FilterModalList: FC<{
2774
</>
2875
)
2976
}
30-
31-
export default FilterModalList

components/portal/search-modal/search-item.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
11
import dayjs from 'dayjs'
22
import Link from 'next/link'
3-
import { FC } from 'react'
3+
import { FC, useRef } from 'react'
44
import { NoteCacheItem } from 'libs/web/cache'
55
import MarkText from 'components/portal/filter-modal/mark-text'
66
import PortalState from 'libs/web/state/portal'
7+
import classNames from 'classnames'
8+
import useScrollView from 'libs/web/hooks/use-scroll-view'
79

810
const SearchItem: FC<{
911
note: NoteCacheItem
1012
keyword?: string
11-
}> = ({ note, keyword }) => {
13+
selected?: boolean
14+
}> = ({ note, keyword, selected }) => {
1215
const {
1316
search: { close },
1417
} = PortalState.useContainer()
18+
const ref = useRef<HTMLLIElement>(null)
19+
20+
useScrollView(ref, selected)
1521

1622
return (
17-
<li className="hover:bg-gray-200 cursor-pointer">
18-
<Link href={`/${note.id}`}>
23+
<li
24+
ref={ref}
25+
className={classNames('hover:bg-gray-200 cursor-pointer', {
26+
'bg-gray-300': selected,
27+
})}
28+
>
29+
<Link href={`/${note.id}`} shallow>
1930
<a className="py-2 px-4 block text-xs text-gray-500" onClick={close}>
2031
<h4 className="text-sm font-bold">
2132
<MarkText text={note.title} keyword={keyword} />

components/portal/search-modal/search-modal.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
import SearchState from 'libs/web/state/search'
2-
import { FC } from 'react'
2+
import { FC, useCallback } from 'react'
33
import FilterModal from 'components/portal/filter-modal/filter-modal'
44
import FilterModalInput from 'components/portal/filter-modal/filter-modal-input'
55
import FilterModalList from 'components/portal/filter-modal/filter-modal-list'
66
import SearchItem from './search-item'
77
import { NoteModel } from 'libs/shared/note'
88
import PortalState from 'libs/web/state/portal'
99
import useI18n from 'libs/web/hooks/use-i18n'
10+
import { useRouter } from 'next/router'
1011

1112
const SearchModal: FC = () => {
1213
const { t } = useI18n()
1314
const { filterNotes, keyword, list } = SearchState.useContainer()
1415
const {
1516
search: { visible, close },
1617
} = PortalState.useContainer()
18+
const router = useRouter()
19+
20+
const onEnter = useCallback(
21+
(item: NoteModel) => {
22+
router.push(`/${item.id}`, `/${item.id}`, { shallow: true })
23+
close()
24+
},
25+
[router, close]
26+
)
1727

1828
return (
1929
<FilterModal open={visible} onClose={close}>
@@ -23,10 +33,11 @@ const SearchModal: FC = () => {
2333
keyword={keyword}
2434
onClose={close}
2535
/>
26-
<FilterModalList
27-
items={list}
28-
ItemComponent={(item: NoteModel) => (
29-
<SearchItem note={item} keyword={keyword} key={item.id} />
36+
<FilterModalList<NoteModel>
37+
onEnter={onEnter}
38+
items={list ?? []}
39+
ItemComponent={(item, props) => (
40+
<SearchItem note={item} keyword={keyword} key={item.id} {...props} />
3041
)}
3142
/>
3243
</FilterModal>

components/portal/trash-modal/trash-item.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import Link from 'next/link'
2-
import { FC, useCallback } from 'react'
2+
import { FC, useCallback, useRef } from 'react'
33
import { NoteCacheItem } from 'libs/web/cache'
44
import MarkText from 'components/portal/filter-modal/mark-text'
55
import IconButton from 'components/icon-button'
66
import HotkeyTooltip from 'components/hotkey-tooltip'
77
import TrashState from 'libs/web/state/trash'
88
import PortalState from 'libs/web/state/portal'
99
import useI18n from 'libs/web/hooks/use-i18n'
10+
import classNames from 'classnames'
11+
import useScrollView from 'libs/web/hooks/use-scroll-view'
1012

1113
const TrashItem: FC<{
1214
note: NoteCacheItem
1315
keyword?: string
14-
}> = ({ note, keyword }) => {
16+
selected?: boolean
17+
}> = ({ note, keyword, selected }) => {
1518
const { t } = useI18n()
1619
const { restoreNote, deleteNote, filterNotes } = TrashState.useContainer()
1720
const {
1821
trash: { close },
1922
} = PortalState.useContainer()
23+
const ref = useRef<HTMLLIElement>(null)
2024

2125
const onClickRestore = useCallback(async () => {
2226
await restoreNote(note)
@@ -28,9 +32,16 @@ const TrashItem: FC<{
2832
filterNotes(keyword)
2933
}, [deleteNote, note.id, filterNotes, keyword])
3034

35+
useScrollView(ref, selected)
36+
3137
return (
32-
<li className="hover:bg-gray-200 cursor-pointer py-2 px-4 flex">
33-
<Link href={`/${note.id}`}>
38+
<li
39+
ref={ref}
40+
className={classNames('hover:bg-gray-200 cursor-pointer py-2 px-4 flex', {
41+
'bg-gray-300': selected,
42+
})}
43+
>
44+
<Link href={`/${note.id}`} shallow>
3445
<a className=" block text-xs text-gray-500 flex-grow" onClick={close}>
3546
<h4 className="text-sm font-bold">
3647
<MarkText text={note.title} keyword={keyword} />

components/portal/trash-modal/trash-modal.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1-
import { FC, useEffect } from 'react'
1+
import { FC, useCallback, useEffect } from 'react'
22
import FilterModal from 'components/portal/filter-modal/filter-modal'
33
import FilterModalInput from 'components/portal/filter-modal/filter-modal-input'
44
import FilterModalList from 'components/portal/filter-modal/filter-modal-list'
55
import TrashItem from './trash-item'
66
import { NoteModel } from 'libs/shared/note'
77
import TrashState from 'libs/web/state/trash'
88
import PortalState from 'libs/web/state/portal'
9+
import { useRouter } from 'next/router'
910

1011
const TrashModal: FC = () => {
1112
const { filterNotes, keyword, list } = TrashState.useContainer()
1213
const {
1314
trash: { visible, close },
1415
} = PortalState.useContainer()
16+
const router = useRouter()
17+
18+
const onEnter = useCallback(
19+
(item: NoteModel) => {
20+
router.push(`/${item.id}`, `/${item.id}`, { shallow: true })
21+
close()
22+
},
23+
[router, close]
24+
)
1525

1626
useEffect(() => {
1727
if (visible) {
@@ -27,10 +37,11 @@ const TrashModal: FC = () => {
2737
keyword={keyword}
2838
onClose={close}
2939
/>
30-
<FilterModalList
31-
items={list}
32-
ItemComponent={(item: NoteModel) => (
33-
<TrashItem note={item} keyword={keyword} key={item.id} />
40+
<FilterModalList<NoteModel>
41+
onEnter={onEnter}
42+
items={list ?? []}
43+
ItemComponent={(item, props) => (
44+
<TrashItem note={item} keyword={keyword} key={item.id} {...props} />
3445
)}
3546
/>
3647
</FilterModal>

components/sidebar/sidebar-list.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,23 @@ const SideBarList = () => {
4646
[moveItem]
4747
)
4848

49+
const onCreateNote = useCallback(() => {
50+
router.push('/new', undefined, { shallow: true })
51+
}, [])
52+
4953
return (
5054
<section className="h-full flex text-sm flex-col flex-grow bg-gray-100 overflow-hidden">
5155
<div className="p-2 text-gray-500 flex items-center">
5256
<span className="flex-auto">{t('My Pages')}</span>
53-
<HotkeyTooltip text={t('Create page')} keys={['cmd', 'n']}>
57+
<HotkeyTooltip
58+
text={t('Create page')}
59+
commandKey
60+
onHotkey={onCreateNote}
61+
keys={['O']}
62+
>
5463
<IconButton
5564
icon="Plus"
56-
onClick={() => {
57-
router.push('/new', undefined, { shallow: true })
58-
}}
65+
onClick={onCreateNote}
5966
className="text-gray-700"
6067
></IconButton>
6168
</HotkeyTooltip>

0 commit comments

Comments
 (0)