From 537df5b43b4d7e72f370581f123db78f779eb26a Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 25 Oct 2023 13:40:22 +0200 Subject: [PATCH] Fix a11y for the slides, add a11y acceptance test. --- acceptance/cypress.config.js | 8 +++++ acceptance/cypress/support/commands.js | 22 +++++++++++++ acceptance/cypress/tests/block.cy.js | 43 ++++++++++++++++++++++++++ src/components/DefaultBody.jsx | 4 ++- src/components/View.jsx | 43 ++++++++++++++++---------- src/theme/main.less | 13 ++++++++ 6 files changed, 115 insertions(+), 18 deletions(-) diff --git a/acceptance/cypress.config.js b/acceptance/cypress.config.js index d841d7a..407293e 100644 --- a/acceptance/cypress.config.js +++ b/acceptance/cypress.config.js @@ -5,5 +5,13 @@ module.exports = defineConfig({ e2e: { baseUrl: 'http://localhost:3000', specPattern: 'cypress/tests/*.cy.{js,jsx}', + setupNodeEvents(on, config) { + on('task', { + table(message) { + console.table(message); + return null; + }, + }); + }, }, }); diff --git a/acceptance/cypress/support/commands.js b/acceptance/cypress/support/commands.js index 44a5000..929e9e0 100644 --- a/acceptance/cypress/support/commands.js +++ b/acceptance/cypress/support/commands.js @@ -1 +1,23 @@ import '@plone/volto-testing/cypress/support/commands'; + +// Print cypress-axe violations to the terminal +function printAccessibilityViolations(violations) { + cy.task( + 'table', + violations.map(({ id, impact, description, nodes }) => ({ + impact, + description: `${description} (${id})`, + nodes: nodes.length, + })), + ); +} + +Cypress.Commands.add( + 'checkAccessibility', + (subject, { skipFailures = false } = {}) => { + cy.checkA11y(subject, null, printAccessibilityViolations, skipFailures); + }, + { + prevSubject: 'optional', + }, +); diff --git a/acceptance/cypress/tests/block.cy.js b/acceptance/cypress/tests/block.cy.js index 1c3d5b0..dda3fc1 100644 --- a/acceptance/cypress/tests/block.cy.js +++ b/acceptance/cypress/tests/block.cy.js @@ -63,4 +63,47 @@ context('Block Acceptance Tests', () => { cy.get('.teaser-item-title').should('be.visible').contains('My Page'); }); + + it('a11y', () => { + cy.intercept('PATCH', '/**').as('save'); + cy.intercept('GET', `/**/*?expand*`).as('content'); + + cy.visit('/document/edit'); + cy.addNewBlock('slider'); + + // First slide + cy.get( + '.objectbrowser-field[aria-labelledby="fieldset-default-field-label-href-0-slides-0"] button[aria-label="Open object browser"]', + ).click(); + cy.get('aside .breadcrumbs svg.home-icon').click(); + cy.findByLabelText('Select My Page').dblclick(); + + // Second slide + cy.findByText('Add item').click(); + // cy.findByLabelText('Show item #2').click(); + cy.get( + '.objectbrowser-field[aria-labelledby="fieldset-default-field-label-href-0-slides-1"] button[aria-label="Open object browser"]', + ).should('be.visible'); + cy.get( + '.objectbrowser-field[aria-labelledby="fieldset-default-field-label-href-0-slides-1"] button[aria-label="Open object browser"]', + ).click(); + cy.get('aside .breadcrumbs svg.home-icon').click(); + cy.findByLabelText('Select My Page').dblclick(); + + cy.get('#toolbar-save').click(); + cy.wait('@save'); + cy.wait('@content'); + + cy.get('.highlight-image-wrapper img') + .should('be.visible') + .and(($img) => { + // "naturalWidth" and "naturalHeight" are set when the image loads + expect($img[0].naturalWidth).to.be.greaterThan(0); + }); + + cy.get('.teaser-item-title').should('be.visible').contains('My Page'); + + cy.injectAxe(); + cy.checkAccessibility(); + }); }); diff --git a/src/components/DefaultBody.jsx b/src/components/DefaultBody.jsx index 09463e9..76a3c92 100644 --- a/src/components/DefaultBody.jsx +++ b/src/components/DefaultBody.jsx @@ -34,6 +34,7 @@ const SliderBody = ({ dataBlock, isEditMode, openObjectBrowser, + isActive, }) => { const intl = useIntl(); const href = data.href?.[0]; @@ -67,6 +68,7 @@ const SliderBody = ({
{!href && isEditMode && ( @@ -101,7 +103,7 @@ const SliderBody = ({ ? '_blank' : null } - tabIndex="-1" + tabIndex={!isActive ? '-1' : null} > {(href?.hasPreviewImage || href.image_field || image) && (
diff --git a/src/components/View.jsx b/src/components/View.jsx index 68465f2..e8cf146 100644 --- a/src/components/View.jsx +++ b/src/components/View.jsx @@ -25,6 +25,7 @@ const PrevArrow = ({ className, style, onClick }) => ( className={className} style={{ ...style, display: 'block' }} onClick={onClick} + aria-label="previous" > @@ -35,6 +36,7 @@ const NextArrow = ({ className, style, onClick }) => ( className={className} style={{ ...style, display: 'block' }} onClick={onClick} + aria-label="next" > @@ -53,6 +55,11 @@ const SliderView = (props) => { } = props; const intl = useIntl(); + // These are the local state in case of view mode + // The ones that control the edit need to be above since they have + // to be drilled down to here AND to the sidebar + const [slideViewIndex, setSlideViewIndex] = React.useState(0); + const sliderRef = React.useRef(); if (sliderRef.current && isEditMode) { @@ -120,26 +127,28 @@ const SliderView = (props) => { // This syncs the current slide with the SliderContext state // responding to the slide change event from the slider itself // (the dots or the arrows) - // There's also the option of doing it before instead than after: - // beforeChange={(current, next) => setSlideIndex(next)} afterChange={(current) => isEditMode && setSlideIndex(current)} + beforeChange={(current) => setSlideViewIndex(current)} > {data.slides && - data.slides.map((item, index) => ( -
- -
- ))} + data.slides.map((item, index) => { + return ( +
+ +
+ ); + })} )}
diff --git a/src/theme/main.less b/src/theme/main.less index d472e36..d97324d 100644 --- a/src/theme/main.less +++ b/src/theme/main.less @@ -78,3 +78,16 @@ flex-direction: column; } } + +// a11y hide the ones that are not visible +.slick-slide { + visibility: hidden; + + &:not(.slick-cloned) .slide-visible { + visibility: visible; + } +} + +.slick-active { + visibility: visible; +}