Skip to content

Commit

Permalink
Add support for active state on dropdowns
Browse files Browse the repository at this point in the history
  • Loading branch information
cameronmurphy committed Mar 30, 2024
1 parent 420da14 commit a6dfb5b
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 19 deletions.
2 changes: 2 additions & 0 deletions DependencyInjection/BootstrapMenuExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public function load(array $configs, ContainerBuilder $container): void
$config = $this->processConfiguration($configuration, $configs);

$container->setParameter('bootstrap_menu.version', $config['version']);
$container->setParameter('bootstrap_menu.dropdown_active_style', $config['dropdown_active_style']);
$container->setParameter('bootstrap_menu.dropdown_item_active_style', $config['dropdown_item_active_style']);
$container->setParameter('bootstrap_menu.menus', $config['menus']);
}
}
6 changes: 6 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ private function addMenusNode(ArrayNodeDefinition $rootNode): void
->min(4)
->defaultValue(5)
->end()
->booleanNode('dropdown_active_style')
->defaultValue(false)
->end()
->booleanNode('dropdown_item_active_style')
->defaultValue(false)
->end()
->arrayNode('menus')
->requiresAtLeastOneElement()
->disallowNewKeysInSubsequentConfigs()
Expand Down
32 changes: 25 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Bootstrap Menu Bundle
[![CircleCI](https://dl.circleci.com/status-badge/img/circleci/34vMYXApuzs6spAruM7wQy/JcJXpZqKaYA52SDiAunsPM/tree/main.svg?style=svg&circle-token=691995787d3ce0e1396b73f77a37252166a990c2)](https://dl.circleci.com/status-badge/redirect/circleci/34vMYXApuzs6spAruM7wQy/JcJXpZqKaYA52SDiAunsPM/tree/main)

A simple [Symfony](https://symfony.com/) bundle for defining your application's menus in configuration and rendering them to work with
[Bootstrap](https://getbootstrap.com/)'s [Navbar](https://getbootstrap.com/docs/5.0/components/navbar/) component. This bundle supports
[Bootstrap](https://getbootstrap.com/)'s [Navbar](https://getbootstrap.com/docs/5.2/components/navbar/) component. This bundle supports
Bootstrap versions 4 and 5.

Installation
Expand All @@ -20,6 +20,8 @@ Below is a very simple menu called `main` with only a single 'Logout' link.
```yaml
bootstrap_menu:
version: 5 # Optional, defaults to Bootstrap 5
dropdown_active_style: false # Optional, defaults to false
dropdown_item_active_style: false # Optional, defaults to false
menus:
main:
items:
Expand All @@ -29,7 +31,7 @@ bootstrap_menu:
```
Then within your template you can render your menu in a Navbar by passing the name of your menu to `render_bootstrap_menu`. This markup is
taken from the [Bootstrap Navbar Fixed example](https://getbootstrap.com/docs/5.0/examples/navbar-fixed/). The Bootstrap 4 version is
taken from the [Bootstrap Navbar Fixed example](https://getbootstrap.com/docs/5.2/examples/navbar-fixed/). The Bootstrap 4 version is
[here](https://getbootstrap.com/docs/4.6/examples/navbar-fixed)
```twig
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
Expand All @@ -50,7 +52,7 @@ taken from the [Bootstrap Navbar Fixed example](https://getbootstrap.com/docs/5.
```
Result:

![Example 1](https://user-images.githubusercontent.com/1300032/54358791-4f00fb00-46b5-11e9-817c-4b8101305a2b.png)
![Example 1](https://github.com/cameronmurphy/bootstrap-menu-bundle/assets/1300032/128ec4b0-2f26-40d9-af14-c7ef5efc0f9d)

### Route parameters
Perhaps your route requires parameters. You can also specify these.
Expand Down Expand Up @@ -98,7 +100,7 @@ bootstrap_menu:
```
Result:

![Example 2](https://user-images.githubusercontent.com/1300032/54359374-9fc52380-46b6-11e9-9c0c-bea934d9f0a2.png)
![Example 2](https://github.com/cameronmurphy/bootstrap-menu-bundle/assets/1300032/88b57cd9-2d1d-4389-870c-c3fbce0613d7)

#### Dividers
Dropdowns can also contain [Dividers](https://getbootstrap.com/docs/4.3/components/dropdowns/#dividers) to separate groups of related menu
Expand All @@ -122,7 +124,7 @@ bootstrap_menu:
```
Result:

![Example 3](https://user-images.githubusercontent.com/1300032/54359921-bf108080-46b7-11e9-8101-faf2526697ef.png)
![Example 3](https://github.com/cameronmurphy/bootstrap-menu-bundle/assets/1300032/06ffb618-32d6-48ad-9641-38a59ccade8c)

#### Headers
Dividers that also contain a `label` become [Headers](https://getbootstrap.com/docs/4.3/components/dropdowns/#headers).
Expand Down Expand Up @@ -150,7 +152,7 @@ bootstrap_menu:
```
Result:

![Example 4](https://user-images.githubusercontent.com/1300032/54360188-73120b80-46b8-11e9-9af7-6150182b8243.png)
![Example 4](https://github.com/cameronmurphy/bootstrap-menu-bundle/assets/1300032/6363a9b3-a5c4-4d39-9f04-a569dc286517)

#### Security
Certain parts of the menu may be locked down by role. This following example only allows administrators to change their password.
Expand Down Expand Up @@ -179,7 +181,7 @@ bootstrap_menu:
```
For a user without `ROLE_ADMINISTRATOR` they would see:

![Example 5](https://user-images.githubusercontent.com/1300032/54361573-60e59c80-46bb-11e9-89db-669a02f4b82b.png)
![Example 5](https://github.com/cameronmurphy/bootstrap-menu-bundle/assets/1300032/aa90ef88-72de-4b27-b474-096579f539b1)

The reason for this is Bootstrap Menu Bundle intelligently prunes Dropdowns to remove unnecessary Dividers. Because the user is not
permitted to see any items between 'Password Stuff' and 'Other Stuff', the 'Password Stuff' Divider is also pruned.
Expand Down Expand Up @@ -218,3 +220,19 @@ bootstrap_menu:
label: 'Users'
route: 'app_user_list'
```

#### Active styles
Out of the box, top level menu items are given the [`active` class](https://getbootstrap.com/docs/5.2/components/navbar/#nav) when the
current route matches the menu item's route. In this example we're on the 'Reports' page.

![Example 6](https://github.com/cameronmurphy/bootstrap-menu-bundle/assets/1300032/8322b338-3f0d-4940-ab61-900e59886714)

##### Dropdown active styles
It's not documented, but the `active` class also works on dropdowns and dropdown items. To enable for dropdowns, set `dropdown_active_style`
to `true` in the config.

![Example 7](https://github.com/cameronmurphy/bootstrap-menu-bundle/assets/1300032/fb9c2645-9826-48c3-9ac4-2c6406e09bab)

To enable for dropdown items, set `dropdown_item_active_style` to true.

![Example 8](https://github.com/cameronmurphy/bootstrap-menu-bundle/assets/1300032/92e12bd9-ab28-44eb-ad2a-91fd3fab2a57)
2 changes: 2 additions & 0 deletions Resources/config/bootstrap_menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
<service id="camurphy.bootstrap_menu.twig.menu_extension" class="Camurphy\BootstrapMenuBundle\Twig\Extension\MenuExtension" public="false">
<argument>%bootstrap_menu.menus%</argument>
<argument>%bootstrap_menu.version%</argument>
<argument>%bootstrap_menu.dropdown_active_style%</argument>
<argument>%bootstrap_menu.dropdown_item_active_style%</argument>
<tag name="twig.extension" />
</service>
</services>
Expand Down
6 changes: 4 additions & 2 deletions Resources/views/dropdown.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
{% set data_attribute_name = 'data-toggle' %}
{% endif %}

{% set is_active = dropdown_active_style and app.request.attributes.get('_route') in menu_item.items|map(item => item.route is defined ? item.route : null) %}

<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" {{ data_attribute_name }}="dropdown" aria-haspopup="true" aria-expanded="false">
<a class="nav-link dropdown-toggle{% if is_active %} active{% endif %}"{% if is_active %} aria-current="page"{% endif %} href="#" role="button" {{ data_attribute_name }}="dropdown" aria-haspopup="true" aria-expanded="false">
{{- menu_item.label -}}
</a>
<div class="dropdown-menu">
{% for menu_item in menu_item.items %}
{% set menu_item = menu_item|merge({ 'index': loop.index0 }) %}
{% include '@BootstrapMenu/dropdown_item.html.twig' with { menu_item } %}
{% include '@BootstrapMenu/dropdown_item.html.twig' with { menu_item, dropdown_item_active_style } %}
{% endfor %}
</div>
</li>
Expand Down
3 changes: 2 additions & 1 deletion Resources/views/dropdown_item.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
<h6 class="dropdown-header">{{ menu_item.label }}</h6>
{% endif %}
{% else %}
<a class="dropdown-item" {% include '@BootstrapMenu/href.html.twig' with { menu_item } %}>
{% set is_active = dropdown_item_active_style and menu_item.route is defined and app.request.attributes.get('_route') == menu_item.route %}
<a class="dropdown-item{% if is_active %} active{% endif %}"{% if is_active %} aria-current="page"{% endif %} {% include '@BootstrapMenu/href.html.twig' with { menu_item } %}>
{{- menu_item.label -}}
</a>
{% endif %}
Expand Down
36 changes: 30 additions & 6 deletions Tests/Twig/Extension/MenuExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,27 @@ public function testRenderMenuBootstrap5(): void
}

/**
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public function testRenderMenuBootstrap5WithActiveDropdownStyles(): void
{
/** @var TwigEnvironment $mockTwigEnvironment */
$mockTwigEnvironment = $this->mockTwigEnvironment('app_dropdown_item_8_route');

$menuExtension = new MenuExtension(self::$mockMenus, 5, true, true);
$menu = $menuExtension->renderMenu($mockTwigEnvironment, 'main');

$this->assertMatchesSnapshot($menu);
}

/**
* @param mixed $activeRoute
*
* @throws LoaderError
*/
private function mockTwigEnvironment(): MockObject
private function mockTwigEnvironment(string $activeRoute = 'app_link_1_route'): MockObject
{
$loader = new FilesystemLoader([], $this->rootPath);
$loader->addPath('Resources/views', 'BootstrapMenu');
Expand All @@ -261,12 +279,20 @@ private function mockTwigEnvironment(): MockObject

$twigMock->addGlobal('app', [
'request' => [
'attributes' => new class() {
'attributes' => new class($activeRoute) {
private $activeRoute;

public function __construct(string $activeRoute)
{
$this->activeRoute = $activeRoute;
}

public function get(string $name): ?string
{
switch ($name) {
case '_route':
return 'app_link_1_route';
return $this->activeRoute;

default:
return null;
}
Expand All @@ -283,9 +309,7 @@ public function get(string $name): ?string

$securityExtensionMock
->method('isGranted')
->willReturnCallback(function ($expression) {
return \in_array($expression, ['ROLE_USER', 'ROLE_SUPPORT'], true);
})
->willReturnCallback(static fn ($expression) => \in_array($expression, ['ROLE_USER', 'ROLE_SUPPORT'], true))
;

$twigMock
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<li class="nav-item dropdown"><a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown Menu 1</a><div class="dropdown-menu"><a class="dropdown-item" href="/app-dropdown-2-route?test=123">Dropdown Item 2 (should render a dropdown-item)</a></div></li><li class="nav-item dropdown"><a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown Menu 2</a><div class="dropdown-menu"><a class="dropdown-item" href="/app-dropdown-item-3-route">Dropdown Item 3 (should render a dropdown-item because user has permission)</a><a class="dropdown-item" href="https://disney.com">Dropdown Item 4 (should render a dropdown-item linking to Disney)</a></div></li><li class="nav-item"><a class="nav-link" href="/app-link-1-route">Link 1 (this should render an active nav-link)</a></li><li class="nav-item dropdown"><a class="nav-link dropdown-toggle active" aria-current="page" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown Menu 4</a><div class="dropdown-menu"><a class="dropdown-item active" aria-current="page" href="/app-dropdown-item-8-route">Dropdown Item 8</a><div class="dropdown-divider"></div><h6 class="dropdown-header">Divider 2 (should render a dropdown-header)</h6><a class="dropdown-item" href="/app-dropdown-item-9-route">Dropdown Item 9</a><div class="dropdown-divider"></div><a class="dropdown-item" href="/app-dropdown-item-10-route">Dropdown Item 10</a><a class="dropdown-item" href="/app-dropdown-item-11-route">Dropdown Item 11</a><div class="dropdown-divider"></div><h6 class="dropdown-header">Divider 5</h6><a class="dropdown-item" href="/app-dropdown-item-13-route">Dropdown Item 13</a></div></li>
16 changes: 13 additions & 3 deletions Twig/Extension/MenuExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,19 @@ class MenuExtension extends AbstractExtension
*/
private $menus;
private $bootstrapVersion;

public function __construct(array $menus, int $bootstrapVersion)
{
private $dropdownActiveStyle;
private $dropdownItemActiveStyle;

public function __construct(
array $menus,
int $bootstrapVersion,
bool $dropdownActiveStyle = false,
bool $dropdownItemActiveStyle = false
) {
$this->menus = $menus;
$this->bootstrapVersion = $bootstrapVersion;
$this->dropdownActiveStyle = $dropdownActiveStyle;
$this->dropdownItemActiveStyle = $dropdownItemActiveStyle;
}

public function getFunctions(): array
Expand Down Expand Up @@ -63,6 +71,8 @@ public function renderMenu(TwigEnvironment $environment, string $menuName): stri
foreach ($menuDefinition['items'] as $menuItem) {
$variables = [
'bootstrap_version' => $this->bootstrapVersion,
'dropdown_active_style' => $this->dropdownActiveStyle,
'dropdown_item_active_style' => $this->dropdownItemActiveStyle,
'menu_item' => $menuItem,
];

Expand Down

0 comments on commit a6dfb5b

Please sign in to comment.