picocrank 1.34.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/table.md +21 -2
- package/package.json +1 -1
- package/vue/components/Table.spec.js +166 -0
- package/vue/components/Table.vue +365 -5
package/docs/table.md
CHANGED
|
@@ -36,6 +36,16 @@ their rows. `Table` reverses its result for descending order and keeps null
|
|
|
36
36
|
values last. `sortRows(rows, sortBy, sortDir, comparator)` accepts the same
|
|
37
37
|
optional comparator.
|
|
38
38
|
|
|
39
|
+
## Column resize
|
|
40
|
+
|
|
41
|
+
Hover a data-column header to reveal a drag handle on its right edge. Drag to
|
|
42
|
+
set a pixel width; the first drag freezes sibling column widths so only the
|
|
43
|
+
active column changes. Double-click the handle to clear that column’s width and
|
|
44
|
+
return it to browser auto sizing. Set `resizable: false` on a header to hide its
|
|
45
|
+
handle. Initial widths still come from optional `header.width`. While any
|
|
46
|
+
column has a resized width, overflowing header and cell content is clipped with
|
|
47
|
+
ellipsis.
|
|
48
|
+
|
|
39
49
|
## Cells and display hooks
|
|
40
50
|
|
|
41
51
|
Named `#cell-{key}` slots and the generic `#cell` slot receive:
|
|
@@ -79,8 +89,17 @@ the native click event.
|
|
|
79
89
|
/>
|
|
80
90
|
```
|
|
81
91
|
|
|
82
|
-
Clicking a data cell emits `cell-click` with the enriched cell context
|
|
83
|
-
the
|
|
92
|
+
Clicking a data cell emits `cell-click` with the enriched cell context, focuses
|
|
93
|
+
the cell when the click target is not interactive, and adds the visibly outlined
|
|
94
|
+
`table-active-cell` class. While an active cell is focused, arrow keys move
|
|
95
|
+
selection within the displayed grid (with focus following).
|
|
96
|
+
|
|
97
|
+
Sortable headers expose `aria-sort` (`none` / `ascending` / `descending`).
|
|
98
|
+
Filterable headers use `aria-haspopup="dialog"` and `aria-expanded`. The table
|
|
99
|
+
sets `aria-busy` while loading and `aria-colcount` for the visible column count
|
|
100
|
+
(including select and column-options columns when present).
|
|
101
|
+
|
|
102
|
+
The component ref exposes:
|
|
84
103
|
|
|
85
104
|
- `clearFilters()`
|
|
86
105
|
- `focusActiveCell(options?)` (returns whether a displayed active cell was focused)
|
package/package.json
CHANGED
|
@@ -740,3 +740,169 @@ describe('Table row context menu', () => {
|
|
|
740
740
|
expect(wrapper.findAll('tbody tr')[0].classes()).not.toContain('row-context-menu-active')
|
|
741
741
|
})
|
|
742
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
|
+
})
|
package/vue/components/Table.vue
CHANGED
|
@@ -4,11 +4,31 @@
|
|
|
4
4
|
:class="needsTableWrapper ? tableWrapperClasses : undefined"
|
|
5
5
|
:style="needsTableWrapper ? undefined : { display: 'contents' }"
|
|
6
6
|
>
|
|
7
|
-
<table
|
|
7
|
+
<table
|
|
8
|
+
ref="tableElementRef"
|
|
9
|
+
class="row-hover"
|
|
10
|
+
:class="{
|
|
11
|
+
loading: isLoading,
|
|
12
|
+
'has-column-widths': hasResizedColumns,
|
|
13
|
+
'is-column-resizing': activeResizeKey != null,
|
|
14
|
+
}"
|
|
15
|
+
:aria-busy="isLoading ? 'true' : undefined"
|
|
16
|
+
:aria-colcount="ariaColCount"
|
|
17
|
+
>
|
|
18
|
+
<colgroup>
|
|
19
|
+
<col v-if="selectable" class="table-select-col-width" />
|
|
20
|
+
<col
|
|
21
|
+
v-for="(header, index) in visibleHeaders"
|
|
22
|
+
:key="header.key || index"
|
|
23
|
+
:style="columnWidthStyle(header)"
|
|
24
|
+
/>
|
|
25
|
+
<col v-if="showColumnOptionsButton" class="table-column-options-col-width" />
|
|
26
|
+
</colgroup>
|
|
8
27
|
<thead>
|
|
9
28
|
<tr>
|
|
10
29
|
<th
|
|
11
30
|
v-if="selectable"
|
|
31
|
+
scope="col"
|
|
12
32
|
class="table-select-col"
|
|
13
33
|
@click.stop
|
|
14
34
|
>
|
|
@@ -25,8 +45,13 @@
|
|
|
25
45
|
<th
|
|
26
46
|
v-for="(header, index) in visibleHeaders"
|
|
27
47
|
:key="header.key || index"
|
|
28
|
-
|
|
29
|
-
:
|
|
48
|
+
scope="col"
|
|
49
|
+
:data-column-key="header.key"
|
|
50
|
+
:class="[headerClasses(header), header.class, { 'is-resizing': activeResizeKey === header.key }]"
|
|
51
|
+
:style="[columnWidthStyle(header), columnOverflowStyle()]"
|
|
52
|
+
:aria-sort="headerAriaSort(header)"
|
|
53
|
+
:aria-haspopup="isHeaderFilterable(header) ? 'dialog' : undefined"
|
|
54
|
+
:aria-expanded="headerFilterExpanded(header)"
|
|
30
55
|
@click="onHeaderClick(header, $event)"
|
|
31
56
|
@contextmenu.prevent="openFilter(header, $event)"
|
|
32
57
|
@pointerdown="onHeaderPointerDown(header, $event)"
|
|
@@ -56,7 +81,7 @@
|
|
|
56
81
|
/>
|
|
57
82
|
</span>
|
|
58
83
|
|
|
59
|
-
<span v-if="header.sortable" class="sort-indicator">
|
|
84
|
+
<span v-if="header.sortable" class="sort-indicator" aria-hidden="true">
|
|
60
85
|
<HugeiconsIcon
|
|
61
86
|
v-if="sortBy === header.key && sortDir === 'asc'"
|
|
62
87
|
:icon="SortByUp02Icon"
|
|
@@ -74,9 +99,25 @@
|
|
|
74
99
|
aria-hidden="true"
|
|
75
100
|
/>
|
|
76
101
|
</span>
|
|
102
|
+
|
|
103
|
+
<span
|
|
104
|
+
v-if="isHeaderResizable(header)"
|
|
105
|
+
class="column-resize-handle"
|
|
106
|
+
role="separator"
|
|
107
|
+
aria-orientation="vertical"
|
|
108
|
+
:aria-label="`Resize ${header.label || header.key} column`"
|
|
109
|
+
:aria-valuemin="MIN_COLUMN_WIDTH"
|
|
110
|
+
:aria-valuenow="columnResizeValueNow(header)"
|
|
111
|
+
:class="{ 'is-active': activeResizeKey === header.key }"
|
|
112
|
+
@pointerdown.stop="onResizePointerDown(header, $event)"
|
|
113
|
+
@dblclick.stop="onResizeDoubleClick(header, $event)"
|
|
114
|
+
@click.stop
|
|
115
|
+
@contextmenu.stop.prevent
|
|
116
|
+
/>
|
|
77
117
|
</th>
|
|
78
118
|
<th
|
|
79
119
|
v-if="showColumnOptionsButton"
|
|
120
|
+
scope="col"
|
|
80
121
|
class="actions table-column-options-header"
|
|
81
122
|
>
|
|
82
123
|
<span class="actions-menu-trigger">
|
|
@@ -179,9 +220,10 @@
|
|
|
179
220
|
{ 'table-active-cell': isActiveCell(item.row, header, item.displayIndex) },
|
|
180
221
|
isLastColumnWithOptions(cellIndex) ? 'table-last-column-with-options' : null,
|
|
181
222
|
]"
|
|
182
|
-
:style="resolveCellStyle(item.row, header, item.displayIndex)"
|
|
223
|
+
:style="[resolveCellStyle(item.row, header, item.displayIndex), columnOverflowStyle()]"
|
|
183
224
|
:tabindex="isActiveCell(item.row, header, item.displayIndex) ? 0 : -1"
|
|
184
225
|
@click="onCellClick(item.row, header, item.displayIndex, $event)"
|
|
226
|
+
@keydown="onCellKeydown(item.row, header, item.displayIndex, $event)"
|
|
185
227
|
>
|
|
186
228
|
<slot
|
|
187
229
|
v-if="slots[`cell-${header.key}`]"
|
|
@@ -338,6 +380,7 @@ import {
|
|
|
338
380
|
} from '../composables/tableGroupBy.js'
|
|
339
381
|
|
|
340
382
|
const DEFAULT_PAGE_SIZE = 10
|
|
383
|
+
const MIN_COLUMN_WIDTH = 48
|
|
341
384
|
|
|
342
385
|
const props = defineProps({
|
|
343
386
|
headers: {
|
|
@@ -674,9 +717,177 @@ const defaultPresetId = ref(null)
|
|
|
674
717
|
const activeLayoutPresetId = ref(null)
|
|
675
718
|
const internalGroupBy = ref(null)
|
|
676
719
|
const collapsedGroupIds = ref([])
|
|
720
|
+
/** @type {import('vue').Ref<Record<string, number | null>>} */
|
|
721
|
+
const columnWidths = ref({})
|
|
722
|
+
const activeResizeKey = ref(null)
|
|
723
|
+
const tableElementRef = ref(null)
|
|
724
|
+
let resizeSession = null
|
|
677
725
|
|
|
678
726
|
const isRemote = computed(() => typeof props.fetchRows === 'function')
|
|
679
727
|
|
|
728
|
+
const hasResizedColumns = computed(() =>
|
|
729
|
+
Object.values(columnWidths.value).some((width) => width != null),
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
function isHeaderResizable(header) {
|
|
733
|
+
return Boolean(header?.key) && header.resizable !== false
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function formatColumnWidth(width) {
|
|
737
|
+
if (width == null || width === '') {
|
|
738
|
+
return undefined
|
|
739
|
+
}
|
|
740
|
+
return typeof width === 'number' ? `${width}px` : width
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function columnWidthStyle(header) {
|
|
744
|
+
if (!header?.key) {
|
|
745
|
+
return header?.width ? { width: formatColumnWidth(header.width) } : undefined
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (Object.prototype.hasOwnProperty.call(columnWidths.value, header.key)) {
|
|
749
|
+
const resized = columnWidths.value[header.key]
|
|
750
|
+
if (resized == null) {
|
|
751
|
+
return undefined
|
|
752
|
+
}
|
|
753
|
+
return { width: `${resized}px` }
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (header.width) {
|
|
757
|
+
return { width: formatColumnWidth(header.width) }
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return undefined
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function columnOverflowStyle() {
|
|
764
|
+
if (!hasResizedColumns.value) {
|
|
765
|
+
return undefined
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
overflow: 'hidden',
|
|
769
|
+
textOverflow: 'ellipsis',
|
|
770
|
+
whiteSpace: 'nowrap',
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function columnResizeValueNow(header) {
|
|
775
|
+
const width = columnWidths.value[header.key]
|
|
776
|
+
return typeof width === 'number' ? width : undefined
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function setColumnWidths(nextWidths) {
|
|
780
|
+
columnWidths.value = nextWidths
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function setColumnWidth(columnKey, width) {
|
|
784
|
+
columnWidths.value = {
|
|
785
|
+
...columnWidths.value,
|
|
786
|
+
[columnKey]: width,
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function freezeSiblingColumnWidths(tableEl, activeKey, activeWidth) {
|
|
791
|
+
const next = { ...columnWidths.value }
|
|
792
|
+
tableEl?.querySelectorAll('thead th[data-column-key]').forEach((th) => {
|
|
793
|
+
const key = th.dataset.columnKey
|
|
794
|
+
if (!key || key === activeKey) {
|
|
795
|
+
return
|
|
796
|
+
}
|
|
797
|
+
if (typeof next[key] === 'number') {
|
|
798
|
+
return
|
|
799
|
+
}
|
|
800
|
+
next[key] = Math.max(MIN_COLUMN_WIDTH, Math.round(th.getBoundingClientRect().width))
|
|
801
|
+
})
|
|
802
|
+
next[activeKey] = activeWidth
|
|
803
|
+
setColumnWidths(next)
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function endColumnResize() {
|
|
807
|
+
if (!resizeSession) {
|
|
808
|
+
return
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const { moveListener, upListener, previousUserSelect, previousCursor } = resizeSession
|
|
812
|
+
window.removeEventListener('pointermove', moveListener)
|
|
813
|
+
window.removeEventListener('pointerup', upListener)
|
|
814
|
+
window.removeEventListener('pointercancel', upListener)
|
|
815
|
+
document.body.style.userSelect = previousUserSelect
|
|
816
|
+
document.body.style.cursor = previousCursor
|
|
817
|
+
activeResizeKey.value = null
|
|
818
|
+
resizeSession = null
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function onResizePointerDown(header, event) {
|
|
822
|
+
if (!isHeaderResizable(header) || event.button !== 0) {
|
|
823
|
+
return
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const handle = event.currentTarget
|
|
827
|
+
const headerCell = handle?.closest('th')
|
|
828
|
+
if (!headerCell) {
|
|
829
|
+
return
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
event.preventDefault()
|
|
833
|
+
endColumnResize()
|
|
834
|
+
|
|
835
|
+
const startX = event.clientX
|
|
836
|
+
const startWidth = Math.max(MIN_COLUMN_WIDTH, Math.round(headerCell.getBoundingClientRect().width))
|
|
837
|
+
activeResizeKey.value = header.key
|
|
838
|
+
freezeSiblingColumnWidths(
|
|
839
|
+
tableElementRef.value ?? headerCell.closest('table'),
|
|
840
|
+
header.key,
|
|
841
|
+
startWidth,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
const previousUserSelect = document.body.style.userSelect
|
|
845
|
+
const previousCursor = document.body.style.cursor
|
|
846
|
+
document.body.style.userSelect = 'none'
|
|
847
|
+
document.body.style.cursor = 'col-resize'
|
|
848
|
+
|
|
849
|
+
const moveListener = (moveEvent) => {
|
|
850
|
+
const nextWidth = Math.max(
|
|
851
|
+
MIN_COLUMN_WIDTH,
|
|
852
|
+
Math.round(startWidth + (moveEvent.clientX - startX)),
|
|
853
|
+
)
|
|
854
|
+
setColumnWidth(header.key, nextWidth)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const upListener = () => {
|
|
858
|
+
endColumnResize()
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
resizeSession = {
|
|
862
|
+
moveListener,
|
|
863
|
+
upListener,
|
|
864
|
+
previousUserSelect,
|
|
865
|
+
previousCursor,
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
window.addEventListener('pointermove', moveListener)
|
|
869
|
+
window.addEventListener('pointerup', upListener)
|
|
870
|
+
window.addEventListener('pointercancel', upListener)
|
|
871
|
+
|
|
872
|
+
if (typeof handle.setPointerCapture === 'function') {
|
|
873
|
+
try {
|
|
874
|
+
handle.setPointerCapture(event.pointerId)
|
|
875
|
+
} catch {
|
|
876
|
+
// Ignore capture failures (e.g. unsupported pointer type in tests).
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function onResizeDoubleClick(header, event) {
|
|
882
|
+
if (!isHeaderResizable(header)) {
|
|
883
|
+
return
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
event.preventDefault()
|
|
887
|
+
endColumnResize()
|
|
888
|
+
setColumnWidth(header.key, null)
|
|
889
|
+
}
|
|
890
|
+
|
|
680
891
|
const viewSettingsHandlers = computed(() =>
|
|
681
892
|
getViewSettingsHandlers(props.viewSettings, props.layoutPresets))
|
|
682
893
|
|
|
@@ -808,6 +1019,17 @@ function isHeaderHidden(header) {
|
|
|
808
1019
|
|
|
809
1020
|
const visibleHeaders = computed(() => orderedHeaders.value.filter((header) => !isHeaderHidden(header)))
|
|
810
1021
|
|
|
1022
|
+
const ariaColCount = computed(() => {
|
|
1023
|
+
let count = visibleHeaders.value.length
|
|
1024
|
+
if (props.selectable) {
|
|
1025
|
+
count += 1
|
|
1026
|
+
}
|
|
1027
|
+
if (showColumnOptionsButton.value) {
|
|
1028
|
+
count += 1
|
|
1029
|
+
}
|
|
1030
|
+
return count > 0 ? count : undefined
|
|
1031
|
+
})
|
|
1032
|
+
|
|
811
1033
|
const visibleColumnKeys = computed(() =>
|
|
812
1034
|
orderedHeaders.value
|
|
813
1035
|
.filter((header) => !isHeaderHidden(header))
|
|
@@ -1043,6 +1265,25 @@ function isHeaderFiltered(header) {
|
|
|
1043
1265
|
return isColumnFiltered(activeFilters.value?.[header.key], header, props.data)
|
|
1044
1266
|
}
|
|
1045
1267
|
|
|
1268
|
+
function headerAriaSort(header) {
|
|
1269
|
+
if (!header?.sortable) {
|
|
1270
|
+
return undefined
|
|
1271
|
+
}
|
|
1272
|
+
if (sortBy.value !== header.key) {
|
|
1273
|
+
return 'none'
|
|
1274
|
+
}
|
|
1275
|
+
return sortDir.value === 'desc' ? 'descending' : 'ascending'
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function headerFilterExpanded(header) {
|
|
1279
|
+
if (!isHeaderFilterable(header)) {
|
|
1280
|
+
return undefined
|
|
1281
|
+
}
|
|
1282
|
+
return filterPopoverOpen.value && activeFilterHeader.value?.key === header.key
|
|
1283
|
+
? 'true'
|
|
1284
|
+
: 'false'
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1046
1287
|
function headerClasses(header) {
|
|
1047
1288
|
const priority = resolvedColumnPriority(header)
|
|
1048
1289
|
return {
|
|
@@ -1308,6 +1549,14 @@ function moveActiveCell(rowDelta, columnDelta, options = {}) {
|
|
|
1308
1549
|
return activateCellAt(rowIndex, columnIndex, options)
|
|
1309
1550
|
}
|
|
1310
1551
|
|
|
1552
|
+
function isInteractiveCellTarget(target) {
|
|
1553
|
+
return Boolean(
|
|
1554
|
+
target?.closest?.(
|
|
1555
|
+
'a, button, input, select, textarea, [contenteditable="true"], [data-row-click-ignore]',
|
|
1556
|
+
),
|
|
1557
|
+
)
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1311
1560
|
function onCellClick(row, header, rowIndex, event) {
|
|
1312
1561
|
const context = cellContext(row, header, rowIndex)
|
|
1313
1562
|
const nextActiveCell = {
|
|
@@ -1315,6 +1564,9 @@ function onCellClick(row, header, rowIndex, event) {
|
|
|
1315
1564
|
columnKey: header.key,
|
|
1316
1565
|
}
|
|
1317
1566
|
setActiveCell(nextActiveCell)
|
|
1567
|
+
if (!isInteractiveCellTarget(event.target)) {
|
|
1568
|
+
scheduleCellFocus(nextActiveCell, { focus: true })
|
|
1569
|
+
}
|
|
1318
1570
|
emit('cell-click', {
|
|
1319
1571
|
...context,
|
|
1320
1572
|
...nextActiveCell,
|
|
@@ -1322,6 +1574,39 @@ function onCellClick(row, header, rowIndex, event) {
|
|
|
1322
1574
|
})
|
|
1323
1575
|
}
|
|
1324
1576
|
|
|
1577
|
+
function onCellKeydown(row, header, rowIndex, event) {
|
|
1578
|
+
if (!activeCell.value || !isActiveCell(row, header, rowIndex)) {
|
|
1579
|
+
return
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (isInteractiveCellTarget(event.target) && event.target !== event.currentTarget) {
|
|
1583
|
+
return
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
let rowDelta = 0
|
|
1587
|
+
let columnDelta = 0
|
|
1588
|
+
|
|
1589
|
+
switch (event.key) {
|
|
1590
|
+
case 'ArrowUp':
|
|
1591
|
+
rowDelta = -1
|
|
1592
|
+
break
|
|
1593
|
+
case 'ArrowDown':
|
|
1594
|
+
rowDelta = 1
|
|
1595
|
+
break
|
|
1596
|
+
case 'ArrowLeft':
|
|
1597
|
+
columnDelta = -1
|
|
1598
|
+
break
|
|
1599
|
+
case 'ArrowRight':
|
|
1600
|
+
columnDelta = 1
|
|
1601
|
+
break
|
|
1602
|
+
default:
|
|
1603
|
+
return
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
event.preventDefault()
|
|
1607
|
+
moveActiveCell(rowDelta, columnDelta, { focus: true })
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1325
1610
|
const activeSelectedKeys = computed(() => (
|
|
1326
1611
|
props.selectedKeys !== undefined ? props.selectedKeys : internalSelectedKeys.value
|
|
1327
1612
|
))
|
|
@@ -2573,6 +2858,7 @@ onMounted(async () => {
|
|
|
2573
2858
|
})
|
|
2574
2859
|
|
|
2575
2860
|
onBeforeUnmount(() => {
|
|
2861
|
+
endColumnResize()
|
|
2576
2862
|
if (fetchAbortController.value) {
|
|
2577
2863
|
fetchAbortController.value.abort()
|
|
2578
2864
|
}
|
|
@@ -2594,11 +2880,85 @@ defineExpose({
|
|
|
2594
2880
|
</script>
|
|
2595
2881
|
|
|
2596
2882
|
<style scoped>
|
|
2883
|
+
table.has-column-widths {
|
|
2884
|
+
table-layout: fixed;
|
|
2885
|
+
width: 100%;
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
/* Force fixed-layout cells to respect column widths so overflow clipping/ellipsis work. */
|
|
2889
|
+
table.has-column-widths th,
|
|
2890
|
+
table.has-column-widths td {
|
|
2891
|
+
max-width: 0;
|
|
2892
|
+
overflow: hidden;
|
|
2893
|
+
text-overflow: ellipsis;
|
|
2894
|
+
white-space: nowrap;
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
table.has-column-widths th.table-select-col,
|
|
2898
|
+
table.has-column-widths td.table-select-col,
|
|
2899
|
+
table.has-column-widths th.table-column-options-header {
|
|
2900
|
+
max-width: none;
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
table.is-column-resizing {
|
|
2904
|
+
cursor: col-resize;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
table.is-column-resizing thead th.sortable:hover {
|
|
2908
|
+
cursor: col-resize;
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
col.table-select-col-width,
|
|
2912
|
+
col.table-column-options-col-width {
|
|
2913
|
+
width: 2.5rem;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
table thead th {
|
|
2917
|
+
position: relative;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2597
2920
|
table thead th.sortable:hover {
|
|
2598
2921
|
cursor: pointer;
|
|
2599
2922
|
color: var(--table-header-active-fg);
|
|
2600
2923
|
}
|
|
2601
2924
|
|
|
2925
|
+
.column-resize-handle {
|
|
2926
|
+
position: absolute;
|
|
2927
|
+
top: 0;
|
|
2928
|
+
right: -0.2rem;
|
|
2929
|
+
z-index: 2;
|
|
2930
|
+
width: 0.55rem;
|
|
2931
|
+
height: 100%;
|
|
2932
|
+
padding: 0;
|
|
2933
|
+
border: 0;
|
|
2934
|
+
background: transparent;
|
|
2935
|
+
cursor: col-resize;
|
|
2936
|
+
touch-action: none;
|
|
2937
|
+
user-select: none;
|
|
2938
|
+
opacity: 0;
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
.column-resize-handle::after {
|
|
2942
|
+
content: '';
|
|
2943
|
+
position: absolute;
|
|
2944
|
+
top: 20%;
|
|
2945
|
+
bottom: 20%;
|
|
2946
|
+
left: 50%;
|
|
2947
|
+
width: 2px;
|
|
2948
|
+
transform: translateX(-50%);
|
|
2949
|
+
border-radius: 1px;
|
|
2950
|
+
background-color: var(--border-color, currentColor);
|
|
2951
|
+
opacity: 0.85;
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
table thead th:hover .column-resize-handle,
|
|
2955
|
+
table thead th.is-resizing .column-resize-handle,
|
|
2956
|
+
.column-resize-handle:hover,
|
|
2957
|
+
.column-resize-handle:focus-visible,
|
|
2958
|
+
.column-resize-handle.is-active {
|
|
2959
|
+
opacity: 1;
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2602
2962
|
table thead th.filterable {
|
|
2603
2963
|
cursor: context-menu;
|
|
2604
2964
|
}
|