Add keyboard and screen reader support to Wagtail user bar (#6994). Fix #6108

Co-authored-by: Thibaud Colas <thibaudcolas@gmail.com>
pull/7291/head
Storm Heg 2021-06-26 21:53:44 +02:00 zatwierdzone przez GitHub
rodzic 297fb10597
commit 3ee060ffd7
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
15 zmienionych plików z 266 dodań i 83 usunięć

Wyświetl plik

@ -12,6 +12,7 @@ Changelog
* Added `WAGTAIL_WORKFLOW_ENABLED` setting for enabling / disabling moderation workflows globally (Matt Westcott) * Added `WAGTAIL_WORKFLOW_ENABLED` setting for enabling / disabling moderation workflows globally (Matt Westcott)
* Allow specifying `max_width` and `max_height` on EmbedBlock (Petr Dlouhý) * Allow specifying `max_width` and `max_height` on EmbedBlock (Petr Dlouhý)
* Add warning when StreamField is used without a StreamFieldPanel (Naomi Morduch Toubman) * Add warning when StreamField is used without a StreamFieldPanel (Naomi Morduch Toubman)
* Added keyboard and screen reader support to Wagtail user bar (LB Johnston, Storm Heg)
* Fix: Invalid filter values for foreign key fields in the API now give an error instead of crashing (Tidjani Dia) * Fix: Invalid filter values for foreign key fields in the API now give an error instead of crashing (Tidjani Dia)
* Fix: Ordering specified in `construct_explorer_page_queryset` hook is now taken into account again by the page explorer API (Andre Fonseca) * Fix: Ordering specified in `construct_explorer_page_queryset` hook is now taken into account again by the page explorer API (Andre Fonseca)
* Fix: Deleting a page from its listing view no longer results in a 404 error (Tidjani Dia) * Fix: Deleting a page from its listing view no longer results in a 404 error (Tidjani Dia)

Wyświetl plik

@ -2,57 +2,206 @@
// Please stick to old JS APIs and avoid importing anything that might require a vendored module // Please stick to old JS APIs and avoid importing anything that might require a vendored module
// More background can be found in webpack.config.js // More background can be found in webpack.config.js
document.addEventListener('DOMContentLoaded', (e) => { document.addEventListener('DOMContentLoaded', () => {
const userbar = document.querySelector('[data-wagtail-userbar]'); const userbar = document.querySelector('[data-wagtail-userbar]');
const trigger = userbar.querySelector('[data-wagtail-userbar-trigger]'); const trigger = userbar.querySelector('[data-wagtail-userbar-trigger]');
const list = userbar.querySelector('.wagtail-userbar-items'); const list = userbar.querySelector('[role=menu]');
const className = 'is-active'; const listItems = list.querySelectorAll('li');
const hasTouch = 'ontouchstart' in window; const isActiveClass = 'is-active';
const clickEvent = 'click';
if (hasTouch) { // querySelector for all items that can be focused.
userbar.classList.add('touch'); // source: https://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus
const focusableItemSelector = `a[href]:not([tabindex='-1']),
// Bind to touchend event, preventDefault to prevent DELAY and CLICK button:not([disabled]):not([tabindex='-1']),
// in accordance with: https://hacks.mozilla.org/2013/04/detecting-touch-its-the-why-not-the-how/ input:not([disabled]):not([tabindex='-1']),
trigger.addEventListener('touchend', (e2) => { [tabindex]:not([tabindex='-1'])`;
e.preventDefault();
// eslint-disable-next-line @typescript-eslint/no-use-before-define
toggleUserbar(e2);
});
} else {
userbar.classList.add('no-touch');
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
trigger.addEventListener(clickEvent, toggleUserbar, false); trigger.addEventListener('click', toggleUserbar, false);
// make sure userbar is hidden when navigating back // make sure userbar is hidden when navigating back
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
window.addEventListener('pageshow', hideUserbar, false); window.addEventListener('pageshow', hideUserbar, false);
function showUserbar() { // Handle keyboard events on the trigger
userbar.classList.add(className); // eslint-disable-next-line @typescript-eslint/no-use-before-define
userbar.addEventListener('keydown', handleTriggerKeyDown);
function showUserbar(shouldFocus) {
userbar.classList.add(isActiveClass);
trigger.setAttribute('aria-expanded', 'true');
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
list.addEventListener(clickEvent, sandboxClick, false); list.addEventListener('click', sandboxClick, false);
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
window.addEventListener(clickEvent, clickOutside, false); window.addEventListener('click', clickOutside, false);
// Start handling keyboard input now that the userbar is open.
// eslint-disable-next-line @typescript-eslint/no-use-before-define
userbar.addEventListener('keydown', handleUserbarItemsKeyDown, false);
// The userbar has role=menu which means that the first link should be focused on popup
// For weird reasons shifting focus only works after some amount of delay
// Which is why we are forced to use setTimeout
if (shouldFocus) {
// Find the first focusable element (if any) and focus it
if (list.querySelector(focusableItemSelector)) {
setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
setFocusToFirstItem();
}, 300); // Less than 300ms doesn't seem to work
}
}
} }
function hideUserbar() { function hideUserbar() {
userbar.classList.remove(className); userbar.classList.remove(isActiveClass);
trigger.setAttribute('aria-expanded', 'false');
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
list.addEventListener(clickEvent, sandboxClick, false); list.addEventListener('click', sandboxClick, false);
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
window.removeEventListener(clickEvent, clickOutside, false); window.removeEventListener('click', clickOutside, false);
// Cease handling keyboard input now that the userbar is closed.
// eslint-disable-next-line @typescript-eslint/no-use-before-define
userbar.removeEventListener('keydown', handleUserbarItemsKeyDown, false);
} }
function toggleUserbar(e2) { function toggleUserbar(e2) {
e2.stopPropagation(); e2.stopPropagation();
if (userbar.classList.contains(className)) { if (userbar.classList.contains(isActiveClass)) {
hideUserbar(); hideUserbar();
} else { } else {
showUserbar(); showUserbar(true);
}
}
function setFocusToTrigger() {
setTimeout(() => trigger.focus(), 300);
}
function isFocusOnItems() {
return document.activeElement && !!document.activeElement.closest('.wagtail-userbar-items');
}
function setFocusToFirstItem() {
if (listItems.length > 0) {
setTimeout(() => {
listItems[0].firstElementChild.focus();
}, 100); // Workaround for focus bug
}
}
function setFocusToLastItem() {
if (listItems.length > 0) {
setTimeout(() => {
listItems[listItems.length - 1].firstElementChild.focus();
}, 100); // Workaround for focus bug
}
}
function setFocusToNextItem() {
listItems.forEach((element, idx) => {
// Check which item is currently focused
if (element.firstElementChild === document.activeElement) {
setTimeout(() => {
if (idx + 1 < listItems.length) {
// Focus the next item
listItems[idx + 1].firstElementChild.focus();
} else {
setFocusToFirstItem();
}
}, 100); // Workaround for focus bug
}
});
}
function setFocusToPreviousItem() {
// Check which item is currently focused
listItems.forEach((element, idx) => {
if (element.firstElementChild === document.activeElement) {
setTimeout(() => {
if (idx > 0) {
// Focus the previous item
listItems[idx - 1].firstElementChild.focus();
} else {
setFocusToLastItem();
}
}, 100); // Workaround for focus bug
}
});
}
/**
This handler is responsible for keyboard input when items inside the userbar are focused.
It should only listen when the userbar is open.
It is responsible for:
- Shifting focus using the arrow / home / end keys.
- Closing the menu when 'Escape' is pressed.
*/
function handleUserbarItemsKeyDown(event) {
// Only handle keyboard input if the userbar is open
if (trigger.getAttribute('aria-expanded') === 'true') {
if (event.key === 'Escape') {
hideUserbar();
setFocusToTrigger();
return;
}
if (isFocusOnItems()) {
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
setFocusToNextItem();
break;
case 'ArrowUp':
event.preventDefault();
setFocusToPreviousItem();
break;
case 'Home':
event.preventDefault();
setFocusToFirstItem();
break;
case 'End':
event.preventDefault();
setFocusToLastItem();
break;
default:
break;
}
}
return;
}
}
/**
This handler is responsible for opening the userbar with the arrow keys
if it's focused and not open yet. It should always be listening.
*/
function handleTriggerKeyDown(event) {
// Check if the userbar is focused (but not open yet) and should be opened by keyboard input
if (trigger === document.activeElement && trigger.getAttribute('aria-expanded') === 'false') {
switch (event.key) {
case 'ArrowUp':
event.preventDefault();
showUserbar(false);
// Workaround for focus bug
// Needs extra delay to account for the userbar open animation. Otherwise won't focus properly.
setTimeout(() => setFocusToLastItem(), 300);
break;
case 'ArrowDown':
event.preventDefault();
showUserbar(false);
// Workaround for focus bug
// Needs extra delay to account for the userbar open animation. Otherwise won't focus properly.
setTimeout(() => setFocusToFirstItem(), 300);
break;
default:
break;
}
} }
} }

Wyświetl plik

@ -817,7 +817,7 @@ Hooks for customising the way users are directed through the process of creating
class UserbarPuppyLinkItem: class UserbarPuppyLinkItem:
def render(self, request): def render(self, request):
return '<li><a href="http://cuteoverload.com/tag/puppehs/" ' \ return '<li><a href="http://cuteoverload.com/tag/puppehs/" ' \
+ 'target="_parent" class="action icon icon-wagtail">Puppies!</a></li>' + 'target="_parent" role="menuitem" class="action icon icon-wagtail">Puppies!</a></li>'
@hooks.register('construct_wagtail_userbar') @hooks.register('construct_wagtail_userbar')
def add_puppy_link_item(request, items): def add_puppy_link_item(request, items):

Wyświetl plik

@ -20,6 +20,7 @@ Other features
* Added ``WAGTAIL_WORKFLOW_ENABLED`` setting for enabling / disabling moderation workflows globally (Matt Westcott) * Added ``WAGTAIL_WORKFLOW_ENABLED`` setting for enabling / disabling moderation workflows globally (Matt Westcott)
* Allow specifying ``max_width`` and ``max_height`` on EmbedBlock (Petr Dlouhý) * Allow specifying ``max_width`` and ``max_height`` on EmbedBlock (Petr Dlouhý)
* Add warning when StreamField is used without a StreamFieldPanel (Naomi Morduch Toubman) * Add warning when StreamField is used without a StreamFieldPanel (Naomi Morduch Toubman)
* Added keyboard and screen reader support to Wagtail user bar (LB Johnston, Storm Heg)
Bug fixes Bug fixes
~~~~~~~~~ ~~~~~~~~~
@ -44,3 +45,10 @@ Removed support for Django 2.2
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Django 2.2 is no longer supported as of this release; please upgrade to Django 3.0 or above before upgrading Wagtail. Django 2.2 is no longer supported as of this release; please upgrade to Django 3.0 or above before upgrading Wagtail.
User bar with keyboard and screen reader support
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Wagtail user bar (“edit bird”) widget now supports keyboard and screen reader navigation. To make the most of this, we now recommend placing the widget near the top of the page ``<body>``, so users can reach it without having to go through the whole page. See :ref:`wagtailuserbar_tag` for more information.
For implementers of custom user bar menu items, we also now require the addition of ``role="menuitem"`` on the ``a`` element to provide the correct semantics. See :ref:`construct_wagtail_userbar` for more information.

Wyświetl plik

@ -246,11 +246,22 @@ This tag provides a contextual flyout menu for logged-in users. The menu gives e
This tag may be used on regular Django views, without page object. The user bar will contain one item pointing to the admin. This tag may be used on regular Django views, without page object. The user bar will contain one item pointing to the admin.
We recommend putting the tag near the top of the ``<body>`` element so keyboard users can reach it. You should consider putting the tag after any `skip links <https://webaim.org/techniques/skipnav/>`_ but before the navigation and main content of your page.
.. code-block:: html+django .. code-block:: html+django
{% load wagtailuserbar %} {% load wagtailuserbar %}
... ...
{% wagtailuserbar %} <body>
<a id="#content">Skip to content</a>
{% wagtailuserbar %} {# This is a good place for the userbar #}
<nav>
...
</nav>
<main id="content">
...
</main>
</body>
By default the User Bar appears in the bottom right of the browser window, inset from the edge. If this conflicts with your design it can be moved by passing a parameter to the template tag. These examples show you how to position the userbar in each corner of the screen: By default the User Bar appears in the bottom right of the browser window, inset from the edge. If this conflicts with your design it can be moved by passing a parameter to the template tag. These examples show you how to position the userbar in each corner of the screen:

Wyświetl plik

@ -116,6 +116,7 @@ $positions: (
// stylelint-disable declaration-no-important // stylelint-disable declaration-no-important
.#{$namespace}-userbar-trigger { .#{$namespace}-userbar-trigger {
all: initial;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -124,6 +125,7 @@ $positions: (
margin: 0 !important; margin: 0 !important;
overflow: hidden; overflow: hidden;
background-color: $color-white; background-color: $color-white;
border: 2px solid transparent;
border-radius: 50%; border-radius: 50%;
color: $color-black; color: $color-black;
padding: 0 !important; padding: 0 !important;
@ -134,15 +136,15 @@ $positions: (
text-decoration: none !important; text-decoration: none !important;
position: relative; position: relative;
.#{$namespace}-userbar.touch.is-active &,
.#{$namespace}-userbar.no-touch &:hover {
box-shadow: $box-shadow-props, 0 3px 15px 0 rgba(107, 214, 230, 0.95);
}
.#{$namespace}-userbar-help-text { .#{$namespace}-userbar-help-text {
// Visually hide the help text
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute; position: absolute;
top: 100%; white-space: nowrap;
left: 0; width: 1px;
} }
.#{$namespace}-icon:before { .#{$namespace}-icon:before {
@ -151,9 +153,14 @@ $positions: (
width: auto; width: auto;
margin: 0; margin: 0;
} }
&:focus {
outline: $color-focus-outline solid 3px;
}
} }
.#{$namespace}-userbar-items { .#{$namespace}-userbar-items {
all: revert;
display: block; display: block;
list-style: none; list-style: none;
position: absolute; position: absolute;
@ -182,6 +189,11 @@ $positions: (
transition-duration: 0.15s; transition-duration: 0.15s;
transition-timing-function: cubic-bezier(0.55, 0, 0.1, 1); transition-timing-function: cubic-bezier(0.55, 0, 0.1, 1);
// stylelint-disable-next-line scss/media-feature-value-dollar-variable
@media (prefers-reduced-motion: reduce) {
transition: none !important;
}
.#{$namespace}-userbar.is-active & { .#{$namespace}-userbar.is-active & {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@ -205,6 +217,7 @@ $positions: (
.#{$namespace}-userbar__item { .#{$namespace}-userbar__item {
all: revert;
margin: 0; margin: 0;
background-color: $color-grey-1; background-color: $color-grey-1;
opacity: 0; opacity: 0;
@ -215,6 +228,14 @@ $positions: (
font-size: 16px !important; font-size: 16px !important;
text-decoration: none !important; text-decoration: none !important;
// stylelint-disable-next-line scss/media-feature-value-dollar-variable
@media (prefers-reduced-motion: reduce) {
transition: none !important;
// Force disable transitions for all items
transition-delay: 0s !important;
}
&:first-child { &:first-child {
border-top-left-radius: $userbar-radius; border-top-left-radius: $userbar-radius;
border-top-right-radius: $userbar-radius; border-top-right-radius: $userbar-radius;
@ -242,11 +263,14 @@ $positions: (
&:hover, &:hover,
&:focus { &:focus {
outline: none;
color: $color-white; color: $color-white;
background-color: rgba(100, 100, 100, 0.15); background-color: rgba(100, 100, 100, 0.15);
} }
&:focus {
outline: $color-focus-outline solid 3px;
}
&-icon { &-icon {
@include svg-icon(1.1em, middle); @include svg-icon(1.1em, middle);
margin-right: 0.5em; margin-right: 0.5em;

Wyświetl plik

@ -4,7 +4,7 @@
<div class="wagtail-userbar wagtail-userbar--{{ position|default:'bottom-right' }}" data-wagtail-userbar> <div class="wagtail-userbar wagtail-userbar--{{ position|default:'bottom-right' }}" data-wagtail-userbar>
<link rel="stylesheet" href="{% versioned_static 'wagtailadmin/css/userbar.css' %}" type="text/css" /> <link rel="stylesheet" href="{% versioned_static 'wagtailadmin/css/userbar.css' %}" type="text/css" />
<div class="wagtail-userbar-nav"> <div class="wagtail-userbar-nav">
<div class="wagtail-userbar-trigger" data-wagtail-userbar-trigger> <button aria-controls="wagtail-userbar-items" aria-haspopup="true" class="wagtail-userbar-trigger" id="wagtail-userbar-trigger" data-wagtail-userbar-trigger>
{% block branding_logo %} {% block branding_logo %}
<div style="display: none"> <div style="display: none">
<svg> <svg>
@ -22,13 +22,13 @@
<use href="#icon-wagtail-icon"></use> <use href="#icon-wagtail-icon"></use>
</svg> </svg>
{% endblock %} {% endblock %}
<span class="wagtail-userbar-help-text">{% trans 'Go to Wagtail admin interface' %}</span> <span class="wagtail-userbar-help-text">{% trans 'View Wagtail quick actions' %}</span>
</div> </button>
<div class='wagtail-userbar-items'> <ul aria-labelledby="wagtail-userbar-trigger" class="wagtail-userbar-items" id="wagtail-userbar-items" role="menu">
{% for item in items %} {% for item in items %}
{{ item|safe }} {{ item|safe }}
{% endfor %} {% endfor %}
</div> </ul>
</div> </div>
<script src="{% versioned_static 'wagtailadmin/js/userbar.js' %}"></script> <script src="{% versioned_static 'wagtailadmin/js/userbar.js' %}"></script>
</div> </div>

Wyświetl plik

@ -2,10 +2,8 @@
{% load i18n wagtailadmin_tags %} {% load i18n wagtailadmin_tags %}
{% block item_content %} {% block item_content %}
<div class="wagtail-action"> <a href="{% url 'wagtailadmin_home' %}" target="_parent" class="wagtail-userbar-link" role="menuitem">
<a href="{% url 'wagtailadmin_home' %}" target="_parent"> {% icon name="wagtail-icon" class_name="wagtail-action-icon" %}
{% icon name="wagtail-icon" class_name="wagtail-action-icon" %} {% trans 'Go to Wagtail admin' %}
{% trans 'Go to Wagtail admin' %} </a>
</a>
</div>
{% endblock %} {% endblock %}

Wyświetl plik

@ -1 +1,3 @@
<div class="wagtail-userbar__item {% block item_classes %}{% endblock %}">{% block item_content %}{% endblock %}</div> <li class="wagtail-userbar__item {% block item_classes %}{% endblock %}" role="presentation">
{% block item_content %}{% endblock %}
</li>

Wyświetl plik

@ -2,10 +2,8 @@
{% load i18n wagtailadmin_tags %} {% load i18n wagtailadmin_tags %}
{% block item_content %} {% block item_content %}
<div class="wagtail-action"> <a href="{% url 'wagtailadmin_pages:add_subpage' self.page.id %}" target="_parent" role="menuitem">
<a href="{% url 'wagtailadmin_pages:add_subpage' self.page.id %}" target="_parent"> {% icon name="plus" class_name="wagtail-action-icon" %}
{% icon name="plus" class_name="wagtail-action-icon" %} {% trans 'Add a child page' %}
{% trans 'Add a child page' %} </a>
</a>
</div>
{% endblock %} {% endblock %}

Wyświetl plik

@ -4,11 +4,9 @@
{% block item_content %} {% block item_content %}
<form action="{% url 'wagtailadmin_pages:approve_moderation' self.revision.id %}" target="_parent" method="post"> <form action="{% url 'wagtailadmin_pages:approve_moderation' self.revision.id %}" target="_parent" method="post">
{% csrf_token %} {% csrf_token %}
<div class="wagtail-action"> <button type="submit" value="{% trans 'Approve' %}" class="button" role="menuitem">
<button type="submit" value="{% trans 'Approve' %}" class="button"> {% icon name="tick" class_name="wagtail-action-icon" %}
{% icon name="tick" class_name="wagtail-action-icon" %} {% trans 'Approve' %}
{% trans 'Approve' %} </button>
</button>
</div>
</form> </form>
{% endblock %} {% endblock %}

Wyświetl plik

@ -2,10 +2,8 @@
{% load i18n wagtailadmin_tags %} {% load i18n wagtailadmin_tags %}
{% block item_content %} {% block item_content %}
<div class="wagtail-action"> <a href="{% url 'wagtailadmin_pages:edit' self.page.id %}" target="_parent" role="menuitem">
<a href="{% url 'wagtailadmin_pages:edit' self.page.id %}" target="_parent"> {% icon name="edit" class_name="wagtail-action-icon" %}
{% icon name="edit" class_name="wagtail-action-icon" %} {% trans 'Edit this page' %}
{% trans 'Edit this page' %} </a>
</a> {% endblock %}
</div>
{% endblock %}

Wyświetl plik

@ -2,10 +2,8 @@
{% load i18n wagtailadmin_tags %} {% load i18n wagtailadmin_tags %}
{% block item_content %} {% block item_content %}
<div class="wagtail-action"> <a href="{% url 'wagtailadmin_explore' self.parent_page.id %}" target="_parent" role="menuitem">
<a href="{% url 'wagtailadmin_explore' self.parent_page.id %}" target="_parent"> {% icon name="folder-open-inverse" class_name="wagtail-action-icon" %}
{% icon name="folder-open-inverse" class_name="wagtail-action-icon" %} {% trans 'Show in Explorer' %}
{% trans 'Show in Explorer' %} </a>
</a> {% endblock %}
</div>
{% endblock %}

Wyświetl plik

@ -4,11 +4,9 @@
{% block item_content %} {% block item_content %}
<form action="{% url 'wagtailadmin_pages:reject_moderation' self.revision.id %}" target="_parent" method="post"> <form action="{% url 'wagtailadmin_pages:reject_moderation' self.revision.id %}" target="_parent" method="post">
{% csrf_token %} {% csrf_token %}
<div class="wagtail-action"> <button type="submit" value="{% trans 'Reject' %}" class="button" role="menuitem">
<button type="submit" value="{% trans 'Reject' %}" class="button"> {% icon name="cross" class_name="wagtail-action-icon" %}
{% icon name="cross" class_name="wagtail-action-icon" %} {% trans 'Reject' %}
{% trans 'Reject' %} </button>
</button>
</div>
</form> </form>
{% endblock %} {% endblock %}

Wyświetl plik

@ -111,7 +111,7 @@ class TestUserbarAddLink(TestCase, WagtailTestUtils):
# page allows subpages, so the 'add page' button should show # page allows subpages, so the 'add page' button should show
expected_url = reverse('wagtailadmin_pages:add_subpage', args=(self.event_index.id, )) expected_url = reverse('wagtailadmin_pages:add_subpage', args=(self.event_index.id, ))
needle = f""" needle = f"""
<a href="{expected_url}" target="_parent"> <a href="{expected_url}" target="_parent" role="menuitem">
<svg class="icon icon-plus wagtail-action-icon" aria-hidden="true" focusable="false"> <svg class="icon icon-plus wagtail-action-icon" aria-hidden="true" focusable="false">
<use href="#icon-plus"></use> <use href="#icon-plus"></use>
</svg> </svg>