Git Komut Rehberi & Terminal Asistanı
Sıfırdan Yeni Bir Depo Başlatın:
Mevcut Bir Git Deposu veya Klasörü Gönderin:
Hataları Geri Alma, İptal & Temizlik:
Son commit'i geri al (yapılan değişiklikleri korur):
git reset --soft HEAD~1
Yapılan yerel değişiklikleri iptal et (çalışma ağacını temizler):
git restore .
Takip edilmeyen gereksiz dosyaları sil:
git clean -fd
Değişiklikleri geçici sakla ve geri getir (stash):
git stash git stash pop
Dal (Branch) ve Sürüm Etiketleri:
Yeni dal aç ve geçiş yap:
git checkout -b yeni-ozellik git push -u origin yeni-ozellik
Sürüm etiketi (tag) oluştur ve gönder:
git tag -a v1.0.0 -m "Sürüm 1.0.0" git push origin --tags
feat: add 15 new WCAG rules, dynamic ARIA checks and bump version to v3.1.4
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AnchorMissingHref extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_ANCHOR_HREF'; }
|
||||
public function getDescription(): string { return 'Anchor elements (<a>) acting as links must have a valid href attribute.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'a') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($element->hasAttribute('name') || $element->hasAttribute('role')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$element->hasAttribute('href') || trim($element->getAttribute('href')) === '') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add a valid href attribute to the <a> element, or use a <button> element if it triggers an action.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AppletBanned extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_APPLET_BANNED'; }
|
||||
public function getDescription(): string { return 'The <applet> element is obsolete and non-accessible. Use modern HTML5 elements instead.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 1; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) === 'applet') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Remove the <applet> element and replace it with modern HTML5 or accessible media embeddings.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AriaExpandedState extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_ARIA_EXPANDED'; }
|
||||
public function getDescription(): string { return 'Collapsible toggle buttons (accordion, dropdown, collapse) must have an aria-expanded attribute.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
$tagName = strtolower($element->tagName);
|
||||
$role = strtolower($element->getAttribute('role'));
|
||||
|
||||
if ($tagName !== 'button' && $tagName !== 'a' && $role !== 'button') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = strtolower($element->getAttribute('class'));
|
||||
$dataBsToggle = strtolower($element->getAttribute('data-bs-toggle'));
|
||||
$dataToggle = strtolower($element->getAttribute('data-toggle'));
|
||||
|
||||
$isCollapsible = str_contains($class, 'dropdown-toggle') ||
|
||||
str_contains($class, 'accordion-button') ||
|
||||
str_contains($class, 'collapse-toggle') ||
|
||||
in_array($dataBsToggle, ['collapse', 'dropdown', 'offcanvas'], true) ||
|
||||
in_array($dataToggle, ['collapse', 'dropdown'], true);
|
||||
|
||||
if (!$isCollapsible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($element->hasAttribute('aria-expanded')) {
|
||||
$val = strtolower($element->getAttribute('aria-expanded'));
|
||||
if (in_array($val, ['true', 'false'], true)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add aria-expanded="false" (or "true") to the toggle element so screen readers announce expanded/collapsed status.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AriaHiddenBody extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_ARIA_HIDDEN_BODY'; }
|
||||
public function getDescription(): string { return 'The <body> or <html> element must not have aria-hidden="true".'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 1; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
$tagName = strtolower($element->tagName);
|
||||
if ($tagName !== 'body' && $tagName !== 'html') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strtolower($element->getAttribute('aria-hidden')) === 'true') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Remove aria-hidden="true" from the <body>/<html> tag to prevent hiding the entire document from screen readers.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AriaLiveStatus extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_3_STATUS_MESSAGES'; }
|
||||
public function getDescription(): string { return 'Dynamic status containers and toast messages must use aria-live or implicit live roles.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
$class = strtolower($element->getAttribute('class'));
|
||||
$role = strtolower($element->getAttribute('role'));
|
||||
|
||||
$statusClasses = ['toast', 'status-message', 'loading-message', 'alert-message', 'live-region'];
|
||||
$isStatusContainer = false;
|
||||
|
||||
foreach ($statusClasses as $statusClass) {
|
||||
if (str_contains($class, $statusClass)) {
|
||||
$isStatusContainer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isStatusContainer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Implicit live roles in ARIA include status, alert, log
|
||||
if (in_array($role, ['status', 'alert', 'log'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($element->hasAttribute('aria-live')) {
|
||||
$liveVal = strtolower($element->getAttribute('aria-live'));
|
||||
if (in_array($liveVal, ['polite', 'assertive'], true)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add aria-live="polite" (or role="status"/role="alert") so screen readers dynamically announce updates to users.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AriaPressedToggle extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_TOGGLE_BUTTON_STATE'; }
|
||||
public function getDescription(): string { return 'Toggle buttons must use aria-pressed="true" or "false" to communicate state changes.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 3; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
$tagName = strtolower($element->tagName);
|
||||
$role = strtolower($element->getAttribute('role'));
|
||||
|
||||
if ($tagName !== 'button' && $role !== 'button') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = strtolower($element->getAttribute('class'));
|
||||
$dataToggle = strtolower($element->getAttribute('data-toggle'));
|
||||
|
||||
$isToggleButton = str_contains($class, 'btn-toggle') || str_contains($class, 'toggle-btn') || $dataToggle === 'button';
|
||||
|
||||
if (!$isToggleButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($element->hasAttribute('aria-pressed')) {
|
||||
$val = strtolower($element->getAttribute('aria-pressed'));
|
||||
if (in_array($val, ['true', 'false', 'mixed'], true)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add aria-pressed="true" or aria-pressed="false" to indicate the toggle state to assistive technology.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class ButtonLoadingState extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_BUTTON_LOADING_STATE'; }
|
||||
public function getDescription(): string { return 'Buttons displaying loading spinners must inform screen readers via aria-busy or aria-label.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 3; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
$tagName = strtolower($element->tagName);
|
||||
$role = strtolower($element->getAttribute('role'));
|
||||
|
||||
if ($tagName !== 'button' && $role !== 'button') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = strtolower($element->getAttribute('class'));
|
||||
$hasLoadingClass = str_contains($class, 'loading') || str_contains($class, 'spinner') || str_contains($class, 'is-busy');
|
||||
|
||||
$hasSpinnerChild = false;
|
||||
foreach ($element->getElementsByTagName('*') as $child) {
|
||||
$childClass = strtolower($child->getAttribute('class'));
|
||||
if (str_contains($childClass, 'spinner') || str_contains($childClass, 'loading') || str_contains($childClass, 'loader')) {
|
||||
$hasSpinnerChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasLoadingClass && !$hasSpinnerChild && strtolower($element->getAttribute('aria-busy')) !== 'true') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Must have aria-busy="true" or an explicit aria-label/aria-live describing the loading process
|
||||
$hasAriaBusy = strtolower($element->getAttribute('aria-busy')) === 'true';
|
||||
$hasAriaLive = $element->hasAttribute('aria-live');
|
||||
$hasAriaLabel = $element->hasAttribute('aria-label') && trim($element->getAttribute('aria-label')) !== '';
|
||||
|
||||
if ($hasAriaBusy || ($hasAriaLabel && $hasAriaLive)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add aria-busy="true" and an aria-label (e.g. aria-label="Loading...") to the button when an async action is in progress.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class FigureCaptionStructure extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_FIGCAPTION_PARENT'; }
|
||||
public function getDescription(): string { return '<figcaption> elements must be direct children of a <figure> element.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'figcaption') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parent = $element->parentNode;
|
||||
if (!$parent instanceof DOMElement || strtolower($parent->tagName) !== 'figure') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Wrap the <figcaption> element inside a parent <figure> element or change it to a standard paragraph/caption element.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class FocusableWithoutRole extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_FOCUSABLE_ROLE'; }
|
||||
public function getDescription(): string { return 'Non-interactive focusable elements (with tabindex >= 0) must have an explicit ARIA role.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 3; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (!$element->hasAttribute('tabindex')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tabindex = (int)$element->getAttribute('tabindex');
|
||||
if ($tabindex < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tagName = strtolower($element->tagName);
|
||||
$nonInteractiveTags = ['div', 'span', 'p', 'li', 'section', 'article', 'td', 'th', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||
|
||||
if (in_array($tagName, $nonInteractiveTags, true) && !$element->hasAttribute('role')) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
sprintf('Add an appropriate ARIA role (e.g., role="button", role="link") to the focusable <%s> element.', $tagName)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class InputMissingAutocomplete extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_5_INPUT_AUTOCOMPLETE'; }
|
||||
public function getDescription(): string { return 'Input fields collecting personal data should have an autocomplete attribute.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 3; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'input') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = strtolower($element->getAttribute('type'));
|
||||
if (in_array($type, ['hidden', 'submit', 'button', 'reset', 'checkbox', 'radio', 'image', 'file'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = strtolower($element->getAttribute('name'));
|
||||
$id = strtolower($element->getAttribute('id'));
|
||||
$targetKeywords = ['email', 'phone', 'tel', 'username', 'user', 'password', 'pass', 'address', 'zip', 'city', 'country', 'name', 'fname', 'lname'];
|
||||
|
||||
$isPersonalField = false;
|
||||
if (in_array($type, ['email', 'tel'], true)) {
|
||||
$isPersonalField = true;
|
||||
} else {
|
||||
foreach ($targetKeywords as $keyword) {
|
||||
if (str_contains($name, $keyword) || str_contains($id, $keyword)) {
|
||||
$isPersonalField = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isPersonalField && (!$element->hasAttribute('autocomplete') || trim($element->getAttribute('autocomplete')) === '')) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add an autocomplete attribute (e.g., autocomplete="email", autocomplete="username") to improve accessibility and user experience.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class ListItemParent extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_LIST_ITEM_PARENT'; }
|
||||
public function getDescription(): string { return 'List items (<li>) must be contained inside a parent <ul>, <ol>, or <menu> element.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'li') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parent = $element->parentNode;
|
||||
if (!$parent instanceof DOMElement) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Wrap the orphan <li> element in a parent <ul> or <ol> tag.'
|
||||
);
|
||||
}
|
||||
|
||||
$parentTag = strtolower($parent->tagName);
|
||||
$validParents = ['ul', 'ol', 'menu'];
|
||||
|
||||
if (!in_array($parentTag, $validParents, true) && strtolower($parent->getAttribute('role')) !== 'list') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
sprintf('Move the <li> element inside a <ul> or <ol> container instead of <%s>.', $parentTag)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class OptgroupLabel extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_OPTGROUP_LABEL'; }
|
||||
public function getDescription(): string { return '<optgroup> elements must have a non-empty label attribute.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'optgroup') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$element->hasAttribute('label') || trim($element->getAttribute('label')) === '') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add a descriptive label attribute to the <optgroup> element.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class RadioGroupFieldset extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_RADIO_GROUP_FIELDSET'; }
|
||||
public function getDescription(): string { return 'Radio button groups sharing the same name should be grouped inside a <fieldset> with a <legend> or role="radiogroup".'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 3; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'input') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strtolower($element->getAttribute('type')) !== 'radio') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = $element->getAttribute('name');
|
||||
if ($name === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if ancestor is fieldset or role="radiogroup"
|
||||
$current = $element->parentNode;
|
||||
while ($current instanceof DOMElement) {
|
||||
$parentTag = strtolower($current->tagName);
|
||||
$parentRole = strtolower($current->getAttribute('role'));
|
||||
|
||||
if ($parentTag === 'fieldset') {
|
||||
$legends = $current->getElementsByTagName('legend');
|
||||
if ($legends->length > 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($parentRole === 'radiogroup' && ($current->hasAttribute('aria-label') || $current->hasAttribute('aria-labelledby'))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$current = $current->parentNode;
|
||||
}
|
||||
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
sprintf('Wrap radio inputs named "%s" inside a <fieldset> with a <legend> to group related radio choices.', $name)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class SummaryParent extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_SUMMARY_PARENT'; }
|
||||
public function getDescription(): string { return '<summary> elements must be direct children of a <details> element.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'summary') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parent = $element->parentNode;
|
||||
if (!$parent instanceof DOMElement || strtolower($parent->tagName) !== 'details') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Wrap the <summary> element inside a <details> element or change it to a standard heading/button.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class TableMultiCaption extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_TABLE_MULTI_CAPTION'; }
|
||||
public function getDescription(): string { return 'A <table> element must not contain more than one <caption> element.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'table') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$captionCount = 0;
|
||||
foreach ($element->childNodes as $child) {
|
||||
if ($child instanceof DOMElement && strtolower($child->tagName) === 'caption') {
|
||||
$captionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($captionCount > 1) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Remove duplicate <caption> elements so that the table has at most one caption.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user