Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,12 @@ mosaic and tree list modes, global search, the tab menu and four dashboard block
out. Moved here from recomaty-panel#648/#649, which carried it in its layout override.
- The application's released version at the foot of the sidebar (2026-10-22): the new
`options.released_version` (default `null`) is shown as it comes, in one neutral `adm-badge`,
in the new additive `adminata_sidebar_release` block (`adm-sidebar-release`), after
`.adm-sidebar-nav` — at the bottom of the rail under a short menu, below the scrolling menu under
a long one, hidden on the collapsed rail. Empty or `null` renders nothing. adminata does not read
in the new additive `adminata_sidebar_release` block (`adm-sidebar-release`), the last thing in
the menu's scrolling box (`.adm-sidebar-nav`, which now takes the rest of the sidebar's height):
right after the last item under a menu taller than the box, scrolling with it, and at the bottom
of the sidebar under a shorter one; hidden on the collapsed rail. It never covers an entry: it
first sat after the box, pinned to the bottom over the end of a long menu, and moved into it the
same day (recomaty-panel#650). Empty or `null` renders nothing. adminata does not read
the environment: the application passes the value in, typically from its deploy, as
`released_version: '%env(default::RELEASED_VERSION)%'`. One key, `sidebar_release` (the badge's
screen-reader label), is translated in `en` and `pl` and ships as `needs-translation` in the
Expand Down
17 changes: 12 additions & 5 deletions assets/css/components/sidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -173,22 +173,29 @@
}
}

/*
* The menu's scrolling box takes the rest of the sidebar's height (its minimum is 0, so a long menu
* scrolls inside it instead of pushing it past the window), so that what ends it — the released
* version — can sit at the bottom of the sidebar under a short menu.
*/
@utility adm-sidebar-nav {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
overflow-y: auto;
padding-bottom: 1.5rem;
}

/*
* The application's released version (`adminata_sidebar_release`), the rail's last child. The rail
* is a flex column and the menu's box does not grow, so under a short menu `margin-top: auto` takes
* the space below it and the line sits at the foot; under a long one the menu's box (a scroll
* container, so its minimum height is 0) shrinks and scrolls, and the line stays in view below it.
* The application's released version (`adminata_sidebar_release`), the last thing in the menu's
* box. Under a menu taller than the box `margin-top: auto` is nothing and the line comes right
* after the last item, scrolling with the menu; under a shorter one it takes the box's free space,
* and the line sits at the bottom of the sidebar. Inside the box, so it never covers an entry.
*/
@utility adm-sidebar-release {
margin-top: auto;
padding-block: 1rem 1.5rem;
padding-top: 1rem;
text-align: center;

/* The rail shows the menu's icons only. */
Expand Down
2 changes: 1 addition & 1 deletion src/Resources/public/app.css

Large diffs are not rendered by default.

39 changes: 22 additions & 17 deletions src/Resources/views/standard_layout.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,28 @@ file that was distributed with this source code.
{% block side_bar_after_nav_content %}{% endblock %}
{% endblock adminata_side_nav %}

{#
The application's released version (`options.released_version`), at the
end of the sidebar: a deploy passes it in from its environment through the
application's configuration, and adminata shows it as it comes, in one quiet
badge. Nothing at all when it is empty. It is the last thing in the menu's
scrolling box, so it never covers an entry: under a menu taller than the box
it comes right after the last item, out of sight until the menu is scrolled
to its end; under a shorter one the box's free space goes above it and it
sits at the bottom of the sidebar. Before the place script below, so the box
already has its full height when the menu is put back in place. The
collapsed rail hides it with the rest of the text.
#}
{% block adminata_sidebar_release %}
{% set _released_version = adminata_config.getOption('released_version') %}
{% if _released_version is not empty %}
<p class="adm-sidebar-release">
<span class="sr-only">{{ 'sidebar_release'|trans({}, 'AdminataBundle') }}</span>
<span class="adm-badge">{{ _released_version }}</span>
</p>
{% endif %}
{% endblock adminata_sidebar_release %}

{#
The sidebar keeps its place across page loads. The menu scrolls inside
this box, and a page load used to start the box at the top, so the entry
Expand Down Expand Up @@ -362,23 +384,6 @@ file that was distributed with this source code.
{% endblock adminata_sidebar_place %}
</div>

{#
The application's released version (`options.released_version`), at the foot
of the rail: a deploy passes it in from its environment through the
application's configuration, and adminata shows it as it comes, in one quiet
badge. Nothing at all when it is empty. It sits after `.adm-sidebar-nav`, not
in it, so a short menu leaves it at the bottom and a long one scrolls above it.
The collapsed rail hides it with the rest of the text.
#}
{% block adminata_sidebar_release %}
{% set _released_version = adminata_config.getOption('released_version') %}
{% if _released_version is not empty %}
<p class="adm-sidebar-release">
<span class="sr-only">{{ 'sidebar_release'|trans({}, 'AdminataBundle') }}</span>
<span class="adm-badge">{{ _released_version }}</span>
</p>
{% endif %}
{% endblock adminata_sidebar_release %}
</aside>
{% endblock adminata_left_side %}

Expand Down
7 changes: 7 additions & 0 deletions tests-adminata/App/config/packages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ parameters:
env(ADMINATA_DEMO_SIDEBAR_LOGOUT): 'true'
env(ADMINATA_DEMO_RELEASED_VERSION): ''

# The browser suites (APP_ENV=browser, Adminata\Tests\Support\DemoServer) see a released version,
# so that DashboardPantherTest can check where the badge lands on screen; the demo, the screenshot
# baselines and the other suites show none.
when@browser:
parameters:
env(ADMINATA_DEMO_RELEASED_VERSION): 'v10.1.0+3·demo'

framework:
secret: adminata-demo
http_method_override: false
Expand Down
84 changes: 84 additions & 0 deletions tests-adminata/Functional/DashboardPantherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Adminata\Tests\App\EventListener\BrowserConsoleRecorderListener;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverDimension;
use Facebook\WebDriver\WebDriverElement;
use Facebook\WebDriver\WebDriverExpectedCondition;
use Facebook\WebDriver\WebDriverKeys;
Expand Down Expand Up @@ -180,6 +181,52 @@
$this->client->executeScript('window.localStorage.removeItem("adminata_sidebar_open");');
}

/**
* The released version ends the sidebar without ever covering the menu (recomaty-panel#650):
* it is the last thing in the menu's scrolling box. Under a menu shorter than the box it sits
* at the bottom of the sidebar; under one taller than the box it comes right after the last
* item, out of sight until the menu is scrolled to its end. The browser environment gives the
* demo a version (tests-adminata/App/config/packages.yaml).
*/
public function testTheReleasedVersionEndsTheSidebarWithoutCoveringTheMenu(): void
{
$window = $this->client->manage()->window();
$size = $window->getSize();

try {
$window->setSize(new WebDriverDimension(1280, 900));
$this->client->request('GET', $this->url('/admin/dashboard'));
if ('collapsed' === $this->sidebarState()) {
$this->client->executeScript('document.querySelector(\'[data-adminata-layout-target="collapseOnly"]\').click();');
}
$this->client->executeScript('document.querySelector(\'[data-adminata-sidebar-toolbar-target="collapse"]\').click();');

$short = $this->releaseGeometry();
static::assertFalse($short['overflows'], 'The demo menu, folded, should fit a 900 px window.');
static::assertLessThanOrEqual(32, $short['belowRelease'], 'Under a short menu the version is not at the bottom of the sidebar.');
static::assertGreaterThan(40, $short['gapAboveRelease'], 'Under a short menu the version did not move down to the bottom.');

$window->setSize(new WebDriverDimension(1280, 420));
$this->client->executeScript('document.querySelector(\'[data-adminata-sidebar-toolbar-target="expand"]\').click();');
$this->client->wait(5)->until(fn (): bool => true === $this->client->executeScript(
'return null === document.querySelector(".adm-sidebar-nav [data-sliding]");'
));

$long = $this->releaseGeometry();
static::assertTrue($long['overflows'], 'The demo menu, unfolded, should overflow a 420 px window.');
static::assertGreaterThanOrEqual(0, $long['gapAboveRelease'], 'The version covers the end of the menu.');
static::assertLessThanOrEqual(40, $long['gapAboveRelease'], 'Under a long menu the version is not right after the last item.');
static::assertGreaterThanOrEqual(0, $long['releaseBelowBox'], 'Under a long menu the version is in view before the menu is scrolled to its end.');

$this->client->executeScript('const box = document.querySelector(".adm-sidebar-nav"); box.scrollTop = box.scrollHeight;');
$end = $this->releaseGeometry();
static::assertLessThanOrEqual(0, $end['releaseBottomPastBox'], 'Scrolled to its end, the menu does not show the version.');
static::assertGreaterThanOrEqual(0, $end['gapAboveRelease'], 'Scrolled to its end, the version covers the last item.');
} finally {
$window->setSize($size);
}
}

/**
* The row above the menu: the dashboard and sign-out are plain links, and the two buttons that
* fold and unfold every group appear once `adminata-sidebar-toolbar` has found the menu. What
Expand All @@ -203,7 +250,7 @@
static::assertSame(array_fill(0, $groups, 'false'), $this->groupStates());

$this->client->reload();
static::assertSame(array_fill(0, $groups, 'false'), $this->groupStates(), 'Collapsing every group was forgotten.');

Check failure on line 253 in tests-adminata/Functional/DashboardPantherTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (Symfony highest)

Parameter #1 $expected of static method PHPUnit\Framework\Assert::assertSame() contains unresolvable type.

Check failure on line 253 in tests-adminata/Functional/DashboardPantherTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (Symfony 7.4.*)

Parameter #1 $expected of static method PHPUnit\Framework\Assert::assertSame() contains unresolvable type.

$this->client->executeScript('document.querySelector(\'[data-adminata-layout-target="collapseOnly"]\').click();');
static::assertSame('collapsed', $this->sidebarState());
Expand Down Expand Up @@ -1099,6 +1146,43 @@
return true === $this->client->executeScript('return document.documentElement.classList.contains("dark");');
}

/**
* Where the released version is, against the menu's box, its last visible entry and the
* sidebar, in CSS pixels.
*
* @return array{overflows: bool, belowRelease: int, gapAboveRelease: int, releaseBelowBox: int, releaseBottomPastBox: int}
*/
private function releaseGeometry(): array
{
$geometry = $this->client->executeScript(<<<'JS'
const box = document.querySelector('.adm-sidebar-nav');
const release = box.querySelector('.adm-sidebar-release');
const entries = [...box.querySelectorAll('nav a[href], nav [data-adminata-menu-target~="toggle"]')]
.filter((entry) => 0 < entry.getClientRects().length);
const last = entries[entries.length - 1].getBoundingClientRect();
const shown = release.getBoundingClientRect();
const frame = box.getBoundingClientRect();
const sidebar = document.querySelector('aside.adm-sidebar').getBoundingClientRect();

return {
overflows: box.scrollHeight > box.clientHeight + 1,
belowRelease: Math.round(sidebar.bottom - shown.bottom),
gapAboveRelease: Math.round(shown.top - last.bottom),
releaseBelowBox: Math.round(shown.top - frame.bottom),
releaseBottomPastBox: Math.round(shown.bottom - frame.bottom),
};
JS);
static::assertIsArray($geometry);

return [
'overflows' => true === $geometry['overflows'],
'belowRelease' => (int) $geometry['belowRelease'],
'gapAboveRelease' => (int) $geometry['gapAboveRelease'],
'releaseBelowBox' => (int) $geometry['releaseBelowBox'],
'releaseBottomPastBox' => (int) $geometry['releaseBottomPastBox'],
];
}

private function sidebarState(): string
{
return (string) $this->client->executeScript('return document.body.dataset.sidebar;');
Expand Down
15 changes: 10 additions & 5 deletions tests-adminata/Functional/DemoSmokeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,11 @@ public function testTheSidebarSignOutCanBeTurnedOff(): void

/**
* `options.released_version` is the application's released version, shown as it comes in one
* badge at the foot of the rail, after the menu's scrolling box, with a screen-reader label
* before it. The value is the application's text, so it is escaped like any other.
* badge at the end of the sidebar, with a screen-reader label before it: the last thing in the
* menu's scrolling box, after the menu, so it never covers an entry (recomaty-panel#650) — and
* before the place script, which needs the box at its full height. The value is the
* application's text, so it is escaped like any other. Where it lands on screen, under a short
* menu and a long one, is DashboardPantherTest's.
*/
public function testTheReleasedVersionIsShownAtTheFootOfTheSidebar(): void
{
Expand All @@ -154,9 +157,11 @@ public function testTheReleasedVersionIsShownAtTheFootOfTheSidebar(): void
$crawler = $client->request('GET', '/admin/dashboard');

static::assertResponseIsSuccessful();
$release = $crawler->filter('aside.adm-sidebar > p.adm-sidebar-release');
static::assertCount(1, $release, 'The released version is not at the foot of the sidebar.');
static::assertCount(1, $crawler->filter('aside.adm-sidebar > .adm-sidebar-nav + p.adm-sidebar-release'), 'The version is not right after the menu.');
$release = $crawler->filter('aside.adm-sidebar > .adm-sidebar-nav > p.adm-sidebar-release');
static::assertCount(1, $release, 'The released version is not in the menu\'s scrolling box.');
static::assertCount(1, $crawler->filter('.adm-sidebar-nav > nav[data-controller~="adminata-menu"] ~ p.adm-sidebar-release'), 'The version is not after the menu.');
static::assertCount(1, $crawler->filter('.adm-sidebar-nav > p.adm-sidebar-release ~ script[data-adminata-sidebar-place]'), 'The version is not before the place script.');
static::assertCount(0, $crawler->filter('aside.adm-sidebar > p.adm-sidebar-release'), 'The version is outside the box again, over the end of a long menu.');
static::assertSame('v10.1.0+3·ab12cd3<b>', $release->filter('.adm-badge')->text(), 'The version is not shown as it was configured.');
static::assertSame('Version', $release->filter('.sr-only')->text());
static::assertStringNotContainsString('ab12cd3<b>', (string) $client->getResponse()->getContent(), 'The version was not escaped.');
Expand Down
3 changes: 2 additions & 1 deletion tests-adminata/fixtures/js/category-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
</nav>



<script data-adminata-sidebar-place >
(() => {
const box = document.currentScript?.closest('.adm-sidebar-nav');
Expand Down Expand Up @@ -215,7 +216,7 @@
</script>
</div>

</aside>
</aside>

<div class="adm-overlay lg:hidden" hidden
data-adminata-layout-target="overlay"
Expand Down
3 changes: 2 additions & 1 deletion tests-adminata/fixtures/js/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
</nav>



<script data-adminata-sidebar-place >
(() => {
const box = document.currentScript?.closest('.adm-sidebar-nav');
Expand Down Expand Up @@ -215,7 +216,7 @@
</script>
</div>

</aside>
</aside>

<div class="adm-overlay lg:hidden" hidden
data-adminata-layout-target="overlay"
Expand Down
3 changes: 2 additions & 1 deletion tests-adminata/fixtures/js/dialog.html
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
</nav>



<script data-adminata-sidebar-place >
(() => {
const box = document.currentScript?.closest('.adm-sidebar-nav');
Expand Down Expand Up @@ -215,7 +216,7 @@
</script>
</div>

</aside>
</aside>

<div class="adm-overlay lg:hidden" hidden
data-adminata-layout-target="overlay"
Expand Down
3 changes: 2 additions & 1 deletion tests-adminata/fixtures/js/product-create.html
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
</nav>



<script data-adminata-sidebar-place >
(() => {
const box = document.currentScript?.closest('.adm-sidebar-nav');
Expand Down Expand Up @@ -215,7 +216,7 @@
</script>
</div>

</aside>
</aside>

<div class="adm-overlay lg:hidden" hidden
data-adminata-layout-target="overlay"
Expand Down
Loading
Loading