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:
1 parent
f47f1aff49
commit
58c78a7f62
9 files changed
+161
-146
No files matched your search
@@ -43,14 +43,12 @@
|
|||||||
"@uiw/react-json-view": "^2.0.0-alpha.17",
|
"@uiw/react-json-view": "^2.0.0-alpha.17",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"focus-trap-react": "^10.2.3",
|
|
||||||
"framer-motion": "^11.0.3",
|
"framer-motion": "^11.0.3",
|
||||||
"i18next": "23.7.11",
|
"i18next": "23.7.11",
|
||||||
"i18next-browser-languagedetector": "7.2.0",
|
"i18next-browser-languagedetector": "7.2.0",
|
||||||
"i18next-http-backend": "2.4.2",
|
"i18next-http-backend": "2.4.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"match-sorter": "^6.3.4",
|
"match-sorter": "^6.3.4",
|
||||||
"prop-types": "^15.8.1",
|
|
||||||
"qs": "^6.12.0",
|
"qs": "^6.12.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-country-flag": "^3.1.0",
|
"react-country-flag": "^3.1.0",
|
||||||
|
|||||||
+14
@@ -136,6 +136,20 @@ const useDataGridShortcuts = () => {
|
|||||||
Windows: ["Shift", "Ctrl", "↑"],
|
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]
|
[t]
|
||||||
)
|
)
|
||||||
|
|||||||
+55
-94
@@ -16,10 +16,8 @@ import {
|
|||||||
useReactTable,
|
useReactTable,
|
||||||
} from "@tanstack/react-table"
|
} from "@tanstack/react-table"
|
||||||
import { VirtualItem, useVirtualizer } from "@tanstack/react-virtual"
|
import { VirtualItem, useVirtualizer } from "@tanstack/react-virtual"
|
||||||
import FocusTrap from "focus-trap-react"
|
import React, {
|
||||||
import {
|
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
MouseEvent,
|
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -48,12 +46,11 @@ import {
|
|||||||
} from "../hooks"
|
} from "../hooks"
|
||||||
import { DataGridMatrix } from "../models"
|
import { DataGridMatrix } from "../models"
|
||||||
import { DataGridCoordinates, GridColumnOption } from "../types"
|
import { DataGridCoordinates, GridColumnOption } from "../types"
|
||||||
import { generateCellId, isCellMatch } from "../utils"
|
import { isCellMatch, isSpecialFocusKey } from "../utils"
|
||||||
import { DataGridKeyboardShortcutModal } from "./data-grid-keyboard-shortcut-modal"
|
import { DataGridKeyboardShortcutModal } from "./data-grid-keyboard-shortcut-modal"
|
||||||
|
|
||||||
export interface DataGridRootProps<
|
export interface DataGridRootProps<
|
||||||
TData,
|
TData,
|
||||||
TFieldValues extends FieldValues = FieldValues
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
> {
|
> {
|
||||||
data?: TData[]
|
data?: TData[]
|
||||||
columns: ColumnDef<TData>[]
|
columns: ColumnDef<TData>[]
|
||||||
@@ -98,7 +95,7 @@ const getCommonPinningStyles = <TData,>(
|
|||||||
|
|
||||||
export const DataGridRoot = <
|
export const DataGridRoot = <
|
||||||
TData,
|
TData,
|
||||||
TFieldValues extends FieldValues = FieldValues
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
>({
|
>({
|
||||||
data = [],
|
data = [],
|
||||||
columns,
|
columns,
|
||||||
@@ -117,7 +114,7 @@ export const DataGridRoot = <
|
|||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = state
|
} = state
|
||||||
|
|
||||||
const [trapActive, setTrapActive] = useState(false)
|
const [trapActive, setTrapActive] = useState(true)
|
||||||
|
|
||||||
const [anchor, setAnchor] = useState<DataGridCoordinates | null>(null)
|
const [anchor, setAnchor] = useState<DataGridCoordinates | null>(null)
|
||||||
const [rangeEnd, setRangeEnd] = useState<DataGridCoordinates | null>(null)
|
const [rangeEnd, setRangeEnd] = useState<DataGridCoordinates | null>(null)
|
||||||
@@ -317,12 +314,15 @@ export const DataGridRoot = <
|
|||||||
anchor,
|
anchor,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { handleKeyDownEvent } = useDataGridKeydownEvent<TData, TFieldValues>({
|
const { handleKeyDownEvent, handleSpecialFocusKeys } =
|
||||||
|
useDataGridKeydownEvent<TData, TFieldValues>({
|
||||||
|
containerRef,
|
||||||
matrix,
|
matrix,
|
||||||
queryTool,
|
queryTool,
|
||||||
anchor,
|
anchor,
|
||||||
rangeEnd,
|
rangeEnd,
|
||||||
isEditing,
|
isEditing,
|
||||||
|
setTrapActive,
|
||||||
setRangeEnd,
|
setRangeEnd,
|
||||||
getSelectionValues,
|
getSelectionValues,
|
||||||
getValues,
|
getValues,
|
||||||
@@ -401,26 +401,20 @@ export const DataGridRoot = <
|
|||||||
* Register all handlers for the grid.
|
* Register all handlers for the grid.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current
|
if (!trapActive) {
|
||||||
|
|
||||||
if (
|
|
||||||
!container ||
|
|
||||||
!container.contains(document.activeElement) ||
|
|
||||||
!trapActive
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
container.addEventListener("keydown", handleKeyDownEvent)
|
window.addEventListener("keydown", handleKeyDownEvent)
|
||||||
container.addEventListener("mouseup", handleMouseUpEvent)
|
window.addEventListener("mouseup", handleMouseUpEvent)
|
||||||
|
|
||||||
// Copy and paste event listeners need to be added to the window
|
// Copy and paste event listeners need to be added to the window
|
||||||
window.addEventListener("copy", handleCopyEvent)
|
window.addEventListener("copy", handleCopyEvent)
|
||||||
window.addEventListener("paste", handlePasteEvent)
|
window.addEventListener("paste", handlePasteEvent)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
container.removeEventListener("keydown", handleKeyDownEvent)
|
window.removeEventListener("keydown", handleKeyDownEvent)
|
||||||
container.removeEventListener("mouseup", handleMouseUpEvent)
|
window.removeEventListener("mouseup", handleMouseUpEvent)
|
||||||
|
|
||||||
window.removeEventListener("copy", handleCopyEvent)
|
window.removeEventListener("copy", handleCopyEvent)
|
||||||
window.removeEventListener("paste", handlePasteEvent)
|
window.removeEventListener("paste", handlePasteEvent)
|
||||||
@@ -433,12 +427,25 @@ export const DataGridRoot = <
|
|||||||
handlePasteEvent,
|
handlePasteEvent,
|
||||||
])
|
])
|
||||||
|
|
||||||
const [isHeaderInteractionActive, setIsHeaderInteractionActive] =
|
useEffect(() => {
|
||||||
useState(false)
|
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) => {
|
const handleHeaderInteractionChange = useCallback((isActive: boolean) => {
|
||||||
setIsHeaderInteractionActive(isActive)
|
if (isActive) {
|
||||||
setTrapActive(!isActive)
|
setTrapActive(false)
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -476,6 +483,7 @@ export const DataGridRoot = <
|
|||||||
control,
|
control,
|
||||||
trapActive,
|
trapActive,
|
||||||
errors,
|
errors,
|
||||||
|
setTrapActive,
|
||||||
setIsSelecting,
|
setIsSelecting,
|
||||||
setIsEditing: onEditingChangeHandler,
|
setIsEditing: onEditingChangeHandler,
|
||||||
setSingleRange,
|
setSingleRange,
|
||||||
@@ -496,6 +504,7 @@ export const DataGridRoot = <
|
|||||||
control,
|
control,
|
||||||
trapActive,
|
trapActive,
|
||||||
errors,
|
errors,
|
||||||
|
setTrapActive,
|
||||||
setIsSelecting,
|
setIsSelecting,
|
||||||
onEditingChangeHandler,
|
onEditingChangeHandler,
|
||||||
setSingleRange,
|
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 (
|
return (
|
||||||
<DataGridContext.Provider value={values}>
|
<DataGridContext.Provider value={values}>
|
||||||
<div className="bg-ui-bg-subtle flex size-full flex-col">
|
<div className="bg-ui-bg-subtle flex size-full flex-col">
|
||||||
@@ -526,77 +548,16 @@ export const DataGridRoot = <
|
|||||||
isHighlighted={isHighlighted}
|
isHighlighted={isHighlighted}
|
||||||
onHeaderInteractionChange={handleHeaderInteractionChange}
|
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 className="size-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
tabIndex={-1}
|
autoFocus
|
||||||
onFocus={() => !trapActive && setTrapActive(true)}
|
tabIndex={0}
|
||||||
className="relative h-full select-none overflow-auto outline-none"
|
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="grid" className="text-ui-fg-subtle grid">
|
||||||
<div
|
<div
|
||||||
@@ -697,7 +658,6 @@ export const DataGridRoot = <
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FocusTrap>
|
|
||||||
</div>
|
</div>
|
||||||
</DataGridContext.Provider>
|
</DataGridContext.Provider>
|
||||||
)
|
)
|
||||||
@@ -783,6 +743,7 @@ const DataGridHeader = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onResetColumns}
|
onClick={onResetColumns}
|
||||||
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
||||||
|
data-id="reset-columns"
|
||||||
>
|
>
|
||||||
{t("dataGrid.columns.resetToDefault")}
|
{t("dataGrid.columns.resetToDefault")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -821,7 +782,7 @@ type DataGridCellProps<TData> = {
|
|||||||
columnIndex: number
|
columnIndex: number
|
||||||
rowIndex: number
|
rowIndex: number
|
||||||
anchor: DataGridCoordinates | null
|
anchor: DataGridCoordinates | null
|
||||||
onDragToFillStart: (e: MouseEvent<HTMLElement>) => void
|
onDragToFillStart: (e: React.MouseEvent<HTMLElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataGridCell = <TData,>({
|
const DataGridCell = <TData,>({
|
||||||
@@ -880,7 +841,7 @@ type DataGridRowProps<TData> = {
|
|||||||
virtualColumns: VirtualItem<Element>[]
|
virtualColumns: VirtualItem<Element>[]
|
||||||
flatColumns: Column<TData, unknown>[]
|
flatColumns: Column<TData, unknown>[]
|
||||||
anchor: DataGridCoordinates | null
|
anchor: DataGridCoordinates | null
|
||||||
onDragToFillStart: (e: MouseEvent<HTMLElement>) => void
|
onDragToFillStart: (e: React.MouseEvent<HTMLElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataGridRow = <TData,>({
|
const DataGridRow = <TData,>({
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type DataGridContextType<TFieldValues extends FieldValues> = {
|
|||||||
// Grid state
|
// Grid state
|
||||||
anchor: DataGridCoordinates | null
|
anchor: DataGridCoordinates | null
|
||||||
trapActive: boolean
|
trapActive: boolean
|
||||||
|
setTrapActive: (value: boolean) => void
|
||||||
errors: FieldErrors<TFieldValues>
|
errors: FieldErrors<TFieldValues>
|
||||||
// Cell handlers
|
// Cell handlers
|
||||||
getIsCellSelected: (coords: DataGridCoordinates) => boolean
|
getIsCellSelected: (coords: DataGridCoordinates) => boolean
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
DataGridCellRenderProps,
|
DataGridCellRenderProps,
|
||||||
DataGridCoordinates,
|
DataGridCoordinates,
|
||||||
} from "../types"
|
} from "../types"
|
||||||
import { isCellMatch } from "../utils"
|
import { isCellMatch, isSpecialFocusKey } from "../utils"
|
||||||
|
|
||||||
type UseDataGridCellOptions<TData, TValue> = {
|
type UseDataGridCellOptions<TData, TValue> = {
|
||||||
context: CellContext<TData, TValue>
|
context: CellContext<TData, TValue>
|
||||||
@@ -162,6 +162,10 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSpecialFocusKey(e.nativeEvent)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const event = new KeyboardEvent(e.type, e.nativeEvent)
|
const event = new KeyboardEvent(e.type, e.nativeEvent)
|
||||||
|
|
||||||
inputRef.current.focus()
|
inputRef.current.focus()
|
||||||
|
|||||||
+77
-15
@@ -15,6 +15,7 @@ import {
|
|||||||
import { DataGridCoordinates } from "../types"
|
import { DataGridCoordinates } from "../types"
|
||||||
|
|
||||||
type UseDataGridKeydownEventOptions<TData, TFieldValues extends FieldValues> = {
|
type UseDataGridKeydownEventOptions<TData, TFieldValues extends FieldValues> = {
|
||||||
|
containerRef: React.RefObject<HTMLDivElement>
|
||||||
matrix: DataGridMatrix<TData, TFieldValues>
|
matrix: DataGridMatrix<TData, TFieldValues>
|
||||||
anchor: DataGridCoordinates | null
|
anchor: DataGridCoordinates | null
|
||||||
rangeEnd: DataGridCoordinates | null
|
rangeEnd: DataGridCoordinates | null
|
||||||
@@ -23,6 +24,7 @@ type UseDataGridKeydownEventOptions<TData, TFieldValues extends FieldValues> = {
|
|||||||
coords: DataGridCoordinates,
|
coords: DataGridCoordinates,
|
||||||
direction: "horizontal" | "vertical" | "both"
|
direction: "horizontal" | "vertical" | "both"
|
||||||
) => void
|
) => void
|
||||||
|
setTrapActive: (active: boolean) => void
|
||||||
setSingleRange: (coordinates: DataGridCoordinates | null) => void
|
setSingleRange: (coordinates: DataGridCoordinates | null) => void
|
||||||
setRangeEnd: (coordinates: DataGridCoordinates | null) => void
|
setRangeEnd: (coordinates: DataGridCoordinates | null) => void
|
||||||
onEditingChangeHandler: (value: boolean) => void
|
onEditingChangeHandler: (value: boolean) => void
|
||||||
@@ -44,12 +46,14 @@ const VERTICAL_KEYS = ["ArrowUp", "ArrowDown"]
|
|||||||
|
|
||||||
export const useDataGridKeydownEvent = <
|
export const useDataGridKeydownEvent = <
|
||||||
TData,
|
TData,
|
||||||
TFieldValues extends FieldValues
|
TFieldValues extends FieldValues,
|
||||||
>({
|
>({
|
||||||
|
containerRef,
|
||||||
matrix,
|
matrix,
|
||||||
anchor,
|
anchor,
|
||||||
rangeEnd,
|
rangeEnd,
|
||||||
isEditing,
|
isEditing,
|
||||||
|
setTrapActive,
|
||||||
scrollToCoordinates,
|
scrollToCoordinates,
|
||||||
setSingleRange,
|
setSingleRange,
|
||||||
setRangeEnd,
|
setRangeEnd,
|
||||||
@@ -115,6 +119,7 @@ export const useDataGridKeydownEvent = <
|
|||||||
|
|
||||||
const handleNavigation = (coords: DataGridCoordinates) => {
|
const handleNavigation = (coords: DataGridCoordinates) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
scrollToCoordinates(coords, direction)
|
scrollToCoordinates(coords, direction)
|
||||||
updater(coords)
|
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(
|
const handleUndo = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -460,27 +492,33 @@ export const useDataGridKeydownEvent = <
|
|||||||
[queryTool, isEditing, anchor, restoreSnapshot]
|
[queryTool, isEditing, anchor, restoreSnapshot]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleTabKey = useCallback(
|
const handleSpecialFocusKeys = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (!anchor || isEditing) {
|
if (!containerRef || isEditing) {
|
||||||
return
|
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(
|
switch (e.key) {
|
||||||
anchor.row,
|
case ".":
|
||||||
anchor.col,
|
focusElement(focusableElements.cancel)
|
||||||
direction,
|
break
|
||||||
e.metaKey || e.ctrlKey
|
case ",":
|
||||||
)
|
focusElement(focusableElements.shortcuts)
|
||||||
|
break
|
||||||
setSingleRange(next)
|
default:
|
||||||
scrollToCoordinates(next, "horizontal")
|
break
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[anchor, isEditing, scrollToCoordinates, setSingleRange, matrix]
|
[anchor, isEditing, setTrapActive, containerRef]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleKeyDownEvent = useCallback(
|
const handleKeyDownEvent = useCallback(
|
||||||
@@ -533,5 +571,29 @@ export const useDataGridKeydownEvent = <
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
handleKeyDownEvent,
|
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
|
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",
|
"selectDown": "Select down",
|
||||||
"selectUp": "Select up",
|
"selectUp": "Select up",
|
||||||
"selectColumnDown": "Select column down",
|
"selectColumnDown": "Select column down",
|
||||||
"selectColumnUp": "Select column up"
|
"selectColumnUp": "Select column up",
|
||||||
|
"focusToolbar": "Focus toolbar",
|
||||||
|
"focusCancel": "Focus cancel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -576,7 +578,6 @@
|
|||||||
"successToast": "Option {{title}} was successfully created."
|
"successToast": "Option {{title}} was successfully created."
|
||||||
},
|
},
|
||||||
"deleteWarning": "You are about to delete the product option: {{title}}. This action cannot be undone."
|
"deleteWarning": "You are about to delete the product option: {{title}}. This action cannot be undone."
|
||||||
|
|
||||||
},
|
},
|
||||||
"organization": {
|
"organization": {
|
||||||
"header": "Organize",
|
"header": "Organize",
|
||||||
|
|||||||
@@ -4551,7 +4551,6 @@ __metadata:
|
|||||||
autoprefixer: ^10.4.17
|
autoprefixer: ^10.4.17
|
||||||
cmdk: ^0.2.0
|
cmdk: ^0.2.0
|
||||||
date-fns: ^3.6.0
|
date-fns: ^3.6.0
|
||||||
focus-trap-react: ^10.2.3
|
|
||||||
framer-motion: ^11.0.3
|
framer-motion: ^11.0.3
|
||||||
i18next: 23.7.11
|
i18next: 23.7.11
|
||||||
i18next-browser-languagedetector: 7.2.0
|
i18next-browser-languagedetector: 7.2.0
|
||||||
@@ -4560,7 +4559,6 @@ __metadata:
|
|||||||
match-sorter: ^6.3.4
|
match-sorter: ^6.3.4
|
||||||
postcss: ^8.4.33
|
postcss: ^8.4.33
|
||||||
prettier: ^3.1.1
|
prettier: ^3.1.1
|
||||||
prop-types: ^15.8.1
|
|
||||||
qs: ^6.12.0
|
qs: ^6.12.0
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
react-country-flag: ^3.1.0
|
react-country-flag: ^3.1.0
|
||||||
@@ -18451,29 +18449,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.4, follow-redirects@npm:^1.15.6":
|
||||||
version: 1.15.6
|
version: 1.15.6
|
||||||
resolution: "follow-redirects@npm:1.15.6"
|
resolution: "follow-redirects@npm:1.15.6"
|
||||||
@@ -28925,13 +28900,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"table@npm:^6.0.9":
|
||||||
version: 6.8.2
|
version: 6.8.2
|
||||||
resolution: "table@npm:6.8.2"
|
resolution: "table@npm:6.8.2"
|
||||||
|
|||||||
Reference in new issue
Block a user