diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d398694..c25721e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Enh #298: Update Bootstrap from 5.0.0-beta to 5.3.8 (@rossaddison) - Bug #299: Link buttons get `btn-secondary` class instead of `btn-link` class (@jdanhutch) - Enh #290: Add `visible` to `Dropdown` and `DropdownItem` (@samdark) +- Enh #286: Add `Nav::activateParents()` that makes parent dropdown active when one of its child items is active (@samdark) ## 1.0.0 April 13, 2025 diff --git a/src/Nav.php b/src/Nav.php index 4a38f7a9..bae6cf6f 100644 --- a/src/Nav.php +++ b/src/Nav.php @@ -62,6 +62,8 @@ final class Nav extends Widget private bool $activateItems = true; + private bool $activateParents = false; + private array $attributes = []; private array $cssClasses = []; @@ -103,6 +105,29 @@ public function activateItems(bool $enabled): self return $new; } + /** + * Whether to activate the parent dropdown item when one of its child items is active. + * + * When set to `true`, if a dropdown item's URL matches {@see currentPath}, the dropdown toggle (parent) will also + * receive the `active` CSS class. + * + * @param bool $enabled Whether to activate parent items. Defaults to `false`. + * + * @return self A new instance with the specified activate parents value. + * + * Example usage: + * ```php + * $nav->activateParents(true); + * ``` + */ + public function activateParents(bool $enabled): self + { + $new = clone $this; + $new->activateParents = $enabled; + + return $new; + } + /** * Adds a set of attributes. * @@ -591,6 +616,24 @@ private function isTabsOrPills(): bool || in_array(NavStyle::PILLS, $this->styleClasses, true); } + /** + * Checks whether any item in the dropdown is active. + * + * @param Dropdown $dropdown The dropdown to check. + * + * @return bool Whether any item in the dropdown is active. + */ + private function hasActiveDropdownItem(Dropdown $dropdown): bool + { + foreach ($dropdown->getItems() as $item) { + if ($item->isActive()) { + return true; + } + } + + return false; + } + /** * Renders the items for the nav component. * @@ -623,6 +666,11 @@ private function renderItems(): array private function renderItemsDropdown(Dropdown $items): Li { $dropDownItems = $this->isDropdownActive($items); + $togglerClasses = ['nav-link', 'dropdown-toggle']; + + if ($this->activateParents && $this->hasActiveDropdownItem($dropDownItems)) { + $togglerClasses[] = self::NAV_LINK_ACTIVE_CLASS; + } return Li::tag() ->addClass(...$this->dropdownCssClasses, ...$dropDownItems->getCssClasses()) @@ -631,7 +679,7 @@ private function renderItemsDropdown(Dropdown $items): Li $dropDownItems ->container(false) ->togglerAsLink() - ->togglerClass('nav-link', 'dropdown-toggle') + ->togglerClass(...$togglerClasses) ->render(), "\n", ) diff --git a/tests/NavTest.php b/tests/NavTest.php index 6a6c3c10..719dc936 100644 --- a/tests/NavTest.php +++ b/tests/NavTest.php @@ -22,6 +22,120 @@ #[Group('nav')] final class NavTest extends TestCase { + public function testActivateParents(): void + { + Assert::equalsWithoutLE( + << + + + + + + HTML, + Nav::widget() + ->activateParents(true) + ->currentPath('/test/link/another-action') + ->items( + NavLink::to('Active', '/test'), + Dropdown::widget() + ->items( + DropdownItem::link('Action', '/test/link/action'), + DropdownItem::link('Another action', '/test/link/another-action'), + DropdownItem::link('Something else here', '/test/link/something-else'), + DropdownItem::divider(), + DropdownItem::link('Separated link', '/test/link/separated-link'), + ) + ->togglerContent('Dropdown'), + NavLink::to('Link', '/test/link'), + NavLink::to('Disabled', '/test/disabled', disabled: true), + ) + ->render(), + ); + } + + public function testActivateParentsWithFalseValue(): void + { + Assert::equalsWithoutLE( + << + + + + + + HTML, + Nav::widget() + ->activateParents(false) + ->currentPath('/test/link/another-action') + ->items( + NavLink::to('Active', '/test'), + Dropdown::widget() + ->items( + DropdownItem::link('Action', '/test/link/action'), + DropdownItem::link('Another action', '/test/link/another-action'), + DropdownItem::link('Something else here', '/test/link/something-else'), + DropdownItem::divider(), + DropdownItem::link('Separated link', '/test/link/separated-link'), + ) + ->togglerContent('Dropdown'), + NavLink::to('Link', '/test/link'), + NavLink::to('Disabled', '/test/disabled', disabled: true), + ) + ->render(), + ); + } + public function testAddAttributes(): void { Assert::equalsWithoutLE( @@ -841,6 +955,7 @@ public function testImmutability(): void $navWidget = Nav::widget(); $this->assertNotSame($navWidget, $navWidget->activateItems(false)); + $this->assertNotSame($navWidget, $navWidget->activateParents(true)); $this->assertNotSame($navWidget, $navWidget->addAttributes([])); $this->assertNotSame($navWidget, $navWidget->addClass('')); $this->assertNotSame($navWidget, $navWidget->addCssStyle(''));