From d67c6cd91a3fe23245836e5bc4a2a442ab9f4ae1 Mon Sep 17 00:00:00 2001 From: Oksana Cyrwus Date: Wed, 10 Jun 2020 09:55:15 -0600 Subject: [PATCH] VACMS-1872: Render child sections. (#1980) * VACMS-1872: Render child sections. * VACMS-1872: Use accordion pattern + add 508 features. * VACMS-1872: Add /user path to accessibility check. * VACMS-1872: iteration on sections styling. * VACMS-1872: Prevent link and icon overlap. Refactor library. * VACMS-1872: Polished margins for sections block. * VACMS-1872: Switch to placing button in suffix instead of prefix. * VACMS-1872: Menu pattern instead of accordion. * VACMS-1872: Support elements order for keyboard navigation. Co-authored-by: Steve Wirt --- .../va_gov_user/css/sections_accordion.css | 81 ++++++++++ .../va_gov_user/images/chevron-disc-down.svg | 1 + .../va_gov_user/images/chevron-disc-up.svg | 1 + .../va_gov_user/js/sections_accordion.js | 23 +++ .../src/Plugin/Block/UserSections.php | 140 ++++++++++++++---- .../src/Service/UserPermsService.php | 38 ++++- .../va_gov_user/va_gov_user.libraries.yml | 6 + tests/accessibility/aXeAccessibilityCheck.js | 1 + 8 files changed, 254 insertions(+), 37 deletions(-) create mode 100644 docroot/modules/custom/va_gov_user/css/sections_accordion.css create mode 100644 docroot/modules/custom/va_gov_user/images/chevron-disc-down.svg create mode 100644 docroot/modules/custom/va_gov_user/images/chevron-disc-up.svg create mode 100644 docroot/modules/custom/va_gov_user/js/sections_accordion.js create mode 100644 docroot/modules/custom/va_gov_user/va_gov_user.libraries.yml diff --git a/docroot/modules/custom/va_gov_user/css/sections_accordion.css b/docroot/modules/custom/va_gov_user/css/sections_accordion.css new file mode 100644 index 0000000000..ac075cebae --- /dev/null +++ b/docroot/modules/custom/va_gov_user/css/sections_accordion.css @@ -0,0 +1,81 @@ +.item-list ul.sections li { + list-style: none; + margin: 0; + padding: 0.1rem; +} + +.item-list ul.sections .item-list ul { + background-color: #ffffff; + margin: 0; + padding: 0 0 0 2em; +} + +.item-list ul.sections .item-list ul.subsections { + padding: 0.5rem 0 0.5rem 2rem; +} + +.item-list > ul.sections > li { + border: 1px solid #aeb0b5; + border-bottom: none; + padding: 0; + position: relative; +} + +.item-list > ul.sections > li:last-child { + border-bottom: 1px solid #aeb0b5; +} + +.item-list > ul.sections > li ul li { + list-style: disc; +} + +.item-list > ul.sections { + margin-left: 0; + max-width: 400px; +} + +.item-list > ul.sections > li > a { + display: block; + padding: 0.5rem; +} + +ul.sections li .toggle { + background-image: url('../images/chevron-disc-down.svg'); + background-size: 1rem; + background-position: 0.75rem .75rem; + background-repeat: no-repeat; + background-color: transparent; + border: 0; + cursor: pointer; + display: block; + height: 100%; + margin: 0; + padding: 0 1.15rem; + position: absolute; + right: 0; + top: 0; + width: 1rem; +} + +ul.sections li .toggle:active, +ul.sections li .toggle:focus, +ul.sections li .toggle:hover { + background-color: #f1f1f1; +} + +.sections > li > a.open { + border-bottom: 1px solid #d6d7d9; +} + +.sections li .toggle.open { + background-color: #f1f1f1; + background-image: url('../images/chevron-disc-up.svg'); +} + +.sections .subsections { + display: block; +} + +.sections .subsections.hidden { + display: none; +} diff --git a/docroot/modules/custom/va_gov_user/images/chevron-disc-down.svg b/docroot/modules/custom/va_gov_user/images/chevron-disc-down.svg new file mode 100644 index 0000000000..6707c3038f --- /dev/null +++ b/docroot/modules/custom/va_gov_user/images/chevron-disc-down.svg @@ -0,0 +1 @@ + diff --git a/docroot/modules/custom/va_gov_user/images/chevron-disc-up.svg b/docroot/modules/custom/va_gov_user/images/chevron-disc-up.svg new file mode 100644 index 0000000000..13d86d925e --- /dev/null +++ b/docroot/modules/custom/va_gov_user/images/chevron-disc-up.svg @@ -0,0 +1 @@ + diff --git a/docroot/modules/custom/va_gov_user/js/sections_accordion.js b/docroot/modules/custom/va_gov_user/js/sections_accordion.js new file mode 100644 index 0000000000..eee01a3b8e --- /dev/null +++ b/docroot/modules/custom/va_gov_user/js/sections_accordion.js @@ -0,0 +1,23 @@ +/** + * @file + */ + +(function ($, Drupal) { + Drupal.behaviors.vaGovSectionsAccordion = { + attach: function (context, settings) { + // Add aria-hidden attribute to all collapsed areas. + $('.sections').find('.subsections').attr('aria-hidden', true).addClass('hidden'); + + $('.sections .toggle', context).on('click', function (e) { + e.preventDefault(); + $(e.target).toggleClass('open'); + $(e.target).closest('li').find('a').toggleClass('open'); + $(e.target).attr('aria-pressed', function (_, attr) { return !(attr === 'true') }); + $(e.target).attr('aria-expanded', function (_, attr) { return !(attr === 'true') }); + $(e.target).closest('li').find('.subsections').attr('aria-hidden', function (_, attr) { return !(attr === 'true') }); + $(e.target).closest('li').find('.subsections').toggleClass('hidden'); + }); + } + }; + +})(jQuery, window.Drupal); diff --git a/docroot/modules/custom/va_gov_user/src/Plugin/Block/UserSections.php b/docroot/modules/custom/va_gov_user/src/Plugin/Block/UserSections.php index 3d787834bb..bf6a55205e 100644 --- a/docroot/modules/custom/va_gov_user/src/Plugin/Block/UserSections.php +++ b/docroot/modules/custom/va_gov_user/src/Plugin/Block/UserSections.php @@ -4,12 +4,14 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Block\BlockBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Link; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Render\Markup; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; use Drupal\va_gov_user\Service\UserPermsService; -use Drupal\views\Views; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -29,13 +31,6 @@ class UserSections extends BlockBase implements ContainerFactoryPluginInterface */ protected $routeMatch; - /** - * Database connection. - * - * @var \Drupal\Core\Database\Database - */ - private $database; - /** * The entity type manager. * @@ -53,9 +48,10 @@ class UserSections extends BlockBase implements ContainerFactoryPluginInterface /** * {@inheritDoc} */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match, UserPermsService $user_perms) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match, EntityTypeManagerInterface $entity_type_manager, UserPermsService $user_perms) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->routeMatch = $route_match; + $this->entityTypeManager = $entity_type_manager; $this->userPerms = $user_perms; } @@ -68,6 +64,7 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_id, $plugin_definition, $container->get('current_route_match'), + $container->get('entity_type.manager'), $container->get('va_gov_user.user_perms') ); } @@ -96,49 +93,130 @@ public function build() { // Get sections assigned to user profile. $sections = $this->userPerms->getSections($user); - // Viewed profile is content_admin or administrator - // OR - // user has access to ALL sections - - // Render sections tree. - if ($user->hasRole('content_admin') || $user->hasRole('administrator') || in_array('All sections', $sections)) { - $view = Views::getView('sections_tree'); - $view->setDisplay('block_1'); - $view->preExecute(); - $view->execute(); - - return $view->buildRenderable(); + // User has access only to some sections. + // Compose list. + $entity_storage = $this->entityTypeManager->getStorage('taxonomy_term'); + $tree = $entity_storage->loadTree('administration'); + + foreach ($tree as $key => $term) { + if (!array_key_exists($term->tid, $sections)) { + unset($tree[$key]); + } } - // Use has access only to some sections. - // Compose list. $links = []; - foreach ($sections as $tid => $term_name) { - $links[$tid] = [ - 'title' => $term_name, - 'url' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $tid]), - ]; + foreach ($tree as $term) { + $parent_path = $term->parents[0] ? $this->getArrayKeyPath($links, $term->parents[0]) : NULL; + + // Compose render array of section links while preserving hierarchy. + // This switch accounts for taxonomy terms that are 5 levels deep. + // Sections vocabulary is 4 levels deep. If it grows over 5, this logic + // must be expanded. + $count = count($parent_path); + switch ($count) { + case 1: + $links[$parent_path[0]]['items']['#attributes'] = [ + 'id' => 'section-' . $parent_path[0], + 'class' => 'subsections', + ]; + $links[$parent_path[0]]['items'][$term->tid] = [ + '#type' => 'link', + '#weight' => $term->weight, + '#title' => $term->name, + '#url' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $term->tid]), + ]; + break; + + case 3: + $links[$parent_path[2]][$parent_path[1]][$parent_path[0]]['items'][$term->tid] = [ + '#type' => 'link', + '#weight' => $term->weight, + '#title' => $term->name, + '#url' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $term->tid]), + ]; + break; + + case 5: + $links[$parent_path[4]][$parent_path[3]][$parent_path[2]][$parent_path[1]][$parent_path[0]]['items'][$term->tid] = [ + '#type' => 'link', + '#weight' => $term->weight, + '#title' => $term->name, + '#url' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $term->tid]), + ]; + break; + + default: + $section_link = Link::fromTextAndUrl($term->name, Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $term->tid]))->toString(); + $expand_button = $sections[$term->tid]['hasChildren'] ? Markup::create('') : NULL; + $links[$term->tid] = [ + '#type' => 'markup', + '#markup' => $section_link . $expand_button, + '#allowed_tags' => [ + 'a', + 'button', + ], + ]; + break; + } } if (!empty($links)) { return [ 'links' => [ - '#theme' => 'links', - '#links' => $links, + '#theme' => 'item_list', + '#list_type' => 'ul', + '#items' => $links, + '#attributes' => ['class' => 'sections'], + '#attached' => [ + 'library' => [ + 'va_gov_user/sections_accordion', + ], + ], + '#prefix' => $this->t('You can edit content in the following VA.gov sections.'), ], ]; } else { // User is not assigned to any sections, display onboarding lingo. - // @todo: Update lingo. + $url = 'mailto:vacmssuport@va.gov?subject=Section%20assignment&body=Dear%20VACMS%20support%20team%2C%0A%5BThis%20is%20a%20template.%20%20You%20can%20delete%20the%20text%20you%20don%E2%80%99t%20need%2C%20and%20feel%20free%20to%20add%20your%20own.%5D%0A%0AI%E2%80%99m%20a%20new%20CMS%20user%2C%20and%20need%20to%20be%20given%20access%20to%20the%20following%20VA.gov%20sections%3A%0A%5BList%20the%20sections%20you%20need%20access%20to%20here.%20If%20you%20aren%E2%80%99t%20sure%2C%20describe%20your%20job%20title%20and%20what%20pages%20you%20need%20to%20work%20on.%5D%0A%0APlease%20assign%20me%20the%20following%20role%3A%20%0A%5BAdd%20which%20role%20you%20need%20here.%5D%0A-%20Content%20editor%3A%20because%20I%20need%20to%20create%2C%20edit%2C%20and%20review%20content%0A-%20Content%20publisher%3A%20because%20I%20also%20need%20to%20publish%20content%0A-%20Content%20admin%3A%20because%20I%20need%20broad%20permissions%2C%20including%20customizing%20URLs%20and%20triggering%20unscheduled%20content%20releases%0A%0AThank%20you.'; return [ 'section_assignment' => [ - '#markup' => $this->t('Contact #cms-support for VA section assignment.'), + '#markup' => $this->t('You don\'t have permission to access content in any VA.gov sections yet. Contact VACMS Support to request access.', [':link' => $url]), ], ]; } } + /** + * Return a path for a specified array key. + * + * @param array $array + * Array. + * @param string $lookup + * Array key to look up. + * + * @return array|null + * Array of elements that compose a path to searched key. + */ + protected function getArrayKeyPath(array $array, $lookup) { + if (array_key_exists($lookup, $array)) { + return [$lookup]; + } + else { + foreach ($array as $key => $subarray) { + if (is_array($subarray) && (is_int($key) || $key === 'items')) { + $path = $this->getArrayKeyPath($subarray, $lookup); + if ($path) { + $path[] = $key; + return $path; + } + } + } + } + return NULL; + } + /** * {@inheritDoc} */ diff --git a/docroot/modules/custom/va_gov_user/src/Service/UserPermsService.php b/docroot/modules/custom/va_gov_user/src/Service/UserPermsService.php index 6f28cdc9dd..0bd9bc4185 100644 --- a/docroot/modules/custom/va_gov_user/src/Service/UserPermsService.php +++ b/docroot/modules/custom/va_gov_user/src/Service/UserPermsService.php @@ -37,6 +37,13 @@ class UserPermsService { */ private $database; + /** + * Scheme. + * + * @var \Drupal\workbench_access\Entity\AccessSchemeInterface + */ + protected $scheme; + /** * {@inheritDoc} */ @@ -79,6 +86,7 @@ private function getUser($user_id = NULL) { */ public function getSections(AccountInterface $user) { $sections = []; + $entity_storage = $this->entityTypeManager->getStorage('taxonomy_term'); // Get ids of sections assigned to user profile. $query = $this->database->select('section_association__user_id', 'sau'); @@ -87,21 +95,39 @@ public function getSections(AccountInterface $user) { $query->fields('sa', ['section_id']); $results = $query->execute()->fetchCol(); - if (($key = array_search('administration', $results)) !== FALSE) { + if (($key = array_search('administration', $results)) !== FALSE || $user->hasPermission('bypass-workbench-access')) { unset($results[$key]); - $sections['administration'] = 'All sections'; + $tree = $entity_storage->loadTree('administration'); + foreach ($tree as $term) { + $results[] = $term->tid; + } } // Use has access only to some sections. // Compose list. - $entity_storage = $this->entityTypeManager->getStorage('taxonomy_term'); $terms = $entity_storage->loadMultiple($results); + $this->addSections($sections, $terms); + + return $sections; + } + /** + * Add parent and child sections to getSections() results. + * + * @param array $sections + * Array of section tids/names. + * @param array $terms + * Taxonomy terms. + */ + protected function addSections(array &$sections, array $terms) { + $entity_storage = $this->entityTypeManager->getStorage('taxonomy_term'); foreach ($terms as $term) { - $sections[$term->id()] = $term->getName(); + $sections[$term->id()] = [ + 'name' => $term->getName(), + 'hasChildren' => empty($entity_storage->loadChildren($term->id())) ? FALSE : TRUE, + ]; + $this->addSections($sections, $entity_storage->loadChildren($term->id())); } - - return $sections; } /** diff --git a/docroot/modules/custom/va_gov_user/va_gov_user.libraries.yml b/docroot/modules/custom/va_gov_user/va_gov_user.libraries.yml new file mode 100644 index 0000000000..c400d8364a --- /dev/null +++ b/docroot/modules/custom/va_gov_user/va_gov_user.libraries.yml @@ -0,0 +1,6 @@ +sections_accordion: + css: + component: + css/sections_accordion.css: {} + js: + js/sections_accordion.js: {} diff --git a/tests/accessibility/aXeAccessibilityCheck.js b/tests/accessibility/aXeAccessibilityCheck.js index 24df275e6f..2fdad0d296 100644 --- a/tests/accessibility/aXeAccessibilityCheck.js +++ b/tests/accessibility/aXeAccessibilityCheck.js @@ -36,6 +36,7 @@ const paths = [ '/node/add/regional_health_care_service_des', '/node/add/news_story', '/node/add/support_service', + '/user', ]; driver.get(URL)