@forcecalendar/interface 1.0.56 → 1.0.57

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forcecalendar/interface",
3
- "version": "1.0.56",
3
+ "version": "1.0.57",
4
4
  "type": "module",
5
5
  "description": "Official interface layer for forceCalendar Core - Enterprise calendar components",
6
6
  "main": "dist/force-calendar-interface.umd.js",
@@ -31,6 +31,8 @@ export class BaseComponent extends HTMLElement {
31
31
  disconnectedCallback() {
32
32
  this.unmount();
33
33
  this.cleanup();
34
+ this._styleEl = null;
35
+ this._contentWrapper = null;
34
36
  }
35
37
 
36
38
  // To be overridden by child classes
@@ -131,18 +133,102 @@ export class BaseComponent extends HTMLElement {
131
133
  // Clean up existing listeners before replacing DOM
132
134
  this.cleanup();
133
135
 
134
- const styles = `
135
- <style>
136
- ${this.getBaseStyles()}
137
- ${this.getStyles()}
138
- </style>
139
- `;
140
-
141
136
  const template = this.template();
142
- this.shadowRoot.innerHTML = styles + template;
137
+
138
+ // First render: create style element and content wrapper
139
+ if (!this._styleEl) {
140
+ this._styleEl = document.createElement('style');
141
+ this.shadowRoot.appendChild(this._styleEl);
142
+ this._contentWrapper = document.createElement('div');
143
+ this._contentWrapper.setAttribute('id', 'fc-root');
144
+ this._contentWrapper.style.display = 'contents';
145
+ this.shadowRoot.appendChild(this._contentWrapper);
146
+ }
147
+
148
+ // Update styles on every render in case getStyles() depends on state
149
+ this._styleEl.textContent = this.getBaseStyles() + '\n' + this.getStyles();
150
+
151
+ // Save scroll positions and focused element before DOM replacement
152
+ const scrollPositions = this._saveScrollPositions();
153
+ const activeSelector = this._getActiveElementSelector();
154
+
155
+ this._contentWrapper.innerHTML = template;
156
+
157
+ // Restore scroll positions and focus
158
+ this._restoreScrollPositions(scrollPositions);
159
+ this._restoreFocus(activeSelector);
160
+
143
161
  this.afterRender();
144
162
  }
145
163
 
164
+ /**
165
+ * Save scroll positions of all scrollable containers within shadow DOM
166
+ * @returns {Map<string, {top: number, left: number}>}
167
+ */
168
+ _saveScrollPositions() {
169
+ const positions = new Map();
170
+ if (!this._contentWrapper) return positions;
171
+ const scrollables = this._contentWrapper.querySelectorAll('[id]');
172
+ scrollables.forEach(el => {
173
+ if (el.scrollTop !== 0 || el.scrollLeft !== 0) {
174
+ positions.set(el.id, { top: el.scrollTop, left: el.scrollLeft });
175
+ }
176
+ });
177
+ return positions;
178
+ }
179
+
180
+ /**
181
+ * Restore previously saved scroll positions
182
+ * @param {Map<string, {top: number, left: number}>} positions
183
+ */
184
+ _restoreScrollPositions(positions) {
185
+ if (!this._contentWrapper || positions.size === 0) return;
186
+ positions.forEach((pos, id) => {
187
+ const el = this._contentWrapper.querySelector(`[id="${CSS.escape(id)}"]`);
188
+ if (el) {
189
+ el.scrollTop = pos.top;
190
+ el.scrollLeft = pos.left;
191
+ }
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Get a CSS selector for the currently focused element within shadow DOM
197
+ * @returns {string|null}
198
+ */
199
+ _getActiveElementSelector() {
200
+ const active = this.shadowRoot.activeElement;
201
+ if (!active || active === this._contentWrapper) return null;
202
+ if (active.id) return `[id="${CSS.escape(active.id)}"]`;
203
+ // Prefer data attributes over className to avoid invalid selectors
204
+ if (active.dataset && active.dataset.action) {
205
+ return `[data-action="${CSS.escape(active.dataset.action)}"]`;
206
+ }
207
+ if (active.dataset && active.dataset.view) {
208
+ return `[data-view="${CSS.escape(active.dataset.view)}"]`;
209
+ }
210
+ if (active.tagName) {
211
+ return active.tagName.toLowerCase();
212
+ }
213
+ return null;
214
+ }
215
+
216
+ /**
217
+ * Restore focus to a previously focused element
218
+ * @param {string|null} selector
219
+ */
220
+ _restoreFocus(selector) {
221
+ if (!selector || !this._contentWrapper) return;
222
+ try {
223
+ const el = this._contentWrapper.querySelector(selector);
224
+ if (el && typeof el.focus === 'function') {
225
+ el.focus();
226
+ }
227
+ } catch (_) {
228
+ // Invalid selector, ignore
229
+ }
230
+ }
231
+
146
232
  template() {
147
233
  // Override in child classes to provide component template
148
234
  return '';