mirror of
https://github.com/docmost/docmost.git
synced 2026-05-17 23:14:07 +08:00
feat(base): keyboard navigation for person cell dropdown
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
|||||||
import { useWorkspaceMembersQuery } from "@/features/workspace/queries/workspace-query";
|
import { useWorkspaceMembersQuery } from "@/features/workspace/queries/workspace-query";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar";
|
import { CustomAvatar } from "@/components/ui/custom-avatar";
|
||||||
import cellClasses from "@/features/base/styles/cells.module.css";
|
import cellClasses from "@/features/base/styles/cells.module.css";
|
||||||
|
import { useListKeyboardNav } from "@/features/base/hooks/use-list-keyboard-nav";
|
||||||
|
|
||||||
type CellPersonProps = {
|
type CellPersonProps = {
|
||||||
value: unknown;
|
value: unknown;
|
||||||
@@ -60,6 +61,8 @@ export function CellPerson({
|
|||||||
)
|
)
|
||||||
: members;
|
: members;
|
||||||
|
|
||||||
|
const nav = useListKeyboardNav(filteredMembers.length, [search, isEditing]);
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(memberId: string) => {
|
(memberId: string) => {
|
||||||
if (allowMultiple) {
|
if (allowMultiple) {
|
||||||
@@ -99,13 +102,21 @@ export function CellPerson({
|
|||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onCancel();
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nav.handleNavKey(e)) return;
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
if (nav.activeIndex < 0 || nav.activeIndex >= filteredMembers.length) return;
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelect(filteredMembers[nav.activeIndex].id);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === "Backspace" && search === "" && personIds.length > 0) {
|
if (e.key === "Backspace" && search === "" && personIds.length > 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleRemove(personIds[personIds.length - 1]);
|
handleRemove(personIds[personIds.length - 1]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onCancel, search, personIds, handleRemove],
|
[onCancel, nav, filteredMembers, handleSelect, search, personIds, handleRemove],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedSet = new Set(personIds);
|
const selectedSet = new Set(personIds);
|
||||||
@@ -170,25 +181,40 @@ export function CellPerson({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={cellClasses.selectDropdown}>
|
<div className={cellClasses.selectDropdown}>
|
||||||
{filteredMembers.map((member) => (
|
{filteredMembers.map((member, idx) => {
|
||||||
<div
|
const isSelected = selectedSet.has(member.id);
|
||||||
key={member.id}
|
const isKeyboardActive = idx === nav.activeIndex;
|
||||||
className={`${cellClasses.selectOption} ${
|
const className = [
|
||||||
selectedSet.has(member.id) ? cellClasses.selectOptionActive : ""
|
cellClasses.selectOption,
|
||||||
}`}
|
isSelected ? cellClasses.selectOptionActive : "",
|
||||||
onClick={() => handleSelect(member.id)}
|
isKeyboardActive ? cellClasses.selectOptionKeyboardActive : "",
|
||||||
>
|
]
|
||||||
<CustomAvatar
|
.filter(Boolean)
|
||||||
avatarUrl={member.avatarUrl}
|
.join(" ");
|
||||||
name={member.name}
|
return (
|
||||||
size={24}
|
<div
|
||||||
radius="xl"
|
key={member.id}
|
||||||
/>
|
ref={nav.setOptionRef(idx)}
|
||||||
<span className={cellClasses.personOptionName}>
|
className={className}
|
||||||
{member.name}
|
onMouseEnter={() => nav.setActiveIndex(idx)}
|
||||||
</span>
|
onMouseDown={(e) => {
|
||||||
</div>
|
// Keep focus on the search input so click doesn't blur + close popover.
|
||||||
))}
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
onClick={() => handleSelect(member.id)}
|
||||||
|
>
|
||||||
|
<CustomAvatar
|
||||||
|
avatarUrl={member.avatarUrl}
|
||||||
|
name={member.name}
|
||||||
|
size={24}
|
||||||
|
radius="xl"
|
||||||
|
/>
|
||||||
|
<span className={cellClasses.personOptionName}>
|
||||||
|
{member.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{filteredMembers.length === 0 && (
|
{filteredMembers.length === 0 && (
|
||||||
<div className={cellClasses.personDropdownHint}>
|
<div className={cellClasses.personDropdownHint}>
|
||||||
No members found
|
No members found
|
||||||
|
|||||||
Reference in New Issue
Block a user