$fullscreen,
+ 'max-sm:h-full max-sm:rounded-none max-sm:p-0 max-sm:pt-[var(--material-safe-top,env(safe-area-inset-top))]' => $fullscreen,
$boxClass,
])>
@if ($fullscreen)
diff --git a/resources/views/showcase/shell.blade.php b/resources/views/showcase/shell.blade.php
index fba59b7c..8bfe722b 100644
--- a/resources/views/showcase/shell.blade.php
+++ b/resources/views/showcase/shell.blade.php
@@ -59,7 +59,7 @@
-
+
diff --git a/tests/Browser/NavigationTest.php b/tests/Browser/NavigationTest.php
index 8c313179..fdbfd3e8 100644
--- a/tests/Browser/NavigationTest.php
+++ b/tests/Browser/NavigationTest.php
@@ -176,3 +176,31 @@ it('skips to the content', function () {
->assertScript("location.hash === '#content'")
->assertScript("document.activeElement === document.getElementById('content')");
});
+
+it('moves an app bar\'s content under a safe area an application sets', function () {
+ $row = "Math.round(document.querySelector('[data-app-bar] [data-app-bar-row]').getBoundingClientRect().top)";
+
+ $page = navigationReady(visit('/material'));
+ $top = (int) $page->script($row);
+
+ $page->script("document.documentElement.style.setProperty('--material-safe-top', '47px')");
+
+ $page->assertScript("{$row} === ".($top + 47))
+ ->assertNoJavaScriptErrors();
+});
+
+it('lifts the snackbar and the page above something docked on the phone\'s bar', function () {
+ $snackbarBottom = "Math.round(parseFloat(getComputedStyle(document.querySelector('[x-data=\"materialSnackbar\"]')).bottom))";
+ $contentPadding = "Math.round(parseFloat(getComputedStyle(document.getElementById('content')).paddingBottom))";
+
+ $page = shellPage(400, 860);
+ $snackbar = (int) $page->script($snackbarBottom);
+ $content = (int) $page->script($contentPadding);
+
+ expect($content)->toBe(64)->and($snackbar)->toBe(80);
+
+ $page->script("document.documentElement.style.setProperty('--material-bottom-extra', '40px')");
+
+ $page->assertScript("{$snackbarBottom} === 120")
+ ->assertScript("{$contentPadding} === 104");
+});
diff --git a/tests/Feature/Components/AppShellTest.php b/tests/Feature/Components/AppShellTest.php
index 5c6f9742..0d3630e8 100644
--- a/tests/Feature/Components/AppShellTest.php
+++ b/tests/Feature/Components/AppShellTest.php
@@ -56,12 +56,21 @@ it('marks the destination at the current URL when none says it is active', funct
it('lifts the snackbar above the bar only when there is a bar', function () {
expect((string) $this->blade(' ', ['destinations' => shellDestinations()]))
- ->toContain('max-sm:[--material-bottom-bar:calc(4rem+env(safe-area-inset-bottom))]')
+ ->toContain('max-sm:[--material-bottom-bar:calc(4rem+var(--material-safe-bottom,env(safe-area-inset-bottom))+var(--material-bottom-extra,0px))]')
->and((string) $this->blade(' ', ['destinations' => [['title' => 'Inbox', 'icon' => 'inbox', 'url' => '/inbox', 'bar' => false]]]))
->not->toContain('data-app-shell-bar')
->not->toContain('--material-bottom-bar:');
});
+it('reads the safe area and anything docked on the bar through variables an application can set', function () {
+ $html = (string) $this->blade(' ', ['destinations' => shellDestinations()]);
+
+ expect($html)
+ ->toContain('+var(--material-bottom-extra,0px))]')
+ ->toContain('focus:top-[calc(var(--material-safe-top,env(safe-area-inset-top))+1rem)]')
+ ->not->toMatch('/(?blade(<<<'BLADE'
diff --git a/tests/Feature/TokensTest.php b/tests/Feature/TokensTest.php
index 8543acfb..4e0c2ade 100644
--- a/tests/Feature/TokensTest.php
+++ b/tests/Feature/TokensTest.php
@@ -64,6 +64,27 @@ it('leaves the choice of theme to the head script, never to a media query', func
}
});
+it('reads every safe-area inset through a variable that can replace it', function () {
+ $files = collect([packageCss(), __DIR__.'/../../resources/views', __DIR__.'/../../resources/js'])
+ ->flatMap(fn (string $path): array => File::allFiles($path));
+
+ $insets = 0;
+
+ foreach ($files as $file) {
+ preg_match_all('/env\(safe-area-inset-(top|bottom|left|right)\)/', $file->getContents(), $matches, PREG_OFFSET_CAPTURE);
+
+ foreach ($matches[1] as [$side, $offset]) {
+ $insets++;
+
+ expect(substr($file->getContents(), 0, $offset - strlen('env(safe-area-inset-')))
+ ->toMatch("/var\\(--material-safe-{$side},\\s?$/", "{$file->getRelativePathname()} reads safe-area-inset-{$side} directly");
+ }
+ }
+
+ expect($insets)->toBeGreaterThan(10)
+ ->and(File::get(packageCss('components/app-bar.css')))->toContain('padding-top: var(--material-safe-top, env(safe-area-inset-top));');
+});
+
it('makes every motion token instant under reduced motion', function () {
$motion = File::get(packageCss('tokens/motion.css'));
$reduced = Str::of($motion)->after('prefers-reduced-motion: reduce')->toString();