@reliability-design/web 1.4.6 → 1.4.8

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.
Files changed (148) hide show
  1. package/CHANGELOG.md +246 -49
  2. package/dist/components/rel-accordion/README.md +133 -132
  3. package/dist/components/rel-alert/README.md +145 -141
  4. package/dist/components/rel-autocomplete/README.md +172 -61
  5. package/dist/components/rel-avatar/README.md +118 -130
  6. package/dist/components/rel-badge/README.md +97 -93
  7. package/dist/components/rel-breadcrumb/README.md +96 -102
  8. package/dist/components/rel-button/README.md +190 -130
  9. package/dist/components/rel-calendar/README.md +109 -109
  10. package/dist/components/rel-card/README.md +75 -94
  11. package/dist/components/rel-cardmedia/README.md +100 -127
  12. package/dist/components/rel-carousel/README.md +186 -166
  13. package/dist/components/rel-checkbox/README.md +96 -75
  14. package/dist/components/rel-chip/README.md +138 -121
  15. package/dist/components/rel-container/README.md +107 -99
  16. package/dist/components/rel-date-picker/README.md +200 -289
  17. package/dist/components/rel-divider/README.md +88 -100
  18. package/dist/components/rel-drawer/README.md +187 -200
  19. package/dist/components/rel-kbd/README.md +141 -151
  20. package/dist/components/rel-layout/README.md +34 -0
  21. package/dist/components/rel-menu/README.md +180 -151
  22. package/dist/components/rel-menubar/README.md +218 -329
  23. package/dist/components/rel-otp/README.md +171 -440
  24. package/dist/components/rel-pagination/README.md +100 -424
  25. package/dist/components/rel-popover/README.md +203 -568
  26. package/dist/components/rel-progress/README.md +88 -110
  27. package/dist/components/rel-radio/README.md +131 -138
  28. package/dist/components/rel-range-picker/README.md +165 -0
  29. package/dist/components/rel-resizable/README.md +199 -140
  30. package/dist/components/rel-segmented-button/README.md +156 -141
  31. package/dist/components/rel-select/README.md +156 -93
  32. package/dist/components/rel-sheet/README.md +168 -224
  33. package/dist/components/rel-skeleton/README.md +104 -0
  34. package/dist/components/rel-slider/README.md +131 -135
  35. package/dist/components/rel-snackbar/README.md +168 -134
  36. package/dist/components/rel-stepper/README.md +162 -160
  37. package/dist/components/rel-switch/README.md +117 -102
  38. package/dist/components/rel-tab/README.md +173 -163
  39. package/dist/components/rel-textfield/README.md +176 -69
  40. package/dist/components/rel-time-picker/README.md +122 -157
  41. package/dist/components/rel-toast/README.md +165 -148
  42. package/dist/components/rel-tooltip/README.md +119 -111
  43. package/dist/components/rel-upload/README.md +220 -0
  44. package/dist/index.d.mts +1726 -447
  45. package/dist/index.d.ts +1726 -447
  46. package/dist/index.js +13715 -10870
  47. package/dist/index.js.map +1 -1
  48. package/dist/index.mjs +14018 -11163
  49. package/dist/index.mjs.map +1 -1
  50. package/dist/styles/focus-ring.ts +84 -0
  51. package/dist/styles/vars.css +892 -866
  52. package/dist/{vars-XNMTSRVN.css → vars-DXX6QOAG.css} +892 -866
  53. package/package.json +3 -2
  54. package/dist/components/rel-accordion/rel-accordion.ts +0 -336
  55. package/dist/components/rel-alert/rel-alert.ts +0 -305
  56. package/dist/components/rel-autocomplete/rel-autocomplete.ts +0 -720
  57. package/dist/components/rel-avatar/rel-avatar.ts +0 -142
  58. package/dist/components/rel-badge/rel-badge.ts +0 -175
  59. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +0 -106
  60. package/dist/components/rel-button/rel-button.ts +0 -560
  61. package/dist/components/rel-calendar/index.ts +0 -1
  62. package/dist/components/rel-calendar/rel-calendar.ts +0 -565
  63. package/dist/components/rel-card/rel-card.ts +0 -88
  64. package/dist/components/rel-cardmedia/rel-cardmedia.ts +0 -88
  65. package/dist/components/rel-carousel/index.ts +0 -1
  66. package/dist/components/rel-carousel/rel-carousel.ts +0 -723
  67. package/dist/components/rel-chart/README.md +0 -286
  68. package/dist/components/rel-chart/index.ts +0 -1
  69. package/dist/components/rel-chart/rel-chart.ts +0 -754
  70. package/dist/components/rel-checkbox/rel-checkbox.ts +0 -382
  71. package/dist/components/rel-chip/rel-chip.ts +0 -483
  72. package/dist/components/rel-container/rel-container.ts +0 -95
  73. package/dist/components/rel-date-picker/date-format.test.ts +0 -43
  74. package/dist/components/rel-date-picker/date-format.ts +0 -47
  75. package/dist/components/rel-date-picker/date-parse.test.ts +0 -116
  76. package/dist/components/rel-date-picker/date-parse.ts +0 -178
  77. package/dist/components/rel-date-picker/index.ts +0 -1
  78. package/dist/components/rel-date-picker/rel-date-picker.ts +0 -1676
  79. package/dist/components/rel-divider/rel-divider.ts +0 -54
  80. package/dist/components/rel-drawer/index.ts +0 -1
  81. package/dist/components/rel-drawer/rel-drawer-footer.ts +0 -27
  82. package/dist/components/rel-drawer/rel-drawer.ts +0 -607
  83. package/dist/components/rel-kbd/rel-kbd.ts +0 -188
  84. package/dist/components/rel-layout/rel-layout.ts +0 -49
  85. package/dist/components/rel-menu/rel-menu.ts +0 -541
  86. package/dist/components/rel-menubar/index.ts +0 -2
  87. package/dist/components/rel-menubar/rel-menubar-menu.css +0 -3
  88. package/dist/components/rel-menubar/rel-menubar-menu.ts +0 -17
  89. package/dist/components/rel-menubar/rel-menubar.ts +0 -777
  90. package/dist/components/rel-nav-menu/README.md +0 -289
  91. package/dist/components/rel-nav-menu/index.ts +0 -3
  92. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +0 -243
  93. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +0 -72
  94. package/dist/components/rel-nav-menu/rel-nav-menu.ts +0 -270
  95. package/dist/components/rel-navbar/README.md +0 -126
  96. package/dist/components/rel-navbar/rel-navbar.ts +0 -105
  97. package/dist/components/rel-otp/index.ts +0 -1
  98. package/dist/components/rel-otp/rel-otp.ts +0 -527
  99. package/dist/components/rel-pagination/index.ts +0 -1
  100. package/dist/components/rel-pagination/rel-pagination.ts +0 -522
  101. package/dist/components/rel-popover/index.ts +0 -2
  102. package/dist/components/rel-popover/rel-popover-content.ts +0 -81
  103. package/dist/components/rel-popover/rel-popover.ts +0 -475
  104. package/dist/components/rel-progress/rel-progress.ts +0 -234
  105. package/dist/components/rel-radio/rel-radio.ts +0 -481
  106. package/dist/components/rel-range-picker/rel-range-picker.ts +0 -1688
  107. package/dist/components/rel-resizable/index.ts +0 -3
  108. package/dist/components/rel-resizable/rel-resizable-group.ts +0 -405
  109. package/dist/components/rel-resizable/rel-resizable-handle.ts +0 -150
  110. package/dist/components/rel-resizable/rel-resizable-panel.ts +0 -58
  111. package/dist/components/rel-segmented-button/rel-segmented-button.ts +0 -217
  112. package/dist/components/rel-select/rel-select.ts +0 -1416
  113. package/dist/components/rel-sheet/rel-sheet.ts +0 -610
  114. package/dist/components/rel-sidebar/README.md +0 -209
  115. package/dist/components/rel-sidebar/rel-sidebar.ts +0 -1191
  116. package/dist/components/rel-skeleton/rel-skeleton.ts +0 -143
  117. package/dist/components/rel-slider/rel-slider.ts +0 -673
  118. package/dist/components/rel-snackbar/rel-snackbar.ts +0 -329
  119. package/dist/components/rel-stepper/rel-stepper.ts +0 -567
  120. package/dist/components/rel-switch/rel-switch.ts +0 -199
  121. package/dist/components/rel-tab/rel-tab.ts +0 -264
  122. package/dist/components/rel-table/README.md +0 -178
  123. package/dist/components/rel-table/rel-table-body.ts +0 -22
  124. package/dist/components/rel-table/rel-table-caption.ts +0 -30
  125. package/dist/components/rel-table/rel-table-cell.ts +0 -43
  126. package/dist/components/rel-table/rel-table-footer.ts +0 -24
  127. package/dist/components/rel-table/rel-table-head.ts +0 -47
  128. package/dist/components/rel-table/rel-table-header.ts +0 -22
  129. package/dist/components/rel-table/rel-table-row.ts +0 -37
  130. package/dist/components/rel-table/rel-table.ts +0 -62
  131. package/dist/components/rel-textfield/rel-textfield.ts +0 -1054
  132. package/dist/components/rel-time-picker/rel-time-picker.ts +0 -1482
  133. package/dist/components/rel-time-picker/time-parse.test.ts +0 -67
  134. package/dist/components/rel-time-picker/time-parse.ts +0 -85
  135. package/dist/components/rel-toast/rel-toast.ts +0 -206
  136. package/dist/components/rel-tooltip/rel-tooltip.ts +0 -193
  137. package/dist/components/rel-topbar/rel-topbar-actions.ts +0 -27
  138. package/dist/components/rel-topbar/rel-topbar-brand.ts +0 -54
  139. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +0 -69
  140. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +0 -31
  141. package/dist/components/rel-topbar/rel-topbar-item.ts +0 -324
  142. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +0 -27
  143. package/dist/components/rel-topbar/rel-topbar-profile.ts +0 -243
  144. package/dist/components/rel-topbar/rel-topbar-utilities.ts +0 -27
  145. package/dist/components/rel-topbar/rel-topbar.ts +0 -446
  146. package/dist/components/rel-upload/UPLOAD-DESIGN-CHANGES.md +0 -134
  147. package/dist/components/rel-upload/rel-upload-actions.ts +0 -163
  148. package/dist/components/rel-upload/rel-upload.ts +0 -968
@@ -1,568 +1,203 @@
1
- # rel-popover
2
-
3
- A flexible popover component that displays rich content anchored to a trigger element. Features smart positioning with collision detection, controlled and uncontrolled modes, focus management, and accessibility support.
4
-
5
- ## What It Is
6
-
7
- The popover consists of two custom elements:
8
- - `<rel-popover>` - Root controller that manages state, positioning, and interactions
9
- - `<rel-popover-content>` - Optional styled container for popover content with consistent design tokens
10
-
11
- ## Basic Usage
12
-
13
- ```html
14
- <rel-popover>
15
- <button slot="trigger">Open Popover</button>
16
- <rel-popover-content>
17
- <h3>Popover Title</h3>
18
- <p>This is the popover content. It can contain any HTML elements.</p>
19
- </rel-popover-content>
20
- </rel-popover>
21
- ```
22
-
23
- ## Controlled Mode
24
-
25
- Control the open state externally:
26
-
27
- ```html
28
- <rel-popover id="my-popover" open>
29
- <button slot="trigger">Open Popover</button>
30
- <rel-popover-content>
31
- <p>Controlled popover content</p>
32
- <button data-popover-close>Close</button>
33
- </rel-popover-content>
34
- </rel-popover>
35
-
36
- <script>
37
- const popover = document.getElementById('my-popover');
38
-
39
- // Listen to open state changes
40
- popover.addEventListener('rel-open-change', (e) => {
41
- console.log('Popover is now:', e.detail.open ? 'open' : 'closed');
42
-
43
- // Update external state
44
- // You control when to update the 'open' attribute
45
- });
46
-
47
- // Programmatically control
48
- document.getElementById('external-btn').addEventListener('click', () => {
49
- popover.open = true;
50
- });
51
- </script>
52
- ```
53
-
54
- ## Positioning Options
55
-
56
- ```html
57
- <!-- Position above trigger, aligned to start -->
58
- <rel-popover side="top" align="start">
59
- <button slot="trigger">Top Start</button>
60
- <rel-popover-content>
61
- <p>Positioned above, aligned to start</p>
62
- </rel-popover-content>
63
- </rel-popover>
64
-
65
- <!-- Position to the right with offset -->
66
- <rel-popover side="right" sideOffset="16" alignOffset="8">
67
- <button slot="trigger">Right with Offset</button>
68
- <rel-popover-content>
69
- <p>Positioned to right with custom spacing</p>
70
- </rel-popover-content>
71
- </rel-popover>
72
- ```
73
-
74
- ## Form Example with Focus Trap
75
-
76
- ```html
77
- <rel-popover trapFocus>
78
- <button slot="trigger">Edit Profile</button>
79
- <rel-popover-content aria-label="Edit profile form">
80
- <h3>Edit Profile</h3>
81
- <form>
82
- <label>
83
- Name:
84
- <input type="text" value="John Doe" />
85
- </label>
86
- <label>
87
- Email:
88
- <input type="email" value="john@example.com" />
89
- </label>
90
- <div style="display: flex; gap: 8px; margin-top: 12px;">
91
- <button type="submit">Save</button>
92
- <button type="button" data-popover-close>Cancel</button>
93
- </div>
94
- </form>
95
- </rel-popover-content>
96
- </rel-popover>
97
- ```
98
-
99
- ## API Reference
100
-
101
- ### rel-popover
102
-
103
- Root controller component that manages popover behavior.
104
-
105
- **Attributes:**
106
-
107
- - `open` (boolean, optional) - Controlled open state. When set, component is in controlled mode
108
- - `defaultOpen` (boolean) - Initial open state for uncontrolled mode, default: `false`
109
- - `disabled` (boolean) - Disables the popover, default: `false`
110
- - `side` ("top" | "right" | "bottom" | "left") - Preferred side to position popover, default: `"bottom"`
111
- - `align` ("start" | "center" | "end") - Alignment relative to trigger, default: `"center"`
112
- - `sideOffset` (number) - Distance from trigger in pixels, default: `8`
113
- - `alignOffset` (number) - Offset along the alignment axis in pixels, default: `0`
114
- - `trapFocus` (boolean) - Trap focus within popover (useful for forms), default: `false`
115
- - `closeOnEscape` (boolean) - Close when Escape key is pressed, default: `true`
116
- - `closeOnOutsideClick` (boolean) - Close when clicking outside, default: `true`
117
-
118
- **Events:**
119
-
120
- - `rel-open-change` - Emitted when open state changes
121
- ```typescript
122
- detail: { open: boolean }
123
- ```
124
-
125
- **Slots:**
126
-
127
- - `trigger` (named) - The trigger element (usually a button)
128
- - Default slot - The popover content
129
-
130
- **Example:**
131
- ```javascript
132
- const popover = document.querySelector('rel-popover');
133
-
134
- // Listen to state changes
135
- popover.addEventListener('rel-open-change', (e) => {
136
- console.log('Open:', e.detail.open);
137
- });
138
-
139
- // Programmatic control (controlled mode)
140
- popover.open = true;
141
- ```
142
-
143
- ### rel-popover-content
144
-
145
- Styled container for popover content with consistent design tokens.
146
-
147
- **Attributes:**
148
-
149
- - `aria-label` (string) - Accessible label for the popover dialog
150
-
151
- **Usage:**
152
- ```html
153
- <rel-popover-content aria-label="User settings">
154
- <h3>Settings</h3>
155
- <p>Configure your preferences</p>
156
- </rel-popover-content>
157
- ```
158
-
159
- **Styling:**
160
- The content component provides consistent styling:
161
- - White background with border and shadow
162
- - Proper padding and spacing
163
- - Heading and paragraph styles with design tokens
164
- - Maximum width of 400px (can be overridden)
165
-
166
- **Custom Content:**
167
- You don't have to use `rel-popover-content`. Any HTML can go in the default slot:
168
- ```html
169
- <rel-popover>
170
- <button slot="trigger">Open</button>
171
- <div class="my-custom-popover">
172
- Custom styled content
173
- </div>
174
- </rel-popover>
175
- ```
176
-
177
- ## Controlled vs Uncontrolled
178
-
179
- ### Uncontrolled (Default)
180
- Component manages its own open state:
181
- ```html
182
- <rel-popover defaultOpen>
183
- <button slot="trigger">Toggle</button>
184
- <rel-popover-content>Content</rel-popover-content>
185
- </rel-popover>
186
- ```
187
-
188
- The component will:
189
- - Toggle open/close when trigger is clicked
190
- - Emit `rel-open-change` events
191
- - Update its internal state automatically
192
-
193
- ### Controlled
194
- You manage the open state externally:
195
- ```html
196
- <rel-popover id="controlled" open="${this.isOpen}">
197
- <button slot="trigger">Toggle</button>
198
- <rel-popover-content>Content</rel-popover-content>
199
- </rel-popover>
200
-
201
- <script>
202
- let isOpen = false;
203
- const popover = document.getElementById('controlled');
204
-
205
- popover.addEventListener('rel-open-change', (e) => {
206
- isOpen = e.detail.open;
207
- popover.open = isOpen; // Update the attribute
208
- });
209
- </script>
210
- ```
211
-
212
- When `open` attribute is set, the component:
213
- - Will NOT update its own state
214
- - Emits `rel-open-change` events
215
- - You must update the `open` attribute in response to events
216
-
217
- ## Positioning Behavior
218
-
219
- The popover uses smart positioning with collision detection:
220
-
221
- ### Side Preference
222
- Specify the preferred side with the `side` attribute:
223
- - `"bottom"` (default) - Opens below trigger
224
- - `"top"` - Opens above trigger
225
- - `"right"` - Opens to the right
226
- - `"left"` - Opens to the left
227
-
228
- ### Collision Detection
229
- If the popover would overflow the viewport, it automatically flips to the opposite side:
230
- - Bottom → Top (if more space above)
231
- - Top → Bottom (if more space below)
232
- - Right → Left (if more space on left)
233
- - Left → Right (if more space on right)
234
-
235
- ### Alignment
236
- Control how the popover aligns to its trigger:
237
-
238
- For top/bottom sides:
239
- - `"start"` - Aligns to left edge of trigger
240
- - `"center"` (default) - Centers horizontally
241
- - `"end"` - Aligns to right edge of trigger
242
-
243
- For left/right sides:
244
- - `"start"` - Aligns to top edge of trigger
245
- - `"center"` (default) - Centers vertically
246
- - `"end"` - Aligns to bottom edge of trigger
247
-
248
- ### Offsets
249
- Fine-tune positioning with offsets:
250
- - `sideOffset` - Distance from trigger (default: 8px)
251
- - `alignOffset` - Shift along alignment axis (default: 0)
252
-
253
- ```html
254
- <!-- 16px away from trigger, shifted 8px to the right -->
255
- <rel-popover side="bottom" sideOffset="16" alignOffset="8">
256
- <button slot="trigger">Custom Offset</button>
257
- <rel-popover-content>Content</rel-popover-content>
258
- </rel-popover>
259
- ```
260
-
261
- ### Dynamic Repositioning
262
- The popover automatically repositions when:
263
- - Window is resized
264
- - Page is scrolled
265
- - Popover content size changes
266
-
267
- ## Close Triggers
268
-
269
- The popover can close in several ways:
270
-
271
- 1. **Click Outside** (default enabled)
272
- - Click anywhere outside the popover
273
- - Controlled by `closeOnOutsideClick` attribute
274
-
275
- 2. **Escape Key** (default enabled)
276
- - Press Escape to close
277
- - Controlled by `closeOnEscape` attribute
278
-
279
- 3. **Close Button**
280
- - Add `data-popover-close` to any element
281
- ```html
282
- <button data-popover-close>Close</button>
283
- <button type="button" data-popover-close>Cancel</button>
284
- ```
285
-
286
- 4. **Programmatic**
287
- ```javascript
288
- popover.open = false;
289
- ```
290
-
291
- ## Focus Management
292
-
293
- ### Default Behavior
294
- When popover opens:
295
- 1. Saves currently focused element
296
- 2. Focuses first focusable element in popover
297
- 3. Returns focus to trigger when closed
298
-
299
- ### Focus Trap
300
- Enable focus trapping for forms and modal-like behavior:
301
- ```html
302
- <rel-popover trapFocus>
303
- <button slot="trigger">Edit</button>
304
- <rel-popover-content>
305
- <form>
306
- <input type="text" />
307
- <button>Save</button>
308
- <button data-popover-close>Cancel</button>
309
- </form>
310
- </rel-popover-content>
311
- </rel-popover>
312
- ```
313
-
314
- With `trapFocus` enabled:
315
- - Tab cycles through focusable elements
316
- - When reaching last element, Tab wraps to first
317
- - Shift+Tab cycles backward
318
- - Cannot tab out of the popover
319
-
320
- ## Keyboard Navigation
321
-
322
- | Key | Action |
323
- |-----|--------|
324
- | `Click Trigger` | Toggle popover open/close |
325
- | `Escape` | Close popover (if `closeOnEscape` is true) |
326
- | `Tab` | Move to next focusable element |
327
- | `Shift + Tab` | Move to previous focusable element |
328
- | `Tab` (with `trapFocus`) | Cycle through elements, wrapping at edges |
329
-
330
- ## Accessibility
331
-
332
- The component implements WCAG accessibility guidelines:
333
-
334
- ### ARIA Attributes
335
- - Trigger has `aria-haspopup="dialog"`
336
- - Trigger has `aria-expanded` reflecting open state
337
- - Trigger has `aria-controls` pointing to content
338
- - Content has `role="dialog"`
339
- - Content can have `aria-label` for screen readers
340
-
341
- ### Focus Management
342
- - Automatically focuses first focusable element
343
- - Returns focus to trigger on close
344
- - Optional focus trapping for modal behavior
345
-
346
- ### Keyboard Support
347
- - Full keyboard navigation
348
- - Escape key to close
349
- - Tab navigation through content
350
-
351
- ### Best Practices
352
- ```html
353
- <!-- Good: Descriptive trigger and labeled content -->
354
- <rel-popover>
355
- <button slot="trigger" aria-label="Open user settings">
356
- <svg>...</svg>
357
- </button>
358
- <rel-popover-content aria-label="User settings">
359
- <h3>Settings</h3>
360
- <!-- ... -->
361
- </rel-popover-content>
362
- </rel-popover>
363
-
364
- <!-- Good: Close button with clear label -->
365
- <button type="button" data-popover-close aria-label="Close settings">
366
- ×
367
- </button>
368
- ```
369
-
370
- ## Styling Tokens
371
-
372
- The component uses design tokens from the Reliability Design System:
373
-
374
- ### Colors (rel-popover-content)
375
- - `--rel-bg-surface` - Content background
376
- - `--rel-border-default` - Border color
377
- - `--rel-text-label` - Text color
378
- - `--rel-text-placeholder` - Paragraph text color
379
-
380
- ### Typography
381
- - `--rel-fonts-font-family` - Font family (Poppins)
382
- - `--rel-fonts-weight-body-semibold` - Heading weight
383
- - `--rel-body-sm-regular-font-size` - Base text size
384
- - `--rel-body-md-regular-line-height` - Paragraph line height
385
-
386
- ### Spacing
387
- - `--rel-spacing-md` (16px) - Content padding
388
- - `--rel-spacing-xs` (8px) - Heading bottom margin
389
- - `--rel-spacing-sm` (12px) - Paragraph bottom margin
390
-
391
- ### Borders & Effects
392
- - `--rel-stroke-sm` (1px) - Border width
393
- - `--rel-border-radius-sm` (8px) - Border radius
394
- - `--rel-shadows-shadow-300` - Drop shadow
395
-
396
- ### Z-Index
397
- - Popover content: `z-index: 1001`
398
-
399
- ### Custom Styling
400
- Use CSS parts to style the component:
401
- ```css
402
- /* Style the trigger wrapper */
403
- rel-popover::part(trigger) {
404
- display: block;
405
- width: 100%;
406
- }
407
-
408
- /* Style the content wrapper */
409
- rel-popover::part(content-wrapper) {
410
- /* Custom styles */
411
- }
412
-
413
- /* Style the content container */
414
- rel-popover-content::part(content) {
415
- max-width: 600px;
416
- padding: 24px;
417
- }
418
- ```
419
-
420
- ## Complete Example
421
-
422
- ```html
423
- <!DOCTYPE html>
424
- <html lang="en">
425
- <head>
426
- <meta charset="UTF-8">
427
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
428
- <title>Popover Demo</title>
429
- <script type="module" src="./dist/index.js"></script>
430
- </head>
431
- <body>
432
- <div style="padding: 100px;">
433
- <!-- Basic Popover -->
434
- <rel-popover id="basic-popover">
435
- <button slot="trigger">Open Popover</button>
436
- <rel-popover-content aria-label="Information">
437
- <h3>About Popovers</h3>
438
- <p>Popovers are floating containers that display rich content anchored to a trigger element.</p>
439
- <button data-popover-close>Got it</button>
440
- </rel-popover-content>
441
- </rel-popover>
442
-
443
- <!-- Positioning Variations -->
444
- <rel-popover side="top" align="start">
445
- <button slot="trigger">Top Start</button>
446
- <rel-popover-content>
447
- <p>Positioned above, aligned to start</p>
448
- </rel-popover-content>
449
- </rel-popover>
450
-
451
- <rel-popover side="right" align="center">
452
- <button slot="trigger">Right Center</button>
453
- <rel-popover-content>
454
- <p>Positioned to the right, centered</p>
455
- </rel-popover-content>
456
- </rel-popover>
457
-
458
- <!-- Form with Focus Trap -->
459
- <rel-popover id="form-popover" trapFocus>
460
- <button slot="trigger">Edit Profile</button>
461
- <rel-popover-content aria-label="Edit profile form">
462
- <h3>Edit Profile</h3>
463
- <form id="profile-form">
464
- <div style="margin-bottom: 12px;">
465
- <label>
466
- Name:<br>
467
- <input type="text" name="name" value="John Doe" style="width: 100%;">
468
- </label>
469
- </div>
470
- <div style="margin-bottom: 12px;">
471
- <label>
472
- Email:<br>
473
- <input type="email" name="email" value="john@example.com" style="width: 100%;">
474
- </label>
475
- </div>
476
- <div style="display: flex; gap: 8px;">
477
- <button type="submit">Save</button>
478
- <button type="button" data-popover-close>Cancel</button>
479
- </div>
480
- </form>
481
- </rel-popover-content>
482
- </rel-popover>
483
-
484
- <!-- Controlled Mode -->
485
- <div>
486
- <button id="external-open">Open Controlled Popover</button>
487
- <button id="external-close">Close Controlled Popover</button>
488
- <p>State: <span id="state-display">closed</span></p>
489
-
490
- <rel-popover id="controlled-popover">
491
- <button slot="trigger">Controlled Trigger</button>
492
- <rel-popover-content>
493
- <h3>Controlled Popover</h3>
494
- <p>This popover's state is controlled externally.</p>
495
- <button data-popover-close>Close</button>
496
- </rel-popover-content>
497
- </rel-popover>
498
- </div>
499
- </div>
500
-
501
- <script>
502
- // Event logging
503
- document.getElementById('basic-popover').addEventListener('rel-open-change', (e) => {
504
- console.log('Basic popover:', e.detail.open ? 'opened' : 'closed');
505
- });
506
-
507
- // Form handling
508
- document.getElementById('profile-form').addEventListener('submit', (e) => {
509
- e.preventDefault();
510
- const formData = new FormData(e.target);
511
- console.log('Saved:', Object.fromEntries(formData));
512
- document.getElementById('form-popover').open = false;
513
- });
514
-
515
- // Controlled mode
516
- const controlledPopover = document.getElementById('controlled-popover');
517
- const stateDisplay = document.getElementById('state-display');
518
-
519
- let isOpen = false;
520
-
521
- controlledPopover.addEventListener('rel-open-change', (e) => {
522
- isOpen = e.detail.open;
523
- controlledPopover.open = isOpen;
524
- stateDisplay.textContent = isOpen ? 'open' : 'closed';
525
- });
526
-
527
- document.getElementById('external-open').addEventListener('click', () => {
528
- isOpen = true;
529
- controlledPopover.open = isOpen;
530
- stateDisplay.textContent = 'open';
531
- });
532
-
533
- document.getElementById('external-close').addEventListener('click', () => {
534
- isOpen = false;
535
- controlledPopover.open = isOpen;
536
- stateDisplay.textContent = 'closed';
537
- });
538
- </script>
539
- </body>
540
- </html>
541
- ```
542
-
543
- ## Browser Support
544
-
545
- Compatible with all modern browsers supporting:
546
- - Web Components (Custom Elements v1)
547
- - Shadow DOM
548
- - ES2020+ features
549
- - ResizeObserver API
550
-
551
- ## Framework Integration
552
-
553
- Works seamlessly with:
554
- - Vanilla JavaScript
555
- - Angular
556
- - React (via wrapper)
557
- - Vue
558
- - Any framework supporting Web Components
559
-
560
- ## Comparison with Tooltip
561
-
562
- While `rel-tooltip` is for brief labels on hover, `rel-popover` is for:
563
- - **Rich interactive content** - Forms, menus, complex layouts
564
- - **Explicit user action** - Click to open (not hover)
565
- - **Focus management** - Can trap focus for forms
566
- - **Controlled state** - External state management support
567
-
568
- Use `rel-tooltip` for simple labels, use `rel-popover` for interactive content.
1
+ # rel-popover
2
+
3
+ A click-to-open floating panel anchored to a trigger, implementing the Figma "Popover" component (including its opt-in chevron). The folder registers two elements: `<rel-popover>` is the controller — its first child is the trigger, its `content` slot is the panel — and `<rel-popover-content>` is the optional styled surface (neutral background, border, radius, shadow, S/M/L padding) you drop into that slot. The panel is promoted to the browser top layer and positioned in viewport coordinates, so it renders correctly even inside a centred `<rel-modal>`. Use it for rich, interactive content that needs an explicit click — small forms, filters, menus, detail cards. For a short hover label use `<rel-tooltip>`; for a task that must be completed before continuing use `<rel-modal>`.
4
+
5
+ ## Usage
6
+
7
+ ```html
8
+ <!-- Basic: first child = trigger, slot="content" = panel -->
9
+ <rel-popover>
10
+ <rel-button>Open</rel-button>
11
+ <rel-popover-content aria-label="About popovers">
12
+ <h3>Popover title</h3>
13
+ <p>Any HTML can go here.</p>
14
+ </rel-popover-content>
15
+ </rel-popover>
16
+
17
+ <!-- Panel padding (Figma Size S / M / L = 12 / 16 / 20px) -->
18
+ <rel-popover>
19
+ <rel-button variant="secondary">Compact</rel-button>
20
+ <rel-popover-content size="small" aria-label="Compact panel"><p>12px padding</p></rel-popover-content>
21
+ </rel-popover>
22
+ <rel-popover>
23
+ <rel-button variant="secondary">Roomy</rel-button>
24
+ <rel-popover-content size="large" aria-label="Roomy panel"><p>20px padding</p></rel-popover-content>
25
+ </rel-popover>
26
+
27
+ <!-- Positioning: side, alignment and pixel offsets -->
28
+ <rel-popover side="top" align="start">
29
+ <rel-button>Top / start</rel-button>
30
+ <rel-popover-content aria-label="Top start"><p>Above the trigger, left edges aligned.</p></rel-popover-content>
31
+ </rel-popover>
32
+ <rel-popover side="right" sideoffset="16" alignoffset="8">
33
+ <rel-button>Right, 16px away</rel-button>
34
+ <rel-popover-content aria-label="Right offset"><p>16px gap, shifted 8px down.</p></rel-popover-content>
35
+ </rel-popover>
36
+
37
+ <!-- Chevron pointing at the trigger -->
38
+ <rel-popover side="bottom" arrow>
39
+ <rel-button>With arrow</rel-button>
40
+ <rel-popover-content aria-label="Arrow demo"><p>The chevron follows the trigger centre.</p></rel-popover-content>
41
+ </rel-popover>
42
+
43
+ <!-- Any element can be the trigger or the panel -->
44
+ <rel-popover>
45
+ <span>Plain text trigger (made focusable automatically)</span>
46
+ <div slot="content" style="padding: 12px; background: var(--rel-bg-neutral); border: 1px solid var(--rel-border-default); border-radius: 8px;">
47
+ Unstyled custom panel
48
+ </div>
49
+ </rel-popover>
50
+
51
+ <!-- Form with focus trap: Tab cycles inside the panel until it closes -->
52
+ <rel-popover trapfocus>
53
+ <rel-button>Edit profile</rel-button>
54
+ <rel-popover-content aria-label="Edit profile">
55
+ <h3>Edit profile</h3>
56
+ <form>
57
+ <rel-textfield label="Name" value="Ada Lovelace"></rel-textfield>
58
+ <rel-textfield label="Email" type="email" value="ada@example.com"></rel-textfield>
59
+ <rel-button>Save</rel-button>
60
+ </form>
61
+ </rel-popover-content>
62
+ </rel-popover>
63
+
64
+ <!-- Controlled: you own `open` and apply every requested change -->
65
+ <rel-popover id="controlled" open>
66
+ <rel-button>Controlled trigger</rel-button>
67
+ <rel-popover-content aria-label="Controlled panel">
68
+ <p>State lives outside the component.</p>
69
+ <rel-button id="done" variant="secondary">Done</rel-button>
70
+ </rel-popover-content>
71
+ </rel-popover>
72
+ <script>
73
+ const popover = document.getElementById('controlled');
74
+ popover.addEventListener('rel-open-change', (e) => {
75
+ popover.open = e.detail.open; // nothing happens until you apply it
76
+ });
77
+ document.getElementById('done').addEventListener('click', () => {
78
+ popover.open = false;
79
+ });
80
+ // Uncontrolled instances: never set `open`; start open with `defaultopen` instead.
81
+ // Default-true booleans are switched off via the property:
82
+ popover.closeOnOutsideClick = false;
83
+ popover.closeOnEscape = false;
84
+ </script>
85
+ ```
86
+
87
+ <!-- readme-gen:start -->
88
+
89
+ <!-- Generated from rel-popover.ts, rel-popover-content.ts — do not edit; run readme-generate.mjs -->
90
+
91
+ ## Elements
92
+
93
+ | Element | Class | Source |
94
+ | --- | --- | --- |
95
+ | `<rel-popover>` | `RelPopover` | `rel-popover.ts` |
96
+ | `<rel-popover-content>` | `RelPopoverContent` | `rel-popover-content.ts` |
97
+
98
+ ## API
99
+
100
+ ### `<rel-popover>`
101
+
102
+ #### Attributes / Properties
103
+
104
+ | Property | Attribute | Type | Default | Description |
105
+ | --- | --- | --- | --- | --- |
106
+ | `open` | `open` | `boolean` | `undefined` | Controlled open state. Leave unset for uncontrolled mode; once set (true or false) the component only emits `rel-open-change` and you must update `open` yourself. Reflected. |
107
+ | `defaultOpen` | `defaultopen` | `boolean` | `false` | Initial state in uncontrolled mode (attribute `defaultopen`); ignored when `open` is set. |
108
+ | `disabled` | `disabled` | `boolean` | `false` | Blocks trigger interaction and dims the host to 60% opacity; does not close an already-open panel. Reflected. |
109
+ | `side` | `side` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` | Which side of the trigger the panel opens on: `top`, `right`, `bottom` (default) or `left`. The panel is clamped 8px inside the viewport but never flips to another side. Reflected. |
110
+ | `align` | `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment along the cross axis: `start` (left / top edge of the trigger), `center` (default) or `end` (right / bottom edge). Reflected. |
111
+ | `sideOffset` | `sideoffset` | `number` | `8` | Gap between trigger and panel in pixels (attribute `sideoffset`, default 8). With `arrow` the effective gap is at least 13px. |
112
+ | `alignOffset` | `alignoffset` | `number` | `0` | Extra shift along the alignment axis in pixels (attribute `alignoffset`, default 0); positive moves right / down. |
113
+ | `trapFocus` | `trapfocus` | `boolean` | `false` | Keep Tab / Shift+Tab cycling inside the panel while open (attribute `trapfocus`); use for forms. |
114
+ | `closeOnEscape` | `closeonescape` | `boolean` | `true` | Close on Escape (attribute `closeonescape`, default true; switch off via the property, e.g. `el.closeOnEscape = false`). |
115
+ | `closeOnOutsideClick` | `closeonoutsideclick` | `boolean` | `true` | Close on a pointer-down outside the trigger and panel (attribute `closeonoutsideclick`, default true; switch off via the property). Also lets another popover's opening dismiss this one. |
116
+ | `arrow` | `arrow` | `boolean` | `false` | Opt-in pointer (audit 23a): a 14×11 triangle on the panel edge that faces the trigger — the Figma Popover "Chevron" — following `side` and tracking the trigger's centre along the cross axis (clamped inside the panel). It is drawn with the panel's tokens (`--rel-popover-arrow-bg` / `--rel-popover-arrow-border`, defaulting to rel-popover-content's background and border). With the arrow on, the panel keeps at least the arrow's height plus 2px away from the trigger so the tip never overlaps it; larger `sideOffset` values still apply. Default off — existing popovers render exactly as before. Reflected. |
117
+
118
+ ### Events
119
+
120
+ | Event | Detail | Bubbles / composed | Description |
121
+ | --- | --- | --- | --- |
122
+ | `rel-open-change` | `{ open: next }` | bubbles / composed | Fired whenever the trigger, Escape, an outside click or another popover requests a state change. Detail: `{ open: boolean }` — the requested state; in controlled mode apply it to `open` yourself. |
123
+
124
+ ### Slots
125
+
126
+ | Slot | Description |
127
+ | --- | --- |
128
+ | (default) | The trigger: the first slotted element toggles the panel on pointer down / Enter / Space and receives `aria-haspopup`, `aria-expanded` and `aria-controls` (a bare `<div>` / `<span>` also gets `tabindex="0"` and `role="button"`). |
129
+ | `content` | The panel body, usually a `<rel-popover-content>`; rendered inside the fixed, top-layer wrapper only while open. |
130
+
131
+ ### CSS Parts
132
+
133
+ | Part | Description |
134
+ | --- | --- |
135
+ | `arrow` | The 14×14 chevron box rendered on the panel edge facing the trigger when `arrow` is set (absent otherwise). |
136
+
137
+ ### Tokens / CSS Variables
138
+
139
+ #### Component hooks
140
+
141
+ Set these on the host (or an ancestor) to customise the element without touching design tokens.
142
+
143
+ | Custom property | Default | Description |
144
+ | --- | --- | --- |
145
+ | `--rel-popover-arrow-bg` | `var(--rel-bg-neutral)` | Fill of the `arrow` chevron (default `--rel-bg-neutral`, matching `<rel-popover-content>`). |
146
+ | `--rel-popover-arrow-border` | `transparent` | **Deprecated.** Stroke of the `arrow` chevron's two slanted edges. The panel no longer draws a border — overlapping surfaces are separated by shadow — so this defaults to `transparent` and the chevron has no edge. Set it to restore a stroke. |
147
+
148
+ ### `<rel-popover-content>`
149
+
150
+ #### Attributes / Properties
151
+
152
+ | Property | Attribute | Type | Default | Description |
153
+ | --- | --- | --- | --- | --- |
154
+ | `ariaLabel` | `aria-label` | `string` | `""` | Accessible name applied to the inner `role="dialog"` box (attribute `aria-label`); describe what the panel is for. |
155
+ | `size` | `size` | `"small" \| "medium" \| "large" \| "sm" \| "md" \| "lg"` | `"medium"` | Figma Size axis: panel padding 12px (`small`), 16px (`medium`, default) or 20px (`large`). `sm` / `md` / `lg` are alternative spellings and stay supported — they are normalised to the full words, not deprecated. Reflected. |
156
+
157
+ ### Events
158
+
159
+ | Event | Detail | Bubbles / composed | Description |
160
+ | --- | --- | --- | --- |
161
+ | — | — | — | This element emits no custom events. |
162
+
163
+ ### Slots
164
+
165
+ | Slot | Description |
166
+ | --- | --- |
167
+ | (default) | The panel content (headings, paragraphs, form controls, buttons). |
168
+
169
+ ### CSS Parts
170
+
171
+ | Part | Description |
172
+ | --- | --- |
173
+ | `content` | The padded `role="dialog"` box that carries the surface, border, shadow and size padding. |
174
+
175
+ <!-- readme-gen:end -->
176
+
177
+ ## Accessibility
178
+
179
+ - The trigger (first slotted element) is decorated with `aria-haspopup="dialog"`, `aria-expanded` (kept in sync with the open state) and `aria-controls` pointing at the panel wrapper. A trigger that is not natively focusable and contains nothing focusable (a bare `<div>` / `<span>`) also gets `tabindex="0"` and `role="button"`; native buttons and `<rel-button>` are left alone so they do not get a second Tab stop.
180
+ - `<rel-popover-content>` renders its box with `role="dialog"`; pass `aria-label` so the dialog has a name. A custom panel element should carry its own role and label.
181
+ - On open, focus moves to the first focusable element inside the panel (links, enabled buttons and inputs, positive `tabindex`), or to the panel wrapper when there is none. On close, focus returns to whatever was focused before opening (normally the trigger).
182
+ - With `trapfocus`, Tab from the last focusable element wraps to the first and Shift+Tab from the first wraps to the last, so keyboard focus cannot leave the panel while it is open. Without it, Tab leaves the panel normally and the panel stays open until an outside click or Escape.
183
+ - There are no screen-reader announcements beyond the `aria-expanded` change; put the important content first in the panel so it is read when focus lands.
184
+
185
+ | Key | Action |
186
+ | --- | --- |
187
+ | `Enter` / `Space` (on the trigger) | Toggle the panel |
188
+ | `Escape` (anywhere, while open) | Close (unless `closeOnEscape` is switched off) |
189
+ | `Tab` / `Shift+Tab` | Move through the panel's focusable elements; with `trapfocus` the order wraps at both ends |
190
+
191
+ ## Notes / Edge Cases
192
+
193
+ - **Slots.** The trigger is the *default* slot's first element; the panel goes in `slot="content"`. There is no `trigger` slot. Only one element should be in the default slot — extra default-slot children render next to the trigger but never toggle the panel.
194
+ - **Top layer / inside a modal.** The panel wrapper is a native `popover="manual"` element shown with `showPopover()`, so a transformed or `overflow: hidden` ancestor (for example `<rel-modal>`'s centred panel) cannot clip it or break its `position: fixed` coordinates. Where the Popover API is unavailable it falls back to `position: fixed; z-index: 1001` and re-measures once to cancel any containing-block offset.
195
+ - **No collision flipping.** The panel always opens on the requested `side`; it is only clamped to stay 8px inside the viewport. Pick the side yourself for edge-of-screen triggers.
196
+ - **One open at a time.** Opening a popover light-dismisses every other open popover that honours `closeOnOutsideClick`, whether it was opened by pointer, keyboard or code. A popover nested inside another's panel keeps its parent open.
197
+ - **Controlled vs uncontrolled.** `open` is `undefined` until you set it; setting it (to `true` *or* `false`) switches the instance to controlled mode permanently, after which the component only emits `rel-open-change` and never changes its own state. Use `defaultopen` for an uncontrolled popover that starts open.
198
+ - **Default-true booleans** (`closeOnEscape`, `closeOnOutsideClick`) cannot be turned off with an attribute — a boolean attribute is true whenever present. Set the property instead.
199
+ - **Positioning updates** run on window resize and on scroll (capture phase). They do not run when the panel's own content changes size; toggle `open` or trigger a resize if you swap content while open.
200
+ - **Arrow.** `arrow` draws a 14×11 chevron on the panel edge facing the trigger, centred on the trigger and clamped 15px in from the panel corners; the trigger gap becomes `max(sideoffset, 13px)`. Colour it with `--rel-popover-arrow-bg` / `--rel-popover-arrow-border` (defaults match `<rel-popover-content>`); if you use a custom panel with a different surface, set both so the chevron blends in.
201
+ - **`<rel-popover-content>`** is `display: block; width: 100%; max-width: 400px`; override `max-width` on the element for wider panels. Slotted `h1`–`h6` get heading-sm type and `p` gets placeholder-tone body text with a bottom margin (none on the last paragraph).
202
+ - **`disabled`** blocks the trigger and dims the host but does not close a panel that is already open — set `open = false` first if needed.
203
+ - The `size` values on `<rel-popover-content>` are `small` / `medium` / `large`. The short spellings `sm` / `md` / `lg` are alternative spellings and keep working — they are normalised to the full words before the attribute reflects, so `rel-popover-content[size="medium"]` matches whichever spelling was written.