@umodeapporg/ui 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -14,6 +14,12 @@ const isTextEntryTarget = (target) => {
|
|
|
14
14
|
if (target.isContentEditable) return true;
|
|
15
15
|
return target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
|
|
16
16
|
};
|
|
17
|
+
const moveKeepingTheEditorOpen = (store, columnDelta, canEditCell) => {
|
|
18
|
+
if (!store.getState().moveActiveCell(0, columnDelta, { wrap: true })) return false;
|
|
19
|
+
const { activeCell, startEditing } = store.getState();
|
|
20
|
+
if (activeCell && canEditCell?.(activeCell)) startEditing(activeCell);
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
17
23
|
const useGridKeyboard = ({ store, canEditCell, onSave }) => useCallback(
|
|
18
24
|
(event) => {
|
|
19
25
|
if (!store) return;
|
|
@@ -40,7 +46,7 @@ const useGridKeyboard = ({ store, canEditCell, onSave }) => useCallback(
|
|
|
40
46
|
}
|
|
41
47
|
if (key === "Tab") {
|
|
42
48
|
state.stopEditing();
|
|
43
|
-
if (
|
|
49
|
+
if (moveKeepingTheEditorOpen(store, event.shiftKey ? -1 : 1, canEditCell)) event.preventDefault();
|
|
44
50
|
}
|
|
45
51
|
return;
|
|
46
52
|
}
|
|
@@ -71,7 +77,7 @@ const useGridKeyboard = ({ store, canEditCell, onSave }) => useCallback(
|
|
|
71
77
|
state.moveActiveCell(0, 1, { extend });
|
|
72
78
|
return;
|
|
73
79
|
case "Tab":
|
|
74
|
-
if (
|
|
80
|
+
if (moveKeepingTheEditorOpen(store, event.shiftKey ? -1 : 1, canEditCell)) event.preventDefault();
|
|
75
81
|
return;
|
|
76
82
|
case "Home":
|
|
77
83
|
event.preventDefault();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useGridKeyboard.js","sources":["../../../../../src/components/UI/DataTable/grid/useGridKeyboard.ts"],"sourcesContent":["import React, { useCallback } from 'react';\nimport { type StoreApi } from 'zustand/vanilla';\n\nimport { type IGridState } from './gridStore';\nimport { GRID_CELL_DATA_ATTR, type IGridCellCoord } from './gridTypes';\n\ninterface IUseGridKeyboardParams {\n store: StoreApi<IGridState> | null;\n /** Whether the cell can open an editor, so Enter/F2 know what to do. */\n canEditCell?: (coord: IGridCellCoord) => boolean;\n onSave?: () => void;\n}\n\nconst CELL_CONTENT_FOCUS_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]',\n]\n .map((selector) => `${selector}:not([tabindex=\"-1\"])`)\n .join(', ');\n\n/** The `<td>` the event came from, so the handler never queries the whole document. */\nconst resolveCellElement = (target: EventTarget | null): HTMLElement | null =>\n target instanceof HTMLElement ? target.closest<HTMLElement>(`[${GRID_CELL_DATA_ATTR}]`) : null;\n\n/**\n * A control that owns the caret (input, textarea, select, contenteditable) keeps\n * every key the grid would otherwise claim: the horizontal arrows walk the text,\n * not the columns.\n */\nconst isTextEntryTarget = (target: EventTarget | null): boolean => {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n\n return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT';\n};\n\n/**\n * The grid's ONLY keyboard listener: one React handler on the scroll container,\n * reached by bubbling from the focused cell. No document listeners, no per-cell\n * handlers, so two tables on the same screen never fight over the same keys.\n */\nexport const useGridKeyboard = ({ store, canEditCell, onSave }: IUseGridKeyboardParams) =>\n useCallback(\n (event: React.KeyboardEvent<HTMLElement>) => {\n if (!store) return;\n\n const state = store.getState();\n const { key } = event;\n\n if ((event.metaKey || event.ctrlKey) && (key === 's' || key === 'S')) {\n if (!onSave) return;\n event.preventDefault();\n onSave();\n return;\n }\n\n const isEditing = !!state.editingCell || isTextEntryTarget(event.target);\n\n if (isEditing) {\n if (key === 'Escape') {\n // The table may live inside a dialog or drawer: leaving the editor must\n // not also close the surface around it.\n event.stopPropagation();\n state.stopEditing('cancel');\n return;\n }\n\n if (key === 'Enter') {\n event.preventDefault();\n state.stopEditing();\n state.moveActiveCell(1, 0);\n return;\n }\n\n if (key === 'Tab') {\n state.stopEditing();\n
|
|
1
|
+
{"version":3,"file":"useGridKeyboard.js","sources":["../../../../../src/components/UI/DataTable/grid/useGridKeyboard.ts"],"sourcesContent":["import React, { useCallback } from 'react';\nimport { type StoreApi } from 'zustand/vanilla';\n\nimport { type IGridState } from './gridStore';\nimport { GRID_CELL_DATA_ATTR, type IGridCellCoord } from './gridTypes';\n\ninterface IUseGridKeyboardParams {\n store: StoreApi<IGridState> | null;\n /** Whether the cell can open an editor, so Enter/F2 know what to do. */\n canEditCell?: (coord: IGridCellCoord) => boolean;\n onSave?: () => void;\n}\n\nconst CELL_CONTENT_FOCUS_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]',\n]\n .map((selector) => `${selector}:not([tabindex=\"-1\"])`)\n .join(', ');\n\n/** The `<td>` the event came from, so the handler never queries the whole document. */\nconst resolveCellElement = (target: EventTarget | null): HTMLElement | null =>\n target instanceof HTMLElement ? target.closest<HTMLElement>(`[${GRID_CELL_DATA_ATTR}]`) : null;\n\n/**\n * A control that owns the caret (input, textarea, select, contenteditable) keeps\n * every key the grid would otherwise claim: the horizontal arrows walk the text,\n * not the columns.\n */\nconst isTextEntryTarget = (target: EventTarget | null): boolean => {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n\n return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT';\n};\n\n/**\n * Tab walks to the neighbouring cell and OPENS its editor when the cell accepts one, so a\n * row is filled by typing and tabbing instead of reopening the editor at every stop.\n * Returns whether the active cell moved, which is what decides if the grid keeps the event.\n */\nconst moveKeepingTheEditorOpen = (\n store: StoreApi<IGridState>,\n columnDelta: 1 | -1,\n canEditCell?: (coord: IGridCellCoord) => boolean,\n): boolean => {\n // Past the last cell the grid gives the focus back to the page: trapping Tab inside a\n // table is a keyboard trap, not a spreadsheet.\n if (!store.getState().moveActiveCell(0, columnDelta, { wrap: true })) return false;\n\n const { activeCell, startEditing } = store.getState();\n if (activeCell && canEditCell?.(activeCell)) startEditing(activeCell);\n\n return true;\n};\n\n/**\n * The grid's ONLY keyboard listener: one React handler on the scroll container,\n * reached by bubbling from the focused cell. No document listeners, no per-cell\n * handlers, so two tables on the same screen never fight over the same keys.\n */\nexport const useGridKeyboard = ({ store, canEditCell, onSave }: IUseGridKeyboardParams) =>\n useCallback(\n (event: React.KeyboardEvent<HTMLElement>) => {\n if (!store) return;\n\n const state = store.getState();\n const { key } = event;\n\n if ((event.metaKey || event.ctrlKey) && (key === 's' || key === 'S')) {\n if (!onSave) return;\n event.preventDefault();\n onSave();\n return;\n }\n\n const isEditing = !!state.editingCell || isTextEntryTarget(event.target);\n\n if (isEditing) {\n if (key === 'Escape') {\n // The table may live inside a dialog or drawer: leaving the editor must\n // not also close the surface around it.\n event.stopPropagation();\n state.stopEditing('cancel');\n return;\n }\n\n if (key === 'Enter') {\n event.preventDefault();\n state.stopEditing();\n state.moveActiveCell(1, 0);\n return;\n }\n\n if (key === 'Tab') {\n state.stopEditing();\n if (moveKeepingTheEditorOpen(store, event.shiftKey ? -1 : 1, canEditCell)) event.preventDefault();\n }\n\n return;\n }\n\n const cellElement = resolveCellElement(event.target);\n const isInsideCellContent = !!cellElement && event.target !== cellElement;\n\n // Focus went into a control inside the cell (an action button, a link).\n // While it is there the cell content owns the keyboard, and Escape brings\n // the focus back to the cell.\n if (isInsideCellContent) {\n if (key !== 'Escape') return;\n event.preventDefault();\n cellElement.focus();\n return;\n }\n\n const extend = event.shiftKey;\n\n switch (key) {\n case 'ArrowUp':\n event.preventDefault();\n state.moveActiveCell(-1, 0, { extend });\n return;\n case 'ArrowDown':\n event.preventDefault();\n state.moveActiveCell(1, 0, { extend });\n return;\n case 'ArrowLeft':\n event.preventDefault();\n state.moveActiveCell(0, -1, { extend });\n return;\n case 'ArrowRight':\n event.preventDefault();\n state.moveActiveCell(0, 1, { extend });\n return;\n case 'Tab':\n if (moveKeepingTheEditorOpen(store, event.shiftKey ? -1 : 1, canEditCell)) event.preventDefault();\n return;\n case 'Home':\n event.preventDefault();\n state.moveActiveToEdge(event.ctrlKey || event.metaKey ? -1 : 0, -1, { extend });\n return;\n case 'End':\n event.preventDefault();\n state.moveActiveToEdge(event.ctrlKey || event.metaKey ? 1 : 0, 1, { extend });\n return;\n case 'Enter':\n case 'F2': {\n const active = state.activeCell;\n if (!active) return;\n\n if (canEditCell?.(active)) {\n event.preventDefault();\n state.startEditing(active);\n return;\n }\n\n // Read-only cell: Enter steps INTO whatever control it holds, which is\n // the only way a keyboard user reaches the per-row actions now that Tab\n // walks the cells.\n const focusableInsideCell = cellElement?.querySelector<HTMLElement>(CELL_CONTENT_FOCUS_SELECTOR);\n if (focusableInsideCell) {\n event.preventDefault();\n focusableInsideCell.focus();\n return;\n }\n\n if (key !== 'Enter') return;\n event.preventDefault();\n state.moveActiveCell(1, 0);\n\n return;\n }\n case 'Escape':\n state.clearSelection();\n return;\n default:\n }\n },\n [store, canEditCell, onSave],\n );\n"],"names":[],"mappings":"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"}
|