@dashforge/tw 0.7.0-beta → 0.8.1-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 +255 -0
  2. package/dist/index.esm.js +1057 -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/ColumnVisibilityMenu.d.ts +40 -0
  15. package/dist/src/components/DataGrid/visibility/ColumnVisibilityMenu.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 +67 -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 +418 -1
  25. package/src/components/DataGrid/DataGrid.tsx +416 -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/ColumnVisibilityMenu.tsx +169 -0
  33. package/src/components/Table/Table.tsx +11 -0
  34. package/src/components/Table/table.types.ts +72 -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,420 @@ 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 a popover menu 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 menu (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
+ // Radix Popover content has role="dialog" too.
688
+ expect(screen.queryByRole('dialog')).not.toBeNull();
689
+ // The menu should expose checkboxes for Name + Email (not Age).
690
+ const menu = screen.getByRole('dialog');
691
+ expect(menu.textContent).toContain('Name');
692
+ expect(menu.textContent).toContain('Email');
693
+ expect(menu.textContent).not.toContain('Age');
694
+ });
695
+
696
+ it('respects col.defaultHidden on first render', () => {
697
+ const cols: TableColumn<User>[] = [
698
+ { field: 'name', header: 'Name' },
699
+ { field: 'email', header: 'Email', defaultHidden: true },
700
+ ];
701
+ render(
702
+ <DataGrid
703
+ rows={generateUsers(5)}
704
+ cols={cols}
705
+ getRowId={getRowId}
706
+ rowHeight={48}
707
+ height="400px"
708
+ />,
709
+ );
710
+ // Email column header should NOT be rendered initially.
711
+ expect(screen.queryByText('Email')).toBeNull();
712
+ // Name column header SHOULD be rendered.
713
+ expect(screen.queryByText('Name')).not.toBeNull();
714
+ });
715
+
716
+ it('toggling a column auto-commits via onHiddenColumnsChange (no Done button)', () => {
717
+ const onHiddenColumnsChange = vi.fn();
718
+ render(
719
+ <DataGrid
720
+ rows={generateUsers(5)}
721
+ cols={baseCols}
722
+ getRowId={getRowId}
723
+ rowHeight={48}
724
+ height="400px"
725
+ onHiddenColumnsChange={onHiddenColumnsChange}
726
+ />,
727
+ );
728
+ fireEvent.click(screen.getByRole('button', { name: 'Columns' }));
729
+ // Uncheck "Email" inside the popover menu
730
+ const menu = screen.getByRole('dialog');
731
+ const emailCheckbox = Array.from(
732
+ menu.querySelectorAll('input[type="checkbox"]'),
733
+ ).find((el) => {
734
+ const li = el.closest('label');
735
+ return li?.textContent?.includes('Email');
736
+ }) as HTMLInputElement | undefined;
737
+ expect(emailCheckbox).toBeDefined();
738
+ fireEvent.click(emailCheckbox!);
739
+ // Auto-commit on toggle — no Done button anymore
740
+ expect(onHiddenColumnsChange).toHaveBeenCalledWith(['email']);
741
+ });
742
+
743
+ it('controlled hiddenColumns hides the matching columns', () => {
744
+ render(
745
+ <DataGrid
746
+ rows={generateUsers(5)}
747
+ cols={baseCols}
748
+ getRowId={getRowId}
749
+ rowHeight={48}
750
+ height="400px"
751
+ hiddenColumns={['email']}
752
+ />,
753
+ );
754
+ expect(screen.queryByText('Email')).toBeNull();
755
+ expect(screen.queryByText('Name')).not.toBeNull();
756
+ });
757
+ });
758
+
759
+ describe('DataGrid — column resize', () => {
760
+ it('renders a resize handle (separator) for each column by default', () => {
761
+ render(
762
+ <DataGrid
763
+ rows={generateUsers(5)}
764
+ cols={baseCols}
765
+ getRowId={getRowId}
766
+ rowHeight={48}
767
+ height="400px"
768
+ />,
769
+ );
770
+ // One separator per column (4 columns).
771
+ const handles = screen.getAllByRole('separator');
772
+ expect(handles.length).toBe(baseCols.length);
773
+ });
774
+
775
+ it('omits resize handles when enableColumnResize=false', () => {
776
+ render(
777
+ <DataGrid
778
+ rows={generateUsers(5)}
779
+ cols={baseCols}
780
+ getRowId={getRowId}
781
+ rowHeight={48}
782
+ height="400px"
783
+ enableColumnResize={false}
784
+ />,
785
+ );
786
+ expect(screen.queryAllByRole('separator')).toHaveLength(0);
787
+ });
788
+
789
+ it('omits resize handle for col.resizable=false', () => {
790
+ const cols: TableColumn<User>[] = [
791
+ { field: 'name', header: 'Name' },
792
+ { field: 'email', header: 'Email', resizable: false },
793
+ ];
794
+ render(
795
+ <DataGrid
796
+ rows={generateUsers(5)}
797
+ cols={cols}
798
+ getRowId={getRowId}
799
+ rowHeight={48}
800
+ height="400px"
801
+ />,
802
+ );
803
+ // Only 1 separator (Name); Email opted out via resizable=false.
804
+ expect(screen.getAllByRole('separator')).toHaveLength(1);
805
+ });
806
+
807
+ it('applies columnWidths to <th> style.width', () => {
808
+ const cols: TableColumn<User>[] = [
809
+ { field: 'name', header: 'Name', width: 100 },
810
+ ];
811
+ render(
812
+ <DataGrid
813
+ rows={generateUsers(5)}
814
+ cols={cols}
815
+ getRowId={getRowId}
816
+ rowHeight={48}
817
+ height="400px"
818
+ columnWidths={{ name: 250 }}
819
+ />,
820
+ );
821
+ const th = screen.getByText('Name').closest('th');
822
+ expect(th?.style.width).toBe('250px');
823
+ });
824
+
825
+ it('falls back to col.width when no resize state for the column', () => {
826
+ const cols: TableColumn<User>[] = [
827
+ { field: 'name', header: 'Name', width: 100 },
828
+ ];
829
+ render(
830
+ <DataGrid
831
+ rows={generateUsers(5)}
832
+ cols={cols}
833
+ getRowId={getRowId}
834
+ rowHeight={48}
835
+ height="400px"
836
+ />,
837
+ );
838
+ const th = screen.getByText('Name').closest('th');
839
+ expect(th?.style.width).toBe('100px');
840
+ });
841
+ });
842
+
843
+ describe('DataGrid — column reorder', () => {
844
+ it('applies the columnOrder prop to header order', () => {
845
+ const { container } = render(
846
+ <DataGrid
847
+ rows={generateUsers(3)}
848
+ cols={baseCols}
849
+ getRowId={getRowId}
850
+ rowHeight={48}
851
+ height="400px"
852
+ columnOrder={['age', 'name', 'email', 'meta.city']}
853
+ />,
854
+ );
855
+ const headerTexts = Array.from(
856
+ container.querySelectorAll('thead th'),
857
+ ).map((th) => th.textContent?.trim() ?? '');
858
+ // First three column headers should match the order
859
+ expect(headerTexts[0]).toContain('Age');
860
+ expect(headerTexts[1]).toContain('Name');
861
+ expect(headerTexts[2]).toContain('Email');
862
+ });
863
+
864
+ it('headers are draggable by default', () => {
865
+ const { container } = render(
866
+ <DataGrid
867
+ rows={generateUsers(3)}
868
+ cols={baseCols}
869
+ getRowId={getRowId}
870
+ rowHeight={48}
871
+ height="400px"
872
+ />,
873
+ );
874
+ const draggableThs = container.querySelectorAll('thead th[draggable="true"]');
875
+ expect(draggableThs.length).toBe(baseCols.length);
876
+ });
877
+
878
+ it('does NOT make headers draggable when enableColumnReorder=false', () => {
879
+ const { container } = render(
880
+ <DataGrid
881
+ rows={generateUsers(3)}
882
+ cols={baseCols}
883
+ getRowId={getRowId}
884
+ rowHeight={48}
885
+ height="400px"
886
+ enableColumnReorder={false}
887
+ />,
888
+ );
889
+ expect(
890
+ container.querySelectorAll('thead th[draggable="true"]'),
891
+ ).toHaveLength(0);
892
+ });
893
+
894
+ it('respects col.reorderable=false per-column', () => {
895
+ const cols: TableColumn<User>[] = [
896
+ { field: 'name', header: 'Name', reorderable: false },
897
+ { field: 'email', header: 'Email' },
898
+ ];
899
+ const { container } = render(
900
+ <DataGrid
901
+ rows={generateUsers(3)}
902
+ cols={cols}
903
+ getRowId={getRowId}
904
+ rowHeight={48}
905
+ height="400px"
906
+ />,
907
+ );
908
+ // Only the Email th should be draggable.
909
+ const draggableThs = container.querySelectorAll('thead th[draggable="true"]');
910
+ expect(draggableThs.length).toBe(1);
911
+ expect(draggableThs[0]?.textContent).toContain('Email');
912
+ });
913
+ });