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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
50 changes: 49 additions & 1 deletion src/Nav.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ final class Nav extends Widget

private bool $activateItems = true;

private bool $activateParents = false;

private array $attributes = [];

private array $cssClasses = [];
Expand Down Expand Up @@ -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
Comment on lines +108 to +119
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PHPDoc states “Defaults to false” for $enabled, but the method signature doesn’t provide a default value. To avoid confusing API consumers, either remove the “Defaults…” wording (and rely on the property default) or change the signature to include an explicit default (if that’s intended).

Copilot uses AI. Check for mistakes.
* $nav->activateParents(true);
* ```
*/
public function activateParents(bool $enabled): self
{
$new = clone $this;
$new->activateParents = $enabled;

return $new;
}

/**
* Adds a set of attributes.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
}
Comment on lines 668 to +673
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$dropDownItems is a Dropdown instance (returned by isDropdownActive()), but the name reads like an array of items and uses inconsistent casing (“dropDown” vs “dropdown”). Consider renaming to something like $dropdown / $dropdownWithActiveItems for clarity and consistency.

Copilot uses AI. Check for mistakes.

return Li::tag()
->addClass(...$this->dropdownCssClasses, ...$dropDownItems->getCssClasses())
Expand All @@ -631,7 +679,7 @@ private function renderItemsDropdown(Dropdown $items): Li
$dropDownItems
->container(false)
->togglerAsLink()
->togglerClass('nav-link', 'dropdown-toggle')
->togglerClass(...$togglerClasses)
->render(),
"\n",
)
Expand Down
115 changes: 115 additions & 0 deletions tests/NavTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,120 @@
#[Group('nav')]
final class NavTest extends TestCase
{
public function testActivateParents(): void
{
Assert::equalsWithoutLE(
<<<HTML
<ul class="nav">
<li class="nav-item">
<a class="nav-link" href="/test">Active</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle active" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="/test/link/action">Action</a>
</li>
<li>
<a class="dropdown-item active" href="/test/link/another-action" aria-current="true">Another action</a>
</li>
<li>
<a class="dropdown-item" href="/test/link/something-else">Something else here</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item" href="/test/link/separated-link">Separated link</a>
</li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="/test/link">Link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="/test/disabled" aria-disabled="true">Disabled</a>
</li>
</ul>
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
<ul class="nav">
<li class="nav-item">
<a class="nav-link" href="/test">Active</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="/test/link/action">Action</a>
</li>
<li>
<a class="dropdown-item active" href="/test/link/another-action" aria-current="true">Another action</a>
</li>
<li>
<a class="dropdown-item" href="/test/link/something-else">Something else here</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item" href="/test/link/separated-link">Separated link</a>
</li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="/test/link">Link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="/test/disabled" aria-disabled="true">Disabled</a>
</li>
</ul>
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(
Expand Down Expand Up @@ -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(''));
Expand Down
Loading