svelte-tably 1.1.1 → 1.2.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.
@@ -8,12 +8,8 @@
8
8
 
9
9
  -->
10
10
 
11
- <script lang="ts">
12
- import { type Snippet } from 'svelte'
13
- import { fly } from 'svelte/transition'
14
- import { sineInOut } from 'svelte/easing'
15
- import reorder, { type ItemState } from 'runic-reorder'
16
- import { Virtualization } from './virtualization.svelte.js'
11
+ <script module lang="ts">
12
+ import type { Snippet } from 'svelte'
17
13
  import {
18
14
  TableState,
19
15
  type HeaderSelectCtx,
@@ -21,36 +17,43 @@
21
17
  type RowSelectCtx,
22
18
  type TableProps
23
19
  } from './table-state.svelte.js'
24
- import Panel from '../panel/Panel.svelte'
25
20
  import Column from '../column/Column.svelte'
26
- import { assignDescriptors, capitalize, fromProps, mounted, segmentize } from '../utility.svelte.js'
27
- import { conditional } from '../conditional.svelte.js'
28
- import { ColumnState, type RowColumnCtx } from '../column/column-state.svelte.js'
21
+ import Panel from '../panel/Panel.svelte'
29
22
  import Expandable from '../expandable/Expandable.svelte'
30
- import { SizeTween } from '../size-tween.svelte.js'
31
- import { on } from 'svelte/events'
32
23
  import Row from '../row/Row.svelte'
33
- import type { CSVOptions } from './csv.js'
34
24
 
35
- type T = $$Generic<Record<PropertyKey, unknown>>
25
+ type ConstructorReturnType<C extends new (...args: any[]) => any> =
26
+ C extends new (...args: any[]) => infer K ? K : never
27
+ type ConstructorParams<C extends new (...args: any[]) => any> =
28
+ C extends new (...args: infer K) => any ? K : never
36
29
 
37
- type ConstructorReturnType<T extends new (...args: any[]) => any> =
38
- T extends new (...args: any[]) => infer K ? K : never
39
- type ConstructorParams<T extends new (...args: any[]) => any> =
40
- T extends new (...args: infer K) => any ? K : never
41
-
42
- type ContentCtx<T extends Record<PropertyKey, unknown>> = {
30
+ export type ContentCtx<Item = any> = {
43
31
  Column: {
44
- new <V>(...args: ConstructorParams<typeof Column<T, V>>): ConstructorReturnType<typeof Column<T, V>>
45
- <V>(...args: Parameters<typeof Column<T, V>>): ReturnType<typeof Column<T, V>>
32
+ new <V>(...args: ConstructorParams<typeof Column<Item, V>>): ConstructorReturnType<typeof Column<Item, V>>
33
+ <V>(...args: Parameters<typeof Column<Item, V>>): ReturnType<typeof Column<Item, V>>
46
34
  }
47
- Panel: typeof Panel<T>
48
- Expandable: typeof Expandable<T>
49
- Row: typeof Row<T>
50
- readonly table: TableState<T>
35
+ Panel: typeof Panel<Item>
36
+ Expandable: typeof Expandable<Item>
37
+ Row: typeof Row<Item>
38
+ readonly table: TableState<Item>
51
39
  }
52
40
 
53
- type ContentSnippet = Snippet<[context: ContentCtx<T>]>
41
+ export type ContentSnippet<Item = any> = Snippet<[context: ContentCtx<Item>]>
42
+ </script>
43
+
44
+ <script lang="ts">
45
+ import { fly } from 'svelte/transition'
46
+ import { sineInOut } from 'svelte/easing'
47
+ import reorder, { type ItemState } from 'runic-reorder'
48
+ import { Virtualization } from './virtualization.svelte.js'
49
+ import { assignDescriptors, capitalize, fromProps, mounted, segmentize } from '../utility.svelte.js'
50
+ import { conditional } from '../conditional.svelte.js'
51
+ import { ColumnState, type RowColumnCtx } from '../column/column-state.svelte.js'
52
+ import { SizeTween } from '../size-tween.svelte.js'
53
+ import { on } from 'svelte/events'
54
+ import type { CSVOptions } from './csv.js'
55
+
56
+ type T = $$Generic
54
57
 
55
58
  let {
56
59
  content,
@@ -59,7 +62,7 @@
59
62
  data: _data = $bindable([]),
60
63
  table: _table = $bindable(),
61
64
  ...restProps
62
- }: TableProps<T> & { content?: ContentSnippet } = $props()
65
+ }: TableProps<T> & { content?: ContentSnippet<T> } = $props()
63
66
 
64
67
  const properties = fromProps(restProps, {
65
68
  selected: [() => _selected, (v) => (_selected = v)],
@@ -69,14 +72,39 @@
69
72
 
70
73
  const mount = mounted()
71
74
 
75
+
76
+ const getRowLabel = (item: T, index: number) => {
77
+ const labelColumn = columns.find((c) => c.id !== '__fixed')
78
+ const raw = labelColumn?.options.value?.(item)
79
+
80
+ if (raw === null || raw === undefined) return `Row ${index + 1}`
81
+ if (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean') {
82
+ const text = String(raw).trim()
83
+ return text ? text : `Row ${index + 1}`
84
+ }
85
+
86
+ return `Row ${index + 1}`
87
+ }
88
+
72
89
  const reorderArea = reorder(rowSnippet)
73
90
 
74
- const elements = $state({}) as Record<
75
- 'headers' | 'statusbar' | 'rows' | 'virtualTop' | 'virtualBottom' | 'selects',
76
- HTMLElement
77
- >
91
+ const elements = $state({}) as Record<'headers' | 'statusbar', HTMLElement>
78
92
 
79
93
  const table = new TableState<T>(properties) as TableState<T>
94
+ const uid = table.cssId
95
+ let expandIdCounter = 0
96
+ const expandIds = new WeakMap<object, string>()
97
+ const getExpandId = (item: T) => {
98
+ if (item && typeof item === 'object') {
99
+ let id = expandIds.get(item)
100
+ if (!id) {
101
+ id = `${uid}-expand-${++expandIdCounter}`
102
+ expandIds.set(item, id)
103
+ }
104
+ return id
105
+ }
106
+ return `${uid}-expand-${String(item)}`
107
+ }
80
108
 
81
109
  const virtualization = new Virtualization(table)
82
110
 
@@ -86,13 +114,38 @@
86
114
  let hoveredColumn: ColumnState | null = $state(null)
87
115
 
88
116
  /** Order of columns */
89
- const fixed = $derived(table.positions.fixed)
90
- const hidden = $derived(table.positions.hidden)
91
- const notHidden = (column: ColumnState) => !table.positions.hidden.includes(column)
117
+ const isColumn = (value: unknown): value is ColumnState =>
118
+ value instanceof ColumnState
119
+ const fixed = $derived(table.positions.fixed.filter(isColumn))
120
+ const hidden = $derived(table.positions.hidden.filter(isColumn))
121
+ const notHidden = (column: ColumnState | undefined) =>
122
+ !!column && !table.positions.hidden.includes(column)
92
123
  const sticky = $derived(table.positions.sticky.filter(notHidden))
93
124
  const scrolled = $derived(table.positions.scroll.filter(notHidden))
94
125
  const columns = $derived([...fixed, ...sticky, ...scrolled])
95
126
 
127
+ const autoSchema = $derived.by(() => {
128
+ const rows = table.dataState.origin as any[]
129
+ const keys = [] as string[]
130
+ const seen = new Set<string>()
131
+ const sample = {} as Record<string, unknown>
132
+
133
+ for (const row of rows.slice(0, 50)) {
134
+ if (!row || typeof row !== 'object') continue
135
+ for (const key of Object.keys(row)) {
136
+ if (!seen.has(key)) {
137
+ seen.add(key)
138
+ keys.push(key)
139
+ }
140
+ if (sample[key] === undefined && row[key] !== undefined) {
141
+ sample[key] = row[key]
142
+ }
143
+ }
144
+ }
145
+
146
+ return { keys, sample }
147
+ })
148
+
96
149
  const getWidth = (key: string, def: number = 150) =>
97
150
  table.columnWidths[key] ??= table.columns[key]?.defaults.width ?? def
98
151
 
@@ -104,7 +157,7 @@
104
157
  return
105
158
  }
106
159
 
107
- const context = table.row?.snippets.context ? table.row?.options.context.width : ''
160
+ const context = table.row?.snippets.context ? ` ${table.row?.options.context.width}` : ''
108
161
 
109
162
  const templateColumns =
110
163
  columns
@@ -115,7 +168,7 @@
115
168
  })
116
169
  .join(' ') + context
117
170
 
118
- const theadTempla3teColumns = `
171
+ const theadTemplateColumns = `
119
172
  [data-svelte-tably="${table.cssId}"] > thead > tr,
120
173
  [data-svelte-tably="${table.cssId}"] > tfoot > tr {
121
174
  grid-template-columns: ${templateColumns};
@@ -124,6 +177,7 @@
124
177
 
125
178
  const tbodyTemplateColumns = `
126
179
  [data-area-class='${table.cssId}'] tr.row,
180
+ [data-area-class='${table.cssId}'] tr.filler,
127
181
  [data-svelte-tably="${table.cssId}"] > tbody::after {
128
182
  grid-template-columns: ${templateColumns};
129
183
  }
@@ -154,7 +208,7 @@
154
208
  )
155
209
  .join('')
156
210
 
157
- style = theadTempla3teColumns + tbodyTemplateColumns + stickyLeft + columnStyling
211
+ style = theadTemplateColumns + tbodyTemplateColumns + stickyLeft + columnStyling
158
212
  })
159
213
 
160
214
  function observeColumnWidth(node: HTMLDivElement, isHeader = false) {
@@ -188,21 +242,41 @@
188
242
  }
189
243
 
190
244
  let tbody = $state({
191
- width: 0
245
+ scrollbar: 0
192
246
  })
193
- async function onscroll() {
194
- const target = virtualization.viewport.element!
247
+
248
+ function observeScrollbar(node: HTMLElement) {
249
+ if (typeof ResizeObserver === 'undefined') return
250
+
251
+ const update = () => {
252
+ // Reserve the same gutter in header/footer as the scrollable body
253
+ tbody.scrollbar = Math.max(0, node.offsetWidth - node.clientWidth)
254
+ }
255
+
256
+ update()
257
+ const observer = new ResizeObserver(update)
258
+ observer.observe(node)
259
+
260
+ return {
261
+ destroy() {
262
+ observer.disconnect()
263
+ }
264
+ }
265
+ }
266
+ function onscroll() {
267
+ const target = virtualization.viewport.element
268
+ if (!target) return
195
269
  if (target.scrollTop !== virtualization.scrollTop) {
196
270
  virtualization.scrollTop = target?.scrollTop ?? virtualization.scrollTop
197
271
  }
198
272
 
199
- if (elements.selects) {
200
- elements.selects.scrollTop = target?.scrollTop
273
+ if (elements.headers) {
274
+ elements.headers.scrollLeft = target.scrollLeft
201
275
  }
202
276
 
203
- if (!elements.headers) return
204
- elements.headers.scrollLeft = target.scrollLeft
205
- elements.statusbar.scrollLeft = target.scrollLeft
277
+ if (elements.statusbar) {
278
+ elements.statusbar.scrollLeft = target.scrollLeft
279
+ }
206
280
  }
207
281
 
208
282
  // * --- CSV --- *
@@ -411,7 +485,11 @@
411
485
  class:fixed={true}
412
486
  use:addRowColumnEvents={[where, column, () => args[1]]}
413
487
  data-column={column.id}
414
- class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
488
+ class:pad={
489
+ (where === 'header' && column.options.padHeader) ||
490
+ (where === 'row' && column.options.padRow) ||
491
+ (where === 'statusbar' && column.options.padStatusbar)
492
+ }
415
493
  class:header={isHeader}
416
494
  class:sortable
417
495
  use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
@@ -439,7 +517,11 @@
439
517
  use:addRowColumnEvents={[where, column, () => args[1]]}
440
518
  use:observeColumnWidth={isHeader}
441
519
  data-column={column.id}
442
- class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
520
+ class:pad={
521
+ (where === 'header' && column.options.padHeader) ||
522
+ (where === 'row' && column.options.padRow) ||
523
+ (where === 'statusbar' && column.options.padStatusbar)
524
+ }
443
525
  class:header={isHeader}
444
526
  class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
445
527
  class:border={i == sticky.length - 1}
@@ -466,7 +548,11 @@
466
548
  class={column.options.class ?? ''}
467
549
  class:column={true}
468
550
  data-column={column.id}
469
- class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
551
+ class:pad={
552
+ (where === 'header' && column.options.padHeader) ||
553
+ (where === 'row' && column.options.padRow) ||
554
+ (where === 'statusbar' && column.options.padStatusbar)
555
+ }
470
556
  use:addRowColumnEvents={[where, column, () => args[1]]}
471
557
  use:observeColumnWidth={isHeader}
472
558
  class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
@@ -486,7 +572,7 @@
486
572
  {/each}
487
573
  {/snippet}
488
574
 
489
- {#snippet defaultRow(item: T, ctx: RowColumnCtx<T, any>)}
575
+ {#snippet defaultRow(item: any, ctx: RowColumnCtx<any, any>)}
490
576
  {ctx.value}
491
577
  {/snippet}
492
578
 
@@ -562,30 +648,46 @@
562
648
  'row'
563
649
  )}
564
650
  {#if table.row?.snippets.context}
565
- {#if table.row?.snippets.contextHeader || !table.row?.options.context.hover || hoveredRow === item}
566
- <td
567
- class="context-col"
568
- class:hover={!table.row?.snippets.contextHeader && table.row?.options.context.hover}
569
- class:hidden={table.row?.options.context.hover &&
570
- table.row?.snippets.contextHeader &&
571
- hoveredRow !== item}
572
- >
573
- {@render table.row?.snippets.context?.(item, ctx)}
574
- </td>
575
- {/if}
651
+ <td
652
+ class="context-col"
653
+ class:hidden={table.row?.options.context.hover && hoveredRow !== item}
654
+ >
655
+ {@render table.row?.snippets.context?.(item, ctx)}
656
+ </td>
576
657
  {/if}
577
658
  </tr>
578
659
 
579
- {@const expandableTween = new SizeTween(() => table.expandable && expandedRow.includes(item), {
580
- min: 1,
660
+ {@const expandableTween = new SizeTween(() => !!table.expandable && expandedRow.includes(item), {
661
+ min: 0,
581
662
  duration: table.expandable?.options.slide.duration,
582
663
  easing: table.expandable?.options.slide.easing
583
664
  })}
584
- {#if expandableTween.current > 0}
585
- <tr class="expandable" style="height: {expandableTween.current}px">
586
- <td colspan={columns.length} style="height: {expandableTween.current}px">
587
- <div bind:offsetHeight={expandableTween.size} style="width: {tbody.width - 3}px">
588
- {@render table.expandable!.snippets.content?.(item, ctx)}
665
+ {@const expanded = !!table.expandable && expandedRow.includes(item)}
666
+ {#if table.expandable && (expanded || expandableTween.current > 0 || expandableTween.transitioning)}
667
+ {@const expandId = getExpandId(item)}
668
+ {@const expandLabelId = `${expandId}-label`}
669
+ <tr class="expandable">
670
+ <td
671
+ colspan={columns.length + (table.row?.snippets.context ? 1 : 0)}
672
+ style="padding: 0"
673
+ >
674
+ <div
675
+ class="expandable-clip"
676
+ style="height: {Math.round(expandableTween.current)}px"
677
+ id={expandId}
678
+ role="region"
679
+ aria-labelledby={expandLabelId}
680
+ aria-hidden={!expanded}
681
+ >
682
+ <span class="sr-only" id={expandLabelId}>
683
+ Expanded content for {getRowLabel(item, index)}
684
+ </span>
685
+ <div
686
+ class="expandable-content"
687
+ bind:offsetHeight={expandableTween.size}
688
+ >
689
+ {@render table.expandable?.snippets.content?.(item, ctx)}
690
+ </div>
589
691
  </div>
590
692
  </td>
591
693
  </tr>
@@ -596,7 +698,7 @@
596
698
  id={table.id}
597
699
  data-svelte-tably={table.cssId}
598
700
  class="table svelte-tably"
599
- style="--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px;"
701
+ style="--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px; --scrollbar: {tbody.scrollbar}px;"
600
702
  aria-rowcount={table.data.length}
601
703
  >
602
704
  {#if columns.some((v) => v.snippets.header)}
@@ -616,23 +718,28 @@
616
718
  ],
617
719
  'header'
618
720
  )}
619
- {#if table.row?.snippets.contextHeader}
620
- <th class="context-col">
621
- {@render table.row?.snippets.contextHeader()}
721
+ {#if table.row?.snippets.context}
722
+ <th
723
+ class="context-col"
724
+ aria-hidden={table.row?.snippets.contextHeader ? undefined : true}
725
+ role={table.row?.snippets.contextHeader ? undefined : 'presentation'}
726
+ >
727
+ {#if table.row?.snippets.contextHeader}
728
+ {@render table.row?.snippets.contextHeader()}
729
+ {/if}
622
730
  </th>
623
731
  {/if}
624
732
  </tr>
625
- <tr style="width:400px;background:none;pointer-events:none;"></tr>
626
733
  </thead>
627
734
  {/if}
628
735
 
629
736
  <tbody
630
737
  class="content"
631
738
  use:reorderArea={{ axis: 'y', class: table.cssId }}
739
+ use:observeScrollbar
632
740
  bind:this={virtualization.viewport.element}
633
741
  onscrollcapture={onscroll}
634
742
  bind:clientHeight={virtualization.viewport.height}
635
- bind:clientWidth={tbody.width}
636
743
  >
637
744
  {#if table.options.reorderable}
638
745
  {@render reorderArea({
@@ -651,6 +758,39 @@
651
758
  {@render rowSnippet(item, { index: i + virtualization.topIndex } as ItemState)}
652
759
  {/each}
653
760
  {/if}
761
+
762
+ {#if columns.length > 0 && virtualization.virtualTop === 0 && virtualization.virtualBottom === 0}
763
+ <tr class="filler" aria-hidden="true">
764
+ {#each fixed as column (column)}
765
+ {#if !hidden.includes(column)}
766
+ <td
767
+ class={`column sticky fixed ${column.options.class ?? ''}`}
768
+ data-column={column.id}
769
+ ></td>
770
+ {/if}
771
+ {/each}
772
+ {#each sticky as column, i (column)}
773
+ {#if !hidden.includes(column)}
774
+ <td
775
+ class={`column sticky ${column.options.class ?? ''}`}
776
+ class:border={i == sticky.length - 1}
777
+ data-column={column.id}
778
+ ></td>
779
+ {/if}
780
+ {/each}
781
+ {#each scrolled as column (column)}
782
+ {#if !hidden.includes(column)}
783
+ <td
784
+ class={`column ${column.options.class ?? ''}`}
785
+ data-column={column.id}
786
+ ></td>
787
+ {/if}
788
+ {/each}
789
+ {#if table.row?.snippets.context}
790
+ <td class="context-col" aria-hidden="true"></td>
791
+ {/if}
792
+ </tr>
793
+ {/if}
654
794
  </tbody>
655
795
 
656
796
  {#if columns.some((v) => v.snippets.statusbar)}
@@ -667,8 +807,10 @@
667
807
  ],
668
808
  'statusbar'
669
809
  )}
810
+ {#if table.row?.snippets.context}
811
+ <td class="context-col" aria-hidden="true"></td>
812
+ {/if}
670
813
  </tr>
671
- <tr style="width:400px;background:none;pointer-events:none;"></tr>
672
814
  </tfoot>
673
815
  {/if}
674
816
 
@@ -704,11 +846,11 @@
704
846
  </caption>
705
847
  </table>
706
848
 
707
- {#snippet headerSelected(ctx: HeaderSelectCtx<T>)}
849
+ {#snippet headerSelected(ctx: HeaderSelectCtx<any>)}
708
850
  <input type="checkbox" indeterminate={ctx.indeterminate} bind:checked={ctx.isSelected} />
709
851
  {/snippet}
710
852
 
711
- {#snippet rowSelected(ctx: RowSelectCtx<T>)}
853
+ {#snippet rowSelected(ctx: RowSelectCtx<any>)}
712
854
  <input type="checkbox" bind:checked={ctx.isSelected} tabindex="-1" />
713
855
  {/snippet}
714
856
 
@@ -794,9 +936,20 @@
794
936
  })}
795
937
  {/if}
796
938
  {#if expandable && expandable?.options.chevron !== 'never'}
797
- <button class="expand-row" tabindex="-1" onclick={() => (row.expanded = !row.expanded)}>
798
- {#if row.expanded || expandable.options.chevron === 'always' || (row.rowHovered && expandable.options.chevron === 'hover')}
799
- {@render chevronSnippet(row.expanded ? 180 : 90)}
939
+ {@const expandId = getExpandId(item)}
940
+ {@const expanded = row.expanded}
941
+ {@const label = expanded ? 'Collapse row' : 'Expand row'}
942
+ <button
943
+ class="expand-row"
944
+ tabindex="-1"
945
+ type="button"
946
+ aria-label={label}
947
+ aria-expanded={expanded}
948
+ aria-controls={expandId}
949
+ onclick={() => (row.expanded = !row.expanded)}
950
+ >
951
+ {#if expanded || expandable.options.chevron === 'always' || (row.rowHovered && expandable.options.chevron === 'hover')}
952
+ {@render chevronSnippet(expanded ? 180 : 90)}
800
953
  {/if}
801
954
  </button>
802
955
  {/if}
@@ -807,12 +960,12 @@
807
960
  {/if}
808
961
 
809
962
  {#if table.options.auto}
810
- {#each Object.keys(table.data[0] || {}) as key}
963
+ {#each autoSchema.keys as key}
811
964
  <Column
812
965
  id={key}
813
- value={(r) => r[key]}
966
+ value={(r) => (r as any)?.[key]}
814
967
  header={capitalize(segmentize(key))}
815
- sort={typeof table.data[0]?.[key] === 'number' ?
968
+ sort={typeof autoSchema.sample?.[key] === 'number' ?
816
969
  (a, b) => a - b
817
970
  : (a, b) => String(a).localeCompare(String(b))}
818
971
  />
@@ -846,14 +999,9 @@
846
999
  height: 100%;
847
1000
  z-index: 3;
848
1001
  padding: 0;
849
-
850
- &.hover {
851
- position: absolute;
852
- }
853
1002
  &.hidden {
854
1003
  pointer-events: none;
855
1004
  user-select: none;
856
- border-left: none;
857
1005
  background: none;
858
1006
  > :global(*) {
859
1007
  opacity: 0;
@@ -861,6 +1009,30 @@
861
1009
  }
862
1010
  }
863
1011
 
1012
+ .table::before {
1013
+ content: '';
1014
+ grid-area: headers;
1015
+ justify-self: end;
1016
+ align-self: stretch;
1017
+ width: var(--scrollbar, 0px);
1018
+ background-color: var(--tably-bg);
1019
+ pointer-events: none;
1020
+ position: relative;
1021
+ z-index: 4;
1022
+ }
1023
+
1024
+ .table::after {
1025
+ content: '';
1026
+ grid-area: statusbar;
1027
+ justify-self: end;
1028
+ align-self: stretch;
1029
+ width: var(--scrollbar, 0px);
1030
+ background-color: var(--tably-statusbar);
1031
+ pointer-events: none;
1032
+ position: relative;
1033
+ z-index: 4;
1034
+ }
1035
+
864
1036
  :global(:root) {
865
1037
  --tably-color: hsl(0, 0%, 0%);
866
1038
  --tably-bg: hsl(0, 0%, 100%);
@@ -877,24 +1049,32 @@
877
1049
 
878
1050
  .svelte-tably {
879
1051
  position: relative;
880
- overflow: visible;
1052
+ overflow: hidden;
1053
+ border-collapse: collapse;
1054
+ border-spacing: 0;
881
1055
  }
882
1056
 
883
1057
  .expandable {
884
- position: relative;
885
-
886
1058
  & > td {
887
- position: sticky;
888
- left: 1px;
889
- > div {
890
- position: absolute;
891
- overflow: auto;
892
- top: -1.5px;
893
- left: 0;
894
- }
1059
+ padding: 0;
1060
+ border: none;
895
1061
  }
896
1062
  }
897
1063
 
1064
+ .expandable-clip {
1065
+ overflow: hidden;
1066
+ width: 100%;
1067
+ background-color: var(--tably-bg);
1068
+ box-shadow: inset 0 -1px 0 var(--tably-border-grid);
1069
+ }
1070
+
1071
+ .expandable-content {
1072
+ overflow: auto;
1073
+ width: 100%;
1074
+ background-color: var(--tably-bg);
1075
+ box-sizing: border-box;
1076
+ }
1077
+
898
1078
  .expand-row {
899
1079
  display: flex;
900
1080
  justify-content: center;
@@ -961,20 +1141,16 @@
961
1141
  }
962
1142
 
963
1143
  tbody::before,
964
- tbody::after,
965
- selects::before,
966
- selects::after {
1144
+ tbody::after {
967
1145
  content: '';
968
- display: grid;
969
- min-height: 100%;
1146
+ display: block;
1147
+ flex: 0 0 auto;
970
1148
  }
971
1149
 
972
- tbody::before,
973
- selects::before {
1150
+ tbody::before {
974
1151
  height: var(--t);
975
1152
  }
976
- tbody::after,
977
- selects::after {
1153
+ tbody::after {
978
1154
  height: var(--b);
979
1155
  }
980
1156
 
@@ -1042,7 +1218,10 @@
1042
1218
 
1043
1219
  .table {
1044
1220
  display: grid;
1045
- height: auto;
1221
+ width: 100%;
1222
+ min-width: 0;
1223
+ min-height: 0;
1224
+ height: 100%;
1046
1225
  max-height: 100%;
1047
1226
  position: relative;
1048
1227
 
@@ -1054,8 +1233,8 @@
1054
1233
  'rows panel'
1055
1234
  'statusbar panel';
1056
1235
 
1057
- grid-template-columns: auto min-content;
1058
- grid-template-rows: auto 1fr auto;
1236
+ grid-template-columns: 1fr min-content;
1237
+ grid-template-rows: auto minmax(0, 1fr) auto;
1059
1238
 
1060
1239
  border: 1px solid var(--tably-border);
1061
1240
  border-radius: var(--tably-radius);
@@ -1066,6 +1245,8 @@
1066
1245
  grid-area: headers;
1067
1246
  z-index: 2;
1068
1247
  overflow: hidden;
1248
+ min-width: 0;
1249
+ padding-right: var(--scrollbar, 0px);
1069
1250
  }
1070
1251
 
1071
1252
  .headers > tr > .column {
@@ -1078,13 +1259,30 @@
1078
1259
  border-left: 1px solid var(--tably-border-grid);
1079
1260
  }
1080
1261
 
1262
+ .headers > tr > .context-col {
1263
+ background-color: var(--tably-bg);
1264
+ }
1265
+
1081
1266
  .content {
1082
- display: grid;
1083
- grid-auto-rows: max-content;
1267
+ display: flex;
1268
+ flex-direction: column;
1269
+ min-width: 0;
1270
+ min-height: 0;
1084
1271
 
1085
1272
  grid-area: rows;
1086
1273
  scrollbar-width: thin;
1087
- overflow: auto;
1274
+ overflow-x: auto;
1275
+ overflow-y: scroll;
1276
+ }
1277
+
1278
+ .content > tr.row,
1279
+ .content > tr.expandable {
1280
+ flex: 0 0 auto;
1281
+ }
1282
+
1283
+ .content > tr.filler {
1284
+ flex: 1 0 0px;
1285
+ min-height: 0;
1088
1286
  }
1089
1287
 
1090
1288
  .statusbar {
@@ -1092,19 +1290,30 @@
1092
1290
  grid-area: statusbar;
1093
1291
  overflow: hidden;
1094
1292
  background-color: var(--tably-statusbar);
1293
+ min-width: 0;
1294
+ padding-right: var(--scrollbar, 0px);
1095
1295
  }
1096
1296
 
1097
1297
  .statusbar > tr > .column {
1098
1298
  border-top: 1px solid var(--tably-border);
1099
1299
  padding: calc(var(--tably-padding-y) / 2) 0;
1100
1300
  }
1301
+ .statusbar > tr > .context-col {
1302
+ border-top: 1px solid var(--tably-border);
1303
+ border-left: 1px solid var(--tably-border-grid);
1304
+ }
1305
+
1306
+ .statusbar > tr > .context-col {
1307
+ background-color: var(--tably-statusbar);
1308
+ }
1101
1309
 
1102
1310
  .headers > tr,
1103
1311
  .row,
1312
+ .expandable,
1313
+ .filler,
1104
1314
  .statusbar > tr {
1105
1315
  display: grid;
1106
1316
  width: 100%;
1107
- height: 100%;
1108
1317
  min-width: max-content;
1109
1318
 
1110
1319
  & > .column {
@@ -1141,11 +1350,30 @@
1141
1350
  border-top: 2px solid var(--tably-border-grid);
1142
1351
  }
1143
1352
 
1144
- .row > * {
1353
+ .row > *,
1354
+ .filler > * {
1145
1355
  border-left: 1px solid var(--tably-border-grid);
1146
1356
  border-bottom: 1px solid var(--tably-border-grid);
1147
1357
  }
1148
1358
 
1359
+ .filler {
1360
+ pointer-events: none;
1361
+ user-select: none;
1362
+ background: none;
1363
+ }
1364
+
1365
+ .sr-only {
1366
+ position: absolute;
1367
+ width: 1px;
1368
+ height: 1px;
1369
+ padding: 0;
1370
+ margin: -1px;
1371
+ overflow: hidden;
1372
+ clip: rect(0, 0, 0, 0);
1373
+ white-space: nowrap;
1374
+ border: 0;
1375
+ }
1376
+
1149
1377
  .panel {
1150
1378
  position: relative;
1151
1379
  grid-area: panel;