Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions packages/@d-zero/markuplint-config/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# `@d-zero/markuplint-config`

D-ZERO用のMarkuplint設定です。アクセシブルで保守性の高いHTML実装を促進するためのルールセットを提供します。

## 個別インストール

```sh
Expand All @@ -16,6 +18,43 @@ npm install -D @d-zero/markuplint-config
}
```

## 含まれるルール

このconfigには以下のD-ZERO独自ルールが含まれています:

### 1. 要素の禁止・必須化

- **`br`要素の禁止**: CSSでスタイル調整を推奨
- **`img`要素のalt属性必須**: アクセシビリティ確保のため
- **`a`要素のhref属性必須**: リンクの明確化

### 2. button要素のInvoker Commands API使用の強制

- **`type="button"`のbutton要素**: `command`属性が必須
- Invoker Commands APIを使用した宣言的なUI実装を推奨
- 例外: `role`属性を持つボタン、フォーム送信ボタン(`type="submit"`/`type="reset"`/typeなし)

- **`popovertarget`属性を持つボタン**: Invoker Commands APIへの移行を推奨
- `commandfor`/`command`属性の使用を推奨
- `popovertarget`は将来的に非推奨となる予定

詳細は[CODING_GUIDELINES_NO_CLICK_EVENT.md](../../CODING_GUIDELINES_NO_CLICK_EVENT.md)を参照してください。

### 3. ファイル名の命名規則

- **画像/メディアファイル**: 小文字のケバブケース(ハイフン区切り)を強制
- 対象: `img`, `video`, `audio`, `source`要素の`src`/`poster`属性
- 大文字、スペース、アンダースコアは使用不可

### 4. 無効な属性

- **`a`要素のhref属性**: `javascript:`スキームを禁止
- 代わりに`button`要素の使用を推奨

### 5. 特殊な属性の許可

- **`html`要素の`prefix`属性**: Open Graph Protocolのため許可

### 拡張

プロジェクトに合わせて設定を追加します。
Expand Down
21 changes: 21 additions & 0 deletions packages/@d-zero/markuplint-config/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,26 @@ export default {
},
},
},
{
selector: 'button[type=button]:not([role]):not([popovertarget])',
rules: {
'required-attr': {
value: 'command',
reason:
'button要素には原則としてcommand属性が必要です。Invoker Commands APIを使用してアクセシブルなUIを実装してください。role属性を持つボタン(role="tab"など)やtype="submit"/type="reset"/typeなしのボタンは例外として許可されます。(D-ZERO独自ルール)',
},
},
},
{
selector:
'button[popovertarget]:not([command]):not([role]):not([type=submit]):not([type=reset])',
rules: {
'required-attr': {
value: 'command',
reason:
'popovertarget属性の代わりにInvoker Commands API(commandfor/command属性)の使用を推奨します。commandfor属性とshow-popover/hide-popover/toggle-popoverコマンドを使用してください。popovertargetは将来的に非推奨となる予定です。(D-ZERO独自ルール)',
},
},
},
],
};
21 changes: 21 additions & 0 deletions test/cli.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,27 @@ describe('markuplint', () => {
);
expect(validNaming).toStrictEqual([]);
});

test('Button Command Attribute', async () => {
const violations = await markuplint(
'test/fixtures/markuplint/button-command.html',
'packages/@d-zero/markuplint-config/base.js',
);
expect(violations).toStrictEqual([
'test/fixtures/markuplint/button-command.html:27:17 The "btn" class name is unmatched with the below patterns: "/^c-(?<ComponentName>[a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/"',
'test/fixtures/markuplint/button-command.html:45:58 The "btn" class name is unmatched with the below patterns: "/^c-(?<ComponentName>[a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/"',
'test/fixtures/markuplint/button-command.html:42:40 The "command" attribute expects either "toggle-popover", "show-popover", "hide-popover", "close", "show-modal". Or, the "command" attribute expects the custom command format. Did you mean "--toggle"? (https://html.spec.whatwg.org/multipage/form-elements.html#valid-custom-command)',
'test/fixtures/markuplint/button-command.html:102:2 Detected perceptible nodes between the trigger and corresponding target',
'test/fixtures/markuplint/button-command.html:109:6 Detected perceptible nodes between the trigger and corresponding target',
'test/fixtures/markuplint/button-command.html:108:2 Require accessible name',
'test/fixtures/markuplint/button-command.html:127:3 Require accessible name',
'test/fixtures/markuplint/button-command.html:18:2 The "button" element expects the "command" attribute',
'test/fixtures/markuplint/button-command.html:101:2 The "button" element expects the "command" attribute',
'test/fixtures/markuplint/button-command.html:102:2 The "button" element expects the "command" attribute',
'test/fixtures/markuplint/button-command.html:55:21 The "aria-selected" ARIA state is not global state',
'test/fixtures/markuplint/button-command.html:63:16 The "button" role is the implicit role of the "button" element',
]);
});
});

describe('stylelint', () => {
Expand Down
130 changes: 130 additions & 0 deletions test/fixtures/markuplint/button-command.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Button Command Test</title>
</head>
<body>
<h1>Button Command Attribute Tests</h1>

<!-- ========================================
Should ERROR: Buttons without command
======================================== -->

<!-- Basic button without any attributes -->
<button>No attributes</button>

<!-- Explicit type="button" without command -->
<button type="button">Explicit type button</button>

<!-- Button with disabled (still needs command) -->
<button disabled>Disabled button</button>

<!-- Button with aria attributes but no command -->
<button aria-label="Action">With aria-label</button>

<!-- Button with class/id but no command -->
<button class="btn" id="my-button">With class and id</button>

<!-- Form buttons OUTSIDE of form element -->
<button type="submit">Submit outside form</button>
<button type="reset">Reset outside form</button>

<!-- ========================================
Should PASS: Buttons with command
======================================== -->

<!-- Basic command usage -->
<button commandfor="dialog" command="show-modal">Open Dialog</button>
<button commandfor="dialog" command="close">Close Dialog</button>
<button commandfor="popover" command="show-popover">Show Popover</button>
<button commandfor="popover" command="hide-popover">Hide Popover</button>
<button commandfor="details" command="toggle">Toggle Details</button>

<!-- Command with other attributes -->
<button commandfor="dialog" command="show-modal" class="btn">With class</button>
<button commandfor="dialog" command="show-modal" disabled>Disabled with command</button>
<button commandfor="dialog" command="show-modal" aria-label="Open">With aria</button>

<!-- ========================================
Should PASS: Role exceptions
======================================== -->

<!-- ARIA roles that require custom handlers -->
<button role="tab">Tab</button>
<button role="tab" aria-selected="true">Selected Tab</button>
<button role="switch" aria-checked="false">Switch</button>
<button role="menuitem">Menu Item</button>
<button role="option">Option</button>
<button role="checkbox" aria-checked="false">Checkbox</button>
<button role="radio" aria-checked="false">Radio</button>

<!-- Explicit role="button" (should also be exception) -->
<button role="button">Explicit button role</button>

<!-- Role with type attribute -->
<button role="tab" type="button">Tab with type</button>

<!-- ========================================
Should PASS: Form buttons (type=submit/reset)
======================================== -->

<!-- Inside form element -->
<form>
<button type="submit">Submit in form</button>
<button type="reset">Reset in form</button>
<button>Default submit (no type)</button>
</form>

<!-- With form attribute referencing external form -->
<button type="submit" form="external-form">Submit for external form</button>
<button type="reset" form="external-form">Reset for external form</button>

<!-- ========================================
Edge cases: Multiple attributes
======================================== -->

<!-- Role takes precedence (should pass) -->
<button role="tab" commandfor="dialog" command="show-modal">Role + Command</button>

<!-- Submit type takes precedence (should pass) -->
<button type="submit" commandfor="dialog" command="show-modal">Submit + Command</button>

<!-- Reset type takes precedence (should pass) -->
<button type="reset" commandfor="dialog" command="show-modal">Reset + Command</button>

<!-- ========================================
Popovertarget attribute (Modern HTML)
======================================== -->

<!-- popovertarget is similar to commandfor (should these need command too?) -->
<button popovertarget="my-popover">With popovertarget</button>
<button popovertarget="my-popover" popovertargetaction="show">With popovertargetaction</button>

<!-- ========================================
Referenced elements
======================================== -->

<dialog id="dialog">
<p>Dialog content</p>
<button commandfor="dialog" command="close">Close</button>
</dialog>

<div id="popover" popover>
<p>Popover content</p>
</div>

<details id="details">
<summary>Summary</summary>
<p>Details content</p>
</details>

<div id="my-popover" popover>
<p>Popover for popovertarget</p>
</div>

<form id="external-form">
<input type="text" name="name">
</form>
</body>
</html>