Show no tooltip on the control a sheet or dialog focuses as it opens
A modal side sheet, bottom sheet and the modal rail move the focus to their first control as they open (x-trap), and `<x-modal>` does in `showModal()`. On a page loaded with the sheet already open (a `wire:model` set from `?workout=` in the URL) the browsers count that focus as `:focus-visible`, and so they do for a sheet or dialog opened from the keyboard, so tooltip.js showed the close button's "Close" tooltip the moment the layer appeared, over its title, with no keyboard use. Confirmed in Chrome, Firefox and Safari for the deep-linked sheet and a full-screen dialog opened with Enter; a deep-linked `<x-modal>`'s own focus was not counted as keyboard focus. M3 lets a tooltip appear on hover or focus and says nothing that asks for one on a focus the component moves itself. layers.js gains `openingFocus(event)`: a focus is a modal layer's own when the nearest open dialog, `aria-modal` panel or modal rail around its target is entered from outside it, or from nowhere, and no Tab is moving it (a Tab moves the focus while its keydown is handled, by the browser or by focus-trap wrapping round the panel). tooltip.js and rich-tooltip.js leave such a focus out; focus moved within the layer, a Tab onto the control and a focus returned to it from a menu inside still show the tooltip. The docblocks, the skill and UPGRADE.md say so. A browser test loads a page with a modal sheet open at 393px, opens a full-screen dialog and a dialog whose first control is a rich tooltip's trigger from the keyboard, and checks that no tooltip is up on the focused control, then that Tab wrapping round to the close button shows its tooltip; it fails without the change in Chrome, Firefox and Safari, each part on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
16c52d46e9
commit
b62c83f244
@@ -1592,3 +1592,102 @@ it('opens and closes a collapse at once under reduced motion, leaving nothing be
|
||||
|
||||
expect($result)->toBe(['opened' => ['open' => true, 'animations' => 0], 'open' => false, 'animations' => 0, 'closing' => false, 'style' => null]);
|
||||
});
|
||||
|
||||
class OpenedLayersProbe extends Component
|
||||
{
|
||||
public ?int $workout = null;
|
||||
|
||||
public bool $setback = false;
|
||||
|
||||
public bool $about = false;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->workout = request()->has('workout') ? (int) request('workout') : null;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return <<<'BLADE'
|
||||
<div>
|
||||
<p>The week's sessions</p>
|
||||
<button type="button" id="open-setback" wire:click="$set('setback', true)">Set back the plan</button>
|
||||
<button type="button" id="open-about" wire:click="$set('about', true)">About expiry</button>
|
||||
<x-drawer wire:model="workout" title="Long run">
|
||||
<input id="sheet-note" aria-label="Note" />
|
||||
</x-drawer>
|
||||
<x-modal wire:model="setback" title="Set back the plan" fullscreen>
|
||||
<input id="dialog-weeks" aria-label="Weeks" />
|
||||
</x-modal>
|
||||
<x-modal wire:model="about" title="Expiry">
|
||||
<x-rich-tooltip text="Recipients lose access after this time.">
|
||||
<x-button id="about-help" icon="help" aria-label="What expiry means" />
|
||||
</x-rich-tooltip>
|
||||
</x-modal>
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the plain tooltip of the ✕ in the sheet or the full-screen dialog's bar is up. */
|
||||
function closeTooltipOpen(string $layer): string
|
||||
{
|
||||
return "document.querySelector('{$layer} button[aria-label=\"Close\"] [data-md-tooltip]').matches(':popover-open')";
|
||||
}
|
||||
|
||||
it('shows no tooltip on the control a sheet or a dialog focuses as it opens, but does once Tab reaches it', function () {
|
||||
Livewire::component('opened-layers-probe', OpenedLayersProbe::class);
|
||||
|
||||
Route::middleware('web')->get('/opened-layers-probe', fn () => Blade::render(<<<'BLADE'
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<x-theme-script />
|
||||
@vite(config('livewire-material.showcase.vite'))
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body style="background-color: var(--md-sys-color-surface);">
|
||||
<livewire:opened-layers-probe />
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
BLADE));
|
||||
|
||||
$sheet = '[data-md-drawer-sheet]';
|
||||
$dialog = 'dialog[data-md-modal]';
|
||||
|
||||
// A phone, where the sheet is modal and traps the focus, which it moves to its ✕ on open: here
|
||||
// on a page loaded with the sheet open, as a link to a session opens it.
|
||||
$page = visit('/opened-layers-probe?workout=7', ['viewport' => ['width' => 393, 'height' => 800]])->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'")
|
||||
->assertScript("document.activeElement.matches('{$sheet} button[aria-label=\"Close\"]')")
|
||||
->wait(0.3)
|
||||
->assertScript('! '.closeTooltipOpen($sheet));
|
||||
|
||||
// Tab from the sheet's last field wraps round to its ✕, and a keyboard's focus shows its label.
|
||||
$page->keys('#sheet-note', 'Tab')
|
||||
->assertScript("document.activeElement.matches('{$sheet} button[aria-label=\"Close\"]')")
|
||||
->assertScript(closeTooltipOpen($sheet));
|
||||
|
||||
// A full-screen dialog opened from the keyboard moves the focus to its bar's ✕ in showModal().
|
||||
$page = visit('/opened-layers-probe', ['viewport' => ['width' => 393, 'height' => 800]])->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Livewire !== 'undefined'");
|
||||
|
||||
$page->keys('#open-setback', 'Enter')
|
||||
->assertScript("document.querySelector('{$dialog}').open && document.activeElement.matches('{$dialog} button[aria-label=\"Close\"]')")
|
||||
->wait(0.3)
|
||||
->assertScript('! '.closeTooltipOpen($dialog));
|
||||
|
||||
$page->keys('#dialog-weeks', 'Tab')
|
||||
->assertScript("document.activeElement.matches('{$dialog} button[aria-label=\"Close\"]')")
|
||||
->assertScript(closeTooltipOpen($dialog));
|
||||
|
||||
// A rich tooltip's trigger as the first control of a dialog opened from the keyboard.
|
||||
$page = visit('/opened-layers-probe', ['viewport' => ['width' => 393, 'height' => 800]])->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Livewire !== 'undefined'");
|
||||
|
||||
$page->keys('#open-about', 'Enter')
|
||||
->assertScript("document.activeElement.id === 'about-help'")
|
||||
->wait(0.3)
|
||||
->assertScript("! document.querySelector('[data-md-rich-tooltip-bubble]').matches(':popover-open')");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user