@alaarab/ogrid-react 2.1.1 → 2.1.3

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.
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback, useEffect } from 'react';
2
3
  import { SideBar } from './SideBar';
3
4
  import { GRID_BORDER_RADIUS } from '@alaarab/ogrid-core';
4
5
  // Stable style objects (avoid re-creating on every render)
@@ -12,6 +13,11 @@ const borderedContainerStyle = {
12
13
  minHeight: 0,
13
14
  background: 'var(--ogrid-bg, #fff)',
14
15
  };
16
+ const fullscreenContainerStyle = {
17
+ ...borderedContainerStyle,
18
+ borderRadius: 0,
19
+ border: 'none',
20
+ };
15
21
  const toolbarStripBase = {
16
22
  display: 'flex',
17
23
  justifyContent: 'space-between',
@@ -72,6 +78,29 @@ const rootStyle = {
72
78
  flexDirection: 'column',
73
79
  height: '100%',
74
80
  };
81
+ const fullscreenRootStyle = {
82
+ position: 'fixed',
83
+ inset: 0,
84
+ zIndex: 9999,
85
+ display: 'flex',
86
+ flexDirection: 'column',
87
+ background: 'var(--ogrid-bg, #fff)',
88
+ };
89
+ const fullscreenBtnStyle = {
90
+ background: 'none',
91
+ border: '1px solid var(--ogrid-border, #e0e0e0)',
92
+ borderRadius: 4,
93
+ padding: '4px 6px',
94
+ cursor: 'pointer',
95
+ display: 'flex',
96
+ alignItems: 'center',
97
+ justifyContent: 'center',
98
+ color: 'var(--ogrid-fg, #242424)',
99
+ };
100
+ // SVG expand icon (enter fullscreen)
101
+ const ExpandIcon = () => (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("polyline", { points: "10 2 14 2 14 6" }), _jsx("polyline", { points: "6 14 2 14 2 10" }), _jsx("line", { x1: "14", y1: "2", x2: "10", y2: "6" }), _jsx("line", { x1: "2", y1: "14", x2: "6", y2: "10" })] }));
102
+ // SVG collapse icon (exit fullscreen)
103
+ const CollapseIcon = () => (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("polyline", { points: "4 10 0 10 0 14" }), _jsx("polyline", { points: "12 6 16 6 16 2" }), _jsx("line", { x1: "0", y1: "10", x2: "4", y2: "6" }), _jsx("line", { x1: "16", y1: "6", x2: "12", y2: "10" })] }));
75
104
  /**
76
105
  * Renders OGrid layout as a unified bordered container:
77
106
  * ┌────────────────────────────────────┐
@@ -83,9 +112,25 @@ const rootStyle = {
83
112
  * └────────────────────────────────────┘
84
113
  */
85
114
  export function OGridLayout(props) {
86
- const { containerComponent: Container = 'div', containerProps = {}, className, toolbar, toolbarEnd, toolbarBelow, children, pagination, sideBar, } = props;
115
+ const { containerComponent: Container = 'div', containerProps = {}, className, toolbar, toolbarEnd, toolbarBelow, children, pagination, sideBar, fullScreen, } = props;
116
+ const [isFullScreen, setIsFullScreen] = useState(false);
117
+ const toggleFullScreen = useCallback(() => {
118
+ setIsFullScreen((prev) => !prev);
119
+ }, []);
120
+ // ESC key to exit fullscreen
121
+ useEffect(() => {
122
+ if (!isFullScreen)
123
+ return;
124
+ const handleKeyDown = (e) => {
125
+ if (e.key === 'Escape')
126
+ setIsFullScreen(false);
127
+ };
128
+ document.addEventListener('keydown', handleKeyDown);
129
+ return () => document.removeEventListener('keydown', handleKeyDown);
130
+ }, [isFullScreen]);
87
131
  const hasSideBar = sideBar != null;
88
132
  const sideBarPosition = sideBar?.position ?? 'right';
89
- const hasToolbar = toolbar != null || toolbarEnd != null;
90
- return (_jsx(Container, { className: className, style: rootStyle, ...containerProps, children: _jsxs("div", { style: borderedContainerStyle, children: [hasToolbar && (_jsxs("div", { style: toolbarBelow ? toolbarStripNoBorderStyle : toolbarStripStyle, children: [_jsx("div", { style: toolbarSectionStyle, children: toolbar }), _jsx("div", { style: toolbarSectionStyle, children: toolbarEnd })] })), toolbarBelow && (_jsx("div", { style: toolbarBelowStyle, children: toolbarBelow })), hasSideBar ? (_jsxs("div", { style: gridAreaFlexStyle, children: [sideBarPosition === 'left' && _jsx(SideBar, { ...sideBar }), _jsx("div", { style: gridChildStyle, children: children }), sideBarPosition !== 'left' && _jsx(SideBar, { ...sideBar })] })) : (_jsx("div", { style: gridAreaSoloStyle, children: children })), pagination && (_jsx("div", { style: footerStripStyle, children: pagination }))] }) }));
133
+ const hasToolbar = toolbar != null || toolbarEnd != null || fullScreen;
134
+ const fullscreenButton = fullScreen ? (_jsx("button", { type: "button", style: fullscreenBtnStyle, onClick: toggleFullScreen, title: isFullScreen ? 'Exit fullscreen' : 'Fullscreen', "aria-label": isFullScreen ? 'Exit fullscreen' : 'Fullscreen', children: isFullScreen ? _jsx(CollapseIcon, {}) : _jsx(ExpandIcon, {}) })) : null;
135
+ return (_jsx(Container, { className: className, style: isFullScreen ? fullscreenRootStyle : rootStyle, ...containerProps, children: _jsxs("div", { style: isFullScreen ? fullscreenContainerStyle : borderedContainerStyle, children: [hasToolbar && (_jsxs("div", { style: toolbarBelow ? toolbarStripNoBorderStyle : toolbarStripStyle, children: [_jsx("div", { style: toolbarSectionStyle, children: toolbar }), _jsxs("div", { style: toolbarSectionStyle, children: [toolbarEnd, fullscreenButton] })] })), toolbarBelow && (_jsx("div", { style: toolbarBelowStyle, children: toolbarBelow })), hasSideBar ? (_jsxs("div", { style: gridAreaFlexStyle, children: [sideBarPosition === 'left' && _jsx(SideBar, { ...sideBar }), _jsx("div", { style: gridChildStyle, children: children }), sideBarPosition !== 'left' && _jsx(SideBar, { ...sideBar })] })) : (_jsx("div", { style: gridAreaSoloStyle, children: children })), pagination && (_jsx("div", { style: footerStripStyle, children: pagination }))] }) }));
91
136
  }
@@ -12,7 +12,7 @@ export function createOGrid(components) {
12
12
  const { DataGridTable, ColumnChooser, PaginationControls, containerComponent, containerProps, } = components;
13
13
  const OGridInner = forwardRef(function OGridInner(props, ref) {
14
14
  const { dataGridProps, pagination, columnChooser, layout } = useOGrid(props, ref);
15
- return (_jsx(OGridLayout, { containerComponent: containerComponent, containerProps: containerProps, className: layout.className, sideBar: layout.sideBarProps, toolbar: layout.toolbar, toolbarBelow: layout.toolbarBelow, toolbarEnd: columnChooser.placement === 'toolbar' ? (_jsx(ColumnChooser, { columns: columnChooser.columns, visibleColumns: columnChooser.visibleColumns, onVisibilityChange: columnChooser.onVisibilityChange, onSetVisibleColumns: columnChooser.onSetVisibleColumns })) : undefined, pagination: _jsx(PaginationControls, { currentPage: pagination.page, pageSize: pagination.pageSize, totalCount: pagination.displayTotalCount, onPageChange: pagination.setPage, onPageSizeChange: pagination.setPageSize, pageSizeOptions: pagination.pageSizeOptions, entityLabelPlural: pagination.entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
15
+ return (_jsx(OGridLayout, { containerComponent: containerComponent, containerProps: containerProps, className: layout.className, sideBar: layout.sideBarProps, toolbar: layout.toolbar, toolbarBelow: layout.toolbarBelow, fullScreen: layout.fullScreen, toolbarEnd: columnChooser.placement === 'toolbar' ? (_jsx(ColumnChooser, { columns: columnChooser.columns, visibleColumns: columnChooser.visibleColumns, onVisibilityChange: columnChooser.onVisibilityChange, onSetVisibleColumns: columnChooser.onSetVisibleColumns })) : undefined, pagination: _jsx(PaginationControls, { currentPage: pagination.page, pageSize: pagination.pageSize, totalCount: pagination.displayTotalCount, onPageChange: pagination.setPage, onPageSizeChange: pagination.setPageSize, pageSizeOptions: pagination.pageSizeOptions, entityLabelPlural: pagination.entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
16
16
  });
17
17
  OGridInner.displayName = 'OGrid';
18
18
  return React.memo(OGridInner);
@@ -35,7 +35,7 @@ export function useDataGridTableOrchestration(params) {
35
35
  const { headerMenu } = pinning;
36
36
  const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
37
37
  // ── Props destructuring ─────────────────────────────────────────────────
38
- const { items, columns, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, visibleColumns, columnOrder, onColumnOrderChange, columnReorder, virtualScroll, rowHeight, density = 'normal', pinnedColumns, currentPage = 1, pageSize: propPageSize = 25, } = props;
38
+ const { items, columns, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', suppressHorizontalScroll, stickyHeader = true, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, visibleColumns, columnOrder, onColumnOrderChange, columnReorder, virtualScroll, rowHeight, density = 'normal', pinnedColumns, currentPage = 1, pageSize: propPageSize = 25, } = props;
39
39
  // ── Derived values ──────────────────────────────────────────────────────
40
40
  const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * propPageSize : 0;
41
41
  const headerRows = useMemo(() => buildHeaderRows(columns, visibleColumns), [columns, visibleColumns]);
@@ -116,6 +116,7 @@ export function useDataGridTableOrchestration(params) {
116
116
  layoutMode,
117
117
  rowSelection,
118
118
  suppressHorizontalScroll,
119
+ stickyHeader,
119
120
  isLoading,
120
121
  loadingMessage,
121
122
  ariaLabel,
@@ -17,7 +17,7 @@ const EMPTY_LOADING_OPTIONS = {};
17
17
  * @returns Grouped props for DataGridTable, pagination controls, column chooser, layout, and filters.
18
18
  */
19
19
  export function useOGrid(props, ref) {
20
- const { columns: columnsProp, getRowId, data, dataSource, page: controlledPage, pageSize: controlledPageSize, sort: controlledSort, filters: controlledFilters, visibleColumns: controlledVisibleColumns, isLoading: controlledLoading, onPageChange, onPageSizeChange, onSortChange, onFiltersChange, onVisibleColumnsChange, columnOrder, onColumnOrderChange, onColumnResized, onColumnPinned, defaultPageSize = DEFAULT_PAGE_SIZE, defaultSortBy, defaultSortDirection = 'asc', toolbar, toolbarBelow, emptyState, entityLabelPlural = 'items', className, layoutMode = 'fill', suppressHorizontalScroll, editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo, rowSelection = 'none', selectedRows, onSelectionChange, showRowNumbers, statusBar, pageSizeOptions, sideBar, onFirstDataRendered, onError, columnChooser: columnChooserProp, columnReorder, virtualScroll, rowHeight, density = 'normal', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
20
+ const { columns: columnsProp, getRowId, data, dataSource, page: controlledPage, pageSize: controlledPageSize, sort: controlledSort, filters: controlledFilters, visibleColumns: controlledVisibleColumns, isLoading: controlledLoading, onPageChange, onPageSizeChange, onSortChange, onFiltersChange, onVisibleColumnsChange, columnOrder, onColumnOrderChange, onColumnResized, onColumnPinned, defaultPageSize = DEFAULT_PAGE_SIZE, defaultSortBy, defaultSortDirection = 'asc', toolbar, toolbarBelow, emptyState, entityLabelPlural = 'items', className, layoutMode = 'fill', suppressHorizontalScroll, editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo, rowSelection = 'none', selectedRows, onSelectionChange, showRowNumbers, statusBar, pageSizeOptions, sideBar, stickyHeader, fullScreen, onFirstDataRendered, onError, columnChooser: columnChooserProp, columnReorder, virtualScroll, rowHeight, density = 'normal', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
21
21
  // --- Derived column state ---
22
22
  const columnChooserPlacement = columnChooserProp === false ? 'none'
23
23
  : columnChooserProp === 'sidebar' ? 'sidebar'
@@ -73,6 +73,22 @@ export function useOGrid(props, ref) {
73
73
  .map((c) => c.columnId);
74
74
  return new Set(visible.length > 0 ? visible : columns.map((c) => c.columnId));
75
75
  });
76
+ // Re-initialize when columns arrive after starting empty (common pattern: columns
77
+ // depend on async data, so the initial render passes columns=[] then re-renders
78
+ // with actual columns once data loads).
79
+ const prevColumnsLengthRef = useRef(columns.length);
80
+ useEffect(() => {
81
+ const prev = prevColumnsLengthRef.current;
82
+ prevColumnsLengthRef.current = columns.length;
83
+ if (controlledVisibleColumns !== undefined)
84
+ return; // controlled — skip
85
+ if (prev === 0 && columns.length > 0 && internalVisibleColumns.size === 0) {
86
+ const visible = columns
87
+ .filter((c) => c.defaultVisible !== false)
88
+ .map((c) => c.columnId);
89
+ setInternalVisibleColumns(new Set(visible.length > 0 ? visible : columns.map((c) => c.columnId)));
90
+ }
91
+ }, [columns, controlledVisibleColumns, internalVisibleColumns.size]);
76
92
  const visibleColumns = controlledVisibleColumns ?? internalVisibleColumns;
77
93
  const setVisibleColumns = useCallback((cols) => {
78
94
  if (controlledVisibleColumns === undefined)
@@ -290,6 +306,7 @@ export function useOGrid(props, ref) {
290
306
  getUserByEmail: dataSource?.getUserByEmail,
291
307
  layoutMode,
292
308
  suppressHorizontalScroll,
309
+ stickyHeader: stickyHeader ?? true,
293
310
  columnReorder,
294
311
  virtualScroll,
295
312
  rowHeight,
@@ -312,7 +329,7 @@ export function useOGrid(props, ref) {
312
329
  paginationState.page, paginationState.pageSize, statusBarConfig,
313
330
  isLoadingResolved, filtersState.filters, filtersState.handleFilterChange,
314
331
  filtersState.clientFilterOptions, dataSource, filtersState.loadingFilterOptions,
315
- layoutMode, suppressHorizontalScroll, columnReorder, virtualScroll,
332
+ layoutMode, suppressHorizontalScroll, stickyHeader, columnReorder, virtualScroll,
316
333
  rowHeight, density, ariaLabel, ariaLabelledBy,
317
334
  filtersState.hasActiveFilters, clearAllFilters, emptyState,
318
335
  ]);
@@ -338,7 +355,8 @@ export function useOGrid(props, ref) {
338
355
  className,
339
356
  emptyState,
340
357
  sideBarProps,
341
- }), [toolbar, toolbarBelow, className, emptyState, sideBarProps]);
358
+ fullScreen,
359
+ }), [toolbar, toolbarBelow, className, emptyState, sideBarProps, fullScreen]);
342
360
  const filtersResult = useMemo(() => ({
343
361
  hasActiveFilters: filtersState.hasActiveFilters,
344
362
  setFilters: filtersState.setFilters,
@@ -23,6 +23,8 @@ export interface OGridLayoutProps {
23
23
  pagination?: React.ReactNode;
24
24
  /** Side bar props. When provided, renders SideBar alongside the grid. */
25
25
  sideBar?: SideBarProps | null;
26
+ /** When true, render a fullscreen toggle button in the toolbar. */
27
+ fullScreen?: boolean;
26
28
  }
27
29
  /**
28
30
  * Renders OGrid layout as a unified bordered container:
@@ -37,6 +37,7 @@ export interface UseDataGridTableOrchestrationResult<T> {
37
37
  layoutMode: 'fill' | 'content';
38
38
  rowSelection: IOGridDataGridProps<T>['rowSelection'];
39
39
  suppressHorizontalScroll: IOGridDataGridProps<T>['suppressHorizontalScroll'];
40
+ stickyHeader: boolean;
40
41
  isLoading: boolean;
41
42
  loadingMessage: string;
42
43
  ariaLabel: string | undefined;
@@ -31,6 +31,7 @@ export interface UseOGridLayout {
31
31
  render?: () => React.ReactNode;
32
32
  };
33
33
  sideBarProps: SideBarProps | null;
34
+ fullScreen?: boolean;
34
35
  }
35
36
  /** Filter state. */
36
37
  export interface UseOGridFilters {
@@ -64,6 +64,10 @@ interface IOGridBaseProps<T> {
64
64
  layoutMode?: 'content' | 'fill';
65
65
  /** When true, horizontal scrolling is suppressed (overflow-x hidden). */
66
66
  suppressHorizontalScroll?: boolean;
67
+ /** When true (default), header row sticks to the top of the scroll container. */
68
+ stickyHeader?: boolean;
69
+ /** When true, shows a fullscreen toggle button in the toolbar. Default: false. */
70
+ fullScreen?: boolean;
67
71
  /** Side bar configuration. `true` shows default panels (columns + filters). Pass ISideBarDef for options. */
68
72
  sideBar?: boolean | ISideBarDef;
69
73
  /** Page size options shown in the pagination dropdown. Default: [10, 20, 50, 100]. */
@@ -123,6 +127,8 @@ export interface IOGridDataGridProps<T> {
123
127
  layoutMode?: 'content' | 'fill';
124
128
  /** When true, horizontal scrolling is suppressed (overflow-x hidden). */
125
129
  suppressHorizontalScroll?: boolean;
130
+ /** When true (default), header row sticks to the top of the scroll container. */
131
+ stickyHeader?: boolean;
126
132
  isLoading?: boolean;
127
133
  loadingMessage?: string;
128
134
  editable?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
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",
@@ -36,18 +36,13 @@
36
36
  "node": ">=18"
37
37
  },
38
38
  "dependencies": {
39
- "@alaarab/ogrid-core": "2.1.1"
39
+ "@alaarab/ogrid-core": "2.1.3",
40
+ "@tanstack/react-virtual": "^3.0.0"
40
41
  },
41
42
  "peerDependencies": {
42
- "@tanstack/react-virtual": "^3.0.0",
43
43
  "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
44
44
  "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
45
45
  },
46
- "peerDependenciesMeta": {
47
- "@tanstack/react-virtual": {
48
- "optional": true
49
- }
50
- },
51
46
  "devDependencies": {
52
47
  "@testing-library/jest-dom": "^6.9.1",
53
48
  "@testing-library/react": "^16.3.2",