Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2.x] Support setting extra attributes for navigation items #1824

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ba547bd
Test the entire component state
caendesilva Jul 10, 2024
5d7fb5e
Add attributes array to the navigation item class
caendesilva Jul 10, 2024
b07ecf5
Annotate scalar array generics
caendesilva Jul 10, 2024
9762449
Add attributes accessor
caendesilva Jul 10, 2024
6c5961b
Return attributes as component attribute bag
caendesilva Jul 10, 2024
15b97f9
Revert "Return attributes as component attribute bag"
caendesilva Jul 10, 2024
66dc9b2
Rename accessor to signify it's for extra attributes
caendesilva Jul 10, 2024
caae636
Annotate the scalar return array
caendesilva Jul 10, 2024
283016f
Merge in the extra attributes
caendesilva Jul 10, 2024
93cef20
Change merge order to preserve output format
caendesilva Jul 10, 2024
395dbfb
Merge in extra attributes last in component
caendesilva Jul 10, 2024
b558720
Update RelativeLinksAcrossPagesRetainsIntegrityTest.php
caendesilva Jul 10, 2024
9c60087
Revert "Update RelativeLinksAcrossPagesRetainsIntegrityTest.php"
caendesilva Jul 10, 2024
c087bbf
Revert "Merge in extra attributes last in component"
caendesilva Jul 10, 2024
33e40d8
Update navigation item facade to support the attribute parameter
caendesilva Jul 10, 2024
1beec7e
Update the make method to support extra attributes
caendesilva Jul 10, 2024
8080709
Update the constructors to support extra attributes
caendesilva Jul 10, 2024
8b6d678
Generate attribute accessors
caendesilva Jul 10, 2024
17a33e0
Revert "Generate attribute accessors"
caendesilva Jul 10, 2024
3f7c34b
Test the view with extra attributes
caendesilva Jul 10, 2024
798d363
Annotate the return array shape
caendesilva Jul 10, 2024
ed615a7
Update YAML parser array shapes
caendesilva Jul 10, 2024
5731738
Support setting navigation items with extra attributes in the config
caendesilva Jul 10, 2024
09db9f1
Annotate the parameter array shape
caendesilva Jul 10, 2024
c8a72a8
Use variable annotation for cleaner looking code
caendesilva Jul 10, 2024
c6cdbab
Test invalid custom navigation configuration throws exception
caendesilva Jul 10, 2024
b0bbfa7
Unpack the array directly
caendesilva Jul 10, 2024
87bb7ef
Introduce local variable
caendesilva Jul 10, 2024
ca28299
Catch and rethrow error caused by bad configuration
caendesilva Jul 10, 2024
3f2878a
Normalize to more generic error message
caendesilva Jul 10, 2024
5ed1082
Add helpers to assert exception was thrown
caendesilva Jul 10, 2024
138e4b9
Add message to custom assertion
caendesilva Jul 10, 2024
6a076b8
Let the generator parse navigation items instead of the loader
caendesilva Jul 10, 2024
997a6ee
Rename closure parameter
caendesilva Jul 10, 2024
912f7b2
Extract experimental helper method to try parsing config values
caendesilva Jul 10, 2024
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<a href="{{ $item }}" {{ $attributes->except('item')->class([
'navigation-link block my-2 md:my-0 md:inline-block py-1 text-gray-700 hover:text-gray-900 dark:text-gray-100',
'navigation-link-active border-l-4 border-indigo-500 md:border-none font-medium -ml-6 pl-5 md:ml-0 md:pl-0 bg-gray-100 dark:bg-gray-800 md:bg-transparent dark:md:bg-transparent' => $item->isActive()
])->merge([
])->merge($item->getExtraAttributes())->merge([
'aria-current' => $item->isActive() ? 'page' : false,
]) }}>{{ $item->getLabel() }}</a>
6 changes: 4 additions & 2 deletions packages/framework/src/Facades/Navigation.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ class Navigation
* @param string<\Hyde\Support\Models\RouteKey>|string $destination Route key, or an external URI.
* @param string|null $label If not provided, Hyde will try to get it from the route's connected page, or from the URL.
* @param int|null $priority If not provided, Hyde will try to get it from the route or the default priority of 500.
* @param array<string, scalar> $attributes Additional attributes for the navigation item.
* @return array{destination: string, label: ?string, priority: ?int, attributes: array<string, scalar>}
*/
public static function item(string $destination, ?string $label = null, ?int $priority = null): array
public static function item(string $destination, ?string $label = null, ?int $priority = null, array $attributes = []): array
{
return compact('destination', 'label', 'priority');
return compact('destination', 'label', 'priority', 'attributes');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

namespace Hyde\Foundation\Internal;

use Throwable;
use Illuminate\Support\Arr;
use Hyde\Facades\Navigation;
use Hyde\Foundation\Application;
use Illuminate\Config\Repository;
use Hyde\Framework\Features\Blogging\Models\PostAuthor;
Expand Down Expand Up @@ -52,10 +50,6 @@ protected function mergeParsedConfiguration(): void
$data['authors'] = $this->parseAuthors($data['authors']);
}

if ($namespace === 'hyde' && isset($data['navigation']['custom'])) {
$data['navigation']['custom'] = $this->parseNavigationItems($data['navigation']['custom']);
}

$this->mergeConfiguration($namespace, Arr::undot($data ?: []));
}
}
Expand All @@ -72,34 +66,9 @@ protected function mergeConfiguration(string $namespace, array $yaml): void
protected function parseAuthors(array $authors): array
{
return Arr::mapWithKeys($authors, function (array $author, string $username): array {
try {
return [$username => PostAuthor::create($author)];
} catch (Throwable $exception) {
throw new InvalidConfigurationException(
'Invalid author configuration detected in the YAML config file. Please double check the syntax.',
previous: $exception
);
}
});
}
$message = 'Invalid author configuration detected in the YAML config file. Please double check the syntax.';

/**
* @experimental Since the main configuration also uses arrays, the only thing this method really does is to rethrow any exceptions.
*
* @param array<array{destination: string, label: ?string, priority: ?int}> $items Where destination is a route key or an external URI.
* @return array<array{destination: string, label: ?string, priority: ?int}>
*/
protected function parseNavigationItems(array $items): array
{
return Arr::map($items, function (array $item): array {
try {
return Navigation::item(...$item);
} catch (Throwable $exception) {
throw new InvalidConfigurationException(
'Invalid navigation item configuration detected in the YAML config file. Please double check the syntax.',
previous: $exception
);
}
return InvalidConfigurationException::try(fn () => [$username => PostAuthor::create($author)], $message);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,18 @@ protected function findConfigLine(string $namespace, string $key): array

return [$file, $line + 1];
}

/**
* @internal
*
* @experimental
*/
public static function try(callable $callback, ?string $message = null): mixed
{
try {
return $callback();
} catch (Throwable $exception) {
throw new static($message ?? $exception->getMessage(), previous: $exception);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,20 @@ class NavigationItem implements Stringable
protected string $label;
protected int $priority;

/** @var array<string, scalar> */
protected array $attributes = [];

/**
* Create a new navigation menu item, automatically filling in the properties from a Route instance if provided.
*
* @param \Hyde\Support\Models\Route|string<\Hyde\Support\Models\RouteKey>|string $destination Route instance or route key, or an external URI.
* @param string|null $label If not provided, Hyde will try to get it from the route's connected page, or from the URL.
* @param int|null $priority If not provided, Hyde will try to get it from the route or the default priority of 500.
* @param array<string, scalar> $attributes Additional attributes for the navigation item.
*/
public function __construct(Route|string $destination, ?string $label = null, ?int $priority = null)
public function __construct(Route|string $destination, ?string $label = null, ?int $priority = null, array $attributes = [])
{
[$this->destination, $this->label, $this->priority] = self::make($destination, $label, $priority);
[$this->destination, $this->label, $this->priority, $this->attributes] = self::make($destination, $label, $priority, $attributes);
}

/**
Expand All @@ -41,10 +45,11 @@ public function __construct(Route|string $destination, ?string $label = null, ?i
* @param \Hyde\Support\Models\Route|string<\Hyde\Support\Models\RouteKey>|string $destination Route instance or route key, or an external URI.
* @param string|null $label If not provided, Hyde will try to get it from the route's connected page, or from the URL.
* @param int|null $priority If not provided, Hyde will try to get it from the route or the default priority of 500.
* @param array<string, scalar> $attributes Additional attributes for the navigation item.
*/
public static function create(Route|string $destination, ?string $label = null, ?int $priority = null): static
public static function create(Route|string $destination, ?string $label = null, ?int $priority = null, array $attributes = []): static
{
return new static(...self::make($destination, $label, $priority));
return new static(...self::make($destination, $label, $priority, $attributes));
}

/**
Expand Down Expand Up @@ -100,8 +105,8 @@ public function isActive(): bool
return Hyde::currentRoute()?->getLink() === $this->getLink();
}

/** @return array{\Hyde\Support\Models\Route|string, string, int} */
protected static function make(Route|string $destination, ?string $label = null, ?int $priority = null): array
/** @return array{\Hyde\Support\Models\Route|string, string, int, array<string, scalar>} */
protected static function make(Route|string $destination, ?string $label = null, ?int $priority = null, array $attributes = []): array
{
// Automatically resolve the destination if it's a route key.
if (is_string($destination) && Routes::has($destination)) {
Expand All @@ -114,6 +119,12 @@ protected static function make(Route|string $destination, ?string $label = null,
$priority ??= $destination->getPage()->navigationMenuPriority();
}

return [$destination, $label ?? $destination, $priority ?? NavigationMenu::DEFAULT];
return [$destination, $label ?? $destination, $priority ?? NavigationMenu::DEFAULT, $attributes];
}

/** @return array<string, scalar> */
public function getExtraAttributes(): array
{
return $this->attributes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Illuminate\Support\Collection;
use Hyde\Foundation\Facades\Routes;
use Hyde\Foundation\Kernel\RouteCollection;
use Hyde\Framework\Exceptions\InvalidConfigurationException;

use function filled;
use function assert;
Expand Down Expand Up @@ -81,9 +82,13 @@ protected function generate(): void
$this->items->push(NavigationItem::create(DocumentationPage::home()));
}
} else {
collect(Config::getArray('hyde.navigation.custom', []))->each(function (array $item): void {
collect(Config::getArray('hyde.navigation.custom', []))->each(function (array $data): void {
/** @var array{destination: string, label: ?string, priority: ?int, attributes: array<string, scalar>} $data */
$message = 'Invalid navigation item configuration detected the configuration file. Please double check the syntax.';
$item = InvalidConfigurationException::try(fn () => NavigationItem::create(...$data), $message);

// Since these were added explicitly by the user, we can assume they should always be shown
$this->items->push(NavigationItem::create($item['destination'], $item['label'] ?? null, $item['priority'] ?? null));
$this->items->push($item);
});
}
}
Expand Down
28 changes: 28 additions & 0 deletions packages/framework/tests/Feature/NavigationMenuTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Hyde\Pages\MarkdownPage;
use Hyde\Testing\TestCase;
use Illuminate\Support\Collection;
use Hyde\Framework\Exceptions\InvalidConfigurationException;
use Hyde\Framework\Features\Navigation\NavigationMenuGenerator;

/**
Expand Down Expand Up @@ -152,6 +153,21 @@ public function testPathLinkCanBeAddedInConfig()
$this->assertEquals($expected, $menu->getItems());
}

public function testCanAddCustomLinksInConfigWithExtraAttributes()
{
config(['hyde.navigation.custom' => [Navigation::item('foo', 'Foo', 100, ['class' => 'foo'])]]);

$menu = $this->createNavigationMenu();

$expected = collect([
NavigationItem::create(Routes::get('index')),
NavigationItem::create('foo', 'Foo', 100, ['class' => 'foo']),
]);

$this->assertCount(count($expected), $menu->getItems());
$this->assertEquals($expected, $menu->getItems());
}

public function testDuplicatesAreNotRemovedWhenAddingInConfig()
{
config(['hyde.navigation.custom' => [
Expand Down Expand Up @@ -208,6 +224,18 @@ public function testConfigItemsTakePrecedenceOverGeneratedItems()
$this->assertEquals($expected, $menu->getItems());
}

public function testInvalidCustomNavigationConfigurationThrowsException()
{
$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('Invalid navigation item configuration detected the configuration file. Please double check the syntax.');

config(['hyde.navigation.custom' => [
['invalid_key' => 'value'],
]]);

$this->createNavigationMenu();
}

public function testDocumentationPagesThatAreNotIndexAreNotAddedToTheMenu()
{
$this->file('_docs/foo.md');
Expand Down
Loading