Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to `livewire-datatables` will be documented in this file

## 3.2.3 ( 2026-08-11 )

### What's Changed

- Fix overlapping desktop pagination controls that made the hover background appear shifted.
- Give desktop and mobile pagination controls stable sizing, spacing, focus states, and accessible labels.
- Add a Chromium regression that verifies hover and keyboard focus do not move pagination buttons.
- Refresh the README structure, links, badges, compatibility details, and default Apricode-theme screenshot.

## 3.2.2 ( 2026-08-11 )

### What's Changed
Expand Down
34 changes: 20 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
<div align="center">

# Livewire Datatables

[![Latest Version on Packagist](https://img.shields.io/packagist/v/arm092/livewire-datatables.svg?style=flat-square)](https://packagist.org/packages/arm092/livewire-datatables)
[![Total Downloads](https://img.shields.io/packagist/dt/arm092/livewire-datatables.svg?style=flat-square)](https://packagist.org/packages/arm092/livewire-datatables)
Build searchable, sortable, filterable and editable admin tables with Laravel, Livewire and Tailwind CSS.

![Livewire Datatables with the default Apricode theme](resources/images/screenshot.png)

[![Latest Version](https://img.shields.io/packagist/v/arm092/livewire-datatables.svg?style=for-the-badge)](https://packagist.org/packages/arm092/livewire-datatables)
[![Total Downloads](https://img.shields.io/packagist/dt/arm092/livewire-datatables.svg?style=for-the-badge)](https://packagist.org/packages/arm092/livewire-datatables)
[![GitHub checks](https://img.shields.io/github/checks-status/arm092/livewire-datatables/master?style=for-the-badge)](https://github.com/arm092/livewire-datatables/actions)
[![License](https://img.shields.io/github/license/arm092/livewire-datatables.svg?style=for-the-badge)](LICENSE.md)

### Features
</div>

## What the package does
- Use a model or query builder to supply data
- Mutate and format columns using preset or custom callbacks
- Sort data using column or computed column
Expand All @@ -13,15 +23,12 @@
- Column groups
- Mass Action (Bulk) Support

## [Demo App Repo](https://github.com/MedicOneSystems/demo-livewire-datatables)

![screenshot](resources/images/screenshot.png "Screenshot")
## Requirements and compatibility

## Requirements
- [Laravel 10|11|12|13](https://laravel.com/docs/13.x)
- [Laravel 10, 11, 12 or 13](https://laravel.com/docs/13.x)
- [Livewire 4.3.5+](https://livewire.laravel.com/)
- [Tailwind](https://tailwindcss.com/)
- [Alpine JS](https://github.com/alpinejs/alpine)
- [Alpine.js](https://alpinejs.dev/)

## Installation

Expand Down Expand Up @@ -95,7 +102,7 @@ other as in the example above.
| **hideable** | _String_ | gives ability to show/hide columns, accepts strings 'inline', 'buttons', or 'select' | ```<livewire:datatable model="App/Post" hideable="inline" />``` |
| **buttonsSlot** | _String_ | blade view to be included immediately after the buttons at the top of the table in the component, which can therefore access public properties | |
| **beforeTableSlot** | _String_ | blade view to be included immediately before the table in the component, which can therefore access public properties | |
| **afterTableSlot** | _String_ | blade view to be included immediately after the table in the component, which can therefore access public properties | [demo](https://livewire-datatables.com/complex) |
| **afterTableSlot** | _String_ | blade view to be included immediately after the table in the component, which can therefore access public properties | |
---


Expand Down Expand Up @@ -575,7 +582,6 @@ Just add ```$complex = true``` to your Datatable Class and all filterable column
- Combine rules and groups of rules using AND/OR logic
- Drag and drop rules around the interface

![image](https://user-images.githubusercontent.com/7000886/128855344-25035758-ca90-42d2-bd19-518c9de45148.png)
---
**Persisting Queries** (Requires AlpineJS v3 with $persist plugin)
- Add ```$persistComplexQuery = true``` to your class and queries will be stored in browser localstorage.
Expand Down Expand Up @@ -679,7 +685,7 @@ class TableWithSaving extends LivewireDatatable
# Styling
I know it's not cool to provide a package with tons of opionated markup and styling. Most other packages seem to have gone down the route of passing optional classes around as arguments or config variables. My take is that because this is just blade with tailwind, you can publish the templates and do whatever you like to them - it should be obvious where the Livewire and Alpine moving parts are.

There are methods for applying styles to rows and cells. ```rowClasses``` receives the ```$row``` and the [laravel loop variable](https://laravel.com/docs/8.x/blade#the-loop-variable) as parameters. ```cellClasses``` receives the ```$row``` and ```$column```
There are methods for applying styles to rows and cells. ```rowClasses``` receives the ```$row``` and the [Laravel loop variable](https://laravel.com/docs/13.x/blade#the-loop-variable) as parameters. ```cellClasses``` receives the ```$row``` and ```$column```

For example:
```php
Expand All @@ -701,7 +707,7 @@ You could also override the render method in your table's class to provide diffe

## Credits and Influences
- [Laravel](https://laravel.com/)
- [Laravel Livewire](https://laravel-livewire.com/docs/quickstart/)
- [Laravel Livewire](https://livewire.laravel.com/docs/quickstart)
- [Tailwind CSS](https://tailwindcss.com/)
- [AlpineJS](https://github.com/alpinejs/alpine)
- [Alpine.js](https://alpinejs.dev/)
- [livewire-datatables by MedicOneSystems](https://github.com/MedicOneSystems/livewire-datatables)
6 changes: 6 additions & 0 deletions playwright.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/Browser',
fullyParallel: false,
workers: 1,
webServer: {
command: 'node tests/Browser/run-test-server.mjs',
url: 'http://127.0.0.1:49187/editable-csp',
reuseExistingServer: false,
},
use: {
baseURL: 'http://127.0.0.1:49187',
browserName: 'chromium',
Expand Down
Binary file modified resources/images/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions resources/views/livewire/datatables/datatable.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,12 @@ class="ld-button ld-button--success px-3 py-2 text-xs font-medium tracking-wider

@unless($this->hidePagination)
<div class="ld-pagination max-w-screen bg-white @unless($complex) rounded-b-lg @endunless border-4 border-t-0 border-b-0 @if($this->activeFilters) border-blue-500 @else border-transparent @endif">
<div class="items-center justify-between p-2 sm:flex">
<div class="ld-pagination-layout items-center justify-between gap-3 p-3 sm:flex">
{{-- check if there is any data --}}
@if(count($this->results))
<div class="flex items-center my-2 sm:my-0">
<select name="perPage" class="ld-per-page block w-full py-2 pl-3 pr-10 mt-1 text-base border-gray-300 form-select leading-6 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 sm:text-sm sm:leading-5" wire:model.live="perPage">
<label for="{{ $name }}-per-page" class="sr-only">{{ __('Results per page') }}</label>
<select id="{{ $name }}-per-page" name="perPage" class="ld-per-page block w-full py-2 pl-3 pr-10 text-base border-gray-300 form-select leading-6 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 sm:text-sm sm:leading-5" wire:model.live="perPage">
@foreach(config('livewire-datatables.per_page_options', [ 10, 25, 50, 100 ]) as $per_page_option)
<option value="{{ $per_page_option }}">{{ $per_page_option }}</option>
@endforeach
Expand All @@ -223,7 +224,7 @@ class="ld-button ld-button--success px-3 py-2 text-xs font-medium tracking-wider
</div>
</div>

<div class="flex justify-end text-gray-600">
<div class="ld-pagination-results flex justify-end text-gray-600">
{{__('Results')}} {{ $this->results->firstItem() }} - {{ $this->results->lastItem() }} {{__('of')}}
{{ $this->results->total() }}
</div>
Expand Down
35 changes: 20 additions & 15 deletions resources/views/livewire/datatables/tailwind-pagination.blade.php
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
<div class="ld-pagination-pages flex overflow-hidden border border-gray-300 divide-x divide-gray-300 rounded pagination">
<nav class="ld-pagination-pages inline-flex overflow-hidden border border-gray-300 rounded pagination" aria-label="Pagination">
<!-- Previous Page Link -->
@if ($paginator->onFirstPage())
<button class="relative inline-flex items-center px-2 py-2 text-sm font-medium leading-5 text-gray-500 bg-white"
disabled>
<span>&laquo;</span>
<button class="ld-pagination-button relative inline-flex items-center justify-center px-3 py-2 text-sm font-medium leading-5 text-gray-500 bg-white"
type="button" aria-label="{{ __('Previous page') }}" disabled>
<span aria-hidden="true">&lsaquo;</span>
</button>
@else
<button wire:click="previousPage"
id="pagination-desktop-page-previous"
class="relative inline-flex items-center px-2 py-2 text-sm font-medium leading-5 text-gray-500 transition duration-150 ease-in-out bg-white hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500">
<span>&laquo;</span>
type="button" aria-label="{{ __('Previous page') }}"
class="ld-pagination-button relative inline-flex items-center justify-center px-3 py-2 text-sm font-medium leading-5 text-gray-500 transition duration-150 ease-in-out bg-white hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500">
<span aria-hidden="true">&lsaquo;</span>
</button>
@endif

<div class="divide-x divide-gray-300">
<div class="ld-pagination-numbers inline-flex">
@foreach ($elements as $element)
@if (is_string($element))
<button class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium leading-5 text-gray-700 bg-white" disabled>
<button class="ld-pagination-button relative inline-flex items-center justify-center px-3 py-2 text-sm font-medium leading-5 text-gray-700 bg-white" type="button" disabled>
<span>{{ $element }}</span>
</button>
@endif
Expand All @@ -27,7 +28,10 @@ class="relative inline-flex items-center px-2 py-2 text-sm font-medium leading-5
@foreach ($element as $page => $url)
<button wire:click="gotoPage({{ $page }})"
id="pagination-desktop-page-{{ $page }}"
class="-mx-1 relative inline-flex items-center px-4 py-2 text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 {{ $page === $paginator->currentPage() ? 'ld-pagination-current bg-gray-200' : 'bg-white' }}">
type="button"
aria-label="{{ __('Go to page :page', ['page' => $page]) }}"
@if($page === $paginator->currentPage()) aria-current="page" @endif
class="ld-pagination-button relative inline-flex items-center justify-center px-3 py-2 text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 {{ $page === $paginator->currentPage() ? 'ld-pagination-current bg-gray-200' : 'bg-white' }}">
{{ $page }}
</button>
@endforeach
Expand All @@ -39,12 +43,13 @@ class="-mx-1 relative inline-flex items-center px-4 py-2 text-sm leading-5 font-
@if ($paginator->hasMorePages())
<button wire:click="nextPage"
id="pagination-desktop-page-next"
class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium leading-5 text-gray-500 transition duration-150 ease-in-out bg-red hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500">
<span>&raquo;</span>
type="button" aria-label="{{ __('Next page') }}"
class="ld-pagination-button relative inline-flex items-center justify-center px-3 py-2 text-sm font-medium leading-5 text-gray-500 transition duration-150 ease-in-out bg-white hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500">
<span aria-hidden="true">&rsaquo;</span>
</button>
@else
<button
class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium leading-5 text-gray-500 bg-white "
disabled><span>&raquo;</span></button>
<button type="button" aria-label="{{ __('Next page') }}"
class="ld-pagination-button relative inline-flex items-center justify-center px-3 py-2 text-sm font-medium leading-5 text-gray-500 bg-white"
disabled><span aria-hidden="true">&rsaquo;</span></button>
@endif
</div>
</nav>
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
<div class="ld-pagination-mobile flex justify-between">
<!-- Previous Page Link -->
@if ($paginator->onFirstPage())
<div class="w-32 flex justify-between items-center relative px-4 py-2 border border-gray-300 text-sm leading-5 font-medium rounded-md text-gray-400 bg-gray-50">
<div class="ld-pagination-mobile-button w-32 flex justify-between items-center relative px-4 py-2 border border-gray-300 text-sm leading-5 font-medium rounded-md text-gray-400 bg-gray-50" aria-disabled="true">
<x-icons.arrow-left />
{{ __('Previous')}}
</div>
@else
<button wire:click="previousPage" id="pagination-mobile-page-previous" class="w-32 flex justify-between items-center relative px-4 py-2 border border-gray-300 text-sm leading-5 font-medium rounded-md text-gray-700 bg-white hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
<button wire:click="previousPage" id="pagination-mobile-page-previous" type="button" class="ld-pagination-mobile-button w-32 flex justify-between items-center relative px-4 py-2 border border-gray-300 text-sm leading-5 font-medium rounded-md text-gray-700 bg-white hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
<x-icons.arrow-left />
{{ __('Previous')}}
</button>
@endif


<!-- Next Page pnk -->
<!-- Next Page Link -->
@if ($paginator->hasMorePages())
<button wire:click="nextPage" id="pagination-mobile-page-next" class="w-32 flex justify-between items-center relative items-center px-4 py-2 border border-gray-300 text-sm leading-5 font-medium rounded-md text-gray-700 bg-white hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
<button wire:click="nextPage" id="pagination-mobile-page-next" type="button" class="ld-pagination-mobile-button w-32 flex justify-between items-center relative px-4 py-2 border border-gray-300 text-sm leading-5 font-medium rounded-md text-gray-700 bg-white hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
{{ __('Next')}}
<x-icons.arrow-right />
</button>
@else
<div class="w-32 flex justify-between items-center relative px-4 py-2 border border-gray-300 text-sm leading-5 font-medium rounded-md text-gray-400 bg-gray-50">
<div class="ld-pagination-mobile-button w-32 flex justify-between items-center relative px-4 py-2 border border-gray-300 text-sm leading-5 font-medium rounded-md text-gray-400 bg-gray-50" aria-disabled="true">
{{ __('Next')}}
<x-icons.arrow-right class="inline" />
</div>
Expand Down
23 changes: 23 additions & 0 deletions resources/views/livewire/datatables/theme.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,32 @@

.ld-table .ld-pagination-pages {
border-color: var(--ld-border) !important;
background: var(--ld-white);
box-shadow: 0 1px 2px rgb(39 40 34 / 6%);
}

.ld-table .ld-pagination-button {
min-width: 2.5rem;
min-height: 2.5rem;
margin: 0 !important;
border: 0 !important;
border-left: 1px solid var(--ld-border) !important;
border-radius: 0 !important;
line-height: 1;
}

.ld-table .ld-pagination-pages > .ld-pagination-button:first-child {
border-left: 0 !important;
}

.ld-table .ld-pagination-numbers {
margin: 0;
}

.ld-table .ld-pagination-mobile-button {
min-height: 2.75rem;
}

.ld-table .ld-pagination-current {
background: var(--ld-primary) !important;
color: var(--ld-ink) !important;
Expand Down
11 changes: 0 additions & 11 deletions tests/Browser/editable-csp.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
import { expect, test } from '@playwright/test';
import { startServer } from './server.mjs';

let server;

test.beforeAll(async () => {
server = await startServer();
});

test.afterAll(async () => {
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
});

test('editable cells remain stable through CSP-safe hydration and morphing', async ({ page }) => {
const errors = [];
Expand Down
Loading