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

ECCI-580: Providing the ability to add service landing page child pag… #2

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions ecc_menu.libraries.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
disclosure:
js:
js/disclosure-nav.js: {}
dependencies:
- core/drupal
- core/once
16 changes: 16 additions & 0 deletions ecc_menu.module
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

function ecc_menu_theme($existing, $type, $theme, $path) {
$return = [];

$return['service_landing_page_child_pages_nav'] = [
'variables' => [
'items' => [],
'menu_name' => '',
'heading_label' => '',
'attributes' => [],
],
];

return $return;
}
227 changes: 227 additions & 0 deletions js/disclosure-nav.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* Supplemental JS for the disclosure menu keyboard behavior
*/
(function (Drupal, once) {
class DisclosureNav {
constructor(domNode) {
this.rootNode = domNode;
this.controlledNodes = [];
this.openIndex = null;
this.useArrowKeys = true;
this.topLevelNodes = [
...this.rootNode.querySelectorAll(
'.main-link, button[aria-expanded][aria-controls]'
),
];

this.topLevelNodes.forEach((node) => {
// handle button + menu
if (
node.tagName.toLowerCase() === 'button' &&
node.hasAttribute('aria-controls')
) {
const menu = node.parentNode.querySelector('ul');
if (menu) {
// save ref controlled menu
this.controlledNodes.push(menu);

// collapse menus
node.setAttribute('aria-expanded', 'false');
this.toggleMenu(menu, false);

// attach event listeners
menu.addEventListener('keydown', this.onMenuKeyDown.bind(this));
node.addEventListener('click', this.onButtonClick.bind(this));
node.addEventListener('keydown', this.onButtonKeyDown.bind(this));
}
}
// handle links
else {
this.controlledNodes.push(null);
node.addEventListener('keydown', this.onLinkKeyDown.bind(this));
}
});

this.rootNode.addEventListener('focusout', this.onBlur.bind(this));
}

controlFocusByKey(keyboardEvent, nodeList, currentIndex) {
switch (keyboardEvent.key) {
case 'ArrowUp':
case 'ArrowLeft':
keyboardEvent.preventDefault();
if (currentIndex > -1) {
var prevIndex = Math.max(0, currentIndex - 1);
nodeList[prevIndex].focus();
}
break;
case 'ArrowDown':
case 'ArrowRight':
keyboardEvent.preventDefault();
if (currentIndex > -1) {
var nextIndex = Math.min(nodeList.length - 1, currentIndex + 1);
nodeList[nextIndex].focus();
}
break;
case 'Home':
keyboardEvent.preventDefault();
nodeList[0].focus();
break;
case 'End':
keyboardEvent.preventDefault();
nodeList[nodeList.length - 1].focus();
break;
}
}

// public function to close open menu
close() {
this.toggleExpand(this.openIndex, false);
}

onBlur(event) {
var menuContainsFocus = this.rootNode.contains(event.relatedTarget);
if (!menuContainsFocus && this.openIndex !== null) {
this.toggleExpand(this.openIndex, false);
}
}

onButtonClick(event) {
var button = event.target;
var buttonIndex = this.topLevelNodes.indexOf(button);
var buttonExpanded = button.getAttribute('aria-expanded') === 'true';
this.toggleExpand(buttonIndex, !buttonExpanded);
}

onButtonKeyDown(event) {
var targetButtonIndex = this.topLevelNodes.indexOf(document.activeElement);

// close on escape
if (event.key === 'Escape') {
this.toggleExpand(this.openIndex, false);
}

// move focus into the open menu if the current menu is open
else if (
this.useArrowKeys &&
this.openIndex === targetButtonIndex &&
event.key === 'ArrowDown'
) {
event.preventDefault();
this.controlledNodes[this.openIndex].querySelector('a').focus();
}

// handle arrow key navigation between top-level buttons, if set
else if (this.useArrowKeys) {
this.controlFocusByKey(event, this.topLevelNodes, targetButtonIndex);
}
}

onLinkKeyDown(event) {
var targetLinkIndex = this.topLevelNodes.indexOf(document.activeElement);

// handle arrow key navigation between top-level buttons, if set
if (this.useArrowKeys) {
this.controlFocusByKey(event, this.topLevelNodes, targetLinkIndex);
}
}

onMenuKeyDown(event) {
if (this.openIndex === null) {
return;
}

var menuLinks = Array.prototype.slice.call(
this.controlledNodes[this.openIndex].querySelectorAll('a')
);
var currentIndex = menuLinks.indexOf(document.activeElement);

// close on escape
if (event.key === 'Escape') {
this.topLevelNodes[this.openIndex].focus();
this.toggleExpand(this.openIndex, false);
}

// handle arrow key navigation within menu links, if set
else if (this.useArrowKeys) {
this.controlFocusByKey(event, menuLinks, currentIndex);
}
}

toggleExpand(index, expanded) {
// close open menu, if applicable
if (this.openIndex !== index) {
this.toggleExpand(this.openIndex, false);
}

// handle menu at called index
if (this.topLevelNodes[index]) {
this.openIndex = expanded ? index : null;
this.topLevelNodes[index].setAttribute('aria-expanded', expanded);
this.toggleMenu(this.controlledNodes[index], expanded);
}
}

toggleMenu(domNode, show) {
if (domNode) {
domNode.style.display = show ? 'block' : 'none';
}
}

updateKeyControls(useArrowKeys) {
this.useArrowKeys = useArrowKeys;
}
}

/* Initialize Disclosure Menus */

window.addEventListener(
'load',
function () {
var menus = document.querySelectorAll('.disclosure-nav');
var disclosureMenus = [];

for (var i = 0; i < menus.length; i++) {
disclosureMenus[i] = new DisclosureNav(menus[i]);
}

// listen to arrow key checkbox
var arrowKeySwitch = document.getElementById('arrow-behavior-switch');
if (arrowKeySwitch) {
arrowKeySwitch.addEventListener('change', function () {
var checked = arrowKeySwitch.checked;
for (var i = 0; i < disclosureMenus.length; i++) {
disclosureMenus[i].updateKeyControls(checked);
}
});
}

// fake link behavior
disclosureMenus.forEach((disclosureNav, i) => {
var links = menus[i].querySelectorAll('[href="#mythical-page-content"]');
var examplePageHeading = document.getElementById('mythical-page-heading');
for (var k = 0; k < links.length; k++) {
// The codepen export script updates the internal link href with a full URL
// we're just manually fixing that behavior here
links[k].href = '#mythical-page-content';

links[k].addEventListener('click', (event) => {
// change the heading text to fake a page change
var pageTitle = event.target.innerText;
examplePageHeading.innerText = pageTitle;

// handle aria-current
for (var n = 0; n < links.length; n++) {
links[n].removeAttribute('aria-current');
}
event.target.setAttribute('aria-current', 'page');
});
}
});
},
false
);
})(Drupal, once);
123 changes: 123 additions & 0 deletions src/Plugin/Block/ServiceLandingChildPageMenuBlock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

declare(strict_types = 1);

namespace Drupal\ecc_menu\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\CurrentRouteMatch;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\node\Entity\Node;

/**
* Provides a ServiceLandingChildPageMenu nav block.
*
* @Block(
* id = "ecc_menu_service_landing_child_page_menu",
* admin_label = @Translation("Service Landing Child Pages Menu"),
* category = @Translation("Custom"),
* )
*/
final class ServiceLandingChildPageMenuBlock extends BlockBase implements ContainerFactoryPluginInterface {

/**
* Constructs the plugin instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
protected CurrentRouteMatch $currentRouteMatch,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('current_route_match'),
);
}

/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
return [
'referenced_landing_page' => '',
];
}

/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state): array {
$form['referenced_landing_page'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Service landing page'),
'#target_type' => 'node',
'#default_value' => $this->configuration['referenced_landing_page'],
'#selection_settings' => ['target_bundles' => ['localgov_services_landing']],
'#tags' => TRUE,
'#size' => 30,
'#maxlength' => 1024,
];
return $form;
}

/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state): void {
$this->configuration['referenced_landing_page'] = $form_state->getValue('referenced_landing_page');
}

/**
* {@inheritdoc}
*/
public function build(): array {
$menu = [];
$menu['#items'] = [];
if ($this->configuration['referenced_landing_page']) {
$node = Node::load($this->configuration['referenced_landing_page'][0]['target_id']);
$menu['#heading_label'] = $node->getTitle();
if($node instanceof NodeInterface) {
/** @var EntityReferenceFieldItemList $child_pages */
$child_pages = $node->get('localgov_destinations');
if ($child_pages) {
try {
$child_page_entities = $child_pages->referencedEntities();
if ($child_page_entities) {
foreach ($child_page_entities as $service) {
if ($service instanceof NodeInterface) {
$menu['#items'][] = [
'label' => $service->title->value,
'url' => $service->toUrl(),
'is_active' => $service->toUrl()->getRouteParameters()['node'] === $this->currentRouteMatch->getCurrentRouteMatch()->getRawParameters()->get('node')
];
}
}
}
} catch(\Exception $e) {
\Drupal::logger('ecc_menu')->notice('Could not retrieve any child page references from the selected service landing page %title.', [
'%title' => $node->getTitle(),
]);
}
}
}
}
$menu['#theme'] = 'service_landing_page_child_pages_nav';
$build['output'] = $menu;

return $build;
}

}
Loading