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.
- package/docs/filter-query-v1.md +5 -1
- package/docs/table.md +21 -2
- package/package.json +1 -1
- package/router.js +33 -0
- package/styles/picocrank-extensions.css +23 -2
- package/vue/components/Accordion.spec.js +48 -0
- package/vue/components/Accordion.vue +46 -0
- package/vue/components/AccordionItem.vue +103 -0
- package/vue/components/FieldValue.spec.js +88 -0
- package/vue/components/FieldValue.vue +222 -0
- package/vue/components/FormField.spec.js +1 -0
- package/vue/components/FormField.vue +38 -10
- package/vue/components/Header.vue +24 -2
- package/vue/components/NotificationBlock.vue +25 -0
- package/vue/components/ReorderList.spec.js +318 -0
- package/vue/components/ReorderList.vue +614 -0
- package/vue/components/Table.spec.js +193 -0
- package/vue/components/Table.vue +821 -115
- package/vue/components/TableColumnFilterPopover.vue +63 -19
- package/vue/components/TableColumnOptionsPopover.vue +248 -273
- package/vue/components/TopBar.vue +6 -2
- package/vue/components/accordionContext.js +1 -0
- package/vue/composables/tableColumnPresets.js +33 -0
- package/vue/composables/tableGroupBy.js +173 -0
- package/vue/composables/tableGroupBy.spec.js +69 -0
- package/vue/composables/tableViewSettings.js +34 -0
- package/vue/composables/tableViewSettings.spec.js +21 -0
- package/vue/composables/useHeaderTopBarFit.js +91 -0
- package/vue/composables/useResponsiveNav.js +5 -3
- package/vue/data/navSectionHubs.js +16 -1
- package/vue/examples/AccordionExample.vue +33 -0
- package/vue/examples/FieldValueExample.vue +143 -0
- package/vue/examples/FormExample.vue +11 -6
- package/vue/examples/ReorderListExample.vue +236 -0
- package/vue/examples/TableExample.vue +5 -1
- 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
|
+
})
|