Skip to content

Commit

Permalink
feat(preset): add dialog-focus rule
Browse files Browse the repository at this point in the history
  • Loading branch information
masuP9 committed Nov 22, 2020
1 parent bcd4322 commit 29ec6ba
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 0 deletions.
61 changes: 61 additions & 0 deletions packages/acot-preset-wcag/docs/rules/dialog-focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# dialog-focus

Move focus to inside dialog or set dialog after trigger.

> When a open dialog, move focus to an element contained in the dialog. Or [Inserting dynamic content into the Document Object Model immediately following its trigger element](https://www.w3.org/WAI/WCAG21/Techniques/client-side-script/SCR26).
[Understanding Success Criterion 2\.4\.3: Focus Order](https://www.w3.org/WAI/WCAG21/Understanding/focus-order.html)

## :white_check_mark: Correct

```html
<button type="button" aria-haspopup="dialog">open</button>

<dialog>
<button type="type">OK</button>
</dialog>

<script>
const dialog = document.querySelector('dialog');
const openButton = document.querySelector('button[aria-haspopup="dialog"]');
openButton.addEventListener('click', (e) => {
dialog.showModal();
});
</script>
```

```html
<button type="button" aria-haspopup="dialog">open</button>

<div role="dialog" hidden>
<button type="type">OK</button>
</div>

<script>
const dialog = document.querySelector('[role=dialog]');
const openButton = document.querySelector('button[aria-haspopup="dialog"]');
openButton.addEventListener('click', (e) => {
dialog.hidden = false;
});
</script>
```

## :warning: Incorrect

```html
<button type="button" aria-haspopup="dialog">open</button>

<a href="https://example.com">link</a>

<div role="dialog" hidden>
<button type="type">OK</button>
</div>

<script>
const dialog = document.querySelector('[role=dialog]');
const openButton = document.querySelector('button[aria-haspopup="dialog"]');
openButton.addEventListener('click', (e) => {
dialog.hidden = false;
});
</script>
```
69 changes: 69 additions & 0 deletions packages/acot-preset-wcag/src/rules/dialog-focus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createRule } from '@acot/core';
import type { ComputedAccessibleNode } from '@acot/types';

type Options = {};

export default createRule<Options>({
type: 'contextual',
selector: '[aria-haspopup="dialog"]',
meta: {
description: 'Move focus to inside dialog or set dialog after trigger.',
tags: ['wcag2.1a', '2.4.3 Focus Order'],
recommended: true,
},

test: async (context, node) => {
try {
await node.click();

const activeElementHasParentDialogRole = async () => {
if (document.activeElement === null) {
return false;
}

const ax = (await (window as any).getComputedAccessibleNode(
document.activeElement,
)) as ComputedAccessibleNode;

const findParentDialogRoleAXNode = (
computedAXNode: ComputedAccessibleNode,
): ComputedAccessibleNode | null => {
if (computedAXNode.role === 'dialog') {
return computedAXNode;
}

return computedAXNode.parent != undefined
? findParentDialogRoleAXNode(computedAXNode.parent)
: null;
};

return findParentDialogRoleAXNode(ax)?.role === 'dialog';
};

const hasDialogRoleInParentByClick = await context.page.evaluate(
activeElementHasParentDialogRole,
);

context.debug({ hasDialogRoleInParentByClick });

if (!hasDialogRoleInParentByClick) {
await context.page.keyboard.press('Tab');

const hasDialogRoleInParentByPressTab = await context.page.evaluate(
activeElementHasParentDialogRole,
);

context.debug({ hasDialogRoleInParentByPressTab });

if (!hasDialogRoleInParentByPressTab) {
await context.report({
node,
message: `Move focus to inside dialog or set dialog after trigger.`,
});
}
}
} catch (e) {
context.debug('error: ', e);
}
},
});
2 changes: 2 additions & 0 deletions packages/acot-preset-wcag/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import interactiveSupportsFocus from './interactive-supports-focus';
import interactiveHasName from './interactive-has-name';
import pageHasTitle from './page-has-title';
import imgHasName from './img-has-name';
import dialogFocus from './dialog-focus';

export const rules: RuleRecord = {
'interactive-has-enough-size': interactiveHasEnoughSize,
'interactive-supports-focus': interactiveSupportsFocus,
'interactive-has-name': interactiveHasName,
'page-has-title': pageHasTitle,
'img-has-name': imgHasName,
'dialog-focus': dialogFocus,
};

0 comments on commit 29ec6ba

Please sign in to comment.