-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPanel.tsx
More file actions
149 lines (137 loc) · 4.96 KB
/
Copy pathPanel.tsx
File metadata and controls
149 lines (137 loc) · 4.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/**
* One of the two Norton-Commander panels: a titled, focusable region showing the
* current folder's path, its entries and a cursor on one of them.
*
* §REQ:TWO-PNL-NVGT-OOX-010 a panel renders the content of its own current folder.
* §REQ:TWO-PNL-NVGT-OOX-020 a panel reports whether it is the active one and asks
* to become active when the user selects it (pointer or keyboard focus).
* §REQ:TWO-PNL-NVGT-OOX-030 opening a folder entry asks the panel's owner to move
* THIS panel to that subfolder.
* §REQ:TWO-PNL-NVGT-OOX-040 the entry under the cursor is the panel's SELECTED
* item, reported to the owner so the commands can act on it.
*/
import { useEffect, useRef, useState, type KeyboardEvent } from 'react'
import type { FilesystemClient } from '../api/filesystemClient'
import type { FsEntry } from '../api/types'
import { usePanelFolder } from '../state/usePanelFolder'
export type PanelSide = 'left' | 'right'
export interface PanelProps {
side: PanelSide
path: string
client: FilesystemClient
isActive: boolean
onActivate: () => void
/** §REQ:TWO-PNL-NVGT-OOX-030 the user asked to open a folder entry of this panel. */
onOpenFolder: (entry: FsEntry) => void
/** §REQ:TWO-PNL-NVGT-OOX-040 the entry the panel's commands would act on. */
onSelectionChange?: (entry: FsEntry | null) => void
/** §REQ:TWO-PNL-NVGT-OOX-040 bumped by the owner after a command changed this folder. */
reloadToken?: number
}
export function panelLabel(side: PanelSide): string {
return side === 'left' ? 'Left panel' : 'Right panel'
}
export function displayPath(path: string): string {
return path === '' ? '/' : `/${path}`
}
export function Panel({
side,
path,
client,
isActive,
onActivate,
onOpenFolder,
onSelectionChange,
reloadToken,
}: PanelProps) {
const folder = usePanelFolder(client, path, reloadToken)
const label = panelLabel(side)
const sectionRef = useRef<HTMLElement>(null)
const [cursor, setCursor] = useState(0)
// §REQ:TWO-PNL-NVGT-OOX-030 a new folder content starts with the cursor at the top
useEffect(() => {
setCursor(0)
}, [path])
const entries = folder.entries
const current = entries[cursor]
// §REQ:TWO-PNL-NVGT-OOX-040 the owner needs the selected item to run a command on it
useEffect(() => {
onSelectionChange?.(current ?? null)
}, [current, onSelectionChange])
// §REQ:TWO-PNL-NVGT-OOX-030 only a FOLDER entry changes the panel's current folder
const open = (entry: FsEntry | undefined) => {
if (entry && entry.type === 'directory') onOpenFolder(entry)
}
const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {
if (event.key === 'ArrowDown') {
event.preventDefault()
setCursor((index) => Math.min(index + 1, Math.max(entries.length - 1, 0)))
} else if (event.key === 'ArrowUp') {
event.preventDefault()
setCursor((index) => Math.max(index - 1, 0))
} else if (event.key === 'Enter') {
// §REQ:TWO-PNL-NVGT-OOX-030 Enter opens the entry under the cursor
event.preventDefault()
open(current)
}
}
return (
<section
ref={sectionRef}
className={`pc-panel${isActive ? ' pc-panel--active' : ''}`}
aria-label={label}
// §REQ:TWO-PNL-NVGT-OOX-020 the selected panel is the one commands act on
aria-current={isActive}
tabIndex={0}
onClick={onActivate}
onFocus={onActivate}
onKeyDown={handleKeyDown}
>
<header className="pc-panel__header">
<span className="pc-panel__path" data-testid="panel-path">
{displayPath(path)}
</span>
</header>
{folder.status === 'error' ? (
<p role="alert" className="pc-panel__error">
{folder.error}
</p>
) : (
<ul
className="pc-panel__list"
role="listbox"
aria-label={`${label} entries`}
aria-activedescendant={current ? entryId(side, current.name) : undefined}
>
{entries.map((entry, index) => (
<li
key={entry.name}
id={entryId(side, entry.name)}
role="option"
aria-selected={index === cursor}
className={`pc-entry pc-entry--${entry.type}${
index === cursor ? ' pc-entry--cursor' : ''
}`}
onClick={() => {
setCursor(index)
sectionRef.current?.focus()
}}
// §REQ:TWO-PNL-NVGT-OOX-030 double-click opens the entry
onDoubleClick={() => open(entry)}
>
<span className="pc-entry__name">
{entry.type === 'directory' ? `[${entry.name}]` : entry.name}
</span>
<span className="pc-entry__size">
{entry.type === 'directory' ? '<DIR>' : entry.size}
</span>
</li>
))}
</ul>
)}
</section>
)
}
function entryId(side: PanelSide, name: string): string {
return `pc-${side}-entry-${encodeURIComponent(name)}`
}