fix(dashboard): Tab behaviour in DataGrid (#8973)
* fix focus in datagrid * cleanup * fix keyboard commands * cleanup * cleanup * always allow handler for special focus to fire * cleanup
This commit is contained in:
committed by
GitHub
parent
f47f1aff49
commit
58c78a7f62
@@ -43,14 +43,12 @@
|
||||
"@uiw/react-json-view": "^2.0.0-alpha.17",
|
||||
"cmdk": "^0.2.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"focus-trap-react": "^10.2.3",
|
||||
"framer-motion": "^11.0.3",
|
||||
"i18next": "23.7.11",
|
||||
"i18next-browser-languagedetector": "7.2.0",
|
||||
"i18next-http-backend": "2.4.2",
|
||||
"lodash": "^4.17.21",
|
||||
"match-sorter": "^6.3.4",
|
||||
"prop-types": "^15.8.1",
|
||||
"qs": "^6.12.0",
|
||||
"react": "^18.2.0",
|
||||
"react-country-flag": "^3.1.0",
|
||||
|
||||
@@ -136,6 +136,20 @@ const useDataGridShortcuts = () => {
|
||||
Windows: ["Shift", "Ctrl", "↑"],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("dataGrid.shortcuts.commands.focusToolbar"),
|
||||
keys: {
|
||||
Mac: ["⌃", "⌥", ","],
|
||||
Windows: ["Ctrl", "Alt", ","],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("dataGrid.shortcuts.commands.focusCancel"),
|
||||
keys: {
|
||||
Mac: ["⌃", "⌥", "."],
|
||||
Windows: ["Ctrl", "Alt", "."],
|
||||
},
|
||||
},
|
||||
],
|
||||
[t]
|
||||
)
|
||||
|
||||
@@ -16,10 +16,8 @@ import {
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { VirtualItem, useVirtualizer } from "@tanstack/react-virtual"
|
||||
import FocusTrap from "focus-trap-react"
|
||||
import {
|
||||
import React, {
|
||||
CSSProperties,
|
||||
MouseEvent,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -48,12 +46,11 @@ import {
|
||||
} from "../hooks"
|
||||
import { DataGridMatrix } from "../models"
|
||||
import { DataGridCoordinates, GridColumnOption } from "../types"
|
||||
import { generateCellId, isCellMatch } from "../utils"
|
||||
import { isCellMatch, isSpecialFocusKey } from "../utils"
|
||||
import { DataGridKeyboardShortcutModal } from "./data-grid-keyboard-shortcut-modal"
|
||||
|
||||
export interface DataGridRootProps<
|
||||
TData,
|
||||
TFieldValues extends FieldValues = FieldValues
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
> {
|
||||
data?: TData[]
|
||||
columns: ColumnDef<TData>[]
|
||||
@@ -98,7 +95,7 @@ const getCommonPinningStyles = <TData,>(
|
||||
|
||||
export const DataGridRoot = <
|
||||
TData,
|
||||
TFieldValues extends FieldValues = FieldValues
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
>({
|
||||
data = [],
|
||||
columns,
|
||||
@@ -117,7 +114,7 @@ export const DataGridRoot = <
|
||||
formState: { errors },
|
||||
} = state
|
||||
|
||||
const [trapActive, setTrapActive] = useState(false)
|
||||
const [trapActive, setTrapActive] = useState(true)
|
||||
|
||||
const [anchor, setAnchor] = useState<DataGridCoordinates | null>(null)
|
||||
const [rangeEnd, setRangeEnd] = useState<DataGridCoordinates | null>(null)
|
||||
@@ -317,25 +314,28 @@ export const DataGridRoot = <
|
||||
anchor,
|
||||
})
|
||||
|
||||
const { handleKeyDownEvent } = useDataGridKeydownEvent<TData, TFieldValues>({
|
||||
matrix,
|
||||
queryTool,
|
||||
anchor,
|
||||
rangeEnd,
|
||||
isEditing,
|
||||
setRangeEnd,
|
||||
getSelectionValues,
|
||||
getValues,
|
||||
setSelectionValues,
|
||||
onEditingChangeHandler,
|
||||
restoreSnapshot,
|
||||
setSingleRange,
|
||||
scrollToCoordinates,
|
||||
execute,
|
||||
undo,
|
||||
redo,
|
||||
setValue,
|
||||
})
|
||||
const { handleKeyDownEvent, handleSpecialFocusKeys } =
|
||||
useDataGridKeydownEvent<TData, TFieldValues>({
|
||||
containerRef,
|
||||
matrix,
|
||||
queryTool,
|
||||
anchor,
|
||||
rangeEnd,
|
||||
isEditing,
|
||||
setTrapActive,
|
||||
setRangeEnd,
|
||||
getSelectionValues,
|
||||
getValues,
|
||||
setSelectionValues,
|
||||
onEditingChangeHandler,
|
||||
restoreSnapshot,
|
||||
setSingleRange,
|
||||
scrollToCoordinates,
|
||||
execute,
|
||||
undo,
|
||||
redo,
|
||||
setValue,
|
||||
})
|
||||
|
||||
const { handleMouseUpEvent } = useDataGridMouseUpEvent<TData, TFieldValues>({
|
||||
matrix,
|
||||
@@ -401,26 +401,20 @@ export const DataGridRoot = <
|
||||
* Register all handlers for the grid.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
|
||||
if (
|
||||
!container ||
|
||||
!container.contains(document.activeElement) ||
|
||||
!trapActive
|
||||
) {
|
||||
if (!trapActive) {
|
||||
return
|
||||
}
|
||||
|
||||
container.addEventListener("keydown", handleKeyDownEvent)
|
||||
container.addEventListener("mouseup", handleMouseUpEvent)
|
||||
window.addEventListener("keydown", handleKeyDownEvent)
|
||||
window.addEventListener("mouseup", handleMouseUpEvent)
|
||||
|
||||
// Copy and paste event listeners need to be added to the window
|
||||
window.addEventListener("copy", handleCopyEvent)
|
||||
window.addEventListener("paste", handlePasteEvent)
|
||||
|
||||
return () => {
|
||||
container.removeEventListener("keydown", handleKeyDownEvent)
|
||||
container.removeEventListener("mouseup", handleMouseUpEvent)
|
||||
window.removeEventListener("keydown", handleKeyDownEvent)
|
||||
window.removeEventListener("mouseup", handleMouseUpEvent)
|
||||
|
||||
window.removeEventListener("copy", handleCopyEvent)
|
||||
window.removeEventListener("paste", handlePasteEvent)
|
||||
@@ -433,12 +427,25 @@ export const DataGridRoot = <
|
||||
handlePasteEvent,
|
||||
])
|
||||
|
||||
const [isHeaderInteractionActive, setIsHeaderInteractionActive] =
|
||||
useState(false)
|
||||
useEffect(() => {
|
||||
const specialFocusHandler = (e: KeyboardEvent) => {
|
||||
if (isSpecialFocusKey(e)) {
|
||||
handleSpecialFocusKeys(e)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", specialFocusHandler)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", specialFocusHandler)
|
||||
}
|
||||
}, [handleSpecialFocusKeys])
|
||||
|
||||
const handleHeaderInteractionChange = useCallback((isActive: boolean) => {
|
||||
setIsHeaderInteractionActive(isActive)
|
||||
setTrapActive(!isActive)
|
||||
if (isActive) {
|
||||
setTrapActive(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
@@ -476,6 +483,7 @@ export const DataGridRoot = <
|
||||
control,
|
||||
trapActive,
|
||||
errors,
|
||||
setTrapActive,
|
||||
setIsSelecting,
|
||||
setIsEditing: onEditingChangeHandler,
|
||||
setSingleRange,
|
||||
@@ -496,6 +504,7 @@ export const DataGridRoot = <
|
||||
control,
|
||||
trapActive,
|
||||
errors,
|
||||
setTrapActive,
|
||||
setIsSelecting,
|
||||
onEditingChangeHandler,
|
||||
setSingleRange,
|
||||
@@ -513,6 +522,19 @@ export const DataGridRoot = <
|
||||
]
|
||||
)
|
||||
|
||||
const handleRestoreGridFocus = useCallback(() => {
|
||||
if (anchor && !trapActive) {
|
||||
setTrapActive(true)
|
||||
|
||||
setSingleRange(anchor)
|
||||
scrollToCoordinates(anchor, "both")
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
queryTool?.getContainer(anchor)?.focus()
|
||||
})
|
||||
}
|
||||
}, [anchor, trapActive, queryTool])
|
||||
|
||||
return (
|
||||
<DataGridContext.Provider value={values}>
|
||||
<div className="bg-ui-bg-subtle flex size-full flex-col">
|
||||
@@ -526,178 +548,116 @@ export const DataGridRoot = <
|
||||
isHighlighted={isHighlighted}
|
||||
onHeaderInteractionChange={handleHeaderInteractionChange}
|
||||
/>
|
||||
<FocusTrap
|
||||
active={trapActive && !isHeaderInteractionActive}
|
||||
focusTrapOptions={{
|
||||
initialFocus: () => {
|
||||
if (!anchor) {
|
||||
const coords = matrix.getFirstNavigableCell()
|
||||
|
||||
if (!coords) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const id = generateCellId(coords)
|
||||
|
||||
return containerRef.current?.querySelector(
|
||||
`[data-container-id="${id}"]`
|
||||
)
|
||||
}
|
||||
|
||||
const id = generateCellId(anchor)
|
||||
|
||||
const anchorContainer = containerRef.current?.querySelector(
|
||||
`[data-container-id="${id}`
|
||||
) as HTMLElement | null
|
||||
|
||||
return anchorContainer ?? undefined
|
||||
},
|
||||
onActivate: () => setTrapActive(true),
|
||||
onDeactivate: () => setTrapActive(false),
|
||||
fallbackFocus: () => {
|
||||
if (!anchor) {
|
||||
const coords = matrix.getFirstNavigableCell()
|
||||
|
||||
if (!coords) {
|
||||
return containerRef.current!
|
||||
}
|
||||
|
||||
const id = generateCellId(coords)
|
||||
|
||||
const firstCell = containerRef.current?.querySelector(
|
||||
`[data-container-id="${id}"]`
|
||||
) as HTMLElement | null
|
||||
|
||||
if (firstCell) {
|
||||
return firstCell
|
||||
}
|
||||
|
||||
return containerRef.current!
|
||||
}
|
||||
|
||||
const id = generateCellId(anchor)
|
||||
|
||||
const anchorContainer = containerRef.current?.querySelector(
|
||||
`[data-container-id="${id}`
|
||||
) as HTMLElement | null
|
||||
|
||||
if (anchorContainer) {
|
||||
return anchorContainer
|
||||
}
|
||||
|
||||
return containerRef.current!
|
||||
},
|
||||
allowOutsideClick: true,
|
||||
escapeDeactivates: false,
|
||||
}}
|
||||
>
|
||||
<div className="size-full overflow-hidden">
|
||||
<div
|
||||
ref={containerRef}
|
||||
tabIndex={-1}
|
||||
onFocus={() => !trapActive && setTrapActive(true)}
|
||||
className="relative h-full select-none overflow-auto outline-none"
|
||||
>
|
||||
<div role="grid" className="text-ui-fg-subtle grid">
|
||||
<div
|
||||
role="rowgroup"
|
||||
className="txt-compact-small-plus bg-ui-bg-subtle sticky top-0 z-[1] grid"
|
||||
>
|
||||
{grid.getHeaderGroups().map((headerGroup) => (
|
||||
<div
|
||||
role="row"
|
||||
key={headerGroup.id}
|
||||
className="flex h-10 w-full"
|
||||
>
|
||||
{virtualPaddingLeft ? (
|
||||
<div
|
||||
role="presentation"
|
||||
style={{ display: "flex", width: virtualPaddingLeft }}
|
||||
/>
|
||||
) : null}
|
||||
{virtualColumns.reduce((acc, vc, index, array) => {
|
||||
const header = headerGroup.headers[vc.index]
|
||||
const previousVC = array[index - 1]
|
||||
|
||||
if (previousVC && vc.index !== previousVC.index + 1) {
|
||||
// If there's a gap between the current and previous virtual columns
|
||||
acc.push(
|
||||
<div
|
||||
key={`padding-${previousVC.index}-${vc.index}`}
|
||||
role="presentation"
|
||||
style={{
|
||||
display: "flex",
|
||||
width: `${vc.start - previousVC.end}px`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className="size-full overflow-hidden">
|
||||
<div
|
||||
ref={containerRef}
|
||||
autoFocus
|
||||
tabIndex={0}
|
||||
className="relative h-full select-none overflow-auto outline-none"
|
||||
onFocus={handleRestoreGridFocus}
|
||||
onClick={handleRestoreGridFocus}
|
||||
data-container={true}
|
||||
role="application"
|
||||
>
|
||||
<div role="grid" className="text-ui-fg-subtle grid">
|
||||
<div
|
||||
role="rowgroup"
|
||||
className="txt-compact-small-plus bg-ui-bg-subtle sticky top-0 z-[1] grid"
|
||||
>
|
||||
{grid.getHeaderGroups().map((headerGroup) => (
|
||||
<div
|
||||
role="row"
|
||||
key={headerGroup.id}
|
||||
className="flex h-10 w-full"
|
||||
>
|
||||
{virtualPaddingLeft ? (
|
||||
<div
|
||||
role="presentation"
|
||||
style={{ display: "flex", width: virtualPaddingLeft }}
|
||||
/>
|
||||
) : null}
|
||||
{virtualColumns.reduce((acc, vc, index, array) => {
|
||||
const header = headerGroup.headers[vc.index]
|
||||
const previousVC = array[index - 1]
|
||||
|
||||
if (previousVC && vc.index !== previousVC.index + 1) {
|
||||
// If there's a gap between the current and previous virtual columns
|
||||
acc.push(
|
||||
<div
|
||||
key={header.id}
|
||||
role="columnheader"
|
||||
data-column-index={vc.index}
|
||||
key={`padding-${previousVC.index}-${vc.index}`}
|
||||
role="presentation"
|
||||
style={{
|
||||
width: header.getSize(),
|
||||
...getCommonPinningStyles(header.column),
|
||||
display: "flex",
|
||||
width: `${vc.start - previousVC.end}px`,
|
||||
}}
|
||||
className="bg-ui-bg-base txt-compact-small-plus flex items-center border-b border-r px-4 py-2.5"
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</div>
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [] as ReactNode[])}
|
||||
{virtualPaddingRight ? (
|
||||
acc.push(
|
||||
<div
|
||||
role="presentation"
|
||||
key={header.id}
|
||||
role="columnheader"
|
||||
data-column-index={vc.index}
|
||||
style={{
|
||||
display: "flex",
|
||||
width: virtualPaddingRight,
|
||||
width: header.getSize(),
|
||||
...getCommonPinningStyles(header.column),
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
role="rowgroup"
|
||||
className="relative grid"
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
}}
|
||||
>
|
||||
{virtualRows.map((virtualRow) => {
|
||||
const row = visibleRows[virtualRow.index] as Row<TData>
|
||||
const rowIndex = flatRows.findIndex((r) => r.id === row.id)
|
||||
className="bg-ui-bg-base txt-compact-small-plus flex items-center border-b border-r px-4 py-2.5"
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<DataGridRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
rowIndex={rowIndex}
|
||||
virtualRow={virtualRow}
|
||||
flatColumns={flatColumns}
|
||||
virtualColumns={virtualColumns}
|
||||
anchor={anchor}
|
||||
virtualPaddingLeft={virtualPaddingLeft}
|
||||
virtualPaddingRight={virtualPaddingRight}
|
||||
onDragToFillStart={onDragToFillStart}
|
||||
return acc
|
||||
}, [] as ReactNode[])}
|
||||
{virtualPaddingRight ? (
|
||||
<div
|
||||
role="presentation"
|
||||
style={{
|
||||
display: "flex",
|
||||
width: virtualPaddingRight,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
role="rowgroup"
|
||||
className="relative grid"
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
}}
|
||||
>
|
||||
{virtualRows.map((virtualRow) => {
|
||||
const row = visibleRows[virtualRow.index] as Row<TData>
|
||||
const rowIndex = flatRows.findIndex((r) => r.id === row.id)
|
||||
|
||||
return (
|
||||
<DataGridRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
rowIndex={rowIndex}
|
||||
virtualRow={virtualRow}
|
||||
flatColumns={flatColumns}
|
||||
virtualColumns={virtualColumns}
|
||||
anchor={anchor}
|
||||
virtualPaddingLeft={virtualPaddingLeft}
|
||||
virtualPaddingRight={virtualPaddingRight}
|
||||
onDragToFillStart={onDragToFillStart}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FocusTrap>
|
||||
</div>
|
||||
</div>
|
||||
</DataGridContext.Provider>
|
||||
)
|
||||
@@ -783,6 +743,7 @@ const DataGridHeader = ({
|
||||
type="button"
|
||||
onClick={onResetColumns}
|
||||
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
||||
data-id="reset-columns"
|
||||
>
|
||||
{t("dataGrid.columns.resetToDefault")}
|
||||
</Button>
|
||||
@@ -821,7 +782,7 @@ type DataGridCellProps<TData> = {
|
||||
columnIndex: number
|
||||
rowIndex: number
|
||||
anchor: DataGridCoordinates | null
|
||||
onDragToFillStart: (e: MouseEvent<HTMLElement>) => void
|
||||
onDragToFillStart: (e: React.MouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
const DataGridCell = <TData,>({
|
||||
@@ -880,7 +841,7 @@ type DataGridRowProps<TData> = {
|
||||
virtualColumns: VirtualItem<Element>[]
|
||||
flatColumns: Column<TData, unknown>[]
|
||||
anchor: DataGridCoordinates | null
|
||||
onDragToFillStart: (e: MouseEvent<HTMLElement>) => void
|
||||
onDragToFillStart: (e: React.MouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
const DataGridRow = <TData,>({
|
||||
|
||||
@@ -12,6 +12,7 @@ type DataGridContextType<TFieldValues extends FieldValues> = {
|
||||
// Grid state
|
||||
anchor: DataGridCoordinates | null
|
||||
trapActive: boolean
|
||||
setTrapActive: (value: boolean) => void
|
||||
errors: FieldErrors<TFieldValues>
|
||||
// Cell handlers
|
||||
getIsCellSelected: (coords: DataGridCoordinates) => boolean
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
DataGridCellRenderProps,
|
||||
DataGridCoordinates,
|
||||
} from "../types"
|
||||
import { isCellMatch } from "../utils"
|
||||
import { isCellMatch, isSpecialFocusKey } from "../utils"
|
||||
|
||||
type UseDataGridCellOptions<TData, TValue> = {
|
||||
context: CellContext<TData, TValue>
|
||||
@@ -162,6 +162,10 @@ export const useDataGridCell = <TData, TValue>({
|
||||
return
|
||||
}
|
||||
|
||||
if (isSpecialFocusKey(e.nativeEvent)) {
|
||||
return
|
||||
}
|
||||
|
||||
const event = new KeyboardEvent(e.type, e.nativeEvent)
|
||||
|
||||
inputRef.current.focus()
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { DataGridCoordinates } from "../types"
|
||||
|
||||
type UseDataGridKeydownEventOptions<TData, TFieldValues extends FieldValues> = {
|
||||
containerRef: React.RefObject<HTMLDivElement>
|
||||
matrix: DataGridMatrix<TData, TFieldValues>
|
||||
anchor: DataGridCoordinates | null
|
||||
rangeEnd: DataGridCoordinates | null
|
||||
@@ -23,6 +24,7 @@ type UseDataGridKeydownEventOptions<TData, TFieldValues extends FieldValues> = {
|
||||
coords: DataGridCoordinates,
|
||||
direction: "horizontal" | "vertical" | "both"
|
||||
) => void
|
||||
setTrapActive: (active: boolean) => void
|
||||
setSingleRange: (coordinates: DataGridCoordinates | null) => void
|
||||
setRangeEnd: (coordinates: DataGridCoordinates | null) => void
|
||||
onEditingChangeHandler: (value: boolean) => void
|
||||
@@ -44,12 +46,14 @@ const VERTICAL_KEYS = ["ArrowUp", "ArrowDown"]
|
||||
|
||||
export const useDataGridKeydownEvent = <
|
||||
TData,
|
||||
TFieldValues extends FieldValues
|
||||
TFieldValues extends FieldValues,
|
||||
>({
|
||||
containerRef,
|
||||
matrix,
|
||||
anchor,
|
||||
rangeEnd,
|
||||
isEditing,
|
||||
setTrapActive,
|
||||
scrollToCoordinates,
|
||||
setSingleRange,
|
||||
setRangeEnd,
|
||||
@@ -104,8 +108,8 @@ export const useDataGridKeydownEvent = <
|
||||
direction === "horizontal"
|
||||
? setSingleRange
|
||||
: e.shiftKey
|
||||
? setRangeEnd
|
||||
: setSingleRange
|
||||
? setRangeEnd
|
||||
: setSingleRange
|
||||
|
||||
if (!basis) {
|
||||
return
|
||||
@@ -115,6 +119,7 @@ export const useDataGridKeydownEvent = <
|
||||
|
||||
const handleNavigation = (coords: DataGridCoordinates) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
scrollToCoordinates(coords, direction)
|
||||
updater(coords)
|
||||
@@ -140,6 +145,33 @@ export const useDataGridKeydownEvent = <
|
||||
]
|
||||
)
|
||||
|
||||
const handleTabKey = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!anchor) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const { row, col } = anchor
|
||||
|
||||
const key = e.shiftKey ? "ArrowLeft" : "ArrowRight"
|
||||
const direction = "horizontal"
|
||||
|
||||
const next = matrix.getValidMovement(
|
||||
row,
|
||||
col,
|
||||
key,
|
||||
e.metaKey || e.ctrlKey
|
||||
)
|
||||
|
||||
scrollToCoordinates(next, direction)
|
||||
setSingleRange(next)
|
||||
},
|
||||
[anchor, scrollToCoordinates, setSingleRange, matrix]
|
||||
)
|
||||
|
||||
const handleUndo = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -460,27 +492,33 @@ export const useDataGridKeydownEvent = <
|
||||
[queryTool, isEditing, anchor, restoreSnapshot]
|
||||
)
|
||||
|
||||
const handleTabKey = useCallback(
|
||||
const handleSpecialFocusKeys = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!anchor || isEditing) {
|
||||
if (!containerRef || isEditing) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
const focusableElements = getFocusableElements(containerRef)
|
||||
|
||||
const direction = e.shiftKey ? "ArrowLeft" : "ArrowRight"
|
||||
const focusElement = (element: HTMLElement | null) => {
|
||||
if (element) {
|
||||
setTrapActive(false)
|
||||
element.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const next = matrix.getValidMovement(
|
||||
anchor.row,
|
||||
anchor.col,
|
||||
direction,
|
||||
e.metaKey || e.ctrlKey
|
||||
)
|
||||
|
||||
setSingleRange(next)
|
||||
scrollToCoordinates(next, "horizontal")
|
||||
switch (e.key) {
|
||||
case ".":
|
||||
focusElement(focusableElements.cancel)
|
||||
break
|
||||
case ",":
|
||||
focusElement(focusableElements.shortcuts)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
[anchor, isEditing, scrollToCoordinates, setSingleRange, matrix]
|
||||
[anchor, isEditing, setTrapActive, containerRef]
|
||||
)
|
||||
|
||||
const handleKeyDownEvent = useCallback(
|
||||
@@ -533,5 +571,29 @@ export const useDataGridKeydownEvent = <
|
||||
|
||||
return {
|
||||
handleKeyDownEvent,
|
||||
handleSpecialFocusKeys,
|
||||
}
|
||||
}
|
||||
|
||||
function getFocusableElements(ref: React.RefObject<HTMLDivElement>) {
|
||||
const focusableElements = Array.from(
|
||||
document.querySelectorAll<HTMLElement>(
|
||||
"[tabindex], a, button, input, select, textarea"
|
||||
)
|
||||
)
|
||||
|
||||
const currentElementIndex = focusableElements.indexOf(ref.current!)
|
||||
|
||||
const shortcuts =
|
||||
currentElementIndex > 0 ? focusableElements[currentElementIndex - 1] : null
|
||||
|
||||
let cancel = null
|
||||
for (let i = currentElementIndex + 1; i < focusableElements.length; i++) {
|
||||
if (!ref.current!.contains(focusableElements[i])) {
|
||||
cancel = focusableElements[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return { shortcuts, cancel }
|
||||
}
|
||||
|
||||
@@ -20,3 +20,9 @@ export function isCellMatch(
|
||||
|
||||
return cell.row === coords.row && cell.col === coords.col
|
||||
}
|
||||
|
||||
const SPECIAL_FOCUS_KEYS = [".", ","]
|
||||
|
||||
export function isSpecialFocusKey(event: KeyboardEvent) {
|
||||
return SPECIAL_FOCUS_KEYS.includes(event.key) && event.ctrlKey && event.altKey
|
||||
}
|
||||
@@ -243,7 +243,9 @@
|
||||
"selectDown": "Select down",
|
||||
"selectUp": "Select up",
|
||||
"selectColumnDown": "Select column down",
|
||||
"selectColumnUp": "Select column up"
|
||||
"selectColumnUp": "Select column up",
|
||||
"focusToolbar": "Focus toolbar",
|
||||
"focusCancel": "Focus cancel"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
@@ -576,7 +578,6 @@
|
||||
"successToast": "Option {{title}} was successfully created."
|
||||
},
|
||||
"deleteWarning": "You are about to delete the product option: {{title}}. This action cannot be undone."
|
||||
|
||||
},
|
||||
"organization": {
|
||||
"header": "Organize",
|
||||
|
||||
32
yarn.lock
32
yarn.lock
@@ -4551,7 +4551,6 @@ __metadata:
|
||||
autoprefixer: ^10.4.17
|
||||
cmdk: ^0.2.0
|
||||
date-fns: ^3.6.0
|
||||
focus-trap-react: ^10.2.3
|
||||
framer-motion: ^11.0.3
|
||||
i18next: 23.7.11
|
||||
i18next-browser-languagedetector: 7.2.0
|
||||
@@ -4560,7 +4559,6 @@ __metadata:
|
||||
match-sorter: ^6.3.4
|
||||
postcss: ^8.4.33
|
||||
prettier: ^3.1.1
|
||||
prop-types: ^15.8.1
|
||||
qs: ^6.12.0
|
||||
react: ^18.2.0
|
||||
react-country-flag: ^3.1.0
|
||||
@@ -18451,29 +18449,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"focus-trap-react@npm:^10.2.3":
|
||||
version: 10.2.3
|
||||
resolution: "focus-trap-react@npm:10.2.3"
|
||||
dependencies:
|
||||
focus-trap: ^7.5.4
|
||||
tabbable: ^6.2.0
|
||||
peerDependencies:
|
||||
prop-types: ^15.8.1
|
||||
react: ">=16.3.0"
|
||||
react-dom: ">=16.3.0"
|
||||
checksum: 18527771cacc1083ecdf472276a4c46581a6289dc98b6caf1c31641091732933cbffdec78e4c86c2c1568ac4f82a83c367d6a4d7e0e84dc96b239d2d4c12c071
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"focus-trap@npm:^7.5.4":
|
||||
version: 7.5.4
|
||||
resolution: "focus-trap@npm:7.5.4"
|
||||
dependencies:
|
||||
tabbable: ^6.2.0
|
||||
checksum: c09e12b957862b2608977ff90de782645f99c3555cc5d93977240c179befa8723b9b1183e93890b4ad9d364d52a1af36416e63a728522ecce656a447d9ddd945
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.4, follow-redirects@npm:^1.15.6":
|
||||
version: 1.15.6
|
||||
resolution: "follow-redirects@npm:1.15.6"
|
||||
@@ -28925,13 +28900,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tabbable@npm:^6.2.0":
|
||||
version: 6.2.0
|
||||
resolution: "tabbable@npm:6.2.0"
|
||||
checksum: ced8b38f05f2de62cd46836d77c2646c42b8c9713f5bd265daf0e78ff5ac73d3ba48a7ca45f348bafeef29b23da7187c72250742d37627883ef89cbd7fa76898
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"table@npm:^6.0.9":
|
||||
version: 6.8.2
|
||||
resolution: "table@npm:6.8.2"
|
||||
|
||||
Reference in New Issue
Block a user