@chromvoid/uikit 0.1.0 → 0.2.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.
Files changed (192) hide show
  1. package/LICENSE +19 -6
  2. package/README.md +1 -0
  3. package/dist/components/cv-accordion-item.d.ts +1 -1
  4. package/dist/components/cv-accordion.d.ts +1 -1
  5. package/dist/components/cv-accordion.js +2 -1
  6. package/dist/components/cv-alert-dialog.d.ts +1 -1
  7. package/dist/components/cv-alert-dialog.js +17 -2
  8. package/dist/components/cv-alert.d.ts +1 -1
  9. package/dist/components/cv-alert.js +2 -1
  10. package/dist/components/cv-badge.d.ts +1 -1
  11. package/dist/components/cv-badge.js +2 -1
  12. package/dist/components/cv-bottom-sheet.d.ts +127 -0
  13. package/dist/components/cv-bottom-sheet.js +513 -0
  14. package/dist/components/cv-breadcrumb-item.d.ts +1 -1
  15. package/dist/components/cv-breadcrumb-item.js +1 -1
  16. package/dist/components/cv-breadcrumb.d.ts +1 -1
  17. package/dist/components/cv-breadcrumb.js +2 -1
  18. package/dist/components/cv-button.d.ts +23 -1
  19. package/dist/components/cv-button.js +194 -37
  20. package/dist/components/cv-callout.d.ts +8 -1
  21. package/dist/components/cv-callout.js +18 -1
  22. package/dist/components/cv-card.d.ts +1 -1
  23. package/dist/components/cv-card.js +2 -2
  24. package/dist/components/cv-carousel-slide.d.ts +1 -1
  25. package/dist/components/cv-carousel.d.ts +1 -1
  26. package/dist/components/cv-carousel.js +2 -1
  27. package/dist/components/cv-checkbox.d.ts +1 -1
  28. package/dist/components/cv-combobox-group.d.ts +1 -1
  29. package/dist/components/cv-combobox-option.d.ts +1 -1
  30. package/dist/components/cv-combobox-option.js +2 -2
  31. package/dist/components/cv-combobox.d.ts +3 -1
  32. package/dist/components/cv-combobox.js +49 -8
  33. package/dist/components/cv-command-item.d.ts +1 -1
  34. package/dist/components/cv-command-item.js +2 -2
  35. package/dist/components/cv-command-palette.d.ts +1 -1
  36. package/dist/components/cv-command-palette.js +21 -1
  37. package/dist/components/cv-context-menu.d.ts +1 -1
  38. package/dist/components/cv-context-menu.js +2 -1
  39. package/dist/components/cv-copy-button.d.ts +37 -9
  40. package/dist/components/cv-copy-button.js +129 -41
  41. package/dist/components/cv-date-picker.d.ts +1 -1
  42. package/dist/components/cv-date-picker.js +20 -1
  43. package/dist/components/cv-dialog.d.ts +44 -2
  44. package/dist/components/cv-dialog.js +686 -74
  45. package/dist/components/cv-disclosure.d.ts +1 -1
  46. package/dist/components/cv-disclosure.js +2 -1
  47. package/dist/components/cv-drawer.d.ts +29 -1
  48. package/dist/components/cv-drawer.js +229 -4
  49. package/dist/components/cv-feed-article.d.ts +1 -1
  50. package/dist/components/cv-feed-article.js +2 -1
  51. package/dist/components/cv-feed.d.ts +1 -1
  52. package/dist/components/cv-feed.js +2 -1
  53. package/dist/components/cv-grid-cell.d.ts +1 -1
  54. package/dist/components/cv-grid-cell.js +3 -3
  55. package/dist/components/cv-grid-column.d.ts +1 -1
  56. package/dist/components/cv-grid-column.js +1 -1
  57. package/dist/components/cv-grid-row.d.ts +1 -1
  58. package/dist/components/cv-grid.d.ts +1 -1
  59. package/dist/components/cv-grid.js +2 -1
  60. package/dist/components/cv-guidance-anchor.d.ts +47 -0
  61. package/dist/components/cv-guidance-anchor.js +113 -0
  62. package/dist/components/cv-guidance-panel.d.ts +29 -0
  63. package/dist/components/cv-guidance-panel.js +245 -0
  64. package/dist/components/cv-icon.d.ts +2 -1
  65. package/dist/components/cv-icon.js +28 -3
  66. package/dist/components/cv-input.d.ts +7 -1
  67. package/dist/components/cv-input.js +33 -1
  68. package/dist/components/cv-landmark.d.ts +1 -1
  69. package/dist/components/cv-landmark.js +2 -1
  70. package/dist/components/cv-link.d.ts +1 -1
  71. package/dist/components/cv-link.js +2 -1
  72. package/dist/components/cv-listbox-group.d.ts +1 -1
  73. package/dist/components/cv-listbox.d.ts +1 -1
  74. package/dist/components/cv-listbox.js +2 -1
  75. package/dist/components/cv-menu-button.d.ts +24 -1
  76. package/dist/components/cv-menu-button.js +226 -18
  77. package/dist/components/cv-menu-group.d.ts +1 -1
  78. package/dist/components/cv-menu-item.d.ts +1 -1
  79. package/dist/components/cv-menu-item.js +6 -2
  80. package/dist/components/cv-menu.d.ts +1 -1
  81. package/dist/components/cv-menu.js +21 -1
  82. package/dist/components/cv-meter.d.ts +1 -1
  83. package/dist/components/cv-meter.js +6 -22
  84. package/dist/components/cv-number.d.ts +1 -1
  85. package/dist/components/cv-option.d.ts +1 -1
  86. package/dist/components/cv-option.js +3 -9
  87. package/dist/components/cv-popover-positioning.d.ts +22 -0
  88. package/dist/components/cv-popover-positioning.js +112 -0
  89. package/dist/components/cv-popover.d.ts +45 -8
  90. package/dist/components/cv-popover.js +395 -113
  91. package/dist/components/cv-progress-ring.d.ts +1 -1
  92. package/dist/components/cv-progress-ring.js +2 -1
  93. package/dist/components/cv-progress.d.ts +8 -1
  94. package/dist/components/cv-progress.js +41 -10
  95. package/dist/components/cv-radio-group.d.ts +1 -1
  96. package/dist/components/cv-radio.d.ts +1 -1
  97. package/dist/components/cv-radio.js +1 -1
  98. package/dist/components/cv-select-group.d.ts +1 -1
  99. package/dist/components/cv-select-option.d.ts +1 -1
  100. package/dist/components/cv-select-option.js +2 -2
  101. package/dist/components/cv-select.d.ts +1 -1
  102. package/dist/components/cv-select.js +28 -1
  103. package/dist/components/cv-sidebar-item.d.ts +1 -1
  104. package/dist/components/cv-sidebar.d.ts +1 -1
  105. package/dist/components/cv-sidebar.js +3 -2
  106. package/dist/components/cv-slider-multi-thumb.d.ts +1 -1
  107. package/dist/components/cv-slider-multi-thumb.js +2 -1
  108. package/dist/components/cv-slider.d.ts +17 -4
  109. package/dist/components/cv-slider.js +63 -21
  110. package/dist/components/cv-spinbutton.d.ts +1 -1
  111. package/dist/components/cv-spinner.d.ts +1 -1
  112. package/dist/components/cv-spinner.js +2 -1
  113. package/dist/components/cv-switch.d.ts +1 -1
  114. package/dist/components/cv-tab-panel.d.ts +1 -1
  115. package/dist/components/cv-tab.d.ts +1 -1
  116. package/dist/components/cv-table-cell.d.ts +1 -1
  117. package/dist/components/cv-table-cell.js +1 -1
  118. package/dist/components/cv-table-column.d.ts +1 -1
  119. package/dist/components/cv-table-column.js +1 -1
  120. package/dist/components/cv-table-row.d.ts +1 -1
  121. package/dist/components/cv-table-row.js +1 -4
  122. package/dist/components/cv-table.d.ts +1 -3
  123. package/dist/components/cv-table.js +4 -11
  124. package/dist/components/cv-tabs.d.ts +1 -1
  125. package/dist/components/cv-tabs.js +3 -2
  126. package/dist/components/cv-textarea.d.ts +11 -1
  127. package/dist/components/cv-textarea.js +33 -0
  128. package/dist/components/cv-toast-region.d.ts +1 -1
  129. package/dist/components/cv-toast-region.js +2 -1
  130. package/dist/components/cv-toast.d.ts +1 -1
  131. package/dist/components/cv-toast.js +20 -27
  132. package/dist/components/cv-toolbar-item.d.ts +1 -1
  133. package/dist/components/cv-toolbar-separator.d.ts +1 -1
  134. package/dist/components/cv-toolbar.d.ts +1 -1
  135. package/dist/components/cv-toolbar.js +2 -1
  136. package/dist/components/cv-tooltip.d.ts +1 -1
  137. package/dist/components/cv-tooltip.js +2 -1
  138. package/dist/components/cv-treegrid-cell.d.ts +1 -1
  139. package/dist/components/cv-treegrid-cell.js +1 -1
  140. package/dist/components/cv-treegrid-column.d.ts +1 -1
  141. package/dist/components/cv-treegrid-column.js +1 -1
  142. package/dist/components/cv-treegrid-row.d.ts +1 -1
  143. package/dist/components/cv-treegrid-row.js +1 -1
  144. package/dist/components/cv-treegrid.d.ts +1 -1
  145. package/dist/components/cv-treegrid.js +4 -3
  146. package/dist/components/cv-treeitem.d.ts +1 -1
  147. package/dist/components/cv-treeitem.js +2 -2
  148. package/dist/components/cv-treeview.d.ts +1 -1
  149. package/dist/components/cv-treeview.js +2 -1
  150. package/dist/components/cv-window-splitter.d.ts +1 -1
  151. package/dist/components/cv-window-splitter.js +2 -1
  152. package/dist/components/index.d.ts +7 -0
  153. package/dist/components/index.js +3 -0
  154. package/dist/dialog/create-dialog-controller.d.ts +12 -4
  155. package/dist/dialog/create-dialog-controller.js +84 -22
  156. package/dist/dialog/index.d.ts +1 -1
  157. package/dist/index.d.ts +1 -1
  158. package/dist/reatom-lit/ReatomLitElement.d.ts +6 -3
  159. package/dist/reatom-lit/ReatomLitElement.js +18 -8
  160. package/dist/reatom-lit/createAfterRenderScheduler.d.ts +10 -0
  161. package/dist/reatom-lit/createAfterRenderScheduler.js +33 -0
  162. package/dist/reatom-lit/index.d.ts +2 -0
  163. package/dist/reatom-lit/index.js +1 -0
  164. package/dist/reatom-lit/watch.d.ts +1 -1
  165. package/dist/reatom-lit/withReatomElement.js +16 -2
  166. package/dist/register.js +4 -1
  167. package/dist/styles/component-styles.js +4 -0
  168. package/dist/styles/uno-generated.d.ts +2 -0
  169. package/dist/styles/uno-generated.js +1 -0
  170. package/dist/styles/uno-utilities.d.ts +5 -0
  171. package/dist/styles/uno-utilities.js +7 -0
  172. package/dist/theme/cv-theme-provider.d.ts +1 -1
  173. package/dist/theme/cv-theme-provider.js +2 -2
  174. package/dist/theme/tokens.css +619 -162
  175. package/package.json +9 -5
  176. package/specs/components/bottom-sheet.md +93 -0
  177. package/specs/components/button.md +8 -0
  178. package/specs/components/callout.md +8 -0
  179. package/specs/components/copy-button.md +54 -17
  180. package/specs/components/dialog.md +72 -43
  181. package/specs/components/drawer.md +18 -13
  182. package/specs/components/guidance-anchor.md +64 -0
  183. package/specs/components/guidance-panel.md +92 -0
  184. package/specs/components/input.md +7 -0
  185. package/specs/components/menu.md +8 -0
  186. package/specs/components/option.md +9 -9
  187. package/specs/components/progress.md +11 -0
  188. package/specs/components/sidebar.md +12 -12
  189. package/specs/components/table.md +13 -13
  190. package/specs/components/theme.md +13 -13
  191. package/specs/components/treegrid.md +15 -15
  192. package/specs/components/treeview.md +10 -10
@@ -1,8 +1,46 @@
1
- import { createPopover } from '@chromvoid/headless-ui/popover';
2
- import { css, html, nothing } from 'lit';
1
+ import { createPopover, } from '@chromvoid/headless-ui/popover';
2
+ import { css, nothing } from 'lit';
3
+ import { html } from '../reatom-lit/index.js';
3
4
  import { ReatomLitElement } from '../reatom-lit/ReatomLitElement.js';
5
+ import { getPlacementFallbacks, getPositionAreaForPlacement, resolvePopoverPosition, } from './cv-popover-positioning.js';
4
6
  const popoverTriggerKeys = new Set(['Enter', ' ', 'Spacebar', 'ArrowDown']);
5
- const supportsNativePopover = typeof HTMLElement !== 'undefined' && typeof HTMLElement.prototype.showPopover === 'function';
7
+ function supportsNativePopover() {
8
+ return (typeof HTMLElement !== 'undefined' &&
9
+ typeof HTMLElement.prototype.showPopover === 'function' &&
10
+ typeof HTMLElement.prototype.hidePopover === 'function');
11
+ }
12
+ function supportsAnchorPositioning() {
13
+ return (typeof CSS !== 'undefined' &&
14
+ typeof CSS.supports === 'function' &&
15
+ CSS.supports('position-area: top left') &&
16
+ CSS.supports('top: anchor(bottom)'));
17
+ }
18
+ function supportsAnchorTryFallbacks() {
19
+ return (typeof CSS !== 'undefined' &&
20
+ typeof CSS.supports === 'function' &&
21
+ CSS.supports('position-try-fallbacks: flip-block'));
22
+ }
23
+ function supportsNativeAnchoredAutoplacement() {
24
+ return supportsNativePopover() && supportsAnchorPositioning() && supportsAnchorTryFallbacks();
25
+ }
26
+ function isPopoverOpen(element) {
27
+ try {
28
+ return element.matches(':popover-open');
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ function toRect(rect) {
35
+ return {
36
+ left: rect.left,
37
+ top: rect.top,
38
+ right: rect.right,
39
+ bottom: rect.bottom,
40
+ width: rect.width,
41
+ height: rect.height,
42
+ };
43
+ }
6
44
  let cvPopoverNonce = 0;
7
45
  export class CVPopover extends ReatomLitElement {
8
46
  static elementName = 'cv-popover';
@@ -16,6 +54,7 @@ export class CVPopover extends ReatomLitElement {
16
54
  closeOnOutsideFocus: { type: Boolean, attribute: 'close-on-outside-focus', reflect: true },
17
55
  placement: { type: String, reflect: true },
18
56
  anchor: { type: String, reflect: true },
57
+ triggerMode: { type: String, attribute: 'trigger-mode', reflect: true },
19
58
  offset: { type: Number, reflect: true },
20
59
  arrow: { type: Boolean, reflect: true },
21
60
  };
@@ -23,6 +62,21 @@ export class CVPopover extends ReatomLitElement {
23
62
  idBase = `cv-popover-${++cvPopoverNonce}`;
24
63
  model;
25
64
  previousOpen = false;
65
+ hasLayoutListeners = false;
66
+ layoutFrame = -1;
67
+ focusContentOnNextUpdate = false;
68
+ restoreFocusTarget = null;
69
+ _sourceEl = null;
70
+ get sourceEl() {
71
+ return this._sourceEl;
72
+ }
73
+ set sourceEl(value) {
74
+ const previous = this._sourceEl;
75
+ if (previous === value)
76
+ return;
77
+ this._sourceEl = value;
78
+ this.requestUpdate('sourceEl', previous);
79
+ }
26
80
  constructor() {
27
81
  super();
28
82
  this.open = false;
@@ -33,6 +87,7 @@ export class CVPopover extends ReatomLitElement {
33
87
  this.closeOnOutsideFocus = true;
34
88
  this.placement = 'bottom-start';
35
89
  this.anchor = 'trigger';
90
+ this.triggerMode = 'internal';
36
91
  this.offset = 4;
37
92
  this.arrow = false;
38
93
  this.model = this.createModel();
@@ -44,12 +99,22 @@ export class CVPopover extends ReatomLitElement {
44
99
  display: inline-block;
45
100
  }
46
101
 
102
+ :host([trigger-mode='external'][anchor='trigger']) {
103
+ display: contents;
104
+ }
105
+
47
106
  [part='base'] {
48
107
  position: relative;
49
108
  display: inline-grid;
50
109
  gap: var(--cv-space-1, 4px);
51
110
  }
52
111
 
112
+ :host([trigger-mode='external'][anchor='trigger']) [part='base'] {
113
+ position: static;
114
+ display: contents;
115
+ gap: 0;
116
+ }
117
+
53
118
  [part='trigger'] {
54
119
  display: inline-flex;
55
120
  align-items: center;
@@ -72,20 +137,42 @@ export class CVPopover extends ReatomLitElement {
72
137
  inset-inline-start: 0;
73
138
  inset-block-start: calc(100% + var(--cv-popover-offset, var(--cv-space-1, 4px)));
74
139
  z-index: var(--cv-popover-z-index, 20);
75
- min-inline-size: var(--cv-popover-min-inline-size, max(220px, 100%));
140
+ min-inline-size: var(
141
+ --cv-popover-min-inline-size,
142
+ max(220px, var(--cv-popover-anchor-inline-size, 0px))
143
+ );
76
144
  max-inline-size: var(--cv-popover-max-inline-size, min(560px, calc(100vw - 32px)));
77
145
  display: grid;
78
- gap: var(--cv-space-2, 8px);
146
+ gap: var(--cv-popover-gap, var(--cv-space-2, 8px));
79
147
  padding: var(--cv-popover-padding, var(--cv-space-3, 12px));
80
148
  border-radius: var(--cv-popover-border-radius, var(--cv-radius-md, 10px));
81
149
  border: 1px solid var(--cv-color-border, #2a3245);
82
150
  background: var(--cv-color-surface-elevated, #1d2432);
83
151
  box-shadow: var(--cv-shadow-1, 0 2px 8px rgba(0, 0, 0, 0.24));
84
152
  color: var(--cv-color-text, #e8ecf6);
153
+ margin: 0;
154
+ opacity: 1;
155
+ transform: translate3d(0, 0, 0);
156
+ transition:
157
+ opacity var(--cv-popover-transition-duration, var(--cv-duration-fast, 120ms))
158
+ var(--cv-easing-standard, ease),
159
+ transform var(--cv-popover-transition-duration, var(--cv-duration-fast, 120ms))
160
+ var(--cv-easing-standard, ease),
161
+ display var(--cv-popover-transition-duration, var(--cv-duration-fast, 120ms)) allow-discrete;
162
+ transition-behavior: allow-discrete;
85
163
  }
86
164
 
87
165
  [part='content'][hidden] {
88
166
  display: none;
167
+ opacity: 0;
168
+ transform: translate3d(0, -2px, 0);
169
+ }
170
+
171
+ @starting-style {
172
+ [part='content']:not([hidden]) {
173
+ opacity: 0;
174
+ transform: translate3d(0, -2px, 0);
175
+ }
89
176
  }
90
177
 
91
178
  [part='content']:focus-visible {
@@ -93,76 +180,6 @@ export class CVPopover extends ReatomLitElement {
93
180
  outline-offset: 1px;
94
181
  }
95
182
 
96
- [part='content'][data-placement='bottom'] {
97
- inset-inline-start: 50%;
98
- transform: translateX(-50%);
99
- }
100
-
101
- [part='content'][data-placement='bottom-end'] {
102
- inset-inline-start: auto;
103
- inset-inline-end: 0;
104
- }
105
-
106
- [part='content'][data-placement='top-start'] {
107
- inset-block-start: auto;
108
- inset-block-end: calc(100% + var(--cv-popover-offset, var(--cv-space-1, 4px)));
109
- }
110
-
111
- [part='content'][data-placement='top'] {
112
- inset-inline-start: 50%;
113
- inset-block-start: auto;
114
- inset-block-end: calc(100% + var(--cv-popover-offset, var(--cv-space-1, 4px)));
115
- transform: translateX(-50%);
116
- }
117
-
118
- [part='content'][data-placement='top-end'] {
119
- inset-inline-start: auto;
120
- inset-inline-end: 0;
121
- inset-block-start: auto;
122
- inset-block-end: calc(100% + var(--cv-popover-offset, var(--cv-space-1, 4px)));
123
- }
124
-
125
- [part='content'][data-placement='right-start'] {
126
- inset-inline-start: calc(100% + var(--cv-popover-offset, var(--cv-space-1, 4px)));
127
- inset-block-start: 0;
128
- }
129
-
130
- [part='content'][data-placement='right'] {
131
- inset-inline-start: calc(100% + var(--cv-popover-offset, var(--cv-space-1, 4px)));
132
- inset-block-start: 50%;
133
- transform: translateY(-50%);
134
- }
135
-
136
- [part='content'][data-placement='right-end'] {
137
- inset-inline-start: calc(100% + var(--cv-popover-offset, var(--cv-space-1, 4px)));
138
- inset-block-start: auto;
139
- inset-block-end: 0;
140
- }
141
-
142
- [part='content'][data-placement='left-start'] {
143
- inset-inline-start: auto;
144
- inset-inline-end: calc(100% + var(--cv-popover-offset, var(--cv-space-1, 4px)));
145
- inset-block-start: 0;
146
- }
147
-
148
- [part='content'][data-placement='left'] {
149
- inset-inline-start: auto;
150
- inset-inline-end: calc(100% + var(--cv-popover-offset, var(--cv-space-1, 4px)));
151
- inset-block-start: 50%;
152
- transform: translateY(-50%);
153
- }
154
-
155
- [part='content'][data-placement='left-end'] {
156
- inset-inline-start: auto;
157
- inset-inline-end: calc(100% + var(--cv-popover-offset, var(--cv-space-1, 4px)));
158
- inset-block-start: auto;
159
- inset-block-end: 0;
160
- }
161
-
162
- [part='content'][data-anchor='host'] {
163
- min-inline-size: min(560px, calc(100vw - 32px));
164
- }
165
-
166
183
  [part='arrow'] {
167
184
  position: absolute;
168
185
  display: block;
@@ -181,8 +198,21 @@ export class CVPopover extends ReatomLitElement {
181
198
  this.syncOutsideListeners();
182
199
  }
183
200
  disconnectedCallback() {
184
- super.disconnectedCallback();
185
201
  this.syncOutsideListeners(true);
202
+ this.toggleLayoutListeners(false);
203
+ this.cancelLayoutFrame();
204
+ if (supportsNativePopover()) {
205
+ const content = this.getContentElement();
206
+ if (content && isPopoverOpen(content)) {
207
+ try {
208
+ content.hidePopover?.();
209
+ }
210
+ catch {
211
+ // ignore
212
+ }
213
+ }
214
+ }
215
+ super.disconnectedCallback();
186
216
  }
187
217
  willUpdate(changedProperties) {
188
218
  super.willUpdate(changedProperties);
@@ -197,18 +227,70 @@ export class CVPopover extends ReatomLitElement {
197
227
  }
198
228
  if (changedProperties.has('open') && this.model.state.isOpen() !== this.open) {
199
229
  if (this.open) {
230
+ this.prepareAnchorForOpen();
200
231
  this.model.actions.open('programmatic');
201
232
  }
202
233
  else {
203
234
  this.model.actions.close('programmatic');
204
235
  }
205
- this.open = this.model.state.isOpen();
206
- this.previousOpen = this.open;
236
+ this.emitToggleEvents();
237
+ this.syncOutsideListeners();
207
238
  }
208
239
  }
209
240
  updated(changedProperties) {
210
241
  super.updated(changedProperties);
211
242
  this.syncOutsideListeners();
243
+ this.syncNativePopover();
244
+ const modelOpen = this.model.state.isOpen();
245
+ const shouldTrackLayout = modelOpen && !supportsNativeAnchoredAutoplacement();
246
+ this.toggleLayoutListeners(shouldTrackLayout);
247
+ if (modelOpen) {
248
+ this.scheduleLayout();
249
+ }
250
+ else {
251
+ this.cancelLayoutFrame();
252
+ const content = this.getContentElement();
253
+ if (content) {
254
+ this.clearInlineLayout(content);
255
+ content.dataset['placement'] = this.placement;
256
+ content.dataset['anchorPositioning'] = 'false';
257
+ }
258
+ }
259
+ if (this.focusContentOnNextUpdate && modelOpen) {
260
+ this.focusContentOnNextUpdate = false;
261
+ this.getContentElement()?.focus();
262
+ }
263
+ if (modelOpen &&
264
+ (changedProperties.has('placement') ||
265
+ changedProperties.has('offset') ||
266
+ changedProperties.has('anchor') ||
267
+ changedProperties.has('triggerMode') ||
268
+ changedProperties.has('sourceEl'))) {
269
+ this.scheduleLayout();
270
+ }
271
+ }
272
+ show(options = {}) {
273
+ if (options.source) {
274
+ this.sourceEl = options.source;
275
+ }
276
+ this.prepareAnchorForOpen(options.source);
277
+ this.model.actions.open(options.openedBy ?? 'programmatic');
278
+ this.emitToggleEvents();
279
+ this.syncOutsideListeners();
280
+ this.requestUpdate();
281
+ }
282
+ hide(intent = 'programmatic') {
283
+ this.model.actions.close(intent);
284
+ this.emitToggleEvents();
285
+ this.syncOutsideListeners();
286
+ this.requestUpdate();
287
+ }
288
+ toggle(options = {}) {
289
+ if (this.model.state.isOpen()) {
290
+ this.hide('programmatic');
291
+ return;
292
+ }
293
+ this.show(options);
212
294
  }
213
295
  createModel(initialOpen = this.open) {
214
296
  return createPopover({
@@ -219,9 +301,33 @@ export class CVPopover extends ReatomLitElement {
219
301
  closeOnEscape: this.closeOnEscape,
220
302
  closeOnOutsidePointer: this.closeOnOutsidePointer,
221
303
  closeOnOutsideFocus: this.closeOnOutsideFocus,
222
- useNativePopover: supportsNativePopover,
304
+ useNativePopover: supportsNativePopover(),
223
305
  });
224
306
  }
307
+ getContentElement() {
308
+ return this.shadowRoot?.querySelector('[part="content"]');
309
+ }
310
+ getTriggerElement() {
311
+ return this.shadowRoot?.querySelector('[part="trigger"]');
312
+ }
313
+ resolveAnchorElement() {
314
+ if (this.anchor === 'host') {
315
+ return this;
316
+ }
317
+ if (this.triggerMode === 'external') {
318
+ return this.sourceEl ?? this;
319
+ }
320
+ return this.getTriggerElement() ?? this;
321
+ }
322
+ prepareAnchorForOpen(source) {
323
+ if (source) {
324
+ this.sourceEl = source;
325
+ }
326
+ const anchor = source ?? this.resolveAnchorElement();
327
+ if (anchor) {
328
+ this.restoreFocusTarget = anchor;
329
+ }
330
+ }
225
331
  buildEventDetail() {
226
332
  return {
227
333
  open: this.model.state.isOpen(),
@@ -229,50 +335,55 @@ export class CVPopover extends ReatomLitElement {
229
335
  dismissIntent: this.model.state.lastDismissIntent(),
230
336
  };
231
337
  }
232
- /**
233
- * Dispatches beforetoggle and toggle events and syncs open state from headless.
234
- * If beforetoggle is canceled on open, reverts headless state.
235
- */
236
338
  emitToggleEvents() {
237
339
  const isOpen = this.model.state.isOpen();
238
- // Only emit if state actually changed
239
340
  if (isOpen === this.previousOpen)
240
341
  return;
241
342
  const detail = this.buildEventDetail();
242
- // Dispatch beforetoggle (cancelable only on open)
243
- const cancelable = detail.open;
244
343
  const beforeToggleEvent = new CustomEvent('beforetoggle', {
245
344
  detail,
246
345
  bubbles: true,
247
346
  composed: true,
248
- cancelable,
347
+ cancelable: detail.open,
249
348
  });
250
349
  this.dispatchEvent(beforeToggleEvent);
251
- // If opening was prevented, revert headless state
252
- if (cancelable && beforeToggleEvent.defaultPrevented) {
350
+ if (detail.open && beforeToggleEvent.defaultPrevented) {
253
351
  this.model.actions.close('programmatic');
254
352
  this.open = false;
255
353
  this.previousOpen = false;
354
+ this.focusContentOnNextUpdate = false;
355
+ this.requestUpdate();
256
356
  return;
257
357
  }
258
- // Sync host attribute
259
358
  this.open = isOpen;
260
359
  this.previousOpen = isOpen;
261
- // Dispatch toggle (not cancelable)
360
+ this.focusContentOnNextUpdate = isOpen;
262
361
  this.dispatchEvent(new CustomEvent('toggle', {
263
362
  detail,
264
363
  bubbles: false,
265
364
  composed: true,
266
365
  cancelable: false,
267
366
  }));
268
- // Restore focus if needed
269
367
  if (!isOpen) {
270
- const restoreId = this.model.state.restoreTargetId();
271
- if (restoreId) {
272
- const trigger = this.shadowRoot?.querySelector(`[id="${restoreId}"]`);
273
- trigger?.focus();
368
+ this.restoreFocus();
369
+ }
370
+ }
371
+ restoreFocus() {
372
+ const target = this.restoreFocusTarget;
373
+ if (target?.isConnected) {
374
+ try {
375
+ target.focus();
376
+ }
377
+ catch {
378
+ // ignore
274
379
  }
380
+ return;
275
381
  }
382
+ const restoreId = this.model.state.restoreTargetId();
383
+ if (!restoreId)
384
+ return;
385
+ const trigger = this.shadowRoot?.querySelector(`[id="${restoreId}"]`);
386
+ trigger?.focus();
276
387
  }
277
388
  syncOutsideListeners(forceOff = false) {
278
389
  const shouldListen = !forceOff && this.model.state.isOpen();
@@ -286,7 +397,7 @@ export class CVPopover extends ReatomLitElement {
286
397
  }
287
398
  }
288
399
  handleDocumentPointerDown = (event) => {
289
- if (!this.model || !this.model.state.isOpen())
400
+ if (!this.model.state.isOpen())
290
401
  return;
291
402
  const path = event.composedPath();
292
403
  if (path.includes(this))
@@ -294,9 +405,10 @@ export class CVPopover extends ReatomLitElement {
294
405
  this.model.contracts.getContentProps().onPointerDownOutside();
295
406
  this.emitToggleEvents();
296
407
  this.syncOutsideListeners();
408
+ this.requestUpdate();
297
409
  };
298
410
  handleDocumentFocusIn = (event) => {
299
- if (!this.model || !this.model.state.isOpen())
411
+ if (!this.model.state.isOpen())
300
412
  return;
301
413
  const path = event.composedPath();
302
414
  if (path.includes(this))
@@ -304,19 +416,24 @@ export class CVPopover extends ReatomLitElement {
304
416
  this.model.contracts.getContentProps().onFocusOutside();
305
417
  this.emitToggleEvents();
306
418
  this.syncOutsideListeners();
419
+ this.requestUpdate();
307
420
  };
308
421
  handleTriggerClick() {
422
+ this.prepareAnchorForOpen(this.getTriggerElement() ?? undefined);
309
423
  this.model.contracts.getTriggerProps().onClick();
310
424
  this.emitToggleEvents();
311
425
  this.syncOutsideListeners();
426
+ this.requestUpdate();
312
427
  }
313
428
  handleTriggerKeyDown(event) {
314
429
  if (popoverTriggerKeys.has(event.key)) {
315
430
  event.preventDefault();
316
431
  }
432
+ this.prepareAnchorForOpen(this.getTriggerElement() ?? undefined);
317
433
  this.model.contracts.getTriggerProps().onKeyDown({ key: event.key });
318
434
  this.emitToggleEvents();
319
435
  this.syncOutsideListeners();
436
+ this.requestUpdate();
320
437
  }
321
438
  handleContentKeyDown(event) {
322
439
  if (event.key === 'Escape') {
@@ -325,26 +442,186 @@ export class CVPopover extends ReatomLitElement {
325
442
  this.model.contracts.getContentProps().onKeyDown({ key: event.key });
326
443
  this.emitToggleEvents();
327
444
  this.syncOutsideListeners();
445
+ this.requestUpdate();
446
+ }
447
+ handleNativeToggle = (event) => {
448
+ if (!supportsNativePopover())
449
+ return;
450
+ const toggleEvent = event;
451
+ const newState = toggleEvent.newState === 'closed' ? 'closed' : 'open';
452
+ this.model.actions.handleNativeToggle(newState);
453
+ this.emitToggleEvents();
454
+ this.syncOutsideListeners();
455
+ this.requestUpdate();
456
+ };
457
+ clearInlineLayout(content) {
458
+ content.style.position = '';
459
+ content.style.top = '';
460
+ content.style.left = '';
461
+ content.style.right = '';
462
+ content.style.bottom = '';
463
+ content.style.insetInlineStart = '';
464
+ content.style.insetBlockStart = '';
465
+ content.style.insetInlineEnd = '';
466
+ content.style.insetBlockEnd = '';
467
+ content.style.transform = '';
468
+ content.style.translate = '';
469
+ content.style.margin = '';
470
+ content.style.marginTop = '';
471
+ content.style.marginRight = '';
472
+ content.style.marginBottom = '';
473
+ content.style.marginLeft = '';
474
+ content.style.removeProperty('position-area');
475
+ content.style.removeProperty('position-try-fallbacks');
476
+ content.style.removeProperty('--cv-popover-anchor-inline-size');
477
+ }
478
+ applyDirectionalOffset(content, placement) {
479
+ content.style.margin = '0';
480
+ content.style.marginTop = '';
481
+ content.style.marginRight = '';
482
+ content.style.marginBottom = '';
483
+ content.style.marginLeft = '';
484
+ const [side] = placement.split('-');
485
+ const value = `${this.offset}px`;
486
+ switch (side) {
487
+ case 'top':
488
+ content.style.marginBottom = value;
489
+ break;
490
+ case 'right':
491
+ content.style.marginLeft = value;
492
+ break;
493
+ case 'bottom':
494
+ content.style.marginTop = value;
495
+ break;
496
+ case 'left':
497
+ content.style.marginRight = value;
498
+ break;
499
+ }
500
+ }
501
+ syncNativePopover() {
502
+ if (!supportsNativePopover())
503
+ return;
504
+ const content = this.getContentElement();
505
+ if (!content)
506
+ return;
507
+ const isOpen = this.model.state.isOpen();
508
+ const popoverOpen = isPopoverOpen(content);
509
+ if (isOpen && !popoverOpen) {
510
+ const anchor = this.resolveAnchorElement();
511
+ try {
512
+ if (anchor) {
513
+ content.showPopover?.({ source: anchor });
514
+ }
515
+ else {
516
+ content.showPopover?.();
517
+ }
518
+ }
519
+ catch {
520
+ // ignore open failures
521
+ }
522
+ return;
523
+ }
524
+ if (!isOpen && popoverOpen) {
525
+ try {
526
+ content.hidePopover?.();
527
+ }
528
+ catch {
529
+ // ignore close failures
530
+ }
531
+ }
532
+ }
533
+ syncPopoverLayout() {
534
+ const content = this.getContentElement();
535
+ const anchor = this.resolveAnchorElement();
536
+ if (!content || !anchor)
537
+ return;
538
+ const anchorRect = toRect(anchor.getBoundingClientRect());
539
+ if (supportsNativeAnchoredAutoplacement()) {
540
+ this.clearInlineLayout(content);
541
+ content.style.setProperty('--cv-popover-anchor-inline-size', `${Math.max(0, Math.round(anchorRect.width))}px`);
542
+ content.dataset['anchorPositioning'] = 'true';
543
+ content.dataset['placement'] = this.placement;
544
+ content.style.position = 'fixed';
545
+ content.style.inset = 'auto';
546
+ content.style.margin = '0';
547
+ this.applyDirectionalOffset(content, this.placement);
548
+ content.style.setProperty('position-area', getPositionAreaForPlacement(this.placement));
549
+ content.style.setProperty('position-try-fallbacks', getPlacementFallbacks(this.placement)
550
+ .slice(1)
551
+ .map((candidate) => getPositionAreaForPlacement(candidate))
552
+ .join(', '));
553
+ return;
554
+ }
555
+ const contentRect = toRect(content.getBoundingClientRect());
556
+ const resolved = resolvePopoverPosition(anchorRect, contentRect, this.placement, this.offset, {
557
+ width: window.innerWidth,
558
+ height: window.innerHeight,
559
+ padding: 8,
560
+ });
561
+ this.clearInlineLayout(content);
562
+ content.style.setProperty('--cv-popover-anchor-inline-size', `${Math.max(0, Math.round(anchorRect.width))}px`);
563
+ content.dataset['anchorPositioning'] = 'false';
564
+ content.dataset['placement'] = resolved.placement;
565
+ content.style.position = 'fixed';
566
+ content.style.top = `${resolved.top}px`;
567
+ content.style.left = `${resolved.left}px`;
568
+ content.style.transform = 'none';
569
+ content.style.translate = 'none';
570
+ }
571
+ cancelLayoutFrame() {
572
+ if (this.layoutFrame === -1)
573
+ return;
574
+ cancelAnimationFrame(this.layoutFrame);
575
+ this.layoutFrame = -1;
576
+ }
577
+ scheduleLayout() {
578
+ this.cancelLayoutFrame();
579
+ this.layoutFrame = requestAnimationFrame(() => {
580
+ this.layoutFrame = -1;
581
+ this.syncPopoverLayout();
582
+ });
583
+ }
584
+ toggleLayoutListeners(nextState) {
585
+ if (this.hasLayoutListeners === nextState)
586
+ return;
587
+ this.hasLayoutListeners = nextState;
588
+ if (nextState) {
589
+ window.addEventListener('resize', this.handleViewportChange);
590
+ window.addEventListener('scroll', this.handleViewportChange, true);
591
+ return;
592
+ }
593
+ window.removeEventListener('resize', this.handleViewportChange);
594
+ window.removeEventListener('scroll', this.handleViewportChange, true);
328
595
  }
596
+ handleViewportChange = () => {
597
+ if (!this.model.state.isOpen())
598
+ return;
599
+ this.scheduleLayout();
600
+ };
329
601
  render() {
330
602
  const triggerProps = this.model.contracts.getTriggerProps();
331
603
  const contentProps = this.model.contracts.getContentProps();
604
+ const renderTrigger = this.triggerMode === 'internal';
332
605
  return html `
333
606
  <div part="base">
334
- <button
335
- id=${triggerProps.id}
336
- role=${triggerProps.role}
337
- tabindex=${triggerProps.tabindex}
338
- aria-haspopup=${triggerProps['aria-haspopup']}
339
- aria-expanded=${triggerProps['aria-expanded']}
340
- aria-controls=${triggerProps['aria-controls']}
341
- part="trigger"
342
- type="button"
343
- @click=${this.handleTriggerClick}
344
- @keydown=${this.handleTriggerKeyDown}
345
- >
346
- <slot name="trigger">Open popover</slot>
347
- </button>
607
+ ${renderTrigger
608
+ ? html `
609
+ <button
610
+ id=${triggerProps.id}
611
+ role=${triggerProps.role}
612
+ tabindex=${triggerProps.tabindex}
613
+ aria-haspopup=${triggerProps['aria-haspopup']}
614
+ aria-expanded=${triggerProps['aria-expanded']}
615
+ aria-controls=${triggerProps['aria-controls']}
616
+ part="trigger"
617
+ type="button"
618
+ @click=${this.handleTriggerClick}
619
+ @keydown=${this.handleTriggerKeyDown}
620
+ >
621
+ <slot name="trigger">Open popover</slot>
622
+ </button>
623
+ `
624
+ : nothing}
348
625
 
349
626
  <div
350
627
  id=${contentProps.id}
@@ -353,12 +630,17 @@ export class CVPopover extends ReatomLitElement {
353
630
  aria-modal=${contentProps['aria-modal']}
354
631
  aria-label=${contentProps['aria-label'] ?? nothing}
355
632
  aria-labelledby=${contentProps['aria-labelledby'] ?? nothing}
356
- ?hidden=${contentProps.hidden}
633
+ popover=${contentProps.popover ?? nothing}
634
+ ?hidden=${contentProps.hidden ?? false}
635
+ class="cv-u-discrete-presence"
357
636
  data-placement=${this.placement}
358
637
  data-anchor=${this.anchor}
638
+ data-trigger-mode=${this.triggerMode}
639
+ data-anchor-positioning="false"
359
640
  style=${`--cv-popover-offset:${this.offset}px;`}
360
641
  part="content"
361
642
  @keydown=${this.handleContentKeyDown}
643
+ @toggle=${this.handleNativeToggle}
362
644
  >
363
645
  <slot></slot>
364
646
  ${this.arrow