diff --git a/examples/grid-pro/components-react/package.json b/examples/grid-pro/components-react/package.json index 851c39f..d216da2 100644 --- a/examples/grid-pro/components-react/package.json +++ b/examples/grid-pro/components-react/package.json @@ -16,9 +16,11 @@ "react-dom": ">=18" }, "devDependencies": { + "@tailwindcss/vite": "^4.3.2", "@types/react": ">=18", "@types/react-dom": ">=18", "@vitejs/plugin-react": "^4.2.0", + "tailwindcss": "^4.3.2", "typescript": "^5.0.0", "vite": "^5.0.0" } diff --git a/examples/grid-pro/components-react/src/App.tsx b/examples/grid-pro/components-react/src/App.tsx index 5859090..1d5a073 100644 --- a/examples/grid-pro/components-react/src/App.tsx +++ b/examples/grid-pro/components-react/src/App.tsx @@ -8,22 +8,41 @@ import { ColumnDefaults, Column, Description, - Pagination + Pagination, + Header } from '@highcharts/grid-pro-react'; const GRID_KEY = 'AAAA-BBBB-CCCC-DDDD-EEEE-FFFF'; function App() { const [dataSource, setDataSource] = useState({ - name: ['Alice', 'Bob', 'Charlie', 'David', 'Eve'], - age: [23, 34, 45, 56, 67], - city: ['New York', 'Oslo', 'Paris', 'Tokyo', 'London'], - salary: [50000, 60000, 70000, 80000, 90000] + name: [ + 'Alice Nguyen', 'Bob Berg', 'Charlie Dupont', 'David Sato', 'Eve Shaw', + 'John Hale', 'Jane Ortiz', 'Jim Novak', 'Jill Meyer', 'Jack Quinn', + 'Nora Ellis', 'Omar Khan', 'Priya Shah', 'Quinn Blake', 'Ruth Adler', + 'Sam Okonkwo', 'Tina Rossi', 'Uma Patel', 'Victor Lang', 'Wendy Cho' + ], + age: [ + 23, 34, 45, 56, 67, 30, 25, 35, 40, 45, + 28, 31, 39, 42, 51, 27, 33, 36, 44, 48 + ], + city: [ + 'New York', 'Oslo', 'Paris', 'Tokyo', 'London', + 'New York', 'Oslo', 'Paris', 'Tokyo', 'London', + 'Berlin', 'Toronto', 'Mumbai', 'Sydney', 'Zurich', + 'Lagos', 'Rome', 'Lisbon', 'Seoul', 'Chicago' + ], + salary: [ + 50000, 60000, 70000, 80000, 90000, + 40000, 35000, 45000, 50000, 55000, + 62000, 71000, 48000, 53000, 88000, + 41000, 59000, 64000, 76000, 82000 + ] }); const onButtonClick = () => { setDataSource({ - name: ['John', 'Jane', 'Jim', 'Jill', 'Jack'], + name: ['John Hale', 'Jane Ortiz', 'Jim Novak', 'Jill Meyer', 'Jack Quinn'], age: [30, 25, 35, 40, 45], city: ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Miami'], salary: [40000, 35000, 45000, 50000, 55000] @@ -35,63 +54,125 @@ function App() { }; return ( - <> - - - - Grid Pro Components - Declarative API with gridKey and event props - +
+ - - - - - -
- + > + + + Team directory +
+ + + + + + + Filter, sort, and page through sample employee rows styled with + utility classes. + + + +
+ +
- +
); } diff --git a/examples/grid-pro/components-react/src/index.css b/examples/grid-pro/components-react/src/index.css index 16edde1..93cfec9 100644 --- a/examples/grid-pro/components-react/src/index.css +++ b/examples/grid-pro/components-react/src/index.css @@ -1,3 +1,18 @@ +@import "tailwindcss"; + +/* Sample viewer theme toggle + system preference */ +@custom-variant dark { + &:where(.highcharts-dark, .highcharts-dark *) { + @slot; + } + + @media (prefers-color-scheme: dark) { + &:where(:not(.highcharts-light):not(.highcharts-light *)) { + @slot; + } + } +} + body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', @@ -6,21 +21,77 @@ body { -moz-osx-font-smoothing: grayscale; } -#root { +.hcg-container { width: 100%; - min-height: 100vh; - padding: 20px; + height: 600px; +} + +/* + * Pagination buttons / select — Core internals, styled via cascade. + */ +.hcg-pagination-controls { + @apply border-l border-r border-slate-200 dark:border-slate-700; +} + +.hcg-pagination-controls .hcg-pagination-pages { + @apply gap-2; +} + +.hcg-pagination-controls .hcg-button { + @apply size-7 rounded border border-transparent text-sm leading-none + text-slate-700 dark:text-slate-200; +} + +.hcg-pagination-controls .hcg-button:hover:not(:disabled) { + @apply bg-slate-200 dark:bg-slate-700; +} + +.hcg-pagination-controls .hcg-button-selected, +.hcg-pagination-controls .hcg-button-selected:hover:not(:disabled) { + @apply bg-teal-700 text-white dark:bg-teal-600; +} + +.demo-pag-size select.hcg-input { + @apply rounded border border-slate-300 bg-white py-1 pl-2 pr-5 text-sm + text-slate-700 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-200; +} + +/* + * Filter popup — Core internals, styled via cascade. + */ +.demo-grid .hcg-container .hcg-popup { + @apply rounded-lg border border-slate-200 bg-white text-sm text-slate-700 + shadow-lg dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 + dark:shadow-black/40; +} + +.demo-grid .hcg-container .hcg-menu-header { + @apply mb-2 px-1 text-xs font-semibold text-slate-500 dark:text-slate-400; +} + +.demo-grid .hcg-container .hcg-column-filter-wrapper { + @apply gap-2; +} + +.demo-grid .hcg-container .hcg-column-filter-wrapper .hcg-input { + @apply w-full rounded border border-slate-300 bg-white px-2 py-1.5 text-sm + text-slate-700 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-200; +} + +.demo-grid .hcg-container .hcg-column-filter-wrapper select.hcg-input { + @apply pr-5; +} + +.demo-grid .hcg-container .hcg-clear-filter-button { + @apply text-xs font-medium text-slate-600 no-underline dark:text-slate-300; } -#controls { - margin-top: 20px; - display: flex; - gap: 10px; +.demo-grid .hcg-container .hcg-clear-filter-button:hover:not(:disabled) { + @apply text-slate-900 underline dark:text-slate-50; } -@media (prefers-color-scheme: dark) { - body { - background-color: #121212; - color: #ffffff; +@container hcg (max-width: 800px) { + .hcg-pagination-controls { + @apply justify-center border-r-0 border-l-0 mt-2 mb-2; } } diff --git a/examples/grid-pro/components-react/vite.config.ts b/examples/grid-pro/components-react/vite.config.ts index a28be86..39805a3 100644 --- a/examples/grid-pro/components-react/vite.config.ts +++ b/examples/grid-pro/components-react/vite.config.ts @@ -1,12 +1,13 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); export default defineConfig({ - plugins: [react()], + plugins: [react(), tailwindcss()], resolve: { alias: [ { @@ -25,6 +26,7 @@ export default defineConfig({ ] }, server: { + host: true, port: 3002 } }); diff --git a/packages/grid-shared-react/src/hooks/useDeclarativeGridOptions.ts b/packages/grid-shared-react/src/hooks/useDeclarativeGridOptions.ts index be657fe..3a3c24b 100644 --- a/packages/grid-shared-react/src/hooks/useDeclarativeGridOptions.ts +++ b/packages/grid-shared-react/src/hooks/useDeclarativeGridOptions.ts @@ -53,6 +53,7 @@ export const useDeclarativeGridOptions: UseDeclarativeGridOptionsFn = ( ); const gridOptions = useMemo( () => build(childOptions, options), + // eslint-disable-next-line react-hooks/exhaustive-deps [childOptions, options, ...buildDeps] ); diff --git a/packages/grid-shared-react/src/hooks/useGrid.test.tsx b/packages/grid-shared-react/src/hooks/useGrid.test.tsx index 53e86d1..1e65bd2 100644 --- a/packages/grid-shared-react/src/hooks/useGrid.test.tsx +++ b/packages/grid-shared-react/src/hooks/useGrid.test.tsx @@ -11,7 +11,10 @@ interface DeferredInit { resolve: () => Promise; } -function createDeferredGrid(initQueue: DeferredInit[]): GridType { +function createDeferredGrid( + initQueue: DeferredInit[], + destroyedIds: number[] = [] +): GridType { let nextId = 0; return { @@ -19,6 +22,7 @@ function createDeferredGrid(initQueue: DeferredInit[]): GridType { const id = ++nextId; const grid: GridInstance = { destroy: () => { + destroyedIds.push(id); container.innerHTML = ''; }, update: () => {} @@ -48,9 +52,10 @@ function createDeferredGrid(initQueue: DeferredInit[]): GridType { describe('useGrid', () => { it('keeps the active grid when StrictMode double-inits', async () => { const initQueue: DeferredInit[] = []; - const Grid = createDeferredGrid(initQueue); + const destroyedIds: number[] = []; + const Grid = createDeferredGrid(initQueue, destroyedIds); - const { container } = render( + const { container, unmount } = render( @@ -68,5 +73,9 @@ describe('useGrid', () => { await waitFor(() => { expect(container.querySelector('[data-grid-id="1"]')).not.toBeNull(); }); + + unmount(); + + expect(destroyedIds).toEqual([1]); }); }); diff --git a/packages/grid-shared-react/src/hooks/useGrid.ts b/packages/grid-shared-react/src/hooks/useGrid.ts index f5e1756..d0ab479 100644 --- a/packages/grid-shared-react/src/hooks/useGrid.ts +++ b/packages/grid-shared-react/src/hooks/useGrid.ts @@ -70,9 +70,18 @@ export function useGrid({ // allow init to complete if re-mounted. destroyOnInitRef.current = false; - // Prevent double initialization + const destroyGrid = () => { + destroyOnInitRef.current = true; + if (currGridRef.current) { + currGridRef.current.destroy(); + currGridRef.current = null; + } + }; + + // Prevent double initialization. Still return destroyGrid so the + // StrictMode remount keeps a destroy handler. if (initStartedRef.current || currGridRef.current) { - return; + return destroyGrid; } initStartedRef.current = true; @@ -114,14 +123,8 @@ export function useGrid({ initGrid(); - return () => { - destroyOnInitRef.current = true; - if (currGridRef.current) { - currGridRef.current.destroy(); - currGridRef.current = null; - } - }; - }, [containerRef, Grid]); + return destroyGrid; + }, [containerRef, Grid, options]); // Effect for options updates - separate from init useEffect(() => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 196d204..eba9be5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -168,6 +168,9 @@ importers: specifier: '>=18' version: 19.2.1(react@19.2.1) devDependencies: + '@tailwindcss/vite': + specifier: ^4.3.2 + version: 4.3.2(vite@5.4.21(@types/node@20.19.26)(lightningcss@1.32.0)) '@types/react': specifier: '>=18' version: 19.2.7 @@ -177,6 +180,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.2.0 version: 4.7.0(vite@5.4.21(@types/node@20.19.26)(lightningcss@1.32.0)) + tailwindcss: + specifier: ^4.3.2 + version: 4.3.2 typescript: specifier: ^5.0.0 version: 5.9.3