! $vertical,
+ // The same drawing, laid out as long as the slider is tall and then turned a quarter
+ // anticlockwise so the value grows upwards. Everything inside it — the segments, the
+ // ticks, the handle and its label — is placed as if the slider were lying down.
+ 'top-1/2 left-1/2 h-[100cqw] w-[calc(100cqh-0.25rem)] -translate-1/2 -rotate-90' => $vertical,
+ ])>
@foreach (['start' => $inactiveInk[$color], 'active' => $activeInk[$color], 'end' => $inactiveInk[$color]] as $part => $ink)
! $vertical,
+ // Turned back the quarter the drawing turns, so the glyph stands up.
+ 'rotate-90' => $vertical,
+ $iconInk[$color],
+ 'group-has-disabled/slider:text-on-surface/38',
+ ])
style="left: {{ $calc($iconAt) }}"
>
@@ -358,11 +397,15 @@
])>
@if ($valueLabel !== 'never')
-
+ ! $vertical, $labelBottom])>
! $vertical,
+ // Turned back the quarter the drawing turns: the pill lands
+ // beside the handle rather than above it, and reads across.
+ 'rotate-90' => $vertical,
'transition-[opacity,scale] duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast',
'opacity-0 scale-75 group-data-pressed/thumb:opacity-100 group-data-pressed/thumb:scale-100 group-data-focused/thumb:opacity-100 group-data-focused/thumb:scale-100' => $valueLabel === 'drag',
])
@@ -383,6 +426,7 @@
step="{{ $continuous ? 'any' : $format($interval) }}"
value="{{ $format($values[$index]) }}"
data-thumb="{{ $thumb }}"
+ @if ($vertical) aria-orientation="vertical" @endif
@if ($range) aria-label="{{ $thumb === 'start' ? __('Range start') : __('Range end') }}" @endif
@if ($described) aria-describedby="{{ $described }}" @endif
@if ($messages !== []) aria-invalid="true" @endif
diff --git a/resources/views/showcase/sections/sliders.blade.php b/resources/views/showcase/sections/sliders.blade.php
index fee5b5c6..03f1c2dc 100644
--- a/resources/views/showcase/sections/sliders.blade.php
+++ b/resources/views/showcase/sections/sliders.blade.php
@@ -22,6 +22,16 @@
BLADE,
+ 'Vertical' => <<<'BLADE'
+ {{-- M3 Expressive's second orientation. A vertical slider needs a height: it is on the wrapper, which the label and hint share. --}}
+
+
+
+
+
+
+
+ BLADE,
'Colours and value labels' => <<<'BLADE'
@@ -48,7 +58,7 @@
Sliders
- <x-slider>: M3 Expressive's slider on native range inputs — standard, range and centred, five sizes, ticks, value labels and an inset icon.
+ <x-slider>: M3 Expressive's slider on native range inputs — standard, range and centred, horizontal or vertical, five sizes, ticks, value labels and an inset icon.
@foreach ($examples as $title => $code)
diff --git a/tests/Feature/Components/SliderTest.php b/tests/Feature/Components/SliderTest.php
index ff6a6743..9b8bd132 100644
--- a/tests/Feature/Components/SliderTest.php
+++ b/tests/Feature/Components/SliderTest.php
@@ -38,7 +38,8 @@ it('draws the first frame on the server: the track split around the handle, and
it('draws M3\'s 44x48 value indicator and puts the stop on the inactive track', function () {
expect((string) $this->blade('
'))
- ->toContain('flex h-11 min-w-12 origin-bottom')
+ ->toContain('flex h-11 min-w-12 items-center')
+ ->toMatch('/data-value-label[^>]*class="[^"]*origin-bottom/')
->toMatch('/data-stop="end"[^>]*class="[^"]*bg-on-secondary-container/');
});
@@ -196,6 +197,33 @@ it('shows the value label on drag, always, or never', function () {
expect((string) $this->blade('
'))->not->toContain('data-value-label');
});
+it('stands a slider up on request, turning the drawing a quarter', function () {
+ $html = (string) $this->blade('
');
+
+ expect($html)
+ ->toContain('data-orientation="vertical"')
+ ->toContain('aria-orientation="vertical"')
+ // The slider is as wide as a horizontal one is tall, and as long as its wrapper.
+ ->toContain('w-12')
+ ->toContain('touch-pan-x h-48 min-h-0 flex-auto [container-type:size]')
+ ->toContain('h-[100cqw] w-[calc(100cqh-0.25rem)] -translate-1/2 -rotate-90')
+ // The value label is turned back, so it lands beside the handle and reads across.
+ ->toMatch('/data-value-label[^>]*class="[^"]*rotate-90/')
+ ->not->toContain('origin-bottom')
+ ->not->toContain('rtl:-scale-x-100');
+
+ // The sizes are the horizontal ones, across instead of along.
+ expect((string) $this->blade('
'))->toContain('w-27')
+ ->and((string) $this->blade('
'))->toContain('ms-12');
+});
+
+it('keeps a range slider horizontal, as M3 asks', function () {
+ expect((string) $this->blade('
'))
+ ->not->toContain('data-orientation')
+ ->not->toContain('aria-orientation')
+ ->toContain('h-12');
+});
+
it('draws in the colour and its container, and greys out when disabled', function (string $color, string $active, string $inactive) {
expect((string) $this->blade('
', ['color' => $color]))
->toContain("absolute inset-y-0 {$active} group-has-disabled/slider:bg-on-surface/38")