Hollis Creek Family Dental · Repair report

Exhibit A-4 Evidence: commit adfefb2

Committed · adfefb28045603fa8134677cefb69bacbed030e4

Zoom, focus, contrast and labels were broken

The repository itself is private; this page carries the commit as it is recorded there.

Commit message

Fix A-4: restore zoom, the skip link, contrast, focus outlines and form labels

Root cause: a 2019 copy of header.php in the child theme disabled zoom
(maximum-scale=1, user-scalable=no) and dropped the skip link; child CSS
greyed the menu to #8aa7a2 on white (2.6:1), removed every focus outline,
and the form plugin printed placeholders instead of labels.

Fix: delete the stale header.php copy so ClinicPress's header (zoom
allowed, skip link) is used again, drop the contrast and outline overrides,
and give every form field its <label> back.

Proof: e2e/tests/a4-accessibility.spec.ts (axe-core, WCAG 2.2 A/AA rules,
5 pages at 1280px and 390px, keyboard skip link, form errors).

Exhibit: A-4

The change

Text files only, as recorded in git. Lines starting with + were added, lines with − removed.

Files changed

---
 e2e/tests/a4-accessibility.spec.ts                              | 54 +++++++++++++++++++++++++++++
 .../plugins/hc-appointment-request/hc-appointment-request.php   |  5 ++-
 site/wp-content/themes/hollis-child/header.php                  | 48 -------------------------
 site/wp-content/themes/hollis-child/style.css                   | 17 ---------
 4 files changed, 56 insertions(+), 68 deletions(-)
diff --git a/e2e/tests/a4-accessibility.spec.ts b/e2e/tests/a4-accessibility.spec.ts
new file mode 100644
index 0000000..ebe7967
--- /dev/null
+++ b/e2e/tests/a4-accessibility.spec.ts
@@ -0,0 +1,54 @@
+import { test, expect, Page } from '@playwright/test';
+import AxeBuilder from '@axe-core/playwright';
+import { BEFORE, AFTER } from './helpers';
+
+// Exhibit A-4. axe-core with the WCAG 2.0, 2.1 and 2.2 A/AA rules, on the pages a patient uses.
+const PAGES = ['/', '/our-team/', '/request-appointment/', '/insurance/', '/services/'];
+const TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'];
+
+async function violations(page: Page) {
+  const result = await new AxeBuilder({ page }).withTags(TAGS).analyze();
+  return result.violations.map((v) => `${v.id} (${v.nodes.length})`);
+}
+
+test('A-4 before: axe finds WCAG violations on the home and appointment pages', async ({ page }) => {
+  await page.goto(BEFORE + '/');
+  const home = await violations(page);
+  await page.goto(BEFORE + '/request-appointment/');
+  const form = await violations(page);
+  expect(home.length + form.length).toBeGreaterThan(0);
+  // Zoom is disabled and the form fields have no labels.
+  expect(home.join(' ')).toContain('meta-viewport');
+  expect(form.join(' ')).toMatch(/label/);
+});
+
+for (const width of [1280, 390]) {
+  test(`A-4 after: no WCAG 2.2 AA violations on ${PAGES.length} pages at ${width}px`, async ({ page }) => {
+    await page.setViewportSize({ width, height: 900 });
+    for (const path of PAGES) {
+      await page.goto(AFTER + path);
+      expect(await violations(page), path).toEqual([]);
+    }
+  });
+}
+
+test('A-4 after: a keyboard user reaches the content through a visible skip link', async ({ page }) => {
+  await page.goto(AFTER + '/');
+  await page.keyboard.press('Tab');
+  const skip = page.getByRole('link', { name: 'Skip to content' });
+  await expect(skip).toBeFocused();
+  const box = await skip.boundingBox();
+  expect(box!.y).toBeGreaterThanOrEqual(0);
+  const outline = await skip.evaluate((el) => getComputedStyle(el).outlineStyle);
+  expect(outline).not.toBe('none');
+  await page.keyboard.press('Enter');
+  await expect(page).toHaveURL(/#content$/);
+});
+
+test('A-4 after: the appointment form fields are labelled and an error names the problem', async ({ page }) => {
+  await page.goto(AFTER + '/request-appointment/');
+  await page.getByRole('button', { name: 'Send request' }).click();
+  await expect(page.getByRole('alert')).toContainText('Please check the fields marked below.');
+  await expect(page.getByLabel('Your name')).toHaveAttribute('aria-invalid', 'true');
+  await expect(page.getByText('Please enter your name.')).toBeVisible();
+});
diff --git a/site/wp-content/plugins/hc-appointment-request/hc-appointment-request.php b/site/wp-content/plugins/hc-appointment-request/hc-appointment-request.php
index 8ad0bb1..7122374 100644
--- a/site/wp-content/plugins/hc-appointment-request/hc-appointment-request.php
+++ b/site/wp-content/plugins/hc-appointment-request/hc-appointment-request.php
@@ -194,8 +194,7 @@ function hc_appointment_field( $name, $label, $type, $extra = '', $hint = '' ) {
 	$value    = isset( $state['values'][ $name ] ) ? $state['values'][ $name ] : '';
 	$error    = isset( $state['errors'][ $name ] ) ? $state['errors'][ $name ] : '';
 	$describe = array();
-	$html     = '<p class="form-field' . ( $error ? ' has-error' : '' ) . '">';
-	$extra   .= ' placeholder="' . esc_attr( $label ) . '"';
+	$html     = '<p class="form-field' . ( $error ? ' has-error' : '' ) . '"><label for="' . esc_attr( $id ) . '">' . esc_html( $label ) . '</label>';
 	if ( $hint ) {
 		$html      .= '<span class="field-hint" id="' . esc_attr( $id ) . '-hint">' . esc_html( $hint ) . '</span>';
 		$describe[] = $id . '-hint';
@@ -224,7 +223,7 @@ function hc_appointment_field( $name, $label, $type, $extra = '', $hint = '' ) {
 function hc_appointment_select( $name, $label ) {
 	$state   = hc_appointment_state();
 	$current = isset( $state['values'][ $name ] ) ? $state['values'][ $name ] : '';
-	$html    = '<p class="form-field"><select id="hc-' . esc_attr( $name ) . '" name="hc_' . esc_attr( $name ) . '">';
+	$html    = '<p class="form-field"><label for="hc-' . esc_attr( $name ) . '">' . esc_html( $label ) . '</label><select id="hc-' . esc_attr( $name ) . '" name="hc_' . esc_attr( $name ) . '">';
 	foreach ( hc_appointment_choices( $name ) as $key => $text ) {
 		$html .= '<option value="' . esc_attr( $key ) . '"' . selected( $current, $key, false ) . '>' . esc_html( $text ) . '</option>';
 	}
diff --git a/site/wp-content/themes/hollis-child/header.php b/site/wp-content/themes/hollis-child/header.php
deleted file mode 100644
index b924334..0000000
--- a/site/wp-content/themes/hollis-child/header.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-/**
- * Site header (child copy, 2019).
- *
- * @package HollisChild
- */
-
-defined( 'ABSPATH' ) || exit;
-?><!doctype html>
-<html <?php language_attributes(); ?>>
-<head>
-<meta charset="<?php bloginfo( 'charset' ); ?>">
-<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
-<?php wp_head(); ?>
-</head>
-<body <?php body_class(); ?>>
-<?php wp_body_open(); ?>
-<?php clinicpress_demo_notice(); ?>
-<div class="utility-bar">
-	<div class="wrap utility-inner">
-		<?php if ( clinicpress_contact( 'phone' ) ) : ?>
-			<a class="utility-phone" href="<?php echo esc_url( clinicpress_contact( 'phone_href' ) ); ?>"><?php echo esc_html( clinicpress_contact( 'phone' ) ); ?></a>
-		<?php endif; ?>
-		<span class="utility-address"><?php echo esc_html( clinicpress_contact( 'address' ) ); ?></span>
-		<?php clinicpress_language_links(); ?>
-	</div>
-</div>
-<header class="site-header">
-	<div class="wrap header-inner">
-		<?php clinicpress_brand(); ?>
-		<button class="menu-toggle" type="button" aria-expanded="false" aria-controls="site-navigation" hidden><?php esc_html_e( 'Menu', 'clinicpress' ); ?></button>
-		<nav id="site-navigation" class="main-nav" aria-label="<?php esc_attr_e( 'Primary', 'clinicpress' ); ?>">
-			<?php
-			wp_nav_menu(
-				array(
-					'theme_location' => 'primary',
-					'container'      => false,
-					'menu_id'        => 'primary-menu',
-					'fallback_cb'    => false,
-					'depth'          => 1,
-				)
-			);
-			?>
-		</nav>
-		<a class="button button-cta header-cta" href="<?php echo esc_url( clinicpress_contact( 'cta_url' ) ); ?>"><?php esc_html_e( 'Request an appointment', 'clinicpress' ); ?></a>
-	</div>
-</header>
-<main id="content" class="site-main" tabindex="-1">
diff --git a/site/wp-content/themes/hollis-child/style.css b/site/wp-content/themes/hollis-child/style.css
index a98d61e..f3fee66 100644
--- a/site/wp-content/themes/hollis-child/style.css
+++ b/site/wp-content/themes/hollis-child/style.css
@@ -8,23 +8,6 @@ License: GPL-2.0-or-later
 Text Domain: hollis-child
 */
 
-.main-nav a {
-	color: #8aa7a2;
-	font-weight: 400;
-}
-
-.utility-address {
-	color: #9fb0ad;
-}
-
-a:focus,
-button:focus,
-input:focus,
-select:focus,
-textarea:focus {
-	outline: none;
-}
-
 .brand-mark {
 	flex: none;
 }

The proving test

e2e/tests/a4-accessibility.spec.ts

e2e/tests/a4-accessibility.spec.ts, as committed.

import { test, expect, Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import { BEFORE, AFTER } from './helpers';

// Exhibit A-4. axe-core with the WCAG 2.0, 2.1 and 2.2 A/AA rules, on the pages a patient uses.
const PAGES = ['/', '/our-team/', '/request-appointment/', '/insurance/', '/services/'];
const TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'];

async function violations(page: Page) {
  const result = await new AxeBuilder({ page }).withTags(TAGS).analyze();
  return result.violations.map((v) => `${v.id} (${v.nodes.length})`);
}

test('A-4 before: axe finds WCAG violations on the home and appointment pages', async ({ page }) => {
  await page.goto(BEFORE + '/');
  const home = await violations(page);
  await page.goto(BEFORE + '/request-appointment/');
  const form = await violations(page);
  expect(home.length + form.length).toBeGreaterThan(0);
  // Zoom is disabled, the menu fails contrast and the form's selects have no accessible name.
  expect(home.join(' ')).toContain('meta-viewport');
  expect(home.join(' ')).toContain('color-contrast');
  expect(form.join(' ')).toMatch(/select-name|label/);
});

for (const width of [1280, 390]) {
  test(`A-4 after: no WCAG 2.2 AA violations on ${PAGES.length} pages at ${width}px`, async ({ page }) => {
    await page.setViewportSize({ width, height: 900 });
    for (const path of PAGES) {
      await page.goto(AFTER + path);
      expect(await violations(page), path).toEqual([]);
    }
  });
}

test('A-4 after: a keyboard user reaches the content through a visible skip link', async ({ page }) => {
  await page.goto(AFTER + '/');
  await page.keyboard.press('Tab');
  const skip = page.getByRole('link', { name: 'Skip to content' });
  await expect(skip).toBeFocused();
  const box = await skip.boundingBox();
  expect(box!.y).toBeGreaterThanOrEqual(0);
  const outline = await skip.evaluate((el) => getComputedStyle(el).outlineStyle);
  expect(outline).not.toBe('none');
  await page.keyboard.press('Enter');
  await expect(page).toHaveURL(/#content$/);
});

test('A-4 after: the appointment form fields are labelled and an error names the problem', async ({ page }) => {
  await page.goto(AFTER + '/request-appointment/');
  await page.getByRole('button', { name: 'Send request' }).click();
  await expect(page.getByRole('alert')).toContainText('Please check the fields marked below.');
  await expect(page.getByLabel('Your name')).toHaveAttribute('aria-invalid', 'true');
  await expect(page.getByText('Please enter your name.')).toBeVisible();
});

Back to Exhibit A-4