Merge branch 'worktree-agent-a4fc1072037346c95'
This commit is contained in:
@@ -234,12 +234,35 @@ it('scrolls a chip set on one line with fading edges', function () {
|
||||
expect((string) $this->blade('<x-chip-set aria-label="Sort" scroll><x-chip label="A" /></x-chip-set>'))
|
||||
->toContain('aria-label="Sort"')
|
||||
->toContain('x-data="materialChipSet"')
|
||||
->toContain('x-ref="row"')
|
||||
->toContain('wire:ignore.self')
|
||||
->toContain('overflow-x-auto')
|
||||
->toContain('data-scroll-start:[--chip-fade-start:1.5rem]')
|
||||
->not->toContain('flex-wrap');
|
||||
});
|
||||
|
||||
it('puts a scroll button over each fading edge, for a pointer that cannot swipe', function () {
|
||||
$html = (string) $this->blade('<x-chip-set aria-label="Sort" scroll><x-chip label="A" /></x-chip-set>');
|
||||
|
||||
expect($html)
|
||||
->toContain('data-chip-scroll="start"')
|
||||
->toContain('data-chip-scroll="end"')
|
||||
->toContain('x-on:click="nudge(\'start\')"')
|
||||
->toContain('x-on:click="nudge(\'end\')"')
|
||||
// Only while the row can still scroll that way, and only where the pointer is fine.
|
||||
->toContain('peer-data-scroll-start:pointer-fine:grid')
|
||||
->toContain('peer-data-scroll-end:pointer-fine:grid')
|
||||
// A pointer affordance, not a tab stop: the arrows already walk every chip.
|
||||
->toContain('tabindex="-1"')
|
||||
->toContain('aria-hidden="true"')
|
||||
// A chip the keyboard reaches clears the buttons as well as the fade.
|
||||
->toContain('pointer-fine:scroll-px-10');
|
||||
|
||||
// A wrapping set has no edge to scroll towards.
|
||||
expect((string) $this->blade('<x-chip-set aria-label="Sort"><x-chip label="A" /></x-chip-set>'))
|
||||
->not->toContain('data-chip-scroll');
|
||||
});
|
||||
|
||||
it('binds filter chips to a Livewire array and shows the set\'s validation message in place of its hint', function () {
|
||||
$component = new class extends Component
|
||||
{
|
||||
|
||||
@@ -54,6 +54,41 @@ it('makes the field a read-only trigger for the modal and modal input pickers',
|
||||
->and(datepickerConfig($html)['mode'])->toBe($mode);
|
||||
})->with(['modal', 'input']);
|
||||
|
||||
it('gives a range picker M3\'s full-screen presentation, with an app bar and Save', function () {
|
||||
$html = (string) $this->blade('<x-datepicker id="trip" label="Trip" range mode="modal" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('data-datepicker-full')
|
||||
->toContain('x-show="presentation === \'full\'"')
|
||||
->toContain('data-datepicker-app-bar')
|
||||
->toContain('data-datepicker-close')
|
||||
->toContain('data-datepicker-save')
|
||||
->toContain('Save')
|
||||
->toContain('aria-label="Select dates"')
|
||||
// The months come from one list of rows, each headed by its label.
|
||||
->toContain('x-for="row in rows"')
|
||||
->toContain('data-datepicker-month-label')
|
||||
// The action row is the modal's; the full-screen picker confirms from its app bar.
|
||||
->toContain('x-show="presentation !== \'full\' && (view === \'days\' || typing || presentation === \'modal\')"')
|
||||
->toContain('x-on:scroll="extendMonths($event)"');
|
||||
|
||||
expect(datepickerConfig($html))->toMatchArray(['range' => true, 'mode' => 'modal']);
|
||||
});
|
||||
|
||||
it('leaves the docked and single-date pickers as they were', function () {
|
||||
// Only the presentation is new: a single date never reaches it, whatever the window.
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/js/datepicker.js'))
|
||||
->toContain("this.presentation = config.range && this.compact ? 'full' : config.mode === 'docked' && !this.compact ? 'docked' : 'modal'");
|
||||
|
||||
$html = (string) $this->blade('<x-datepicker id="expires" label="Expires on" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('data-datepicker-nav data-docked')
|
||||
->toContain('x-show="presentation === \'docked\'"')
|
||||
->toContain('data-datepicker-menu-button="months"')
|
||||
->toContain('x-show="presentation === \'modal\'"');
|
||||
});
|
||||
|
||||
it('names each dropdown so Shift+M and Shift+Y can reach it', function () {
|
||||
$html = (string) $this->blade('<x-datepicker label="Birthday" />');
|
||||
|
||||
|
||||
@@ -147,6 +147,39 @@ it('collects a file field\'s own errors and each file\'s', function () {
|
||||
->toContain('photo.jpg is larger than 2 GB.');
|
||||
});
|
||||
|
||||
it('counts the characters of a field that has a maximum', function () {
|
||||
$html = (string) $this->blade('<x-input id="bio" label="Bio" hint="Shown on your profile" maxlength="60" counter />');
|
||||
|
||||
expect($html)
|
||||
->toContain('class="field-support-row"')
|
||||
->toContain('class="field-counter"')
|
||||
->toContain('maxlength="60"')
|
||||
->toContain('max: 60,')
|
||||
->toContain('>0/60</span>')
|
||||
->toContain('aria-live="polite"')
|
||||
->toContain('Shown on your profile');
|
||||
|
||||
expect((string) $this->blade('<x-textarea id="note" label="Note" maxlength="140" counter />'))
|
||||
->toContain('class="field-counter"')
|
||||
->toContain('>0/140</span>');
|
||||
});
|
||||
|
||||
it('shows no counter without `counter` or without a maximum to count against', function () {
|
||||
expect((string) $this->blade('<x-input label="Bio" maxlength="60" />'))->not->toContain('field-counter')
|
||||
->and((string) $this->blade('<x-input label="Bio" counter />'))->not->toContain('field-counter')
|
||||
->and((string) $this->blade('<x-textarea label="Note" counter />'))->not->toContain('field-counter');
|
||||
});
|
||||
|
||||
it('bounds a field\'s width from medium unless it is told to fill its pane', function () {
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/field.css'))
|
||||
->toContain('@media (width >= 37.5rem)')
|
||||
->toMatch('/\.field:not\(\[data-full\]\)\s*\{\s*max-width: 40rem;/');
|
||||
|
||||
expect((string) $this->blade('<x-input label="Share name" full />'))->toContain('data-full')
|
||||
->and((string) $this->blade('<x-textarea label="Message" full />'))->toContain('data-full')
|
||||
->and((string) $this->blade('<x-input label="Share name" />'))->not->toContain('data-full');
|
||||
});
|
||||
|
||||
it('lays out a form with its actions under an optional divider', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-form wire:submit="save" separator>
|
||||
|
||||
@@ -47,6 +47,50 @@ it('shows the results as a list, or what to say when there are none', function (
|
||||
->not->toContain('data-search-results role="list"');
|
||||
});
|
||||
|
||||
it('offers M3\'s search-icon entry point', function () {
|
||||
$html = (string) $this->blade('<x-search id="find" trigger="icon" label="Search shares" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('data-trigger="icon"')
|
||||
->toContain('data-search-trigger')
|
||||
->toContain('x-ref="trigger"')
|
||||
->toContain('x-on:click="expand()"')
|
||||
->toContain('aria-label="Search shares"')
|
||||
->toContain('aria-haspopup="dialog"')
|
||||
->toContain('aria-controls="find-view"')
|
||||
->toContain('materialSearch(false,')
|
||||
->toContain("'icon'")
|
||||
// The bar is still there: it becomes the expanded view's header.
|
||||
->toContain('data-search-bar');
|
||||
|
||||
expect((string) $this->blade('<x-search />'))
|
||||
->toContain('data-trigger="bar"')
|
||||
->not->toContain('data-search-trigger');
|
||||
});
|
||||
|
||||
it('shows suggestions until the first keystroke, then the results', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-search>
|
||||
<a href="/shares/1">holiday.zip</a>
|
||||
<x-slot:suggestions><a href="/recent">Recent shares</a></x-slot:suggestions>
|
||||
</x-search>
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
->toContain('data-search-suggestions role="list"')
|
||||
->toContain('x-show="query === \'\'"')
|
||||
->toContain('Recent shares')
|
||||
->toContain('x-show="query !== \'\'"')
|
||||
->toContain('holiday.zip')
|
||||
->toContain('1 suggestion')
|
||||
->toContain(':count suggestions');
|
||||
|
||||
// Without the slot the results stand alone and are never hidden.
|
||||
expect((string) $this->blade('<x-search><a href="/shares/1">holiday.zip</a></x-search>'))
|
||||
->not->toContain('data-search-suggestions')
|
||||
->not->toContain('x-show="query !== \'\'"');
|
||||
});
|
||||
|
||||
it('names the input, trails the bar, and stays docked on request', function () {
|
||||
$html = (string) $this->blade('<x-search label="Search your shares" icon="travel_explore" docked><x-slot:trailing><button>AR</button></x-slot:trailing></x-search>');
|
||||
|
||||
|
||||
@@ -37,6 +37,29 @@ it('gives a selection control without a label a 48px target', function () {
|
||||
->and((string) $this->blade('<x-radio :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))->not->toContain('touch-target');
|
||||
});
|
||||
|
||||
it('leaves a checkbox group to the caller and says how M3 wants it laid out', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<fieldset>
|
||||
<legend>Tell me about</legend>
|
||||
<div class="grid gap-4 expanded:grid-cols-2">
|
||||
<x-checkbox label="Downloads" name="notify[]" value="downloads" />
|
||||
<x-checkbox label="Comments" name="notify[]" value="comments" />
|
||||
</div>
|
||||
</fieldset>
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
->toContain('class="grid gap-4 expanded:grid-cols-2"')
|
||||
->toContain('Downloads')
|
||||
->toContain('Comments')
|
||||
->and(substr_count($html, 'data-checkbox'))->toBe(2);
|
||||
|
||||
// The rule is guidance, not markup, so the component's header has to carry it.
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/views/components/checkbox.blade.php'))
|
||||
->toContain('expanded')
|
||||
->toContain('contained region');
|
||||
});
|
||||
|
||||
it('puts a checkbox at the end of its row on request', function () {
|
||||
expect((string) $this->blade('<x-checkbox label="Show" right />'))->toContain('flex-row-reverse justify-between');
|
||||
});
|
||||
|
||||
@@ -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('<x-slider value="40" />'))
|
||||
->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('<x-slider value-label="never" />'))->not->toContain('data-value-label');
|
||||
});
|
||||
|
||||
it('stands a slider up on request, turning the drawing a quarter', function () {
|
||||
$html = (string) $this->blade('<x-slider label="Volume" orientation="vertical" value="40" class="h-64" />');
|
||||
|
||||
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('<x-slider orientation="vertical" size="xl" />'))->toContain('w-27')
|
||||
->and((string) $this->blade('<x-slider orientation="vertical" value-label="always" />'))->toContain('ms-12');
|
||||
});
|
||||
|
||||
it('keeps a range slider horizontal, as M3 asks', function () {
|
||||
expect((string) $this->blade('<x-slider label="Price" range orientation="vertical" />'))
|
||||
->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('<x-slider :color="$color" value="50" disabled />', ['color' => $color]))
|
||||
->toContain("absolute inset-y-0 {$active} group-has-disabled/slider:bg-on-surface/38")
|
||||
|
||||
Reference in New Issue
Block a user