@rcarls/rc-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/README.md +271 -0
- package/dist/custom-elements.json +536 -0
- package/dist/demo.css +85 -0
- package/dist/demo.js +40 -0
- package/dist/rc-dialog-BGtJgQDG.js +177 -0
- package/dist/rc-dialog-BGtJgQDG.js.map +1 -0
- package/dist/rc-dialog-define.js +6 -0
- package/dist/rc-dialog-define.js.map +1 -0
- package/dist/rc-dialog.js +5 -0
- package/dist/rc-dialog.js.map +1 -0
- package/dist/types/packages/rc-dialog/src/define.d.ts +1 -0
- package/dist/types/packages/rc-dialog/src/index.d.ts +1 -0
- package/dist/types/packages/rc-dialog/src/rc-dialog.d.ts +139 -0
- package/dist/types/packages/rc-dialog/src/rc-dialog.test.d.ts +0 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# `@rcarls/rc-dialog`
|
|
2
|
+
|
|
3
|
+
A WAI-ARIA compliant web component that enhances a consumer-provided `<dialog>` element with drag, resize, cancelable close events, and light-dismiss. Built with [Lit 3](https://lit.dev).
|
|
4
|
+
|
|
5
|
+
The component is a **headless wrapper** — it injects no HTML of its own. Your `<dialog>` stays in the document's light DOM with full CSS and assistive-technology access.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @rcarls/rc-dialog
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Import
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import '@rcarls/rc-dialog'; // side-effect: registers <rc-dialog>
|
|
19
|
+
import { RCDialog } from '@rcarls/rc-dialog'; // named class export
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Basic usage
|
|
25
|
+
|
|
26
|
+
Place a `<dialog>` element directly inside `<rc-dialog>`. The inner `<dialog>` must have `aria-labelledby` or `aria-label` to satisfy the [WAI-ARIA Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/).
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<rc-dialog id="my-dialog">
|
|
30
|
+
<dialog aria-labelledby="dlg-title">
|
|
31
|
+
<h2 id="dlg-title">Hello</h2>
|
|
32
|
+
<p>Dialog content.</p>
|
|
33
|
+
<button onclick="document.querySelector('#my-dialog').close()">Close</button>
|
|
34
|
+
</dialog>
|
|
35
|
+
</rc-dialog>
|
|
36
|
+
|
|
37
|
+
<button onclick="document.querySelector('#my-dialog').showModal()">Open</button>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
### Properties / attributes
|
|
45
|
+
|
|
46
|
+
| Property | Attribute | Type | Default | Description |
|
|
47
|
+
|---|---|---|---|---|
|
|
48
|
+
| `movable` | `movable` | `boolean` | `false` | Enable drag-to-move. Named `movable` (not `draggable`) to avoid colliding with the HTML `draggable` attribute. |
|
|
49
|
+
| `moveHandle` | `move-handle` | `string` | `''` | CSS selector for the drag handle within the inner `<dialog>` (e.g. `'.titlebar'`). Defaults to the whole dialog. |
|
|
50
|
+
| `moveBounds` | `move-bounds` | `'viewport' \| 'parent'` | `'viewport'` | Constrains drag within the viewport or the nearest positioned ancestor. |
|
|
51
|
+
| `moveStep` | `move-step` | `number` | `4` | Keyboard arrow-key step in px when moving. Shift multiplies by 10×. |
|
|
52
|
+
| `resize` | `resize` | `'none' \| 'both' \| 'horizontal' \| 'vertical'` | `'none'` | Enables edge/corner resizing, mirroring CSS `resize` semantics. |
|
|
53
|
+
| `resizeThreshold` | `resize-threshold` | `number` | `8` | Edge hit-test band in px (straddles the border — half inside, half outside). |
|
|
54
|
+
| `resizeStep` | `resize-step` | `number` | `4` | Keyboard arrow-key step in px when resizing. Shift multiplies by 10×. |
|
|
55
|
+
| `closedBy` | `closed-by` | `'any' \| 'closerequest' \| 'none' \| ''` | `''` | Proxied to the inner `<dialog closedby="...">` attribute (Chrome 134+, Safari 18.4+, Firefox 139+). `'any'` = Escape or backdrop click; `'closerequest'` = Escape only; `'none'` = programmatic only. |
|
|
56
|
+
| `lightDismiss` | `light-dismiss` | `boolean` | `false` | JS fallback for backdrop-click dismissal. Detects clicks whose target is the `<dialog>` element itself and calls `requestClose()`. Works in all browsers alongside or instead of `closed-by`. |
|
|
57
|
+
|
|
58
|
+
### Methods
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
showModal(): void // Opens as modal (traps focus, shows backdrop).
|
|
62
|
+
show(): void // Opens as non-modal.
|
|
63
|
+
close(returnValue?): void // Closes immediately; sets returnValue.
|
|
64
|
+
requestClose(returnValue?): void
|
|
65
|
+
// Requests close: fires rc-dialog-request-close first. If not prevented,
|
|
66
|
+
// proceeds to close. Falls back to a synthesized cancel event on older
|
|
67
|
+
// browsers that lack native HTMLDialogElement.requestClose().
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Read-only getters
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
open: boolean // Whether the inner <dialog> is currently open.
|
|
74
|
+
returnValue: string // The return value set when the dialog last closed.
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Events
|
|
78
|
+
|
|
79
|
+
| Event | Cancelable | Detail | Description |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| `rc-dialog-request-close` | **Yes** | `{ returnValue: string }` | Fired before close (Escape, backdrop click, or `requestClose()`). Call `preventDefault()` to block. |
|
|
82
|
+
| `rc-dialog-cancel` | No | — | Fired after `rc-dialog-request-close` when the close was not prevented. Backward-compatible alias. |
|
|
83
|
+
| `rc-dialog-close` | No | `{ returnValue: string }` | Fired after the dialog has closed. |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Examples
|
|
88
|
+
|
|
89
|
+
### Movable dialog with a drag handle
|
|
90
|
+
|
|
91
|
+
```html
|
|
92
|
+
<rc-dialog id="dlg" movable move-handle=".titlebar">
|
|
93
|
+
<dialog aria-labelledby="dlg-title">
|
|
94
|
+
<div class="titlebar">
|
|
95
|
+
<span id="dlg-title">Settings</span>
|
|
96
|
+
<button onclick="document.querySelector('#dlg').close()">✕</button>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="body">…</div>
|
|
99
|
+
</dialog>
|
|
100
|
+
</rc-dialog>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Drag the `.titlebar` to reposition. Focus the titlebar and use **Arrow keys** to move it (Shift = 10× step).
|
|
104
|
+
|
|
105
|
+
### Movable + resizable
|
|
106
|
+
|
|
107
|
+
```html
|
|
108
|
+
<rc-dialog id="dlg" movable move-handle=".titlebar" resize="both">
|
|
109
|
+
<dialog aria-labelledby="dlg-title" style="min-width: 20rem; min-height: 10rem;">
|
|
110
|
+
…
|
|
111
|
+
</dialog>
|
|
112
|
+
</rc-dialog>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
All 8 resize handles are active (n, s, e, w, ne, nw, se, sw). Opposite-edge handles anchor the far side and move `left`/`top`, mirroring OS-window resize semantics. A small keyboard-accessible resize button is injected at the bottom-right corner.
|
|
116
|
+
|
|
117
|
+
### Minimal / no-header dialog
|
|
118
|
+
|
|
119
|
+
No titlebar or footer structure is required. Any layout is valid.
|
|
120
|
+
|
|
121
|
+
```html
|
|
122
|
+
<rc-dialog id="dlg">
|
|
123
|
+
<dialog aria-labelledby="dlg-title" aria-describedby="dlg-desc"
|
|
124
|
+
style="border-radius: 12px; padding: 2rem; position: relative;">
|
|
125
|
+
<button style="position:absolute;top:.5rem;right:.5rem;"
|
|
126
|
+
onclick="document.querySelector('#dlg').close()"
|
|
127
|
+
aria-label="Close">✕</button>
|
|
128
|
+
<h3 id="dlg-title">Quick note</h3>
|
|
129
|
+
<p id="dlg-desc">Content here.</p>
|
|
130
|
+
</dialog>
|
|
131
|
+
</rc-dialog>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Alert / confirm dialog
|
|
135
|
+
|
|
136
|
+
Use `role="alertdialog"` with `aria-describedby` pointing to the message text. Assistive technology treats this with higher urgency.
|
|
137
|
+
|
|
138
|
+
```html
|
|
139
|
+
<rc-dialog id="confirm">
|
|
140
|
+
<dialog role="alertdialog"
|
|
141
|
+
aria-labelledby="confirm-title"
|
|
142
|
+
aria-describedby="confirm-msg">
|
|
143
|
+
<div class="titlebar"><span id="confirm-title">Delete item?</span></div>
|
|
144
|
+
<p id="confirm-msg">This action cannot be undone.</p>
|
|
145
|
+
<div class="footer">
|
|
146
|
+
<button onclick="document.querySelector('#confirm').close('cancel')">Cancel</button>
|
|
147
|
+
<button onclick="document.querySelector('#confirm').close('delete')">Delete</button>
|
|
148
|
+
</div>
|
|
149
|
+
</dialog>
|
|
150
|
+
</rc-dialog>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Native form integration
|
|
154
|
+
|
|
155
|
+
`<form method="dialog">` submits to the dialog: the submit button's `value` becomes `returnValue` and the dialog closes — no JavaScript needed.
|
|
156
|
+
|
|
157
|
+
```html
|
|
158
|
+
<rc-dialog id="dlg">
|
|
159
|
+
<dialog aria-labelledby="dlg-title">
|
|
160
|
+
<div class="titlebar">
|
|
161
|
+
<span id="dlg-title">New contact</span>
|
|
162
|
+
<button formmethod="dialog" form="contact-form" value="cancel">✕</button>
|
|
163
|
+
</div>
|
|
164
|
+
<form id="contact-form" method="dialog">
|
|
165
|
+
<label>Name <input name="name" type="text" /></label>
|
|
166
|
+
<label>Email <input name="email" type="email" /></label>
|
|
167
|
+
</form>
|
|
168
|
+
<div class="footer">
|
|
169
|
+
<button formmethod="dialog" form="contact-form" value="cancel">Cancel</button>
|
|
170
|
+
<button type="submit" form="contact-form" value="save">Save</button>
|
|
171
|
+
</div>
|
|
172
|
+
</dialog>
|
|
173
|
+
</rc-dialog>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Cancelable close guard (unsaved changes)
|
|
177
|
+
|
|
178
|
+
```html
|
|
179
|
+
<rc-dialog id="dlg" movable move-handle=".titlebar">
|
|
180
|
+
<dialog aria-labelledby="dlg-title">
|
|
181
|
+
<div class="titlebar">
|
|
182
|
+
<span id="dlg-title">Edit note</span>
|
|
183
|
+
<!-- requestClose() fires rc-dialog-request-close; close() bypasses it -->
|
|
184
|
+
<button onclick="document.querySelector('#dlg').requestClose('cancel')">✕</button>
|
|
185
|
+
</div>
|
|
186
|
+
<textarea id="note" rows="5"></textarea>
|
|
187
|
+
<p id="warning" hidden role="alert">Unsaved changes — save or discard first.</p>
|
|
188
|
+
<div class="footer">
|
|
189
|
+
<button onclick="note.value=''; dlg.close('discard')">Discard</button>
|
|
190
|
+
<button onclick="dlg.close('save')">Save</button>
|
|
191
|
+
</div>
|
|
192
|
+
</dialog>
|
|
193
|
+
</rc-dialog>
|
|
194
|
+
|
|
195
|
+
<script>
|
|
196
|
+
document.querySelector('#dlg').addEventListener('rc-dialog-request-close', (e) => {
|
|
197
|
+
if (document.querySelector('#note').value.trim()) {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
document.querySelector('#warning').hidden = false;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
</script>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Light-dismiss (click backdrop to close)
|
|
206
|
+
|
|
207
|
+
```html
|
|
208
|
+
<!-- Native (Chrome 134+ / Safari 18.4+ / Firefox 139+) + JS fallback -->
|
|
209
|
+
<rc-dialog id="dlg" closed-by="any" light-dismiss>
|
|
210
|
+
<dialog aria-labelledby="dlg-title" aria-describedby="dlg-body">
|
|
211
|
+
<div class="header"><span id="dlg-title">Info</span></div>
|
|
212
|
+
<div id="dlg-body">Click the backdrop or press Escape to dismiss.</div>
|
|
213
|
+
</dialog>
|
|
214
|
+
</rc-dialog>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`closed-by="any"` delegates to the browser's native light-dismiss on supporting browsers. `light-dismiss` adds a JS click-on-backdrop fallback that works everywhere and routes through `requestClose()` (so close guards still apply).
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Accessibility
|
|
222
|
+
|
|
223
|
+
`<rc-dialog>` delegates entirely to the native `<dialog>` element:
|
|
224
|
+
|
|
225
|
+
- **Focus trapping** — built into `showModal()` (Tab/Shift+Tab stays inside the dialog).
|
|
226
|
+
- **Focus restoration** — the browser returns focus to the triggering element on close.
|
|
227
|
+
- **Escape to close** — native behaviour; routes through `rc-dialog-request-close` so guards still apply.
|
|
228
|
+
- **`aria-modal`** — `showModal()` implies `aria-modal="true"` without an explicit attribute.
|
|
229
|
+
|
|
230
|
+
**Required:** The inner `<dialog>` must have `aria-labelledby` or `aria-label`. In development mode the component logs a console warning if either is absent.
|
|
231
|
+
|
|
232
|
+
**Alert dialogs:** When `role="alertdialog"` is set, add `aria-describedby` pointing to the message text. A dev-mode warning fires if it is missing.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## CSS layout for scrollable dialogs
|
|
237
|
+
|
|
238
|
+
When the dialog needs scrollable body content (especially when resizable), use flex layout so the body grows and the header/footer stay fixed:
|
|
239
|
+
|
|
240
|
+
```css
|
|
241
|
+
dialog[open] {
|
|
242
|
+
display: flex;
|
|
243
|
+
flex-direction: column;
|
|
244
|
+
overflow: hidden; /* dialog box doesn't scroll; body does */
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.dlg-header,
|
|
248
|
+
.dlg-footer {
|
|
249
|
+
flex-shrink: 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.dlg-body {
|
|
253
|
+
flex: 1;
|
|
254
|
+
overflow: auto;
|
|
255
|
+
min-height: 0; /* lets flex child shrink below content height */
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
> **Note:** `display: flex` must be scoped to `dialog[open]`, not `dialog`. Author styles take precedence over the UA `dialog:not([open]) { display: none }` rule — scoping to `[open]` prevents all dialogs from being visible on page load.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Browser support
|
|
264
|
+
|
|
265
|
+
| Feature | Requirement |
|
|
266
|
+
|---|---|
|
|
267
|
+
| Core (`<dialog>` delegation, events) | Chrome 37+, Firefox 98+, Safari 15.4+ |
|
|
268
|
+
| Drag / resize | Any browser supporting Pointer Events |
|
|
269
|
+
| `closed-by` attribute proxy | Chrome 134+, Safari 18.4+, Firefox 139+ |
|
|
270
|
+
| `requestClose()` native method | Chrome 134+, Safari 18.4+, Firefox 139+ (fallback active on older browsers) |
|
|
271
|
+
| `light-dismiss` JS fallback | All browsers |
|