Content checks design upgrade: help text and separate cards (#12090)

Co-authored-by: Thibaud Colas <thibaudcolas@gmail.com>
pull/12160/head
Albina 2024-07-17 00:27:27 +03:00 zatwierdzone przez GitHub
rodzic 62674d3fbb
commit 2d568dd825
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
10 zmienionych plików z 144 dodań i 141 usunięć

Wyświetl plik

@ -23,6 +23,7 @@ Changelog
* Make `routable_resolver_match` attribute available on RoutablePageMixin responses (Andy Chosak)
* Support customizations to `UserViewSet` via the app config (Sage Abdullah)
* Add word count and reading time metrics within the page editor (Albina Starykova. Sponsored by The Motley Fool)
* Implement a new design for accessibility checks (Albina Starykova)
* Fix: Make `WAGTAILIMAGES_CHOOSER_PAGE_SIZE` setting functional again (Rohit Sharma)
* Fix: Enable `richtext` template tag to convert lazy translation values (Benjamin Bach)
* Fix: Ensure permission labels on group permissions page are translated where available (Matt Westcott)

Wyświetl plik

@ -1,10 +1,12 @@
.w-a11y-result__row {
@include box;
padding: theme('spacing.4');
display: flex;
justify-content: space-between;
}
.w-a11y-result__header {
margin: 0;
margin: 0 0 theme('spacing.[0.5]');
width: 100%;
display: flex;
justify-content: space-between;
@ -21,40 +23,34 @@
font-weight: theme('fontWeight.semibold');
}
.w-a11y-result__container {
display: flex;
flex-wrap: wrap;
gap: theme('spacing.[2.5]');
padding-top: theme('spacing.3');
}
.w-a11y-result__subtotal_count {
color: theme('colors.icon-primary');
width: theme('spacing.5');
text-align: center;
font-size: theme('fontSize.11');
font-weight: theme('fontWeight.normal');
.w-a11y-result__help {
color: theme('colors.text-placeholder');
font-size: theme('fontSize.14');
.w-dialog--userbar & {
font-size: theme('fontSize.14');
font-size: theme('fontSize.16');
}
}
.w-a11y-result__selector {
display: flex;
align-items: center;
background: theme('colors.surface-field-inactive');
color: theme('colors.text-context');
justify-content: center;
background: theme('colors.surface-page');
border-radius: theme('borderRadius.DEFAULT');
padding: theme('spacing.[1.5]');
margin-top: calc(theme('spacing.[2.5]') * -1);
margin-inline-end: calc(theme('spacing.[2.5]') * -1);
height: theme('spacing.[7.5]');
width: theme('spacing.[7.5]');
&:hover,
&:focus {
background: theme('colors.surface-button-default');
color: theme('colors.text-button');
background: theme('colors.surface-header');
.w-a11y-result__icon {
fill: theme('colors.text-button');
@apply w-scale-110;
fill: theme('colors.text-context');
}
}
@ -64,11 +60,12 @@
}
.w-a11y-result__icon {
@apply w-transition hover:w-transform;
flex-shrink: 0;
fill: theme('colors.surface-button-default');
height: theme('spacing.[3.5]');
width: theme('spacing.[3.5]');
margin-inline-end: theme('spacing.[1.5]');
}
.w-a11y-result__count {
@ -79,7 +76,6 @@
background-color: theme('colors.positive.100');
border-radius: theme('borderRadius.full');
font-size: theme('fontSize.11');
line-height: theme('lineHeight.none');
height: theme('spacing.4');
width: theme('spacing.4');
color: theme('colors.text-button');
@ -91,4 +87,8 @@
@media (forced-colors: active) {
border: theme('spacing.px') solid ButtonText;
}
.w-userbar & {
line-height: theme('lineHeight.none');
}
}

Wyświetl plik

@ -29,9 +29,6 @@ const runContentChecks = async () => {
const runAccessibilityChecks = async (onClickSelector) => {
const a11yRowTemplate = document.querySelector('#w-a11y-result-row-template');
const a11ySelectorTemplate = document.querySelector(
'#w-a11y-result-selector-template',
);
const checksPanel = document.querySelector('[data-checks-panel]');
const config = getAxeConfiguration(document.body);
const toggleCounter = document.querySelector(
@ -41,13 +38,7 @@ const runAccessibilityChecks = async (onClickSelector) => {
'[data-side-panel="checks"] [data-a11y-result-count]',
);
if (
!a11yRowTemplate ||
!a11ySelectorTemplate ||
!config ||
!toggleCounter ||
!panelCounter
) {
if (!a11yRowTemplate || !config || !toggleCounter || !panelCounter) {
return;
}
@ -75,7 +66,6 @@ const runAccessibilityChecks = async (onClickSelector) => {
results,
config,
a11yRowTemplate,
a11ySelectorTemplate,
onClickSelector,
);
};

Wyświetl plik

@ -40,10 +40,15 @@ export const sortAxeViolations = (violations: Result[]) =>
* Wagtail's Axe configuration object. This should reflect what's returned by
* `wagtail.admin.userbar.AccessibilityItem.get_axe_configuration()`.
*/
interface ErrorMessage {
error_name: string;
help_text: string;
}
export interface WagtailAxeConfiguration {
context: ElementContext;
options: RunOptions;
messages: Record<string, string>;
messages: Record<string, ErrorMessage>;
spec: Spec;
}
@ -151,7 +156,6 @@ export const renderA11yResults = (
results: AxeResults,
config: WagtailAxeConfiguration,
a11yRowTemplate: HTMLTemplateElement,
a11ySelectorTemplate: HTMLTemplateElement,
onClickSelector: (selectorName: string, event: MouseEvent) => void,
) => {
// Reset contents ahead of rendering new results.
@ -160,54 +164,47 @@ export const renderA11yResults = (
if (results.violations.length) {
const sortedViolations = sortAxeViolations(results.violations);
sortedViolations.forEach((violation, violationIndex) => {
container.appendChild(a11yRowTemplate.content.cloneNode(true));
const currentA11yRow = container.querySelectorAll<HTMLDivElement>(
'[data-a11y-result-row]',
)[violationIndex];
let nodeCounter = 0;
sortedViolations.forEach((violation) => {
violation.nodes.forEach((node) => {
container.appendChild(a11yRowTemplate.content.cloneNode(true));
const a11yErrorName = currentA11yRow.querySelector(
'[data-a11y-result-name]',
) as HTMLSpanElement;
a11yErrorName.id = `w-a11y-result__name-${violationIndex}`;
// Display custom error messages supplied by Wagtail if available,
// fallback to default error message from Axe
a11yErrorName.textContent =
config.messages[violation.id] || violation.help;
const a11yErrorCount = currentA11yRow.querySelector(
'[data-a11y-result-count]',
) as HTMLSpanElement;
a11yErrorCount.textContent = `${violation.nodes.length}`;
const currentA11yRow = container.querySelectorAll<HTMLDivElement>(
'[data-a11y-result-row]',
)[nodeCounter];
nodeCounter += 1;
const a11yErrorContainer = currentA11yRow.querySelector(
'[data-a11y-result-container]',
) as HTMLDivElement;
violation.nodes.forEach((node, nodeIndex) => {
a11yErrorContainer.appendChild(
a11ySelectorTemplate.content.cloneNode(true),
);
const currentA11ySelector =
a11yErrorContainer.querySelectorAll<HTMLButtonElement>(
'[data-a11y-result-selector]',
)[nodeIndex];
currentA11ySelector.setAttribute('aria-describedby', a11yErrorName.id);
const currentA11ySelectorText = currentA11ySelector.querySelector(
'[data-a11y-result-selector-text]',
const a11yErrorName = currentA11yRow.querySelector(
'[data-a11y-result-name]',
) as HTMLSpanElement;
const a11yErrorHelp = currentA11yRow.querySelector(
'[data-a11y-result-help]',
) as HTMLDivElement;
a11yErrorName.id = `w-a11y-result__name-${nodeCounter}`;
// Display custom error messages supplied by Wagtail if available,
// fallback to default error message from Axe
const messages = config.messages[violation.id];
const name =
(typeof messages === 'string' ? messages : messages?.error_name) ||
violation.help;
a11yErrorName.textContent = name;
a11yErrorHelp.textContent =
messages?.help_text || violation.description;
// Special-case when displaying accessibility results within the admin interface.
const selectorName = toSelector(
node.target[0] === '#preview-iframe'
? node.target[1]
: node.target[0],
);
// Remove unnecessary details before displaying selectors to the user
currentA11ySelectorText.textContent = selectorName.replace(
/\[data-block-key="\w{5}"\]/,
'',
);
currentA11ySelector.addEventListener(
const a11ySelector = currentA11yRow.querySelector(
'[data-a11y-result-selector]',
) as HTMLButtonElement;
a11ySelector.setAttribute('aria-describedby', a11yErrorName.id);
a11ySelector?.addEventListener(
'click',
onClickSelector.bind(null, selectorName),
);

Wyświetl plik

@ -362,21 +362,12 @@ export class Userbar extends HTMLElement {
const a11yRowTemplate = this.shadowRoot.querySelector<HTMLTemplateElement>(
'#w-a11y-result-row-template',
);
const a11ySelectorTemplate =
this.shadowRoot.querySelector<HTMLTemplateElement>(
'#w-a11y-result-selector-template',
);
const a11yOutlineTemplate =
this.shadowRoot.querySelector<HTMLTemplateElement>(
'#w-a11y-result-outline-template',
);
if (
!accessibilityResultsBox ||
!a11yRowTemplate ||
!a11ySelectorTemplate ||
!a11yOutlineTemplate
) {
if (!accessibilityResultsBox || !a11yRowTemplate || !a11yOutlineTemplate) {
return;
}
@ -460,7 +451,6 @@ export class Userbar extends HTMLElement {
results,
config,
a11yRowTemplate,
a11ySelectorTemplate,
onClickSelector,
);
} else {

Wyświetl plik

@ -43,7 +43,7 @@ This feature was developed by Albina Starykova and sponsored by The Motley Fool.
* Implement universal listings UI for report views (Sage Abdullah)
* Make `routable_resolver_match` attribute available on RoutablePageMixin responses (Andy Chosak)
* Support customizations to `UserViewSet` via the app config (Sage Abdullah)
* Implement a new design for accessibility checks (Albina Starykova, sponsored by The Motley Fool)
### Bug fixes

Wyświetl plik

@ -1,20 +1,18 @@
{% load i18n wagtailadmin_tags %}
<template id="w-a11y-result-row-template">
<div class="w-a11y-result__row" data-a11y-result-row>
<h3 class="w-a11y-result__header">
<span class="w-a11y-result__name" data-a11y-result-name></span>
<span class="w-sr-only">{% trans 'Issues found' %}</span><span class="w-a11y-result__subtotal_count" data-a11y-result-count></span>
</h3>
<div class="w-a11y-result__container" data-a11y-result-container></div>
<div>
<h3 class="w-a11y-result__header">
<span class="w-a11y-result__name" data-a11y-result-name></span>
</h3>
<div class="w-a11y-result__help" data-a11y-result-help></div>
</div>
<button class="w-a11y-result__selector"
data-a11y-result-selector
aria-label="{% trans 'Show issue' %}"
type="button">{% icon name="crosshairs" classname="w-a11y-result__icon" %}</button>
</div>
</template>
<template id="w-a11y-result-selector-template">
<button class="w-a11y-result__selector" data-a11y-result-selector type="button">
{% icon name="link-external" classname="w-a11y-result__icon" %}
<span data-a11y-result-selector-text></span>
</button>
</template>
<div class="w-divide-y w-divide-border-furniture w-py-6 w-pl-2 lg:w-pl-8">
<div>
<h2 class="w-my-5 w-text-16 w-font-bold w-text-text-label">
@ -31,8 +29,8 @@
</div>
</div>
</div>
<div>
<h2 class="w-flex w-items-center w-gap-2 w-my-5 w-text-16 w-font-bold w-text-text-label">
<div class="w-mt-3">
<h2 class="w-flex w-items-center w-gap-2 w-my-5 w-text-16 w-font-bold">
<span>{% trans 'Issues found' %}</span><span class="w-a11y-result__count" data-a11y-result-count>0</span>
</h2>
<div class="w-flex w-flex-col w-gap-2.5" data-checks-panel></div>

Wyświetl plik

@ -17,19 +17,17 @@
{# Contents of the dialog created in JS based on these templates. #}
<template id="w-a11y-result-row-template">
<div class="w-a11y-result__row" data-a11y-result-row>
<h3 class="w-a11y-result__header">
<span class="w-a11y-result__name" data-a11y-result-name></span>
<span class="w-sr-only">{% trans 'Issues found' %}</span><span class="w-a11y-result__subtotal_count" data-a11y-result-count></span>
</h3>
<div class="w-a11y-result__container" data-a11y-result-container></div>
<div>
<h3 class="w-a11y-result__header">
<span class="w-a11y-result__name" data-a11y-result-name></span>
</h3>
<div class="w-a11y-result__help" data-a11y-result-help></div>
</div>
<button class="w-a11y-result__selector" data-a11y-result-selector aria-label="{% trans 'Show issue' %}" type="button">
{% icon name="crosshairs" classname="w-a11y-result__icon" %}
</button>
</div>
</template>
<template id="w-a11y-result-selector-template">
<button class="w-a11y-result__selector" data-a11y-result-selector type="button">
{% icon name="crosshairs" classname="w-a11y-result__icon" %}
<span data-a11y-result-selector-text></span>
</button>
</template>
<template id="w-a11y-result-outline-template">
<div class="w-a11y-result__outline" data-a11y-result-outline></div>
</template>

Wyświetl plik

@ -208,22 +208,32 @@ class TestAccessibilityCheckerConfig(WagtailTestUtils, TestCase):
config = self.get_config()
self.assertIsInstance(config.get("messages"), dict)
self.assertEqual(
config["messages"]["empty-heading"],
"Empty heading found. Use meaningful text for screen reader users.",
config["messages"]["empty-heading"]["error_name"],
"Empty heading found",
)
self.assertEqual(
config["messages"]["empty-heading"]["help_text"],
"Use meaningful text for screen reader users",
)
def test_custom_message(self):
class CustomMessageAccessibilityItem(AccessibilityItem):
# Override via class attribute
axe_messages = {
"empty-heading": "Headings should not be empty!",
"empty-heading": {
"error_name": "Headings should not be empty!",
"help_text": "Use meaningful text!",
},
}
# Override via method
def get_axe_messages(self, request):
return {
**super().get_axe_messages(request),
"color-contrast-enhanced": "Increase colour contrast!",
"color-contrast-enhanced": {
"error_name": "Insufficient colour contrast!",
"help_text": "Ensure contrast ratio of at least 4.5:1",
},
}
with hooks.register_temporarily(
@ -234,8 +244,14 @@ class TestAccessibilityCheckerConfig(WagtailTestUtils, TestCase):
self.assertEqual(
config["messages"],
{
"empty-heading": "Headings should not be empty!",
"color-contrast-enhanced": "Increase colour contrast!",
"empty-heading": {
"error_name": "Headings should not be empty!",
"help_text": "Use meaningful text!",
},
"color-contrast-enhanced": {
"error_name": "Insufficient colour contrast!",
"help_text": "Ensure contrast ratio of at least 4.5:1",
},
},
)

Wyświetl plik

@ -92,29 +92,42 @@ class AccessibilityItem(BaseItem):
#: to use as the error messages. If an enabled rule does not exist in this
#: dictionary, Axe's error message for the rule will be used as fallback.
axe_messages = {
"button-name": _(
"Button text is empty. Use meaningful text for screen reader users."
),
"empty-heading": _(
"Empty heading found. Use meaningful text for screen reader users."
),
"empty-table-header": _(
"Table header text is empty. Use meaningful text for screen reader users."
),
"frame-title": _(
"Empty frame title found. Use a meaningful title for screen reader users."
),
"heading-order": _("Incorrect heading hierarchy. Avoid skipping levels."),
"input-button-name": _(
"Input button text is empty. Use meaningful text for screen reader users."
),
"link-name": _(
"Link text is empty. Use meaningful text for screen reader users."
),
"p-as-heading": _("Misusing paragraphs as headings. Use proper heading tags."),
"alt-text-quality": _(
"Image alt text has inappropriate pattern. Use meaningful text."
),
"button-name": {
"error_name": _("Button text is empty"),
"help_text": _("Use meaningful text for screen reader users"),
},
"empty-heading": {
"error_name": _("Empty heading found"),
"help_text": _("Use meaningful text for screen reader users"),
},
"empty-table-header": {
"error_name": _("Table header text is empty"),
"help_text": _("Use meaningful text for screen reader users"),
},
"frame-title": {
"error_name": _("Empty frame title found"),
"help_text": _("Use a meaningful title for screen reader users"),
},
"heading-order": {
"error_name": _("Incorrect heading hierarchy"),
"help_text": _("Avoid skipping levels"),
},
"input-button-name": {
"error_name": _("Input button text is empty"),
"help_text": _("Use meaningful text for screen reader users"),
},
"link-name": {
"error_name": _("Link text is empty"),
"help_text": _("Use meaningful text for screen reader users"),
},
"p-as-heading": {
"error_name": _("Misusing paragraphs as headings"),
"help_text": _("Use proper heading tags"),
},
"alt-text-quality": {
"error_name": _("Image alt text has inappropriate pattern"),
"help_text": _("Use meaningful text"),
},
}
def get_axe_include(self, request):