diff --git a/UPGRADE.md b/UPGRADE.md index fa247116..4a73f69d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -60,6 +60,14 @@ ran. A standard sheet is now out of the row until then, and one whose `wire:model` property is open is rendered open and stands at its width. One opened from an Alpine scope alone still appears when Alpine starts, which only the script can know. +- **A long value in `` ends before the arrow, with an ellipsis.** Where the browser has + the customizable select (Chrome, Safari), the closed select drew its value in a box of its own + that took neither `text-overflow` nor a width from the select, so a long option in a narrow + field, or at `size="sm"` and `xs`, ran on under the arrow; the native select (Firefox) cut it at + the arrow with no ellipsis. The select's first child is now a ` + @if (filled($placeholder)) @endif diff --git a/tests/Browser/FieldsTest.php b/tests/Browser/FieldsTest.php index c23f4b83..1f1562e9 100644 --- a/tests/Browser/FieldsTest.php +++ b/tests/Browser/FieldsTest.php @@ -168,6 +168,83 @@ it('opens the customizable select picker as M3\'s menu, where the browser suppor ->assertScript("parseFloat(getComputedStyle({$option}).minBlockSize) === 48"); }); +class SelectValueProbe extends Component +{ + public string $format = 'dots'; + + public function render(): string + { + return <<<'BLADE' +
+

format: {{ $format }}

+ + @php($formats = [['id' => 'dots', 'name' => 'Day first, dots — 17.08.2026'], ['id' => 'slashes', 'name' => 'Month first, slashes — 08/17/2026']]) + + + + + + +
+ BLADE; + } +} + +/** + * Where a select's value ends against its arrow, and how it is cut: `[ends before the arrow, + * text-overflow, white-space, overflow, the value's text]`. The customizable select draws the value + * in its ``; the native one in the select's own box, less its end padding. + */ +function selectValue(string $id): string +{ + return "(() => { + const select = document.getElementById('{$id}'); + const arrow = select.closest('[data-md-field]').querySelector('[data-md-field-arrow]').getBoundingClientRect(); + const value = CSS.supports('appearance', 'base-select') ? select.querySelector('selectedcontent') : select; + + if (! value) { + return 'nothing draws the value'; + } + + const style = getComputedStyle(value); + const end = value.getBoundingClientRect().right - parseFloat(style.paddingRight) - parseFloat(style.borderRightWidth); + + return [end <= arrow.left, style.textOverflow, style.whiteSpace, style.overflowX, (value === select ? select.selectedOptions[0] : value).textContent.trim()]; + })()"; +} + +it('ends a select\'s value before its arrow with an ellipsis, at every size and after a render', function () { + Livewire::component('select-value-probe', SelectValueProbe::class); + + Route::middleware('web')->get('/select-value-probe', fn () => Blade::render(<<<'BLADE' + + + + + @vite(config('livewire-material.showcase.vite')) + @livewireStyles + + + + @livewireScripts + + + BLADE)); + + $page = visit('/select-value-probe')->waitForEvent('networkidle') + ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); + + foreach (['format-md', 'format-sm', 'format-xs', 'format-filled', 'format-icon'] as $id) { + expect($page->script(selectValue($id)))->toBe([true, 'ellipsis', 'nowrap', 'hidden', 'Day first, dots — 17.08.2026']); + } + + // The customizable select copies the chosen option into its , which a + // Livewire render must not empty again. + $page->select('#format-md', 'slashes')->assertSeeIn('#format', 'slashes'); + + expect($page->script(selectValue('format-md')))->toBe([true, 'ellipsis', 'nowrap', 'hidden', 'Month first, slashes — 08/17/2026']); +}); + it('keeps a partly ticked checkbox in step with the server', function () { $all = "document.querySelector('#all')"; diff --git a/tests/Feature/Components/FieldTest.php b/tests/Feature/Components/FieldTest.php index c1b53c1d..744da674 100644 --- a/tests/Feature/Components/FieldTest.php +++ b/tests/Feature/Components/FieldTest.php @@ -139,6 +139,8 @@ it('draws a native select whose label always floats', function () { ->toContain('data-md-select') ->toMatch('/]*data-md-field-control)[^>]*>/') ->toContain('toContain('') ->toContain('wire:model="hours"') ->toContain('') ->toContain('')