@alaarab/ogrid-react 2.1.3 → 2.1.5

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.
Files changed (72) hide show
  1. package/dist/esm/index.js +7233 -26
  2. package/package.json +7 -4
  3. package/dist/esm/components/BaseColumnHeaderMenu.js +0 -78
  4. package/dist/esm/components/BaseDropIndicator.js +0 -4
  5. package/dist/esm/components/BaseEmptyState.js +0 -4
  6. package/dist/esm/components/BaseInlineCellEditor.js +0 -167
  7. package/dist/esm/components/BaseLoadingOverlay.js +0 -4
  8. package/dist/esm/components/CellErrorBoundary.js +0 -43
  9. package/dist/esm/components/ColumnChooserProps.js +0 -6
  10. package/dist/esm/components/ColumnHeaderFilterContent.js +0 -33
  11. package/dist/esm/components/ColumnHeaderFilterRenderers.js +0 -67
  12. package/dist/esm/components/EmptyState.js +0 -19
  13. package/dist/esm/components/GridContextMenu.js +0 -35
  14. package/dist/esm/components/MarchingAntsOverlay.js +0 -90
  15. package/dist/esm/components/OGridLayout.js +0 -136
  16. package/dist/esm/components/PaginationControlsProps.js +0 -6
  17. package/dist/esm/components/SideBar.js +0 -123
  18. package/dist/esm/components/StatusBar.js +0 -6
  19. package/dist/esm/components/createOGrid.js +0 -19
  20. package/dist/esm/constants/domHelpers.js +0 -16
  21. package/dist/esm/hooks/index.js +0 -43
  22. package/dist/esm/hooks/useActiveCell.js +0 -75
  23. package/dist/esm/hooks/useCellEditing.js +0 -15
  24. package/dist/esm/hooks/useCellSelection.js +0 -389
  25. package/dist/esm/hooks/useClipboard.js +0 -106
  26. package/dist/esm/hooks/useColumnChooserState.js +0 -74
  27. package/dist/esm/hooks/useColumnHeaderFilterState.js +0 -191
  28. package/dist/esm/hooks/useColumnHeaderMenuState.js +0 -106
  29. package/dist/esm/hooks/useColumnMeta.js +0 -61
  30. package/dist/esm/hooks/useColumnPinning.js +0 -67
  31. package/dist/esm/hooks/useColumnReorder.js +0 -143
  32. package/dist/esm/hooks/useColumnResize.js +0 -127
  33. package/dist/esm/hooks/useContextMenu.js +0 -21
  34. package/dist/esm/hooks/useDataGridContextMenu.js +0 -24
  35. package/dist/esm/hooks/useDataGridEditing.js +0 -56
  36. package/dist/esm/hooks/useDataGridInteraction.js +0 -109
  37. package/dist/esm/hooks/useDataGridLayout.js +0 -172
  38. package/dist/esm/hooks/useDataGridState.js +0 -169
  39. package/dist/esm/hooks/useDataGridTableOrchestration.js +0 -199
  40. package/dist/esm/hooks/useDateFilterState.js +0 -34
  41. package/dist/esm/hooks/useDebounce.js +0 -35
  42. package/dist/esm/hooks/useFillHandle.js +0 -200
  43. package/dist/esm/hooks/useFilterOptions.js +0 -55
  44. package/dist/esm/hooks/useInlineCellEditorState.js +0 -38
  45. package/dist/esm/hooks/useKeyboardNavigation.js +0 -261
  46. package/dist/esm/hooks/useLatestRef.js +0 -11
  47. package/dist/esm/hooks/useListVirtualizer.js +0 -29
  48. package/dist/esm/hooks/useMultiSelectFilterState.js +0 -59
  49. package/dist/esm/hooks/useOGrid.js +0 -371
  50. package/dist/esm/hooks/useOGridDataFetching.js +0 -74
  51. package/dist/esm/hooks/useOGridFilters.js +0 -59
  52. package/dist/esm/hooks/useOGridPagination.js +0 -24
  53. package/dist/esm/hooks/useOGridSorting.js +0 -24
  54. package/dist/esm/hooks/usePaginationControls.js +0 -16
  55. package/dist/esm/hooks/usePeopleFilterState.js +0 -73
  56. package/dist/esm/hooks/useRichSelectState.js +0 -60
  57. package/dist/esm/hooks/useRowSelection.js +0 -69
  58. package/dist/esm/hooks/useSelectState.js +0 -62
  59. package/dist/esm/hooks/useShallowEqualMemo.js +0 -14
  60. package/dist/esm/hooks/useSideBarState.js +0 -39
  61. package/dist/esm/hooks/useTableLayout.js +0 -69
  62. package/dist/esm/hooks/useTextFilterState.js +0 -25
  63. package/dist/esm/hooks/useUndoRedo.js +0 -84
  64. package/dist/esm/hooks/useVirtualScroll.js +0 -69
  65. package/dist/esm/storybook/index.js +0 -1
  66. package/dist/esm/storybook/mockData.js +0 -73
  67. package/dist/esm/types/columnTypes.js +0 -1
  68. package/dist/esm/types/dataGridTypes.js +0 -1
  69. package/dist/esm/types/index.js +0 -1
  70. package/dist/esm/utils/dataGridViewModel.js +0 -54
  71. package/dist/esm/utils/gridRowComparator.js +0 -2
  72. package/dist/esm/utils/index.js +0 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
4
  "description": "OGrid React – React hooks, headless components, and utilities for OGrid data grids.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -9,11 +9,14 @@
9
9
  ".": {
10
10
  "types": "./dist/types/index.d.ts",
11
11
  "import": "./dist/esm/index.js",
12
- "require": "./dist/esm/index.js"
12
+ "default": "./dist/esm/index.js"
13
+ },
14
+ "./testing": {
15
+ "types": "./dist/types/testing/index.d.ts"
13
16
  }
14
17
  },
15
18
  "scripts": {
16
- "build": "rimraf dist && tsc -p tsconfig.build.json",
19
+ "build": "rimraf dist && tsup && tsc -p tsconfig.build.json",
17
20
  "test": "jest"
18
21
  },
19
22
  "keywords": [
@@ -36,7 +39,7 @@
36
39
  "node": ">=18"
37
40
  },
38
41
  "dependencies": {
39
- "@alaarab/ogrid-core": "2.1.3",
42
+ "@alaarab/ogrid-core": "2.1.5",
40
43
  "@tanstack/react-virtual": "^3.0.0"
41
44
  },
42
45
  "peerDependencies": {
@@ -1,78 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import * as React from 'react';
3
- import { createPortal } from 'react-dom';
4
- import { getColumnHeaderMenuItems } from '../utils';
5
- /**
6
- * Base column header dropdown menu for pin/sort/autosize actions.
7
- * Uses positioned div with portal rendering.
8
- * Shared by Radix and Fluent UI packages (Material uses MUI Menu instead).
9
- */
10
- export function BaseColumnHeaderMenu(props) {
11
- const { isOpen, anchorElement, onClose, onPinLeft, onPinRight, onUnpin, onSortAsc, onSortDesc, onClearSort, onAutosizeThis, onAutosizeAll, canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable, classNames, getPortalTarget, } = props;
12
- const [position, setPosition] = React.useState(null);
13
- const menuRef = React.useRef(null);
14
- React.useEffect(() => {
15
- if (!isOpen || !anchorElement) {
16
- setPosition(null);
17
- return;
18
- }
19
- const rect = anchorElement.getBoundingClientRect();
20
- setPosition({
21
- top: rect.bottom + 4,
22
- left: rect.left,
23
- });
24
- const handleClickOutside = (e) => {
25
- const target = e.target;
26
- // Don't close if clicking inside the menu itself (portal) — let onClick fire first
27
- if (menuRef.current && menuRef.current.contains(target))
28
- return;
29
- if (anchorElement && !anchorElement.contains(target)) {
30
- onClose();
31
- }
32
- };
33
- const handleEscape = (e) => {
34
- if (e.key === 'Escape') {
35
- onClose();
36
- }
37
- };
38
- document.addEventListener('mousedown', handleClickOutside);
39
- document.addEventListener('keydown', handleEscape);
40
- return () => {
41
- document.removeEventListener('mousedown', handleClickOutside);
42
- document.removeEventListener('keydown', handleEscape);
43
- };
44
- }, [isOpen, anchorElement, onClose]);
45
- const menuInput = React.useMemo(() => ({
46
- canPinLeft,
47
- canPinRight,
48
- canUnpin,
49
- currentSort,
50
- isSortable,
51
- isResizable,
52
- }), [canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable]);
53
- const items = React.useMemo(() => getColumnHeaderMenuItems(menuInput), [menuInput]);
54
- const handlers = React.useMemo(() => ({
55
- pinLeft: onPinLeft,
56
- pinRight: onPinRight,
57
- unpin: onUnpin,
58
- sortAsc: onSortAsc,
59
- sortDesc: onSortDesc,
60
- clearSort: onClearSort,
61
- autosizeThis: onAutosizeThis,
62
- autosizeAll: onAutosizeAll,
63
- }), [onPinLeft, onPinRight, onUnpin, onSortAsc, onSortDesc, onClearSort, onAutosizeThis, onAutosizeAll]);
64
- if (!isOpen || !position)
65
- return null;
66
- const portalTarget = anchorElement && getPortalTarget
67
- ? getPortalTarget(anchorElement)
68
- : document.body;
69
- return createPortal(_jsx("div", { ref: menuRef, className: classNames?.content, style: {
70
- position: 'fixed',
71
- top: position.top,
72
- left: position.left,
73
- zIndex: 1000,
74
- }, children: items.map((item, idx) => (_jsxs(React.Fragment, { children: [_jsx("button", { className: classNames?.item, disabled: item.disabled, onClick: () => {
75
- handlers[item.id]();
76
- onClose();
77
- }, children: item.label }), item.divider && idx < items.length - 1 && (_jsx("div", { className: classNames?.separator }))] }, item.id))) }), portalTarget);
78
- }
@@ -1,4 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- export function BaseDropIndicator({ dropIndicatorX, wrapperLeft, className }) {
3
- return (_jsx("div", { className: className, style: { left: dropIndicatorX - wrapperLeft } }));
4
- }
@@ -1,4 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- export function BaseEmptyState({ emptyState, classNames, icon }) {
3
- return (_jsx("div", { className: classNames.emptyStateInGrid, children: _jsx("div", { className: classNames.emptyStateInGridInner, children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [icon != null && (_jsx("span", { className: classNames.emptyStateInGridIcon, "aria-hidden": true, children: icon })), _jsx("div", { className: classNames.emptyStateInGridTitle, children: "No results found" }), _jsx("div", { className: classNames.emptyStateInGridMessage, children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx("button", { type: "button", className: classNames.emptyStateInGridLink, onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }) }));
4
- }
@@ -1,167 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import * as React from 'react';
3
- import { useInlineCellEditorState, useRichSelectState, useSelectState } from '../hooks';
4
- // ── Shared editor style constants (used across all 3 UI packages) ──
5
- export const editorWrapperStyle = {
6
- width: '100%',
7
- height: '100%',
8
- display: 'flex',
9
- alignItems: 'center',
10
- padding: '6px 10px',
11
- boxSizing: 'border-box',
12
- overflow: 'hidden',
13
- minWidth: 0,
14
- };
15
- export const editorInputStyle = {
16
- width: '100%',
17
- padding: 0,
18
- border: 'none',
19
- background: 'transparent',
20
- color: 'inherit',
21
- font: 'inherit',
22
- fontSize: '13px',
23
- outline: 'none',
24
- minWidth: 0,
25
- };
26
- export const richSelectWrapperStyle = {
27
- ...editorWrapperStyle,
28
- position: 'relative',
29
- };
30
- export const richSelectDropdownStyle = {
31
- position: 'absolute',
32
- top: '100%',
33
- left: 0,
34
- right: 0,
35
- maxHeight: 200,
36
- overflowY: 'auto',
37
- background: 'var(--ogrid-bg, #fff)',
38
- border: '1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12))',
39
- zIndex: 10,
40
- boxShadow: 'var(--ogrid-shadow, 0 4px 16px rgba(0,0,0,0.2))',
41
- };
42
- export const richSelectOptionStyle = {
43
- padding: '6px 8px',
44
- cursor: 'pointer',
45
- color: 'var(--ogrid-fg, #242424)',
46
- };
47
- export const richSelectOptionHighlightedStyle = {
48
- ...richSelectOptionStyle,
49
- background: 'var(--ogrid-bg-hover, #e8f0fe)',
50
- };
51
- export const richSelectNoMatchesStyle = {
52
- padding: '6px 8px',
53
- color: 'var(--ogrid-muted, #999)',
54
- };
55
- export const selectEditorStyle = {
56
- width: '100%',
57
- padding: 0,
58
- border: 'none',
59
- background: 'transparent',
60
- color: 'inherit',
61
- font: 'inherit',
62
- fontSize: '13px',
63
- cursor: 'pointer',
64
- outline: 'none',
65
- };
66
- export const selectDisplayStyle = {
67
- display: 'flex',
68
- alignItems: 'center',
69
- justifyContent: 'space-between',
70
- width: '100%',
71
- cursor: 'pointer',
72
- fontSize: '13px',
73
- color: 'inherit',
74
- };
75
- export const selectChevronStyle = {
76
- marginLeft: 4,
77
- fontSize: '10px',
78
- opacity: 0.5,
79
- };
80
- /**
81
- * Base inline cell editor with shared logic for all editor types except checkbox
82
- * (which is framework-specific). Used by all 3 UI packages to avoid duplication.
83
- *
84
- * Text, date, select, and richSelect editors are fully shared.
85
- * Checkbox is delegated via renderCheckbox render prop.
86
- */
87
- export function BaseInlineCellEditor(props) {
88
- const { value, column, editorType, onCommit, onCancel, renderCheckbox } = props;
89
- const wrapperRef = React.useRef(null);
90
- const { localValue, setLocalValue, handleKeyDown, handleBlur, commit, cancel } = useInlineCellEditorState({ value, editorType, onCommit, onCancel });
91
- const editorValues = column.cellEditorParams?.values ?? [];
92
- const editorFormatValue = column.cellEditorParams?.formatValue;
93
- const richSelect = useRichSelectState({
94
- values: editorValues,
95
- formatValue: editorFormatValue,
96
- initialValue: value,
97
- onCommit,
98
- onCancel,
99
- });
100
- const selectState = useSelectState({
101
- values: editorValues,
102
- formatValue: editorFormatValue,
103
- initialValue: value,
104
- onCommit,
105
- onCancel,
106
- });
107
- // Fixed dropdown positioning to escape ancestor overflow clipping (.tableWrapper)
108
- const [fixedDropdownStyle, setFixedDropdownStyle] = React.useState(null);
109
- React.useLayoutEffect(() => {
110
- if (editorType !== 'select' && editorType !== 'richSelect')
111
- return;
112
- const wrapper = wrapperRef.current;
113
- if (!wrapper)
114
- return;
115
- const rect = wrapper.getBoundingClientRect();
116
- const maxH = 200;
117
- const spaceBelow = window.innerHeight - rect.bottom;
118
- const flipUp = spaceBelow < maxH && rect.top > spaceBelow;
119
- setFixedDropdownStyle({
120
- position: 'fixed',
121
- ...(flipUp ? { bottom: window.innerHeight - rect.top } : { top: rect.bottom }),
122
- left: rect.left,
123
- width: rect.width,
124
- maxHeight: maxH,
125
- overflowY: 'auto',
126
- background: 'var(--ogrid-bg, #fff)',
127
- border: '1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12))',
128
- zIndex: 9999,
129
- boxShadow: 'var(--ogrid-shadow, 0 4px 16px rgba(0,0,0,0.2))',
130
- });
131
- }, [editorType]);
132
- const computedDropdownStyle = fixedDropdownStyle ?? richSelectDropdownStyle;
133
- React.useEffect(() => {
134
- const wrapper = wrapperRef.current;
135
- if (!wrapper)
136
- return;
137
- const input = wrapper.querySelector('input');
138
- if (input) {
139
- input.focus();
140
- // Select all text for easy replacement (like Excel)
141
- input.select();
142
- }
143
- else {
144
- // Focus the wrapper for keyboard events (select editor has no input)
145
- wrapper.focus();
146
- }
147
- }, []);
148
- // Rich select (shared across all frameworks)
149
- if (editorType === 'richSelect') {
150
- return (_jsxs("div", { ref: wrapperRef, style: richSelectWrapperStyle, children: [_jsx("input", { type: "text", value: richSelect.searchText, onChange: (e) => richSelect.setSearchText(e.target.value), onKeyDown: richSelect.handleKeyDown, placeholder: "Search...", autoFocus: true, style: editorInputStyle }), _jsxs("div", { style: computedDropdownStyle, role: "listbox", children: [richSelect.filteredValues.map((v, i) => (_jsx("div", { role: "option", "aria-selected": i === richSelect.highlightedIndex, onClick: () => richSelect.selectValue(v), style: i === richSelect.highlightedIndex ? richSelectOptionHighlightedStyle : richSelectOptionStyle, children: richSelect.getDisplayText(v) }, String(v)))), richSelect.filteredValues.length === 0 && (_jsx("div", { style: richSelectNoMatchesStyle, children: "No matches" }))] })] }));
151
- }
152
- // Checkbox (framework-specific)
153
- if (editorType === 'checkbox') {
154
- const checked = value === true;
155
- return _jsx(_Fragment, { children: renderCheckbox(checked, (val) => commit(val), cancel) });
156
- }
157
- // Select (custom dropdown, shared across all frameworks)
158
- if (editorType === 'select') {
159
- return (_jsxs("div", { ref: wrapperRef, style: richSelectWrapperStyle, onKeyDown: selectState.handleKeyDown, tabIndex: 0, children: [_jsxs("div", { style: selectDisplayStyle, children: [_jsx("span", { children: selectState.getDisplayText(value) }), _jsx("span", { style: selectChevronStyle, children: "\u25BE" })] }), _jsx("div", { style: computedDropdownStyle, ref: selectState.dropdownRef, role: "listbox", children: editorValues.map((v, i) => (_jsx("div", { role: "option", "aria-selected": i === selectState.highlightedIndex, onClick: () => selectState.selectValue(v), style: i === selectState.highlightedIndex ? richSelectOptionHighlightedStyle : richSelectOptionStyle, children: selectState.getDisplayText(v) }, String(v)))) })] }));
160
- }
161
- // Date editor (shared across all frameworks)
162
- if (editorType === 'date') {
163
- return (_jsx("div", { ref: wrapperRef, style: editorWrapperStyle, children: _jsx("input", { type: "date", value: localValue, onChange: (e) => setLocalValue(e.target.value), onBlur: handleBlur, onKeyDown: handleKeyDown, style: editorInputStyle, autoFocus: true }) }));
164
- }
165
- // Text editor (default, shared across all frameworks)
166
- return (_jsx("div", { ref: wrapperRef, style: editorWrapperStyle, children: _jsx("input", { type: "text", value: localValue, onChange: (e) => setLocalValue(e.target.value), onBlur: handleBlur, onKeyDown: handleKeyDown, style: editorInputStyle, autoFocus: true }) }));
167
- }
@@ -1,4 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- export function BaseLoadingOverlay({ message, classNames }) {
3
- return (_jsx("div", { className: classNames.loadingOverlay, "aria-live": "polite", children: _jsxs("div", { className: classNames.loadingOverlayContent, children: [_jsx("div", { className: classNames.spinner }), _jsx("span", { className: classNames.loadingOverlayText, children: message })] }) }));
4
- }
@@ -1,43 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import * as React from 'react';
3
- const DEFAULT_FALLBACK_STYLE = {
4
- color: 'var(--ogrid-error, #d32f2f)',
5
- fontSize: '0.75rem',
6
- padding: '2px 4px',
7
- };
8
- /**
9
- * Error boundary for cell renderers and custom editors.
10
- * Prevents a runtime error in a cell from crashing the entire grid.
11
- */
12
- export class CellErrorBoundary extends React.Component {
13
- constructor(props) {
14
- super(props);
15
- this.state = { hasError: false };
16
- }
17
- static getDerivedStateFromError() {
18
- return { hasError: true };
19
- }
20
- componentDidCatch(error, errorInfo) {
21
- if (this.props.onError) {
22
- this.props.onError(error, errorInfo);
23
- }
24
- }
25
- componentDidUpdate(prevProps) {
26
- // Reset error state when children change (e.g., navigating to a different cell)
27
- if (prevProps.children !== this.props.children && this.state.hasError) {
28
- this.setState({ hasError: false });
29
- }
30
- }
31
- resetErrorBoundary() {
32
- this.setState({ hasError: false });
33
- }
34
- render() {
35
- if (this.state.hasError) {
36
- if (this.props.fallback !== undefined) {
37
- return this.props.fallback;
38
- }
39
- return _jsx("span", { style: DEFAULT_FALLBACK_STYLE, children: "\u26A0 Error" });
40
- }
41
- return this.props.children;
42
- }
43
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Shared props interface for ColumnChooser across all React UI packages.
3
- * Each UI package renders its own framework-specific trigger, popover, and checkboxes
4
- * but shares this common prop shape.
5
- */
6
- export {};
@@ -1,33 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- export const DateFilterContent = ({ tempDateFrom, setTempDateFrom, tempDateTo, setTempDateTo, onApply, onClear, classNames, }) => (_jsxs(_Fragment, { children: [_jsxs("div", { style: { padding: '8px 12px', display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }, children: ["From:", _jsx("input", { type: "date", value: tempDateFrom, onChange: (e) => setTempDateFrom(e.target.value), style: { flex: 1 } })] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }, children: ["To:", _jsx("input", { type: "date", value: tempDateTo, onChange: (e) => setTempDateTo(e.target.value), style: { flex: 1 } })] })] }), _jsxs("div", { className: classNames?.popoverActions, children: [_jsx("button", { className: classNames?.clearButton, onClick: onClear, disabled: !tempDateFrom && !tempDateTo, children: "Clear" }), _jsx("button", { className: classNames?.applyButton, onClick: onApply, children: "Apply" })] })] }));
3
- DateFilterContent.displayName = 'DateFilterContent';
4
- // ---- Utility to extract useColumnHeaderFilterState params from props ----
5
- export function getColumnHeaderFilterStateParams(props) {
6
- return {
7
- filterType: props.filterType,
8
- onSort: props.onSort,
9
- selectedValues: props.selectedValues,
10
- onFilterChange: props.onFilterChange,
11
- options: props.options,
12
- isLoadingOptions: props.isLoadingOptions ?? false,
13
- textValue: props.textValue ?? '',
14
- onTextChange: props.onTextChange,
15
- selectedUser: props.selectedUser,
16
- onUserChange: props.onUserChange,
17
- peopleSearch: props.peopleSearch,
18
- dateValue: props.dateValue,
19
- onDateChange: props.onDateChange,
20
- };
21
- }
22
- // ---- Helper to build date filter props from state ----
23
- export function getDateFilterContentProps(state, classNames) {
24
- return {
25
- tempDateFrom: state.tempDateFrom,
26
- setTempDateFrom: state.setTempDateFrom,
27
- tempDateTo: state.tempDateTo,
28
- setTempDateTo: state.setTempDateTo,
29
- onApply: state.handlers.handleDateApply,
30
- onClear: state.handlers.handleDateClear,
31
- classNames,
32
- };
33
- }
@@ -1,67 +0,0 @@
1
- /**
2
- * Shared filter content dispatching for ColumnHeaderFilter across all React UI packages.
3
- *
4
- * Each UI package provides framework-specific sub-filter components (TextFilterPopover,
5
- * MultiSelectFilterPopover, PeopleFilterPopover, date content). This utility dispatches
6
- * to the correct renderer based on filterType, eliminating the duplicated if/switch chain
7
- * that was previously in each UI package's ColumnHeaderFilter component.
8
- */
9
- /**
10
- * Dispatches to the appropriate filter content renderer based on filterType.
11
- * Eliminates the duplicated if/switch chain in each UI package's ColumnHeaderFilter.
12
- *
13
- * @param filterType - The column's filter type
14
- * @param state - The result from useColumnHeaderFilterState
15
- * @param options - The filter options array (for multiSelect)
16
- * @param isLoadingOptions - Whether options are loading
17
- * @param selectedUser - The currently selected user (for people filter)
18
- * @param renderers - Framework-specific renderer functions
19
- * @returns The rendered filter content, or null for unsupported filter types
20
- */
21
- export function renderFilterContent(filterType, state, options, isLoadingOptions, selectedUser, renderers) {
22
- if (filterType === 'multiSelect') {
23
- return renderers.renderMultiSelect({
24
- searchText: state.searchText,
25
- onSearchChange: state.setSearchText,
26
- options,
27
- filteredOptions: state.filteredOptions,
28
- selected: state.tempSelected,
29
- onOptionToggle: state.handlers.handleCheckboxChange,
30
- onSelectAll: state.handlers.handleSelectAll,
31
- onClearSelection: state.handlers.handleClearSelection,
32
- onApply: state.handlers.handleApplyMultiSelect,
33
- isLoading: isLoadingOptions,
34
- });
35
- }
36
- if (filterType === 'text') {
37
- return renderers.renderText({
38
- value: state.tempTextValue,
39
- onValueChange: state.setTempTextValue,
40
- onApply: state.handlers.handleTextApply,
41
- onClear: state.handlers.handleTextClear,
42
- });
43
- }
44
- if (filterType === 'people') {
45
- return renderers.renderPeople({
46
- selectedUser,
47
- searchText: state.peopleSearchText,
48
- onSearchChange: state.setPeopleSearchText,
49
- suggestions: state.peopleSuggestions,
50
- isLoading: state.isPeopleLoading,
51
- onUserSelect: state.handlers.handleUserSelect,
52
- onClearUser: state.handlers.handleClearUser,
53
- inputRef: state.peopleInputRef,
54
- });
55
- }
56
- if (filterType === 'date') {
57
- return renderers.renderDate({
58
- tempDateFrom: state.tempDateFrom,
59
- setTempDateFrom: state.setTempDateFrom,
60
- tempDateTo: state.tempDateTo,
61
- setTempDateTo: state.setTempDateTo,
62
- onApply: state.handlers.handleDateApply,
63
- onClear: state.handlers.handleDateClear,
64
- });
65
- }
66
- return null;
67
- }
@@ -1,19 +0,0 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- const clearButtonStyle = { background: 'none', border: 'none', color: 'inherit', textDecoration: 'underline', cursor: 'pointer', padding: 0, font: 'inherit' };
3
- /**
4
- * Headless empty state component with default rendering logic.
5
- * Framework-specific wrappers provide styling.
6
- *
7
- * Default behavior:
8
- * - Shows "No results found" title
9
- * - If hasActiveFilters=true: shows "clear all filters" link
10
- * - If message provided: shows custom message
11
- * - If render provided: uses custom renderer
12
- */
13
- export function EmptyState(props) {
14
- const { message, hasActiveFilters, onClearAll, render } = props;
15
- if (render) {
16
- return _jsx(_Fragment, { children: render() });
17
- }
18
- return (_jsx(_Fragment, { children: message != null ? (message) : hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx("button", { type: "button", onClick: onClearAll, style: clearButtonStyle, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') }));
19
- }
@@ -1,35 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import * as React from 'react';
3
- import { GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut } from '../utils';
4
- const menuPositionStyle = (x, y) => ({ left: x, top: y });
5
- export function GridContextMenu(props) {
6
- const { x, y, hasSelection, canUndo, canRedo, onClose, onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, classNames } = props;
7
- const ref = React.useRef(null);
8
- const handlers = React.useMemo(() => getContextMenuHandlers({ onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, onClose }), [onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, onClose]);
9
- const isDisabled = React.useCallback((item) => {
10
- if (item.disabledWhenNoSelection && !hasSelection)
11
- return true;
12
- if (item.id === 'undo' && !canUndo)
13
- return true;
14
- if (item.id === 'redo' && !canRedo)
15
- return true;
16
- return false;
17
- }, [hasSelection, canUndo, canRedo]);
18
- React.useEffect(() => {
19
- const handleClickOutside = (e) => {
20
- if (ref.current && !ref.current.contains(e.target))
21
- onClose();
22
- };
23
- const handleKeyDown = (e) => {
24
- if (e.key === 'Escape')
25
- onClose();
26
- };
27
- document.addEventListener('mousedown', handleClickOutside, true);
28
- document.addEventListener('keydown', handleKeyDown, true);
29
- return () => {
30
- document.removeEventListener('mousedown', handleClickOutside, true);
31
- document.removeEventListener('keydown', handleKeyDown, true);
32
- };
33
- }, [onClose]);
34
- return (_jsx("div", { ref: ref, className: classNames?.contextMenu, role: "menu", style: menuPositionStyle(x, y), "aria-label": "Grid context menu", children: GRID_CONTEXT_MENU_ITEMS.map((item) => (_jsxs(React.Fragment, { children: [item.dividerBefore && _jsx("div", { className: classNames?.contextMenuDivider }), _jsxs("button", { type: "button", className: classNames?.contextMenuItem, onClick: handlers[item.id], disabled: isDisabled(item), children: [_jsx("span", { className: classNames?.contextMenuItemLabel, children: item.label }), item.shortcut && (_jsx("span", { className: classNames?.contextMenuItemShortcut, children: formatShortcut(item.shortcut) }))] })] }, item.id))) }));
35
- }
@@ -1,90 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- /**
3
- * MarchingAntsOverlay — Renders range overlays on top of the grid:
4
- *
5
- * 1. **Selection range**: solid green border around the current selection
6
- * 2. **Copy/Cut range**: animated dashed border (marching ants) like Excel
7
- *
8
- * Uses SVG rects positioned via cell data-attribute measurements.
9
- */
10
- import { useEffect, useRef, useState, useCallback } from 'react';
11
- import { measureRange, injectGlobalStyles } from '@alaarab/ogrid-core';
12
- const MARCHING_ANTS_ANIMATION = { animation: 'ogrid-marching-ants 0.5s linear infinite' };
13
- export function MarchingAntsOverlay({ containerRef, selectionRange, copyRange, cutRange, colOffset, items, visibleColumns, columnSizingOverrides, columnOrder, }) {
14
- const [selRect, setSelRect] = useState(null);
15
- const [clipRect, setClipRect] = useState(null);
16
- const rafRef = useRef(0);
17
- const clipRange = copyRange ?? cutRange;
18
- const measureAll = useCallback(() => {
19
- const container = containerRef.current;
20
- if (!container) {
21
- setSelRect(null);
22
- setClipRect(null);
23
- return;
24
- }
25
- setSelRect(selectionRange ? measureRange(container, selectionRange, colOffset) : null);
26
- setClipRect(clipRange ? measureRange(container, clipRange, colOffset) : null);
27
- }, [selectionRange, clipRange, containerRef, colOffset]);
28
- // Inject keyframes on mount
29
- useEffect(() => {
30
- injectGlobalStyles('ogrid-marching-ants-keyframes', '@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}');
31
- }, []);
32
- // Measure when any range changes; re-measure on resize
33
- useEffect(() => {
34
- if (!selectionRange && !clipRange) {
35
- setSelRect(null);
36
- setClipRect(null);
37
- return;
38
- }
39
- // Delay one frame so cells are rendered
40
- rafRef.current = requestAnimationFrame(measureAll);
41
- const container = containerRef.current;
42
- let ro;
43
- if (container) {
44
- ro = new ResizeObserver(measureAll);
45
- ro.observe(container);
46
- }
47
- return () => {
48
- cancelAnimationFrame(rafRef.current);
49
- ro?.disconnect();
50
- };
51
- }, [selectionRange, clipRange, measureAll, containerRef, items, visibleColumns, columnSizingOverrides, columnOrder]);
52
- if (!selRect && !clipRect)
53
- return null;
54
- // When clipboard range matches the selection range, hide the solid selection border
55
- // so the marching ants animation is clearly visible (not obscured by solid stroke underneath).
56
- const clipRangeMatchesSel = selectionRange != null &&
57
- clipRange != null &&
58
- selectionRange.startRow === clipRange.startRow &&
59
- selectionRange.startCol === clipRange.startCol &&
60
- selectionRange.endRow === clipRange.endRow &&
61
- selectionRange.endCol === clipRange.endCol;
62
- // Round to integer pixels so the stroke aligns to the pixel grid and corners connect cleanly
63
- const roundRect = (r) => ({
64
- top: Math.round(r.top),
65
- left: Math.round(r.left),
66
- width: Math.round(r.width),
67
- height: Math.round(r.height),
68
- });
69
- const selR = selRect ? roundRect(selRect) : null;
70
- const clipR = clipRect ? roundRect(clipRect) : null;
71
- return (_jsxs(_Fragment, { children: [selR && !clipRangeMatchesSel && (_jsx("svg", { style: {
72
- position: 'absolute',
73
- top: selR.top,
74
- left: selR.left,
75
- width: selR.width,
76
- height: selR.height,
77
- pointerEvents: 'none',
78
- zIndex: 4,
79
- overflow: 'visible',
80
- }, "aria-hidden": "true", children: _jsx("rect", { x: "1", y: "1", width: Math.max(0, selR.width - 2), height: Math.max(0, selR.height - 2), fill: "none", stroke: "var(--ogrid-selection, #217346)", strokeWidth: "2", style: { shapeRendering: 'crispEdges' } }) })), clipR && (_jsx("svg", { style: {
81
- position: 'absolute',
82
- top: clipR.top,
83
- left: clipR.left,
84
- width: clipR.width,
85
- height: clipR.height,
86
- pointerEvents: 'none',
87
- zIndex: 5,
88
- overflow: 'visible',
89
- }, "aria-hidden": "true", children: _jsx("rect", { x: "1", y: "1", width: Math.max(0, clipR.width - 2), height: Math.max(0, clipR.height - 2), fill: "none", stroke: "var(--ogrid-selection, #217346)", strokeWidth: "2", strokeDasharray: "4 4", style: { ...MARCHING_ANTS_ANIMATION, shapeRendering: 'crispEdges' } }) }))] }));
90
- }