picocrank 1.33.0 → 1.35.0

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 (36) hide show
  1. package/docs/filter-query-v1.md +5 -1
  2. package/docs/table.md +21 -2
  3. package/package.json +1 -1
  4. package/router.js +33 -0
  5. package/styles/picocrank-extensions.css +23 -2
  6. package/vue/components/Accordion.spec.js +48 -0
  7. package/vue/components/Accordion.vue +46 -0
  8. package/vue/components/AccordionItem.vue +103 -0
  9. package/vue/components/FieldValue.spec.js +88 -0
  10. package/vue/components/FieldValue.vue +222 -0
  11. package/vue/components/FormField.spec.js +1 -0
  12. package/vue/components/FormField.vue +38 -10
  13. package/vue/components/Header.vue +24 -2
  14. package/vue/components/NotificationBlock.vue +25 -0
  15. package/vue/components/ReorderList.spec.js +318 -0
  16. package/vue/components/ReorderList.vue +614 -0
  17. package/vue/components/Table.spec.js +193 -0
  18. package/vue/components/Table.vue +821 -115
  19. package/vue/components/TableColumnFilterPopover.vue +63 -19
  20. package/vue/components/TableColumnOptionsPopover.vue +248 -273
  21. package/vue/components/TopBar.vue +6 -2
  22. package/vue/components/accordionContext.js +1 -0
  23. package/vue/composables/tableColumnPresets.js +33 -0
  24. package/vue/composables/tableGroupBy.js +173 -0
  25. package/vue/composables/tableGroupBy.spec.js +69 -0
  26. package/vue/composables/tableViewSettings.js +34 -0
  27. package/vue/composables/tableViewSettings.spec.js +21 -0
  28. package/vue/composables/useHeaderTopBarFit.js +91 -0
  29. package/vue/composables/useResponsiveNav.js +5 -3
  30. package/vue/data/navSectionHubs.js +16 -1
  31. package/vue/examples/AccordionExample.vue +33 -0
  32. package/vue/examples/FieldValueExample.vue +143 -0
  33. package/vue/examples/FormExample.vue +11 -6
  34. package/vue/examples/ReorderListExample.vue +236 -0
  35. package/vue/examples/TableExample.vue +5 -1
  36. package/vue/examples/TableRemoteExample.vue +28 -4
@@ -689,6 +689,33 @@ describe('Table row context menu', () => {
689
689
  expect(action.mock.calls[0][0].keys).toEqual(['Katherine'])
690
690
  })
691
691
 
692
+ it('renders group header rows when groupBy is set', async () => {
693
+ const wrapper = mountTable({
694
+ props: {
695
+ selectable: false,
696
+ groupBy: 'city',
697
+ },
698
+ })
699
+
700
+ expect(wrapper.findAll('tbody tr.table-group-row')).toHaveLength(3)
701
+ expect(wrapper.find('.table-group-label').text()).toBe('London')
702
+ expect(wrapper.text()).toContain('(1)')
703
+ })
704
+
705
+ it('includes groupBy in query-change payloads', async () => {
706
+ const wrapper = mountTable({
707
+ props: {
708
+ selectable: false,
709
+ groupBy: 'city',
710
+ },
711
+ })
712
+
713
+ await nextTick()
714
+
715
+ const latestQuery = wrapper.emitted('query-change')?.at(-1)?.[0]
716
+ expect(latestQuery?.groupBy).toBe('city')
717
+ })
718
+
692
719
  it('keeps a hover class on target rows while the menu is open', async () => {
693
720
  const wrapper = mountTable({
694
721
  props: {
@@ -713,3 +740,169 @@ describe('Table row context menu', () => {
713
740
  expect(wrapper.findAll('tbody tr')[0].classes()).not.toContain('row-context-menu-active')
714
741
  })
715
742
  })
743
+
744
+ describe('Table column resize', () => {
745
+ it('renders resize handles for resizable headers', () => {
746
+ const wrapper = mountTable({
747
+ props: {
748
+ selectable: false,
749
+ headers: [
750
+ { key: 'name', label: 'Name', sortable: true },
751
+ { key: 'city', label: 'City', resizable: false },
752
+ ],
753
+ },
754
+ })
755
+
756
+ expect(wrapper.findAll('.column-resize-handle')).toHaveLength(1)
757
+ expect(wrapper.find('th[data-column-key="name"] .column-resize-handle').exists()).toBe(true)
758
+ expect(wrapper.find('th[data-column-key="city"] .column-resize-handle').exists()).toBe(false)
759
+ })
760
+
761
+ function mockHeaderWidths(wrapper, widthsByKey) {
762
+ wrapper.findAll('th[data-column-key]').forEach((headerCell) => {
763
+ const key = headerCell.attributes('data-column-key')
764
+ const width = widthsByKey[key] ?? 100
765
+ vi.spyOn(headerCell.element, 'getBoundingClientRect').mockReturnValue({
766
+ width,
767
+ height: 32,
768
+ top: 0,
769
+ left: 0,
770
+ right: width,
771
+ bottom: 32,
772
+ x: 0,
773
+ y: 0,
774
+ toJSON: () => ({}),
775
+ })
776
+ })
777
+ }
778
+
779
+ it('sets a pixel width while dragging, freezes siblings, and restores auto on double-click', async () => {
780
+ const wrapper = mountTable({
781
+ props: {
782
+ selectable: false,
783
+ },
784
+ })
785
+
786
+ const headerCell = wrapper.find('th[data-column-key="name"]')
787
+ const cityHeader = wrapper.find('th[data-column-key="city"]')
788
+ const handle = headerCell.find('.column-resize-handle')
789
+ mockHeaderWidths(wrapper, { name: 120, city: 160 })
790
+
791
+ await handle.trigger('pointerdown', { button: 0, clientX: 100, pointerId: 1 })
792
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 160 }))
793
+ window.dispatchEvent(new PointerEvent('pointerup', { clientX: 160 }))
794
+ await nextTick()
795
+
796
+ expect(wrapper.find('table').classes()).toContain('has-column-widths')
797
+ expect(headerCell.attributes('style')).toContain('width: 180px')
798
+ expect(cityHeader.attributes('style')).toContain('width: 160px')
799
+
800
+ await handle.trigger('dblclick')
801
+ await nextTick()
802
+
803
+ expect(headerCell.attributes('style') || '').not.toMatch(/(?:^|;)\s*width:/)
804
+ expect(cityHeader.attributes('style')).toContain('width: 160px')
805
+ expect(wrapper.find('table').classes()).toContain('has-column-widths')
806
+ })
807
+
808
+ it('clips overflowing cell content with ellipsis while columns are resized', async () => {
809
+ const wrapper = mountTable({
810
+ props: {
811
+ selectable: false,
812
+ },
813
+ })
814
+
815
+ const headerCell = wrapper.find('th[data-column-key="name"]')
816
+ const handle = headerCell.find('.column-resize-handle')
817
+ mockHeaderWidths(wrapper, { name: 120, city: 160 })
818
+
819
+ await handle.trigger('pointerdown', { button: 0, clientX: 100, pointerId: 1 })
820
+ window.dispatchEvent(new PointerEvent('pointerup', { clientX: 100 }))
821
+ await nextTick()
822
+
823
+ const cell = wrapper.find('tbody td')
824
+ const style = getComputedStyle(cell.element)
825
+ expect(style.overflow).toBe('hidden')
826
+ expect(style.textOverflow).toBe('ellipsis')
827
+ expect(style.whiteSpace).toBe('nowrap')
828
+ })
829
+ })
830
+
831
+ describe('Table aria and keyboard navigation', () => {
832
+ it('exposes aria-sort and related header/table hints', async () => {
833
+ const wrapper = mountTable({
834
+ props: {
835
+ selectable: false,
836
+ filterable: true,
837
+ headers: [
838
+ { key: 'name', label: 'Name', sortable: true, filterable: true },
839
+ { key: 'city', label: 'City', sortable: true },
840
+ ],
841
+ },
842
+ })
843
+
844
+ const nameHeader = wrapper.find('th[data-column-key="name"]')
845
+ const cityHeader = wrapper.find('th[data-column-key="city"]')
846
+
847
+ expect(wrapper.find('table').attributes('aria-colcount')).toBe('2')
848
+ expect(nameHeader.attributes('scope')).toBe('col')
849
+ expect(nameHeader.attributes('aria-sort')).toBe('none')
850
+ expect(nameHeader.attributes('aria-haspopup')).toBe('dialog')
851
+ expect(nameHeader.attributes('aria-expanded')).toBe('false')
852
+
853
+ await nameHeader.trigger('click')
854
+ await nextTick()
855
+
856
+ expect(nameHeader.attributes('aria-sort')).toBe('ascending')
857
+ expect(cityHeader.attributes('aria-sort')).toBe('none')
858
+
859
+ await nameHeader.trigger('click')
860
+ await nextTick()
861
+
862
+ expect(nameHeader.attributes('aria-sort')).toBe('descending')
863
+ })
864
+
865
+ it('moves the active cell with arrow keys when one is set', async () => {
866
+ const wrapper = mountTable({
867
+ props: {
868
+ selectable: false,
869
+ },
870
+ })
871
+
872
+ const firstCell = wrapper.find('tbody td')
873
+ await firstCell.trigger('click')
874
+ await nextTick()
875
+
876
+ expect(wrapper.find('.table-active-cell').text()).toContain('Ada')
877
+ expect(document.activeElement).toBe(wrapper.find('.table-active-cell').element)
878
+
879
+ await wrapper.find('.table-active-cell').trigger('keydown', { key: 'ArrowRight' })
880
+ await nextTick()
881
+ expect(wrapper.emitted('update:activeCell')?.at(-1)?.[0]).toEqual({
882
+ rowKey: 'Ada',
883
+ columnKey: 'city',
884
+ })
885
+ expect(wrapper.find('.table-active-cell').text()).toContain('London')
886
+
887
+ await wrapper.find('.table-active-cell').trigger('keydown', { key: 'ArrowDown' })
888
+ await nextTick()
889
+ expect(wrapper.emitted('update:activeCell')?.at(-1)?.[0]).toEqual({
890
+ rowKey: 'Grace',
891
+ columnKey: 'city',
892
+ })
893
+
894
+ await wrapper.find('.table-active-cell').trigger('keydown', { key: 'ArrowLeft' })
895
+ await nextTick()
896
+ expect(wrapper.emitted('update:activeCell')?.at(-1)?.[0]).toEqual({
897
+ rowKey: 'Grace',
898
+ columnKey: 'name',
899
+ })
900
+
901
+ await wrapper.find('.table-active-cell').trigger('keydown', { key: 'ArrowUp' })
902
+ await nextTick()
903
+ expect(wrapper.emitted('update:activeCell')?.at(-1)?.[0]).toEqual({
904
+ rowKey: 'Ada',
905
+ columnKey: 'name',
906
+ })
907
+ })
908
+ })