@dashforge/tw 0.7.0-beta → 0.8.0-beta

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 (37) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/dist/index.esm.js +1092 -74
  3. package/dist/src/components/DataGrid/DataGrid.d.ts.map +1 -1
  4. package/dist/src/components/DataGrid/dataGrid.types.d.ts +43 -0
  5. package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -1
  6. package/dist/src/components/DataGrid/dataGrid.variants.d.ts +6 -0
  7. package/dist/src/components/DataGrid/dataGrid.variants.d.ts.map +1 -1
  8. package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts +23 -0
  9. package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts.map +1 -0
  10. package/dist/src/components/DataGrid/reorder/useColumnReorder.d.ts +50 -0
  11. package/dist/src/components/DataGrid/reorder/useColumnReorder.d.ts.map +1 -0
  12. package/dist/src/components/DataGrid/resize/useColumnResize.d.ts +32 -0
  13. package/dist/src/components/DataGrid/resize/useColumnResize.d.ts.map +1 -0
  14. package/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.d.ts +33 -0
  15. package/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.d.ts.map +1 -0
  16. package/dist/src/components/Table/Table.d.ts.map +1 -1
  17. package/dist/src/components/Table/table.types.d.ts +69 -5
  18. package/dist/src/components/Table/table.types.d.ts.map +1 -1
  19. package/dist/src/components/_shared/data/useTableFilter.d.ts +30 -7
  20. package/dist/src/components/_shared/data/useTableFilter.d.ts.map +1 -1
  21. package/dist/src/index.d.ts +1 -1
  22. package/dist/src/index.d.ts.map +1 -1
  23. package/package.json +3 -3
  24. package/src/components/DataGrid/DataGrid.test.tsx +422 -1
  25. package/src/components/DataGrid/DataGrid.tsx +421 -53
  26. package/src/components/DataGrid/dataGrid.types.ts +50 -1
  27. package/src/components/DataGrid/dataGrid.variants.ts +9 -1
  28. package/src/components/DataGrid/filters/ColumnFilters.tsx +432 -0
  29. package/src/components/DataGrid/reorder/useColumnReorder.test.ts +38 -0
  30. package/src/components/DataGrid/reorder/useColumnReorder.ts +144 -0
  31. package/src/components/DataGrid/resize/useColumnResize.ts +106 -0
  32. package/src/components/DataGrid/visibility/ColumnVisibilityDialog.tsx +200 -0
  33. package/src/components/Table/Table.tsx +14 -0
  34. package/src/components/Table/table.types.ts +74 -5
  35. package/src/components/_shared/data/useTableFilter.test.ts +166 -0
  36. package/src/components/_shared/data/useTableFilter.ts +97 -16
  37. package/src/index.ts +2 -0
@@ -1,6 +1,6 @@
1
1
  // @vitest-environment jsdom
2
2
  import * as React from 'react';
3
- import { describe, it, expect, afterEach, beforeAll } from 'vitest';
3
+ import { describe, it, expect, afterEach, beforeAll, vi } from 'vitest';
4
4
  import { render, screen, fireEvent, cleanup } from '@testing-library/react';
5
5
  import { DataGrid } from './DataGrid.js';
6
6
  import type { TableColumn } from '../Table/table.types.js';
@@ -494,3 +494,424 @@ describe('DataGrid — i18n', () => {
494
494
  expect(screen.queryByLabelText('Seleziona tutto')).not.toBeNull();
495
495
  });
496
496
  });
497
+
498
+ describe('DataGrid — per-column filter UI', () => {
499
+ it('renders a filter trigger button only for filterable columns', () => {
500
+ const cols: TableColumn<User>[] = [
501
+ { field: 'name', header: 'Name', filterable: true },
502
+ { field: 'email', header: 'Email' },
503
+ { field: 'age', header: 'Age', filterable: true },
504
+ ];
505
+ render(
506
+ <DataGrid
507
+ rows={generateUsers(5)}
508
+ cols={cols}
509
+ getRowId={getRowId}
510
+ rowHeight={48}
511
+ height="400px"
512
+ />,
513
+ );
514
+ // 2 filterable cols → 2 filter buttons
515
+ const triggers = screen.getAllByRole('button', { name: 'Filter' });
516
+ expect(triggers).toHaveLength(2);
517
+ });
518
+
519
+ it('shows active state (aria-pressed=true) when a filter is applied', () => {
520
+ const cols: TableColumn<User>[] = [
521
+ { field: 'name', header: 'Name', filterable: true },
522
+ ];
523
+ render(
524
+ <DataGrid
525
+ rows={generateUsers(5)}
526
+ cols={cols}
527
+ getRowId={getRowId}
528
+ rowHeight={48}
529
+ height="400px"
530
+ filterModel={[{ field: 'name', op: 'contains', value: 'User 1' }]}
531
+ />,
532
+ );
533
+ const trigger = screen.getByRole('button', { name: 'Filter' });
534
+ expect(trigger.getAttribute('aria-pressed')).toBe('true');
535
+ });
536
+
537
+ it('opens a text input when filterable string column is clicked', () => {
538
+ const cols: TableColumn<User>[] = [
539
+ { field: 'name', header: 'Name', filterable: true },
540
+ ];
541
+ render(
542
+ <DataGrid
543
+ rows={generateUsers(5)}
544
+ cols={cols}
545
+ getRowId={getRowId}
546
+ rowHeight={48}
547
+ height="400px"
548
+ />,
549
+ );
550
+ fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
551
+ // Apply button means the Popover opened
552
+ expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeNull();
553
+ });
554
+
555
+ it('opens Min/Max inputs for filterable number column', () => {
556
+ const cols: TableColumn<User>[] = [
557
+ { field: 'age', header: 'Age', filterable: true },
558
+ ];
559
+ render(
560
+ <DataGrid
561
+ rows={generateUsers(5)}
562
+ cols={cols}
563
+ getRowId={getRowId}
564
+ rowHeight={48}
565
+ height="400px"
566
+ />,
567
+ );
568
+ fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
569
+ expect(screen.queryByText('Min')).not.toBeNull();
570
+ expect(screen.queryByText('Max')).not.toBeNull();
571
+ });
572
+
573
+ it('explicit col.filterType="text" overrides number autodetect', () => {
574
+ const cols: TableColumn<User>[] = [
575
+ { field: 'age', header: 'Age', filterable: true, filterType: 'text' },
576
+ ];
577
+ render(
578
+ <DataGrid
579
+ rows={generateUsers(5)}
580
+ cols={cols}
581
+ getRowId={getRowId}
582
+ rowHeight={48}
583
+ height="400px"
584
+ />,
585
+ );
586
+ fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
587
+ // Text filter has Apply but no Min/Max
588
+ expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeNull();
589
+ expect(screen.queryByText('Min')).toBeNull();
590
+ });
591
+
592
+ it('emits onFilterChange when Apply is clicked', () => {
593
+ const onFilterChange = vi.fn();
594
+ const cols: TableColumn<User>[] = [
595
+ { field: 'name', header: 'Name', filterable: true },
596
+ ];
597
+ render(
598
+ <DataGrid
599
+ rows={generateUsers(5)}
600
+ cols={cols}
601
+ getRowId={getRowId}
602
+ rowHeight={48}
603
+ height="400px"
604
+ onFilterChange={onFilterChange}
605
+ />,
606
+ );
607
+ fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
608
+ const input = screen.getByPlaceholderText('…') as HTMLInputElement;
609
+ fireEvent.change(input, { target: { value: 'User 2' } });
610
+ fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
611
+ expect(onFilterChange).toHaveBeenCalledWith([
612
+ { field: 'name', op: 'contains', value: 'User 2' },
613
+ ]);
614
+ });
615
+
616
+ it('translates filter labels via the labels prop', () => {
617
+ const cols: TableColumn<User>[] = [
618
+ { field: 'age', header: 'Età', filterable: true },
619
+ ];
620
+ render(
621
+ <DataGrid
622
+ rows={generateUsers(5)}
623
+ cols={cols}
624
+ getRowId={getRowId}
625
+ rowHeight={48}
626
+ height="400px"
627
+ labels={{
628
+ filterColumn: 'Filtra',
629
+ filterApply: 'Applica',
630
+ filterClear: 'Pulisci',
631
+ filterMin: 'Min',
632
+ filterMax: 'Max',
633
+ }}
634
+ />,
635
+ );
636
+ fireEvent.click(screen.getByRole('button', { name: 'Filtra' }));
637
+ expect(screen.queryByRole('button', { name: 'Applica' })).not.toBeNull();
638
+ expect(screen.queryByRole('button', { name: 'Pulisci' })).not.toBeNull();
639
+ });
640
+ });
641
+
642
+ describe('DataGrid — column visibility', () => {
643
+ it('renders the Columns toolbar button by default', () => {
644
+ render(
645
+ <DataGrid
646
+ rows={generateUsers(5)}
647
+ cols={baseCols}
648
+ getRowId={getRowId}
649
+ rowHeight={48}
650
+ height="400px"
651
+ />,
652
+ );
653
+ expect(screen.queryByRole('button', { name: 'Columns' })).not.toBeNull();
654
+ });
655
+
656
+ it('does NOT render the Columns button when enableColumnVisibility=false', () => {
657
+ render(
658
+ <DataGrid
659
+ rows={generateUsers(5)}
660
+ cols={baseCols}
661
+ getRowId={getRowId}
662
+ rowHeight={48}
663
+ height="400px"
664
+ enableColumnVisibility={false}
665
+ />,
666
+ );
667
+ expect(screen.queryByRole('button', { name: 'Columns' })).toBeNull();
668
+ });
669
+
670
+ it('opens dialog with one checkbox per hideable column', () => {
671
+ const cols: TableColumn<User>[] = [
672
+ { field: 'name', header: 'Name' },
673
+ { field: 'email', header: 'Email' },
674
+ // hideable: false → not shown in the dialog (structurally required)
675
+ { field: 'age', header: 'Age', hideable: false },
676
+ ];
677
+ render(
678
+ <DataGrid
679
+ rows={generateUsers(5)}
680
+ cols={cols}
681
+ getRowId={getRowId}
682
+ rowHeight={48}
683
+ height="400px"
684
+ />,
685
+ );
686
+ fireEvent.click(screen.getByRole('button', { name: 'Columns' }));
687
+ expect(screen.queryByRole('dialog')).not.toBeNull();
688
+ // The dialog should expose checkboxes for Name + Email (not Age).
689
+ const labels = screen.getAllByText(/Name|Email|Age/);
690
+ // Name + Email appear in BOTH the table header and the dialog; Age
691
+ // appears only in the header (hideable=false → excluded from dialog).
692
+ const dialog = screen.getByRole('dialog');
693
+ expect(dialog.textContent).toContain('Name');
694
+ expect(dialog.textContent).toContain('Email');
695
+ expect(dialog.textContent).not.toContain('Age');
696
+ void labels;
697
+ });
698
+
699
+ it('respects col.defaultHidden on first render', () => {
700
+ const cols: TableColumn<User>[] = [
701
+ { field: 'name', header: 'Name' },
702
+ { field: 'email', header: 'Email', defaultHidden: true },
703
+ ];
704
+ render(
705
+ <DataGrid
706
+ rows={generateUsers(5)}
707
+ cols={cols}
708
+ getRowId={getRowId}
709
+ rowHeight={48}
710
+ height="400px"
711
+ />,
712
+ );
713
+ // Email column header should NOT be rendered initially.
714
+ expect(screen.queryByText('Email')).toBeNull();
715
+ // Name column header SHOULD be rendered.
716
+ expect(screen.queryByText('Name')).not.toBeNull();
717
+ });
718
+
719
+ it('toggling a column hides it from the table after Done', () => {
720
+ const onHiddenColumnsChange = vi.fn();
721
+ render(
722
+ <DataGrid
723
+ rows={generateUsers(5)}
724
+ cols={baseCols}
725
+ getRowId={getRowId}
726
+ rowHeight={48}
727
+ height="400px"
728
+ onHiddenColumnsChange={onHiddenColumnsChange}
729
+ />,
730
+ );
731
+ fireEvent.click(screen.getByRole('button', { name: 'Columns' }));
732
+ // Uncheck "Email" inside the dialog
733
+ const dialog = screen.getByRole('dialog');
734
+ const emailCheckbox = Array.from(
735
+ dialog.querySelectorAll('input[type="checkbox"]'),
736
+ ).find((el) => {
737
+ const li = el.closest('label');
738
+ return li?.textContent?.includes('Email');
739
+ }) as HTMLInputElement | undefined;
740
+ expect(emailCheckbox).toBeDefined();
741
+ fireEvent.click(emailCheckbox!);
742
+ // Commit via Done
743
+ fireEvent.click(screen.getByRole('button', { name: 'Done' }));
744
+ expect(onHiddenColumnsChange).toHaveBeenCalledWith(['email']);
745
+ });
746
+
747
+ it('controlled hiddenColumns hides the matching columns', () => {
748
+ render(
749
+ <DataGrid
750
+ rows={generateUsers(5)}
751
+ cols={baseCols}
752
+ getRowId={getRowId}
753
+ rowHeight={48}
754
+ height="400px"
755
+ hiddenColumns={['email']}
756
+ />,
757
+ );
758
+ expect(screen.queryByText('Email')).toBeNull();
759
+ expect(screen.queryByText('Name')).not.toBeNull();
760
+ });
761
+ });
762
+
763
+ describe('DataGrid — column resize', () => {
764
+ it('renders a resize handle (separator) for each column by default', () => {
765
+ render(
766
+ <DataGrid
767
+ rows={generateUsers(5)}
768
+ cols={baseCols}
769
+ getRowId={getRowId}
770
+ rowHeight={48}
771
+ height="400px"
772
+ />,
773
+ );
774
+ // One separator per column (4 columns).
775
+ const handles = screen.getAllByRole('separator');
776
+ expect(handles.length).toBe(baseCols.length);
777
+ });
778
+
779
+ it('omits resize handles when enableColumnResize=false', () => {
780
+ render(
781
+ <DataGrid
782
+ rows={generateUsers(5)}
783
+ cols={baseCols}
784
+ getRowId={getRowId}
785
+ rowHeight={48}
786
+ height="400px"
787
+ enableColumnResize={false}
788
+ />,
789
+ );
790
+ expect(screen.queryAllByRole('separator')).toHaveLength(0);
791
+ });
792
+
793
+ it('omits resize handle for col.resizable=false', () => {
794
+ const cols: TableColumn<User>[] = [
795
+ { field: 'name', header: 'Name' },
796
+ { field: 'email', header: 'Email', resizable: false },
797
+ ];
798
+ render(
799
+ <DataGrid
800
+ rows={generateUsers(5)}
801
+ cols={cols}
802
+ getRowId={getRowId}
803
+ rowHeight={48}
804
+ height="400px"
805
+ />,
806
+ );
807
+ // Only 1 separator (Name); Email opted out via resizable=false.
808
+ expect(screen.getAllByRole('separator')).toHaveLength(1);
809
+ });
810
+
811
+ it('applies columnWidths to <th> style.width', () => {
812
+ const cols: TableColumn<User>[] = [
813
+ { field: 'name', header: 'Name', width: 100 },
814
+ ];
815
+ render(
816
+ <DataGrid
817
+ rows={generateUsers(5)}
818
+ cols={cols}
819
+ getRowId={getRowId}
820
+ rowHeight={48}
821
+ height="400px"
822
+ columnWidths={{ name: 250 }}
823
+ />,
824
+ );
825
+ const th = screen.getByText('Name').closest('th');
826
+ expect(th?.style.width).toBe('250px');
827
+ });
828
+
829
+ it('falls back to col.width when no resize state for the column', () => {
830
+ const cols: TableColumn<User>[] = [
831
+ { field: 'name', header: 'Name', width: 100 },
832
+ ];
833
+ render(
834
+ <DataGrid
835
+ rows={generateUsers(5)}
836
+ cols={cols}
837
+ getRowId={getRowId}
838
+ rowHeight={48}
839
+ height="400px"
840
+ />,
841
+ );
842
+ const th = screen.getByText('Name').closest('th');
843
+ expect(th?.style.width).toBe('100px');
844
+ });
845
+ });
846
+
847
+ describe('DataGrid — column reorder', () => {
848
+ it('applies the columnOrder prop to header order', () => {
849
+ const { container } = render(
850
+ <DataGrid
851
+ rows={generateUsers(3)}
852
+ cols={baseCols}
853
+ getRowId={getRowId}
854
+ rowHeight={48}
855
+ height="400px"
856
+ columnOrder={['age', 'name', 'email', 'meta.city']}
857
+ />,
858
+ );
859
+ const headerTexts = Array.from(
860
+ container.querySelectorAll('thead th'),
861
+ ).map((th) => th.textContent?.trim() ?? '');
862
+ // First three column headers should match the order
863
+ expect(headerTexts[0]).toContain('Age');
864
+ expect(headerTexts[1]).toContain('Name');
865
+ expect(headerTexts[2]).toContain('Email');
866
+ });
867
+
868
+ it('headers are draggable by default', () => {
869
+ const { container } = render(
870
+ <DataGrid
871
+ rows={generateUsers(3)}
872
+ cols={baseCols}
873
+ getRowId={getRowId}
874
+ rowHeight={48}
875
+ height="400px"
876
+ />,
877
+ );
878
+ const draggableThs = container.querySelectorAll('thead th[draggable="true"]');
879
+ expect(draggableThs.length).toBe(baseCols.length);
880
+ });
881
+
882
+ it('does NOT make headers draggable when enableColumnReorder=false', () => {
883
+ const { container } = render(
884
+ <DataGrid
885
+ rows={generateUsers(3)}
886
+ cols={baseCols}
887
+ getRowId={getRowId}
888
+ rowHeight={48}
889
+ height="400px"
890
+ enableColumnReorder={false}
891
+ />,
892
+ );
893
+ expect(
894
+ container.querySelectorAll('thead th[draggable="true"]'),
895
+ ).toHaveLength(0);
896
+ });
897
+
898
+ it('respects col.reorderable=false per-column', () => {
899
+ const cols: TableColumn<User>[] = [
900
+ { field: 'name', header: 'Name', reorderable: false },
901
+ { field: 'email', header: 'Email' },
902
+ ];
903
+ const { container } = render(
904
+ <DataGrid
905
+ rows={generateUsers(3)}
906
+ cols={cols}
907
+ getRowId={getRowId}
908
+ rowHeight={48}
909
+ height="400px"
910
+ />,
911
+ );
912
+ // Only the Email th should be draggable.
913
+ const draggableThs = container.querySelectorAll('thead th[draggable="true"]');
914
+ expect(draggableThs.length).toBe(1);
915
+ expect(draggableThs[0]?.textContent).toContain('Email');
916
+ });
917
+ });