Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.38% covered (warning)
52.38%
33 / 63
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
NavigationHelper
52.38% covered (warning)
52.38%
33 / 63
40.00% covered (danger)
40.00%
2 / 5
27.55
0.00% covered (danger)
0.00%
0 / 1
 renderMainMenu
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
2
 renderNavLink
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 isActive
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 activeClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 renderBreadcrumbs
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2declare(strict_types=1);
3
4namespace DefaultTheme\View\Helper;
5
6use Cake\View\Helper;
7
8/**
9 * NavigationHelper
10 *
11 * Handles navigation menu rendering with active state detection
12 * and proper ARIA attributes for accessibility.
13 *
14 * @property \Cake\View\Helper\HtmlHelper $Html
15 * @property \Cake\View\Helper\UrlHelper $Url
16 */
17class NavigationHelper extends Helper
18{
19    /**
20     * List of helpers used by this helper
21     *
22     * @var array
23     */
24    protected array $helpers = ['Html', 'Url'];
25
26    /**
27     * Render main navigation menu
28     *
29     * @param array $menuPages Array of menu page items
30     * @param int $marginBottom Bottom margin amount (Bootstrap mb-* class)
31     * @return string HTML navigation menu
32     */
33    public function renderMainMenu(array $menuPages, int $marginBottom = 0): string
34    {
35        $currentUrl = $this->getView()->getRequest()->getPath();
36        $output = [];
37
38        $output[] = sprintf('<div class="nav-scroller py-1 mb-%d border-bottom">', $marginBottom);
39        $output[] = '    <nav class="nav nav-underline justify-content-center" role="navigation" aria-label="' . __('Main navigation') . '">';
40        $output[] = '';
41
42        // Blog/Home link
43        $output[] = $this->renderNavLink(
44            __('Blog'),
45            ['_name' => 'home'],
46            $currentUrl
47        );
48
49        // Menu pages
50        foreach ($menuPages as $menuPage) {
51            $output[] = $this->renderNavLink(
52                htmlspecialchars_decode($menuPage['title']),
53                ['_name' => 'page-by-slug', 'slug' => $menuPage['slug']],
54                $currentUrl,
55                ['escape' => false]
56            );
57        }
58
59        // GitHub link (external)
60        $output[] = '        <a class="nav-item nav-link link-body-emphasis fw-medium px-3" ';
61        $output[] = '           href="https://www.github.com/matthewdeaves/willow">';
62        $output[] = '           GitHub';
63        $output[] = '        </a>';
64
65        $output[] = '    </nav>';
66        $output[] = '</div>';
67        $output[] = '';
68
69        return implode("\n", $output);
70    }
71
72    /**
73     * Render a single navigation link with active state detection
74     *
75     * @param string $title Link text
76     * @param array|string $url URL array or string
77     * @param string $currentUrl Current page URL for active state detection
78     * @param array $options Additional link options
79     * @return string HTML link element
80     */
81    public function renderNavLink(string $title, array|string $url, string $currentUrl, array $options = []): string
82    {
83        $generatedUrl = $this->Url->build($url);
84        $isActive = ($currentUrl === $generatedUrl);
85
86        $defaultOptions = [
87            'class' => 'nav-item nav-link link-body-emphasis fw-medium px-3' . ($isActive ? ' active' : ''),
88            'aria-current' => $isActive ? 'page' : false,
89        ];
90
91        $mergedOptions = array_merge($defaultOptions, $options);
92
93        return '        ' . $this->Html->link($title, $url, $mergedOptions);
94    }
95
96    /**
97     * Check if a URL is the current page
98     *
99     * @param array|string $url URL to check
100     * @return bool True if URL matches current page
101     */
102    public function isActive(array|string $url): bool
103    {
104        $currentUrl = $this->getView()->getRequest()->getPath();
105        $generatedUrl = $this->Url->build($url);
106
107        return $currentUrl === $generatedUrl;
108    }
109
110    /**
111     * Get CSS class for active state
112     *
113     * @param array|string $url URL to check
114     * @param string $activeClass Class to return if active
115     * @param string $inactiveClass Class to return if inactive
116     * @return string CSS class
117     */
118    public function activeClass(array|string $url, string $activeClass = 'active', string $inactiveClass = ''): string
119    {
120        return $this->isActive($url) ? $activeClass : $inactiveClass;
121    }
122
123    /**
124     * Render breadcrumb navigation
125     *
126     * @param array $crumbs Array of breadcrumb items
127     * @return string HTML breadcrumb navigation
128     */
129    public function renderBreadcrumbs(array $crumbs): string
130    {
131        if (empty($crumbs)) {
132            return '';
133        }
134
135        $output = [];
136        $output[] = '<nav aria-label="' . __('Breadcrumb') . '">';
137        $output[] = '    <ol class="breadcrumb">';
138
139        $crumbsArray = $crumbs->toArray();
140        $lastIndex = count($crumbsArray) - 1;
141
142        foreach ($crumbsArray as $index => $crumb) {
143            $isLast = ($index === $lastIndex);
144
145            if ($isLast) {
146                $output[] = sprintf(
147                    '        <li class="breadcrumb-item active" aria-current="page">%s</li>',
148                    h($crumb->title)
149                );
150            } else {
151                $output[] = '        <li class="breadcrumb-item">';
152                $output[] = sprintf(
153                    '            %s',
154                    $this->Html->link(
155                        h($crumb->title),
156                        ['_name' => 'page-by-slug', 'slug' => $crumb->slug]
157                    )
158                );
159                $output[] = '        </li>';
160            }
161        }
162
163        $output[] = '    </ol>';
164        $output[] = '</nav>';
165
166        return implode("\n", $output);
167    }
168}