Kolom Frozen dan Pinned
Freezing menjaga kolom tetap terlihat saat bagian tabel lainnya digulir secara horizontal. Gunakan fitur ini pada tabel yang cukup lebar hingga membutuhkan horizontal scroll: tanpa kolom frozen, menggulir sampai kolom keempat belas dapat membuat nama yang mengidentifikasi baris keluar dari layar, sehingga sel-sel berikutnya terlihat seperti nilai tanpa konteks.
Fitur ini tersedia pada PandaPanel\Tables\Columns\Column, sehingga dapat digunakan pada semua tipe kolom.
Contoh minimal tabel dengan kolom frozen
use PandaPanel\Tables\Columns\NumberColumn;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Enums\ColumnPin;
use PandaPanel\Tables\TableSchema;
return $table
->columns([
TextColumn::make('reference')->frozen()->toggleable(false),
TextColumn::make('customer'),
TextColumn::make('address'),
TextColumn::make('notes'),
NumberColumn::make('balance')->frozen(ColumnPin::End),
])
->frozenActions();2
3
4
5
6
7
8
9
10
11
12
13
14
reference tetap berada di sisi kiri, balance di sisi kanan, dan tombol action pada baris tetap berada bersama balance.
Column::frozen()
frozen(ColumnPin|bool $pin = true): static| Argumen | Hasil |
|---|---|
true (default) | dipasang pada sisi awal — ColumnPin::Start |
ColumnPin::Start | sama seperti di atas, tetapi dinyatakan secara eksplisit |
ColumnPin::End | dipasang pada sisi akhir |
false | tidak dipasang |
TextColumn::make('name')->frozen(); // to the left edge
TextColumn::make('total')->frozen(ColumnPin::End); // to the right
TextColumn::make('name')->frozen(false); // unpin, for a column a shared base schema froze2
3
getFrozen(): ?ColumnPin membaca kembali konfigurasi tersebut. PandaPanel\Tables\Enums\ColumnPin adalah enum tertutup dengan nilai Start dan End. Enum digunakan alih-alih boolean karena istilah "frozen" saja menjadi ambigu ketika tabel cukup lebar: kolom identitas seharusnya berada di kiri, sedangkan action baris biasanya berada di kanan.
TableSchema::frozenActions()
frozenActions(bool $frozen = true): self
hasFrozenActions(): bool2
Nonaktif secara default. Memasang tombol action baris mengambil ruang horizontal, dan tabel sempit yang tidak perlu digulir tidak mendapatkan manfaat apa pun. Aktifkan ketika pengguna harus menggulir ke kanan untuk membaca nilai lalu menggulir kembali ke kiri hanya untuk menjalankan action pada baris yang sama.
Informasi yang dikirim payload
Ada dua informasi terpisah sehingga frontend tidak perlu menebak kembali sisi mana yang dipasang:
$schema->toArray()['frozen']; // ['start' => true, 'actions' => false]
$column->toArray()['frozen']; // 'start' | 'end' | null2
hasFrozenStart(): bool bernilai true ketika ada kolom yang dipasang ke sisi awal. Handle reorder dan checkbox selection ikut dibuat frozen bersama kolom tersebut, bukan karena keduanya memiliki konfigurasi frozen sendiri. Keduanya berada di sebelah kiri semua kolom data; membiarkannya ikut scroll sementara kolom di sebelahnya tetap diam akan membuat elemen-elemen pada baris memiliki definisi berbeda tentang titik awal baris.
Kolom pinned selalu digambar di sisi tempatnya dipasang
Terlepas dari posisi ketika kolom dideklarasikan. Ini bukan sekadar keputusan styling. Posisi sel sticky dihitung dari total lebar kolom frozen sebelum sel tersebut. Jika kolom frozen tetap dibiarkan berada di tengah susunan deklarasi, offset-nya akan menumpuk di atas kolom yang dideklarasikan sebelumnya. Memindahkannya ke sisi yang dipilih adalah makna pinning pada tabel, dan hasilnya terlihat jelas — jauh lebih baik daripada kolom yang diam-diam gagal menjadi frozen.
Renderer membagi kolom visible menjadi tiga daftar: frozen === 'start', frozen === null, dan frozen === 'end', lalu menggambarnya dalam urutan tersebut.
Membuat kolom frozen juga menjaga elemen struktural pada sisi yang sama: handle reorder dan checkbox selection di kiri, serta action baris di kanan ketika frozenActions() diaktifkan.
Offset diukur, bukan dideklarasikan
Kolom frozen tidak wajib memiliki width(). Browser membaca lebar aktual setiap header cell melalui ResizeObserver, lalu mengukurnya kembali setiap kali ukurannya berubah.
Menjumlahkan declared width dari PHP justru akan salah pada situasi yang paling penting: kolom yang mengikuti ukuran kontennya adalah kasus normal. Kolom frozen yang bergeser satu pixel karena nama panjang lebih buruk daripada tidak menggunakan freezing sama sekali.
width() tetap berfungsi dan berguna untuk stabilitas. Namun, ini bukan persyaratan freezing:
TextColumn::make('reference')->frozen()->width('12rem');Tiga detail rendering
Ketiganya akan menjadi bug visual yang mudah terlihat jika hilang:
- Sel frozen menggunakan
bg-inherit, sehingga tetap opaque. Sel sticky transparan akan membuat konten yang sedang digulir terlihat di bawahnya, sementarabg-inherittetap mempertahankan background hover atau selected milik baris. - Sel frozen terakhir pada setiap sisi memiliki hairline dan gradient pendek, sehingga batas antara area frozen dan area scroll mudah dikenali dan kolom tidak terlihat seperti berpindah secara tiba-tiba.
- Header, baris pencarian per kolom, dan footer summary ikut dipasang bersama body. Memasang body tanpa bagian lain akan membuat alignment kolom rusak saat tabel mulai digulir.
Pinning otomatis dilepas pada layar sempit
Kolom frozen hanya boleh menggunakan maksimal 60% dari lebar tabel yang terlihat. Melewati batas tersebut, freezing berhenti membantu dan justru menjadi masalah: pada ponsel, tiga kolom pinned dapat menyisakan ruang terlalu sempit untuk membaca kolom lainnya, sementara pengguna tidak dapat menggulir keluar karena kolom pinned justru menghalangi area tersebut.
Di atas threshold, tabel berperilaku seperti tabel biasa. Pemeriksaan dilakukan setiap kali resize, bukan hanya sekali, sehingga memutar ponsel atau membuka sidebar dapat mengaktifkan kembali pinning ketika ruang kembali mencukupi.
Pengujian
Bagian server dapat diuji secara langsung:
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Enums\ColumnPin;
use PandaPanel\Tables\TableSchema;
expect(TextColumn::make('name')->frozen()->toArray()['frozen'])->toBe('start')
->and(TextColumn::make('total')->frozen(ColumnPin::End)->getFrozen())->toBe(ColumnPin::End)
->and(TextColumn::make('name')->frozen()->frozen(false)->toArray()['frozen'])->toBeNull();
$table = TableSchema::make()
->columns([TextColumn::make('name')->frozen(), TextColumn::make('email')])
->frozenActions()
->toArray();
expect($table['frozen'])->toBe(['start' => true, 'actions' => true]);2
3
4
5
6
7
8
9
10
11
12
13
14
Bagian browser — offset yang diukur, opacity, dan threshold — dicakup oleh frontend contract tests yang membaca resources/js/panel/tables/DataTable.vue dan useFrozenColumns.ts, lalu memastikan invariant tersebut tetap ada.
Hal yang perlu diperhatikan
- Freezing mengubah urutan tampilan tabel. Kolom yang dipasang ke sisi akhir selalu digambar paling akhir, apa pun susunan dari column manager. Ini disengaja dan merupakan satu-satunya cara menjaga sticky offset tetap benar.
frozen()adalah keputusan per kolom, sedangkanfrozenActions()adalah keputusan per tabel. Tidak ada method untuk membuat checkbox selection frozen secara mandiri; checkbox mengikuti kolom frozen pertama pada sisi start.- Membuat terlalu banyak kolom frozen justru merusak manfaatnya. Setelah area pinned melewati 60% lebar tabel, pinning dilepas sepenuhnya. Pin satu atau dua kolom yang benar-benar mengidentifikasi baris, bukan semuanya.
- Kolom frozen tanpa declared width tetap dapat diukur.
ImageColumndenganlabel('')tetap merupakan kolom frozen yang valid; lebar header cell-nya yang akan diukur. - Menyembunyikan kolom frozen melalui column manager menghapusnya dari set frozen untuk pengguna tersebut, kemudian offset dihitung ulang. Komponen lain tidak perlu diberi tahu secara manual.