Skip to content

Commit

Permalink
VACMS-1872: Render child sections. (#1980)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
oksana-c and swirtSJW authored Jun 10, 2020
1 parent 97abcb2 commit d67c6cd
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 37 deletions.
81 changes: 81 additions & 0 deletions docroot/modules/custom/va_gov_user/css/sections_accordion.css
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions docroot/modules/custom/va_gov_user/js/sections_accordion.js
Original file line number Diff line number Diff line change
@@ -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);
140 changes: 109 additions & 31 deletions docroot/modules/custom/va_gov_user/src/Plugin/Block/UserSections.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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.
*
Expand All @@ -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;
}

Expand All @@ -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')
);
}
Expand Down Expand Up @@ -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('<button class="toggle" aria-label="Toggle ' . $term->name . ' section" aria-pressed="false" aria-expanded="false" aria-controls="section-' . $term->tid . '"></button>') : 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:[email protected]?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. <a href=":link">Contact VACMS Support to request access</a>.', [':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}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ class UserPermsService {
*/
private $database;

/**
* Scheme.
*
* @var \Drupal\workbench_access\Entity\AccessSchemeInterface
*/
protected $scheme;

/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -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');
Expand All @@ -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;
}

/**
Expand Down
6 changes: 6 additions & 0 deletions docroot/modules/custom/va_gov_user/va_gov_user.libraries.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
sections_accordion:
css:
component:
css/sections_accordion.css: {}
js:
js/sections_accordion.js: {}
1 change: 1 addition & 0 deletions tests/accessibility/aXeAccessibilityCheck.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

1 comment on commit d67c6cd

@va-cms-bot
Copy link
Collaborator

Choose a reason for hiding this comment

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

❌ Test Failed: va/tests/accessibility
composer va:test:accessibility
> [email protected] install /var/www/cms/node_modules/phantomjs-prebuilt
> node install.js

PhantomJS not found on PATH
Downloading https://github.com/Medium/phantomjs/releases/download/v2.1.1/phantomjs-2.1.1-linux-x86_64.tar.bz2
Saving to /tmp/phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2
Receiving...

Received 22866K total.
Extracting tar contents (via spawned process)
Removing /var/www/cms/node_modules/phantomjs-prebuilt/lib/phantom
Copying extracted folder /tmp/phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2-extract-1591806479043/phantomjs-2.1.1-linux-x86_64 -> /var/www/cms/node_modules/phantomjs-prebuilt/lib/phantom
Writing location.js file
Done. Phantomjs binary available at /var/www/cms/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs

> [email protected] postinstall /var/www/cms/node_modules/core-js
> node scripts/postinstall || echo "ignore"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon: 
> https://opencollective.com/core-js 
> https://www.patreon.com/zloirock 

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)

added 134 packages from 190 contributors and audited 134 packages in 12.783s
found 3 low severity vulnerabilities
  run `npm audit fix` to fix them, or `npm audit` for details

> [email protected] test /var/www/cms
> node ./tests/accessibility/aXeAccessibilityCheck.js

!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com 1
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/sections  = 0
[]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/page  = 2
[ { id: 'duplicate-id',
    impact: 'minor',
    tags: [ 'cat.parsing', 'wcag2a', 'wcag411' ],
    description: 'Ensures every id attribute value is unique',
    help: 'id attribute value must be unique',
    helpUrl:
     'https://dequeuniversity.com/rules/axe/3.3/duplicate-id?application=webdriverjs',
    nodes: [ [Object], [Object] ] },
  { id: 'label',
    impact: 'critical',
    tags:
     [ 'cat.forms',
       'wcag2a',
       'wcag332',
       'wcag131',
       'section508',
       'section508.22.n' ],
    description: 'Ensures every form element has a label',
    help: 'Form elements must have labels',
    helpUrl:
     'https://dequeuniversity.com/rules/axe/3.3/label?application=webdriverjs',
    nodes: [ [Object] ] } ]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/landing_page  = 0
[]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/documentation_page  = 1
[ { id: 'label',
    impact: 'critical',
    tags:
     [ 'cat.forms',
       'wcag2a',
       'wcag332',
       'wcag131',
       'section508',
       'section508.22.n' ],
    description: 'Ensures every form element has a label',
    help: 'Form elements must have labels',
    helpUrl:
     'https://dequeuniversity.com/rules/axe/3.3/label?application=webdriverjs',
    nodes: [ [Object] ] } ]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/event  = 0
[]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/health_care_local_facility  = 1
[ { id: 'label',
    impact: 'critical',
    tags:
     [ 'cat.forms',
       'wcag2a',
       'wcag332',
       'wcag131',
       'section508',
       'section508.22.n' ],
    description: 'Ensures every form element has a label',
    help: 'Form elements must have labels',
    helpUrl:
     'https://dequeuniversity.com/rules/axe/3.3/label?application=webdriverjs',
    nodes:
     [ [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object] ] } ]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/health_care_region_detail_page  = 2
[ { id: 'duplicate-id',
    impact: 'minor',
    tags: [ 'cat.parsing', 'wcag2a', 'wcag411' ],
    description: 'Ensures every id attribute value is unique',
    help: 'id attribute value must be unique',
    helpUrl:
     'https://dequeuniversity.com/rules/axe/3.3/duplicate-id?application=webdriverjs',
    nodes: [ [Object], [Object] ] },
  { id: 'label',
    impact: 'critical',
    tags:
     [ 'cat.forms',
       'wcag2a',
       'wcag332',
       'wcag131',
       'section508',
       'section508.22.n' ],
    description: 'Ensures every form element has a label',
    help: 'Form elements must have labels',
    helpUrl:
     'https://dequeuniversity.com/rules/axe/3.3/label?application=webdriverjs',
    nodes: [ [Object] ] } ]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/health_care_region_page  = 0
[]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/office  = 0
[]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/outreach_asset  = 0
[]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/person_profile  = 0
[]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/press_release  = 1
[ { id: 'aria-allowed-attr',
    impact: 'critical',
    tags: [ 'cat.aria', 'wcag2a', 'wcag412' ],
    description: 'Ensures ARIA attributes are allowed for an element\'s role',
    help: 'Elements must only use allowed ARIA attributes',
    helpUrl:
     'https://dequeuniversity.com/rules/axe/3.3/aria-allowed-attr?application=webdriverjs',
    nodes: [ [Object] ] } ]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/regional_health_care_service_des  = 0
[]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/news_story  = 0
[]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/node/add/support_service  = 0
[]
!!!  NUMBER OF NEW VIOLATIONS on http://internal-dsva-vagov-staging-cms-1188006.us-gov-west-1.elb.amazonaws.com/user  = 1
[ { id: 'color-contrast',
    impact: 'serious',
    tags: [ 'cat.color', 'wcag2aa', 'wcag143' ],
    description:
     'Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds',
    help: 'Elements must have sufficient color contrast',
    helpUrl:
     'https://dequeuniversity.com/rules/axe/3.3/color-contrast?application=webdriverjs',
    nodes:
     [ [Object], [Object], [Object], [Object], [Object], [Object] ] } ]
!!!  VIOLATION TYPES FOUND: 9 PROCESS EXITED WITH CODE 1  !!!
> npm install --only=production
> npm test
Wed, 10 Jun 2020 16:28:03 GMT axe-webdriverjs deprecated Error must be handled as the first argument of axe.analyze. See: #83 at tests/accessibility/aXeAccessibilityCheck.js:45:14
Wed, 10 Jun 2020 16:28:13 GMT axe-webdriverjs deprecated Error must be handled as the first argument of axe.analyze. See: #83 at tests/accessibility/aXeAccessibilityCheck.js:57:42
npm ERR! Test failed.  See above for more details.
Script npm test handling the va:test:accessibility event returned with error code 1
  • On: ip-10-247-35-81
  • In: 02:42

Please sign in to comment.