Shahi LegalFlowSuite

Custom Accessibility Fixers Development

Overview

Create custom accessibility fixers to extend the 96 built-in fixers with your own accessibility solutions.

Fixer Architecture

All fixers inherit from BaseFixer and implement the fixer interface.

Create a Custom Fixer

Step 1: Create Fixer Class

`php
wcagcriterion(): string
{
return ‘2.5.3’; // Level AAA
}

/**
* Get severity level
*/
public function get_severity(): string
{
return ‘major’; // critical, major, minor
}

/**
* Check if issue exists
*/
public function check_element($element): bool
{
// Check if button needs a label
if ($element->tagName !== ‘button’) {
return false;
}

$text = trim($element->textContent);
$aria_label = $element->getAttribute(‘aria-label’);

// Return true if button has no text and no aria-label
return empty($text) && empty($aria_label);
}

/**
* Fix the accessibility issue
*/
public function fix($content): string
{
// Use DOMDocument to parse
$dom = new \DOMDocument(‘1.0’, ‘UTF-8’);
@$dom->loadHTML(
‘ . $content,
LIBXMLHTMLNOXMLATT
);

if (!$dom->documentElement) {
return $content;
}

$xpath = new \DOMXPath($dom);

// Find all buttons without text or aria-label
$buttons = $xpath->query(‘//button’);

foreach ($buttons as $button) {
if ($this->check_element($button)) {
// Try to infer label from context
$label = $this->inferbuttonlabel($button);

if (!empty($label)) {
$button->setAttribute(‘aria-label’, $label);
}
}
}

// Return fixed HTML
$fixed = ”;
if ($dom->documentElement) {
foreach ($dom->documentElement->childNodes as $node) {
$fixed .= $dom->saveHTML($node);
}
}

return !empty($fixed) ? $fixed : $content;
}

/**
* Infer button label from context
*/
private function inferbuttonlabel($button): string
{
// Check for icon content
$icon = $button->querySelector(‘i, svg, img’);
if ($icon) {
$classes = $icon->getAttribute(‘class’);
if (strpos($classes, ‘fa-close’) !== false) {
return ‘Close’;
}
if (strpos($classes, ‘fa-menu’) !== false) {
return ‘Menu’;
}
if (strpos($classes, ‘fa-search’) !== false) {
return ‘Search’;
}
}

// Check button position/context
$parent = $button->parentNode;
while ($parent) {
$parent_id = $parent->getAttribute(‘id’);
$parent_class = $parent->getAttribute(‘class’);

if (strpos($parent_class, ‘modal-header’) !== false) {
return ‘Close Dialog’;
}

if (strpos($parent_class, ‘search-form’) !== false) {
return ‘Submit Search’;
}

$parent = $parent->parentNode;
}

return ‘Button’; // Fallback
}

/**
* Get test cases for this fixer
*/
public static function gettestcases(): array
{
return [
[
‘input’ => ‘‘,
‘expected’ => ‘Close’,
‘description’ => ‘Close button with icon’
],
[
‘input’ => ‘‘,
‘expected’ => ‘Button’,
‘description’ => ‘Button with SVG icon’
],
[
‘input’ => ‘‘,
‘expected’ => false, // Don’t fix if already has label
‘description’ => ‘Button already has aria-label’
]
];
}
}
`

Step 2: Register Your Fixer

`php
// In your plugin initialization
addfilter(‘slosaccessibility_fixers’, function($fixers) {
$fixers[‘custom-button-label’] = ‘YourNamespace\Accessibility\Fixers\CustomButtonLabelFixer’;
return $fixers;
});
`

Step 3: Register Test Fixtures

`php
addaction(‘slosfixertests’, function($testrunner) {
$testrunner->registertests(
‘CustomButtonLabelFixer’,
CustomButtonLabelFixer::gettestcases()
);
});
`

BaseFixer Methods

Required Methods

`php
public function get_id(): string
// Return unique fixer identifier

public function get_description(): string
// Return human-readable description

public function fix($content): string
// Return fixed HTML content

public function check_element($element): bool
// Return true if element has issue
`

Optional Methods

`php
public function getwcagcriterion(): string
// Return WCAG reference (e.g., ‘2.5.3’)
// Default: Not specified

public function get_severity(): string
// Return ‘critical’, ‘major’, or ‘minor’
// Default: ‘major’

public function requires_javascript(): bool
// Return true if needs JavaScript fix
// Default: false

public function getperformanceimpact(): string
// Return ‘low’, ‘medium’, ‘high’
// Default: ‘low’

public static function gettestcases(): array
// Return array of test cases
// Default: empty array
`

Fixer Patterns

Pattern 1: Simple Attribute Addition

`php
public function fix($content): string
{
$dom = new \DOMDocument(‘1.0’, ‘UTF-8’);
@$dom->loadHTML(‘‘ . $content);

$xpath = new \DOMXPath($dom);
$elements = $xpath->query(‘//div[@role=”alert”]’);

foreach ($elements as $element) {
if (!$element->hasAttribute(‘aria-live’)) {
$element->setAttribute(‘aria-live’, ‘polite’);
$element->setAttribute(‘aria-atomic’, ‘true’);
}
}

return $this->getfixedhtml($dom, $content);
}
`

Pattern 2: Element Wrapping

`php
public function fix($content): string
{
$dom = new \DOMDocument(‘1.0’, ‘UTF-8’);
@$dom->loadHTML(‘‘ . $content);

$xpath = new \DOMXPath($dom);
$buttons = $xpath->query(‘//button[not(@aria-label)]’);

foreach ($buttons as $button) {
$wrapper = $dom->createElement(‘span’);
$wrapper->setAttribute(‘class’, ‘button-accessible’);

$button->parentNode->insertBefore($wrapper, $button);
$wrapper->appendChild($button);
}

return $this->getfixedhtml($dom, $content);
}
`

Pattern 3: Attribute Modification

`php
public function fix($content): string
{
$dom = new \DOMDocument(‘1.0’, ‘UTF-8’);
@$dom->loadHTML(‘‘ . $content);

$xpath = new \DOMXPath($dom);
$images = $xpath->query(‘//img[not(@alt)]’);

foreach ($images as $image) {
$alt = $this->generatealttext($image);
$image->setAttribute(‘alt’, $alt);
}

return $this->getfixedhtml($dom, $content);
}

private function generatealttext($image): string
{
$src = $image->getAttribute(‘src’);
$filename = basename($src, pathinfo($src, PATHINFO_EXTENSION));
return strreplace([‘-‘, ‘‘], ‘ ‘, $filename);
}
`

Pattern 4: CSS Injection

`php
public function requires_css(): bool
{
return true;
}

public function get_css(): string
{
return ‘
/ High contrast mode support /
.focus-indicator:focus {
outline: 3px solid;
outline-offset: 2px;
}

/ Ensure visible focus indicator /
@media (prefers-reduced-motion: no-preference) {
.focus-indicator:focus {
box-shadow: 0 0 0 3px rgba(0,0,0,0.3);
}
}
‘;
}

public function fix($content): string
{
// Add CSS to head if not present
if (strpos($content, ‘focus-indicator-css’) === false) {
$css = ‘

‘;
$content = str_replace(‘‘, $css . ‘‘, $content);
}

return $content;
}
`

Pattern 5: JavaScript Enhancement

`php
public function requires_javascript(): bool
{
return true;
}

public function get_javascript(): string
{
return ‘
document.addEventListener(“DOMContentLoaded”, function() {
// Focus management
document.querySelectorAll(“[data-focus-trap]”).forEach(el => {
const focusableElements = el.querySelectorAll(
“button, [href], input, select, textarea, [tabindex]:not([tabindex=\”-1\”])”
);
if (focusableElements.length > 0) {
el.addEventListener(“keydown”, (e) => {
if (e.key === “Tab”) {
const first = focusableElements[0];
const last = focusableElements[focusableElements.length – 1];
if (e.shiftKey && document.activeElement === first) {
last.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === last) {
first.focus();
e.preventDefault();
}
}
});
}
});
});
‘;
}
`

Testing Fixers

Create Test Class

`php
class CustomButtonLabelFixerTest extends \WP_UnitTestCase
{
public function testaddsarialabeltoiconbutton()
{
$fixer = new CustomButtonLabelFixer();

$input = ‘‘;
$output = $fixer->fix($input);

$this->assertStringContainsString(‘aria-label=”Close”‘, $output);
}

public function testskipsbuttonwithtext()
{
$fixer = new CustomButtonLabelFixer();

$input = ‘‘;
$output = $fixer->fix($input);

$this->assertStringNotContainsString(‘aria-label’, $output);
}

public function testskipsexistingarialabel()
{
$fixer = new CustomButtonLabelFixer();

$input = ‘‘;
$original = $input;
$output = $fixer->fix($input);

// Should not change existing aria-label
$this->assertStringContainsString(‘aria-label=”Custom”‘, $output);
}
}
`

Run Tests

`bash
cd your-plugin/tests
phpunit –bootstrap=bootstrap.php CustomButtonLabelFixerTest.php
`

Performance Optimization

Best Practices

  1. Minimize DOM Traversal
  2. – Use XPath efficiently
    – Query only what’s needed
    – Cache results if possible

  3. Batch Operations
  4. – Group similar fixes
    – Modify multiple elements in one pass

  5. Avoid Unnecessary Parsing
  6. – Check for issue before fixing
    – Use regex for simple replacements

  7. Limit Scope
  8. – Target specific elements
    – Skip processed elements

    Example Optimization

    `php
    public function fix($content): string
    {
    // First check if any issues exist with regex
    if (preg_match(‘/]>(?!.<\/button>)/s’, $content) === 0) {
    // No empty buttons, skip DOM parsing
    return $content;
    }

    // Only parse if likely to find issues
    return $this->parseandfix($content);
    }
    `

    Debugging

    Enable Debug Mode

    `php
    define(‘SLOSFIXERDEBUG’, true);
    `

    Add Debug Output

    `php
    public function fix($content): string
    {
    if (defined(‘SLOSFIXERDEBUG’) && SLOSFIXERDEBUG) {
    errorlog(‘Fixing with: ‘ . $this->getid());
    error_log(‘Content length: ‘ . strlen($content));
    }

    // … fix logic

    if (defined(‘SLOSFIXERDEBUG’) && SLOSFIXERDEBUG) {
    errorlog(‘Fixed content length: ‘ . strlen($fixedcontent));
    }

    return $fixed_content;
    }
    `

    Distribution

    Package as Plugin

  9. Create plugin folder: my-slos-fixers/
  10. Create plugin file: my-slos-fixers.php
  11. Include fixer classes
  12. Register in initialization
  13. Package as ZIP
  14. Distribute via GitHub/WordPress.org
  15. Example Plugin File

    `php
    once plugindir_path(FILE) . ‘fixers/CustomButtonLabelFixer.php’;

    addfilter(‘slosaccessibility_fixers’, function($fixers) {
    $fixers[‘custom-button-label’] = ‘YourNamespace\Accessibility\Fixers\CustomButtonLabelFixer’;
    return $fixers;
    });
    `

    Examples

    See the official fixers in:
    includes/Modules/AccessibilityScanner/Fixes/Fixers/

    Next Steps

  16. Identify accessibility issues on your site
  17. Create custom fixer for that issue
  18. Test thoroughly
  19. Register with SLOS
  20. Monitor effectiveness
  21. Share with community!
  22. Related

Share this article

Was this article helpful?

Help us improve our documentation

Still need help?

Our support team is ready to assist you with personalized guidance for your workspace.

Submit a support ticket