a11y-alert-dialog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/A11Y.md +207 -0
- package/LICENSE +21 -0
- package/README.md +180 -0
- package/dist/index.cjs +147 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +119 -0
- package/package.json +47 -0
package/A11Y.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Accessibility Contract (A11Y.md)
|
|
2
|
+
|
|
3
|
+
This document defines the **non-negotiable accessibility guarantees** of the
|
|
4
|
+
Accessible Alert library.
|
|
5
|
+
|
|
6
|
+
If any change violates this contract, it is considered a **breaking bug**,
|
|
7
|
+
not a feature request.
|
|
8
|
+
|
|
9
|
+
Accessibility is **not optional** in this project.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 🎯 Core Principle
|
|
14
|
+
|
|
15
|
+
> If a user can operate a keyboard, they must be able to fully operate this dialog.
|
|
16
|
+
|
|
17
|
+
This library is designed to work for:
|
|
18
|
+
- Keyboard-only users
|
|
19
|
+
- Screen reader users
|
|
20
|
+
- Users with motor impairments
|
|
21
|
+
- Users relying on assistive technologies (switch devices, voice control)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🚫 Non-Negotiable Rules
|
|
26
|
+
|
|
27
|
+
The following rules **must never be disabled, bypassed, or overridden**.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. Focus Management
|
|
32
|
+
|
|
33
|
+
### 1.1 Before Opening a Dialog
|
|
34
|
+
- The currently focused element **MUST** be stored.
|
|
35
|
+
- If no element is focused, `document.body` is used as fallback.
|
|
36
|
+
|
|
37
|
+
### 1.2 When Dialog Opens
|
|
38
|
+
- Focus **MUST** move inside the dialog immediately.
|
|
39
|
+
- Focus **MUST NOT** remain on background content.
|
|
40
|
+
- Initial focus priority:
|
|
41
|
+
1. Primary action button (Confirm)
|
|
42
|
+
2. First focusable element inside the dialog
|
|
43
|
+
|
|
44
|
+
### 1.3 While Dialog Is Open
|
|
45
|
+
- Focus **MUST NOT** escape the dialog.
|
|
46
|
+
- Background content **MUST NOT** be focusable.
|
|
47
|
+
|
|
48
|
+
### 1.4 When Dialog Closes
|
|
49
|
+
- Focus **MUST** be restored to the previously focused element.
|
|
50
|
+
- If the original element no longer exists, focus moves to:
|
|
51
|
+
- The closest available parent
|
|
52
|
+
- Or `document.body` as final fallback
|
|
53
|
+
|
|
54
|
+
❌ Losing focus after closing is a **critical accessibility failure**.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 2. Focus Trap
|
|
59
|
+
|
|
60
|
+
### Requirements
|
|
61
|
+
- `Tab` and `Shift + Tab` navigation **MUST** loop inside the dialog.
|
|
62
|
+
- Focus trapping **MUST** work without relying on CSS hacks.
|
|
63
|
+
- Focus trap **MUST** handle dynamically added or removed focusable elements.
|
|
64
|
+
|
|
65
|
+
### Forbidden
|
|
66
|
+
- Disabling focus trap
|
|
67
|
+
- Allowing focus to move into the page behind the dialog
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 3. Keyboard Interaction
|
|
72
|
+
|
|
73
|
+
Keyboard support is **mandatory and always enabled**.
|
|
74
|
+
|
|
75
|
+
### Required Key Bindings
|
|
76
|
+
|
|
77
|
+
| Key | Behavior |
|
|
78
|
+
|----|---------|
|
|
79
|
+
| Tab | Move focus forward |
|
|
80
|
+
| Shift + Tab | Move focus backward |
|
|
81
|
+
| Enter | Activate primary action |
|
|
82
|
+
| Esc | Cancel or close dialog |
|
|
83
|
+
|
|
84
|
+
### Rules
|
|
85
|
+
- Keyboard behavior **MUST NOT** depend on mouse interaction.
|
|
86
|
+
- Keyboard handling **MUST NOT** interfere with text input fields.
|
|
87
|
+
- `Esc` **MUST ALWAYS** close the dialog.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 4. Screen Reader Semantics
|
|
92
|
+
|
|
93
|
+
### Dialog Roles
|
|
94
|
+
- Use `role="dialog"` for normal dialogs
|
|
95
|
+
- Use `role="alertdialog"` for destructive or dangerous actions
|
|
96
|
+
|
|
97
|
+
### ARIA Attributes
|
|
98
|
+
Each dialog **MUST** include:
|
|
99
|
+
- `aria-modal="true"`
|
|
100
|
+
- `aria-labelledby` referencing the dialog title
|
|
101
|
+
- `aria-describedby` referencing the dialog description (if present)
|
|
102
|
+
|
|
103
|
+
### Announcements
|
|
104
|
+
- Screen readers **MUST** announce:
|
|
105
|
+
- Dialog type (dialog / alert dialog)
|
|
106
|
+
- Title
|
|
107
|
+
- Description
|
|
108
|
+
- Announcement **MUST** occur immediately when the dialog opens.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 5. Visual & Motion Safety
|
|
113
|
+
|
|
114
|
+
### Motion
|
|
115
|
+
- Respect `prefers-reduced-motion`
|
|
116
|
+
- Avoid animations that:
|
|
117
|
+
- Cause dizziness
|
|
118
|
+
- Delay keyboard interaction
|
|
119
|
+
- Block screen reader announcements
|
|
120
|
+
|
|
121
|
+
### Visibility
|
|
122
|
+
- Focused elements **MUST** have visible focus indicators.
|
|
123
|
+
- Focus indicators **MUST NOT** be removed.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 6. Timing & Control
|
|
128
|
+
|
|
129
|
+
### Forbidden Behaviors
|
|
130
|
+
- Auto-close timers
|
|
131
|
+
- Forced timeouts
|
|
132
|
+
- Auto-confirm actions
|
|
133
|
+
|
|
134
|
+
Users **MUST** remain in control at all times.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 7. HTML Semantics
|
|
139
|
+
|
|
140
|
+
### Required
|
|
141
|
+
- Use native interactive elements:
|
|
142
|
+
- `<button>` for actions
|
|
143
|
+
- `<input>` for form fields
|
|
144
|
+
|
|
145
|
+
### Forbidden
|
|
146
|
+
- Clickable `<div>` or `<span>` for core actions
|
|
147
|
+
- Custom elements without proper ARIA roles
|
|
148
|
+
|
|
149
|
+
Voice control and assistive tools depend on correct semantics.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 8. API-Level Constraints
|
|
154
|
+
|
|
155
|
+
The public API **MUST NOT** expose options that allow:
|
|
156
|
+
|
|
157
|
+
- Disabling focus management
|
|
158
|
+
- Disabling keyboard handling
|
|
159
|
+
- Injecting custom HTML for critical controls
|
|
160
|
+
- Bypassing screen reader semantics
|
|
161
|
+
|
|
162
|
+
If an API option can break accessibility, it must be removed.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 9. Testing Requirements
|
|
167
|
+
|
|
168
|
+
### Manual Testing (Required)
|
|
169
|
+
Every release **MUST** be tested with:
|
|
170
|
+
- Keyboard only (no mouse)
|
|
171
|
+
- At least one screen reader:
|
|
172
|
+
- NVDA (Windows) or
|
|
173
|
+
- VoiceOver (macOS)
|
|
174
|
+
|
|
175
|
+
### Automated Testing
|
|
176
|
+
Recommended (not sufficient alone):
|
|
177
|
+
- jest-axe
|
|
178
|
+
- Keyboard navigation tests (Playwright)
|
|
179
|
+
|
|
180
|
+
Automated tests **DO NOT** replace manual accessibility testing.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 10. Definition of Done (Accessibility)
|
|
185
|
+
|
|
186
|
+
A feature is considered **DONE** only if:
|
|
187
|
+
|
|
188
|
+
- It works without a mouse
|
|
189
|
+
- It works with a keyboard only
|
|
190
|
+
- It announces correctly in a screen reader
|
|
191
|
+
- It does not break focus restoration
|
|
192
|
+
- It does not violate any rule in this document
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## ⚠️ Final Warning
|
|
197
|
+
|
|
198
|
+
Accessibility bugs are treated as **critical bugs**, not minor issues.
|
|
199
|
+
|
|
200
|
+
No feature, customization, or visual improvement is allowed to violate this contract.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 💬 Philosophy
|
|
205
|
+
|
|
206
|
+
> Accessibility is not about edge cases.
|
|
207
|
+
> It is about real people trying to use your software.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 xShiroeNguyenx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Accessible Alert
|
|
2
|
+
|
|
3
|
+
> An alert / dialog library that works without a mouse.
|
|
4
|
+
|
|
5
|
+
**Accessible Alert** is a **framework-agnostic JavaScript library** for displaying
|
|
6
|
+
`alert`, `confirm`, and `prompt` dialogs with **accessibility-first (a11y-first)**
|
|
7
|
+
design.
|
|
8
|
+
|
|
9
|
+
If you can use a **keyboard**, you can use this library.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## ✨ Why does this library exist?
|
|
14
|
+
|
|
15
|
+
Most alert/modal libraries:
|
|
16
|
+
- Prioritize visuals and animations
|
|
17
|
+
- Treat accessibility as an afterthought
|
|
18
|
+
- Break keyboard navigation and screen readers easily
|
|
19
|
+
|
|
20
|
+
👉 **Accessible Alert** is built the opposite way:
|
|
21
|
+
|
|
22
|
+
> Accessibility is the foundation, not a feature added later.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## ✅ Accessibility Guarantees (A11Y Contract)
|
|
27
|
+
|
|
28
|
+
This library **always guarantees**:
|
|
29
|
+
|
|
30
|
+
- No mouse required
|
|
31
|
+
- Fully usable with keyboard only (Tab / Enter / Esc)
|
|
32
|
+
- Correct focus management at all times
|
|
33
|
+
- Focus is never lost after closing dialogs
|
|
34
|
+
- Proper screen reader announcements
|
|
35
|
+
- No auto-close behaviors that steal control from users
|
|
36
|
+
|
|
37
|
+
Standards:
|
|
38
|
+
- WCAG 2.1 AA
|
|
39
|
+
- WAI-ARIA Authoring Practices (Dialog)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 📦 Installation
|
|
44
|
+
|
|
45
|
+
### NPM
|
|
46
|
+
```bash
|
|
47
|
+
npm install accessible-alert
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### CDN
|
|
51
|
+
```html
|
|
52
|
+
<script src="https://unpkg.com/accessible-alert"></script>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 🚀 Basic Usage
|
|
58
|
+
|
|
59
|
+
### Alert
|
|
60
|
+
```js
|
|
61
|
+
await alert("Saved successfully")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Confirm
|
|
65
|
+
```js
|
|
66
|
+
const ok = await confirm({
|
|
67
|
+
title: "Delete user",
|
|
68
|
+
description: "This action cannot be undone",
|
|
69
|
+
danger: true
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if (ok) {
|
|
73
|
+
deleteUser()
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Prompt
|
|
78
|
+
```js
|
|
79
|
+
const name = await prompt({
|
|
80
|
+
title: "Your name",
|
|
81
|
+
placeholder: "Enter your name"
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## ⌨️ Keyboard Behavior (Always Enabled)
|
|
88
|
+
|
|
89
|
+
| Key | Action |
|
|
90
|
+
|----|-------|
|
|
91
|
+
| Tab | Move forward within the dialog |
|
|
92
|
+
| Shift + Tab | Move backward |
|
|
93
|
+
| Enter | Confirm primary action |
|
|
94
|
+
| Esc | Cancel / Close dialog |
|
|
95
|
+
|
|
96
|
+
Keyboard support **cannot be disabled**.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 🧠 How Accessibility Is Handled
|
|
101
|
+
|
|
102
|
+
### Focus Management
|
|
103
|
+
- Saves the currently focused element before opening
|
|
104
|
+
- Automatically moves focus into the dialog
|
|
105
|
+
- Restores focus to the original element after closing
|
|
106
|
+
|
|
107
|
+
### Focus Trap
|
|
108
|
+
- Tab navigation is trapped inside the dialog
|
|
109
|
+
- Focus never escapes to the background
|
|
110
|
+
- No CSS hacks or pointer-event tricks
|
|
111
|
+
|
|
112
|
+
### Screen Reader Support
|
|
113
|
+
- Uses `role="dialog"` or `role="alertdialog"`
|
|
114
|
+
- Proper `aria-labelledby` and `aria-describedby`
|
|
115
|
+
- Dialog content is announced immediately on open
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 🧩 API Design Principles
|
|
120
|
+
|
|
121
|
+
- Promise-based API
|
|
122
|
+
- No callbacks
|
|
123
|
+
- No configuration options that can break accessibility
|
|
124
|
+
- No custom HTML injection for critical controls
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
await confirm({ title: "Are you sure?" })
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
> If an API allows breaking accessibility, the API is wrong.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## ❌ What This Library Does NOT Support
|
|
135
|
+
|
|
136
|
+
To protect accessibility, this library intentionally does NOT support:
|
|
137
|
+
|
|
138
|
+
- Auto-close timers
|
|
139
|
+
- Disabling focus trapping
|
|
140
|
+
- Custom button HTML
|
|
141
|
+
- Complex animations
|
|
142
|
+
- Theme systems
|
|
143
|
+
- Toast-style notifications
|
|
144
|
+
|
|
145
|
+
👉 If you want flashy UI effects, this library is **not** for you.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 🧪 Testing
|
|
150
|
+
|
|
151
|
+
Manually tested with:
|
|
152
|
+
- Keyboard-only navigation
|
|
153
|
+
- NVDA (Windows)
|
|
154
|
+
- VoiceOver (macOS)
|
|
155
|
+
|
|
156
|
+
Automated testing includes:
|
|
157
|
+
- jest-axe
|
|
158
|
+
- Playwright keyboard tests
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 🎯 When Should You Use This Library?
|
|
163
|
+
|
|
164
|
+
✔ Internal tools
|
|
165
|
+
✔ Admin dashboards
|
|
166
|
+
✔ SaaS products
|
|
167
|
+
✔ Enterprise / Government / Fintech
|
|
168
|
+
✔ Any project where accessibility is a real requirement
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 📄 License
|
|
173
|
+
|
|
174
|
+
MIT
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 💬 Philosophy
|
|
179
|
+
|
|
180
|
+
> “If you can use a keyboard, you can use this alert.”
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
alert: () => alert,
|
|
24
|
+
confirm: () => confirm
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/core/focus.ts
|
|
29
|
+
var previousFocus = null;
|
|
30
|
+
function saveFocus() {
|
|
31
|
+
previousFocus = document.activeElement;
|
|
32
|
+
}
|
|
33
|
+
function restoreFocus() {
|
|
34
|
+
if (previousFocus && document.contains(previousFocus)) {
|
|
35
|
+
previousFocus.focus();
|
|
36
|
+
} else {
|
|
37
|
+
document.body.focus();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/core/focusTrap.ts
|
|
42
|
+
function trapFocus(container) {
|
|
43
|
+
function getFocusable() {
|
|
44
|
+
return Array.from(
|
|
45
|
+
container.querySelectorAll(
|
|
46
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
function handle(e) {
|
|
51
|
+
if (e.key !== "Tab") return;
|
|
52
|
+
const items = getFocusable();
|
|
53
|
+
if (!items.length) return;
|
|
54
|
+
const first = items[0];
|
|
55
|
+
const last = items[items.length - 1];
|
|
56
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
last.focus();
|
|
59
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
first.focus();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
container.addEventListener("keydown", handle);
|
|
65
|
+
return () => container.removeEventListener("keydown", handle);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/core/Dialog.ts
|
|
69
|
+
function openDialog(options) {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
saveFocus();
|
|
72
|
+
const overlay = document.createElement("div");
|
|
73
|
+
overlay.style.position = "fixed";
|
|
74
|
+
overlay.style.inset = "0";
|
|
75
|
+
overlay.style.background = "rgba(0,0,0,0.4)";
|
|
76
|
+
const dialog = document.createElement("div");
|
|
77
|
+
dialog.setAttribute("role", options.role ?? "dialog");
|
|
78
|
+
dialog.setAttribute("aria-modal", "true");
|
|
79
|
+
dialog.style.background = "white";
|
|
80
|
+
dialog.style.padding = "1rem";
|
|
81
|
+
dialog.style.maxWidth = "400px";
|
|
82
|
+
dialog.style.margin = "20vh auto";
|
|
83
|
+
const title = document.createElement("h2");
|
|
84
|
+
title.id = "dialog-title";
|
|
85
|
+
title.textContent = options.title;
|
|
86
|
+
dialog.setAttribute("aria-labelledby", title.id);
|
|
87
|
+
dialog.appendChild(title);
|
|
88
|
+
if (options.description) {
|
|
89
|
+
const desc = document.createElement("p");
|
|
90
|
+
desc.id = "dialog-desc";
|
|
91
|
+
desc.textContent = options.description;
|
|
92
|
+
dialog.setAttribute("aria-describedby", desc.id);
|
|
93
|
+
dialog.appendChild(desc);
|
|
94
|
+
}
|
|
95
|
+
const actions = document.createElement("div");
|
|
96
|
+
const btnConfirm = document.createElement("button");
|
|
97
|
+
btnConfirm.textContent = options.confirmText ?? "OK";
|
|
98
|
+
actions.appendChild(btnConfirm);
|
|
99
|
+
let btnCancel = null;
|
|
100
|
+
if (options.cancelText !== null) {
|
|
101
|
+
btnCancel = document.createElement("button");
|
|
102
|
+
btnCancel.textContent = options.cancelText ?? "Cancel";
|
|
103
|
+
actions.appendChild(btnCancel);
|
|
104
|
+
}
|
|
105
|
+
dialog.appendChild(actions);
|
|
106
|
+
overlay.appendChild(dialog);
|
|
107
|
+
document.body.appendChild(overlay);
|
|
108
|
+
const releaseTrap = trapFocus(dialog);
|
|
109
|
+
btnConfirm.focus();
|
|
110
|
+
function close(result) {
|
|
111
|
+
releaseTrap();
|
|
112
|
+
overlay.remove();
|
|
113
|
+
restoreFocus();
|
|
114
|
+
resolve(result);
|
|
115
|
+
}
|
|
116
|
+
btnConfirm.onclick = () => close(true);
|
|
117
|
+
btnCancel && (btnCancel.onclick = () => close(false));
|
|
118
|
+
dialog.addEventListener("keydown", (e) => {
|
|
119
|
+
if (e.key === "Escape") close(false);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/api/alert.ts
|
|
125
|
+
function alert(message) {
|
|
126
|
+
return openDialog({
|
|
127
|
+
title: message,
|
|
128
|
+
role: "dialog",
|
|
129
|
+
confirmText: "OK",
|
|
130
|
+
cancelText: null
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/api/confirm.ts
|
|
135
|
+
function confirm(options) {
|
|
136
|
+
return openDialog({
|
|
137
|
+
...options,
|
|
138
|
+
role: "alertdialog",
|
|
139
|
+
confirmText: "Confirm",
|
|
140
|
+
cancelText: "Cancel"
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
144
|
+
0 && (module.exports = {
|
|
145
|
+
alert,
|
|
146
|
+
confirm
|
|
147
|
+
});
|
package/dist/index.d.cts
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// src/core/focus.ts
|
|
2
|
+
var previousFocus = null;
|
|
3
|
+
function saveFocus() {
|
|
4
|
+
previousFocus = document.activeElement;
|
|
5
|
+
}
|
|
6
|
+
function restoreFocus() {
|
|
7
|
+
if (previousFocus && document.contains(previousFocus)) {
|
|
8
|
+
previousFocus.focus();
|
|
9
|
+
} else {
|
|
10
|
+
document.body.focus();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/core/focusTrap.ts
|
|
15
|
+
function trapFocus(container) {
|
|
16
|
+
function getFocusable() {
|
|
17
|
+
return Array.from(
|
|
18
|
+
container.querySelectorAll(
|
|
19
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
function handle(e) {
|
|
24
|
+
if (e.key !== "Tab") return;
|
|
25
|
+
const items = getFocusable();
|
|
26
|
+
if (!items.length) return;
|
|
27
|
+
const first = items[0];
|
|
28
|
+
const last = items[items.length - 1];
|
|
29
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
last.focus();
|
|
32
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
first.focus();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
container.addEventListener("keydown", handle);
|
|
38
|
+
return () => container.removeEventListener("keydown", handle);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/core/Dialog.ts
|
|
42
|
+
function openDialog(options) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
saveFocus();
|
|
45
|
+
const overlay = document.createElement("div");
|
|
46
|
+
overlay.style.position = "fixed";
|
|
47
|
+
overlay.style.inset = "0";
|
|
48
|
+
overlay.style.background = "rgba(0,0,0,0.4)";
|
|
49
|
+
const dialog = document.createElement("div");
|
|
50
|
+
dialog.setAttribute("role", options.role ?? "dialog");
|
|
51
|
+
dialog.setAttribute("aria-modal", "true");
|
|
52
|
+
dialog.style.background = "white";
|
|
53
|
+
dialog.style.padding = "1rem";
|
|
54
|
+
dialog.style.maxWidth = "400px";
|
|
55
|
+
dialog.style.margin = "20vh auto";
|
|
56
|
+
const title = document.createElement("h2");
|
|
57
|
+
title.id = "dialog-title";
|
|
58
|
+
title.textContent = options.title;
|
|
59
|
+
dialog.setAttribute("aria-labelledby", title.id);
|
|
60
|
+
dialog.appendChild(title);
|
|
61
|
+
if (options.description) {
|
|
62
|
+
const desc = document.createElement("p");
|
|
63
|
+
desc.id = "dialog-desc";
|
|
64
|
+
desc.textContent = options.description;
|
|
65
|
+
dialog.setAttribute("aria-describedby", desc.id);
|
|
66
|
+
dialog.appendChild(desc);
|
|
67
|
+
}
|
|
68
|
+
const actions = document.createElement("div");
|
|
69
|
+
const btnConfirm = document.createElement("button");
|
|
70
|
+
btnConfirm.textContent = options.confirmText ?? "OK";
|
|
71
|
+
actions.appendChild(btnConfirm);
|
|
72
|
+
let btnCancel = null;
|
|
73
|
+
if (options.cancelText !== null) {
|
|
74
|
+
btnCancel = document.createElement("button");
|
|
75
|
+
btnCancel.textContent = options.cancelText ?? "Cancel";
|
|
76
|
+
actions.appendChild(btnCancel);
|
|
77
|
+
}
|
|
78
|
+
dialog.appendChild(actions);
|
|
79
|
+
overlay.appendChild(dialog);
|
|
80
|
+
document.body.appendChild(overlay);
|
|
81
|
+
const releaseTrap = trapFocus(dialog);
|
|
82
|
+
btnConfirm.focus();
|
|
83
|
+
function close(result) {
|
|
84
|
+
releaseTrap();
|
|
85
|
+
overlay.remove();
|
|
86
|
+
restoreFocus();
|
|
87
|
+
resolve(result);
|
|
88
|
+
}
|
|
89
|
+
btnConfirm.onclick = () => close(true);
|
|
90
|
+
btnCancel && (btnCancel.onclick = () => close(false));
|
|
91
|
+
dialog.addEventListener("keydown", (e) => {
|
|
92
|
+
if (e.key === "Escape") close(false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/api/alert.ts
|
|
98
|
+
function alert(message) {
|
|
99
|
+
return openDialog({
|
|
100
|
+
title: message,
|
|
101
|
+
role: "dialog",
|
|
102
|
+
confirmText: "OK",
|
|
103
|
+
cancelText: null
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/api/confirm.ts
|
|
108
|
+
function confirm(options) {
|
|
109
|
+
return openDialog({
|
|
110
|
+
...options,
|
|
111
|
+
role: "alertdialog",
|
|
112
|
+
confirmText: "Confirm",
|
|
113
|
+
cancelText: "Cancel"
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
export {
|
|
117
|
+
alert,
|
|
118
|
+
confirm
|
|
119
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "a11y-alert-dialog",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Accessibility-first alert and dialog library that works without a mouse",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Nguyen Khanh",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/nguyenkhanh/a11y-alert-dialog"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"require": "./dist/index.cjs",
|
|
16
|
+
"types": "./dist/index.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.cjs",
|
|
20
|
+
"module": "./dist/index.mjs",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md",
|
|
25
|
+
"A11Y.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"test": "playwright test",
|
|
30
|
+
"prepublishOnly": "npm run build && npm test"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"accessibility",
|
|
34
|
+
"a11y",
|
|
35
|
+
"alert",
|
|
36
|
+
"dialog",
|
|
37
|
+
"modal",
|
|
38
|
+
"keyboard",
|
|
39
|
+
"wcag",
|
|
40
|
+
"aria"
|
|
41
|
+
],
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"tsup": "^8.0.0",
|
|
44
|
+
"typescript": "^5.0.0",
|
|
45
|
+
"@playwright/test": "^1.40.0"
|
|
46
|
+
}
|
|
47
|
+
}
|