griffincss-ui 0.25.0-dev → 0.26.2

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 (49) hide show
  1. package/dist/griffincss-ui-scoped.css +1 -1
  2. package/dist/griffincss-ui.css +1 -1
  3. package/dist/griffincss-ui.js +1 -1
  4. package/dist/griffincss-ui.js.map +1 -1
  5. package/dist/griffinjs-core.js +1 -1
  6. package/dist/griffinjs-core.js.map +1 -1
  7. package/dist/griffinjs-dialog.js +1 -1
  8. package/dist/griffinjs-dialog.js.map +1 -1
  9. package/dist/griffinjs-dropdown.js +1 -1
  10. package/dist/griffinjs-dropdown.js.map +1 -1
  11. package/dist/griffinjs-fields.css +1 -1
  12. package/dist/griffinjs-fields.js +1 -1
  13. package/dist/griffinjs-fields.js.map +1 -1
  14. package/dist/griffinjs-loader.js +3 -0
  15. package/dist/griffinjs-loader.js.map +1 -0
  16. package/dist/griffinjs-megamenu.js +1 -1
  17. package/dist/griffinjs-megamenu.js.map +1 -1
  18. package/dist/griffinjs-validate.js +1 -1
  19. package/dist/griffinjs-validate.js.map +1 -1
  20. package/dist/griffinjs.css +1 -1
  21. package/dist/griffinjs.js +1 -1
  22. package/dist/griffinjs.js.map +1 -1
  23. package/package.json +4 -3
  24. package/scss/_accordion.scss +5 -2
  25. package/scss/_alert.scss +1 -1
  26. package/scss/_card.scss +33 -7
  27. package/scss/_choice.scss +81 -10
  28. package/scss/_dropdown.scss +7 -3
  29. package/scss/_form.scss +42 -1
  30. package/scss/_menu.scss +4 -1
  31. package/scss/_modal.scss +44 -7
  32. package/scss/_nav.scss +5 -2
  33. package/scss/_pagination.scss +8 -1
  34. package/scss/_rating.scss +7 -1
  35. package/scss/_table.scss +91 -0
  36. package/scss/_tabs.scss +4 -1
  37. package/scss/griffincss-ui-scoped.scss +17 -7
  38. package/scss/griffincss-ui.scss +16 -10
  39. package/scss/griffinjs/_track.scss +43 -4
  40. package/scss/griffinjs/fields/_number.scss +9 -11
  41. package/scss/griffinjs/fields/fields.scss +1 -1
  42. package/scss/griffinjs/griffinjs.scss +1 -1
  43. package/src/griffincss-ui.js +2 -2
  44. package/src/griffinjs/core/griffinjs-core.js +57 -11
  45. package/src/griffinjs/core/loader.js +124 -0
  46. package/src/griffinjs/fields/validate.js +24 -3
  47. package/src/griffinjs/widgets/dialog.js +77 -2
  48. package/src/griffinjs/widgets/dropdown.js +94 -4
  49. package/src/griffinjs/widgets/megamenu.js +90 -5
@@ -21,10 +21,22 @@
21
21
  * * позицию popover-панели у кнопки: считается здесь, а не CSS anchor
22
22
  * positioning, — одинаково во всех движках (нужен griffinjs-anchor.js);
23
23
  * * наведение с задержкой намерения (hover) — только для pointerType
24
- * mouse/pen: касание открывает щелчком, как без скрипта.
24
+ * mouse/pen: касание открывает щелчком, как без скрипта;
25
+ * * закрытие <details> с состоянием (Этап 48d): панель, чей уход тема
26
+ * анимирует, не пропадает в первом же кадре — обёртка получает
27
+ * data-gr-state="closing", open снимается по transitionend/animationend
28
+ * самой панели или по таймауту в длительность --gr-transition плюс
29
+ * 50 мс. Щелчок по кнопке открытого дропдауна идёт тем же путём.
30
+ * Во всех браузерах, без развилки по ::details-content: Firefox 153
31
+ * поддерживает селектор и allow-discrete, но закрытие не анимирует —
32
+ * замер в browser/closing.spec.mjs (решение владельца 2026-09-21).
33
+ * CSS слоя переходов не задаёт — при 0s закрывается сразу.
34
+ * Popover-панель анимирует платформа через display allow-discrete —
35
+ * ей это не нужно.
25
36
  *
26
37
  * Открытое состояние — data-gr-state="open" на обёртке: у варианта
27
38
  * по наведению (.gr-dropdown-hover) панель по нему показывает CSS слоя.
39
+ * Между open и ready у <details> бывает closing.
28
40
  */
29
41
  (function (G) {
30
42
  'use strict';
@@ -70,6 +82,18 @@
70
82
  return document.activeElement === node;
71
83
  }
72
84
 
85
+ // Длительность --gr-transition у узла, мс: «0.2s ease» → 200, «150ms» → 150,
86
+ // пусто или 0s → 0. Одно чтение вычисленного стиля при закрытии.
87
+ function duration(node) {
88
+ var value = '';
89
+
90
+ try { value = getComputedStyle(node).getPropertyValue('--gr-transition'); } catch (e) { /* без CSSOM */ }
91
+
92
+ var m = /(\d*\.?\d+)(m?s)/.exec(value);
93
+
94
+ return m ? parseFloat(m[1]) * (m[2] === 's' ? 1000 : 1) : 0;
95
+ }
96
+
73
97
  G.defineWidget('dropdown', { needs: ['anchor'] }, function (el, opts) {
74
98
  var o = G.merge(DEFAULTS, opts);
75
99
  var attrs = G.recorder();
@@ -86,6 +110,7 @@
86
110
  var opened = false; // для popover и css: <details> знает сам
87
111
  var viaKeyboard = false; // открыто с клавиатуры — фокус уходит в меню
88
112
  var timer = null;
113
+ var closing = null; // таймаут состояния closing у <details>
89
114
 
90
115
  // --- Состояние ------------------------------------------------------------
91
116
 
@@ -111,6 +136,7 @@
111
136
 
112
137
  function sync(now) {
113
138
  if (kind !== 'details') opened = now;
139
+ if (!now) settleClosing();
114
140
 
115
141
  setAttr(trigger, 'aria-expanded', now ? 'true' : 'false');
116
142
  setAttr(el, 'data-gr-state', now ? 'open' : 'ready');
@@ -121,6 +147,59 @@
121
147
  viaKeyboard = false;
122
148
  }
123
149
 
150
+ // --- Закрытие <details> с состоянием closing ------------------------------
151
+
152
+ // open снимается не сразу: обёртка получает data-gr-state="closing",
153
+ // панель уходит переходом темы; конец — transitionend/animationend самой
154
+ // панели (не потомка) или таймаут. Затем toggle → sync(false) → ready.
155
+ function closeDetails() {
156
+ if (closing) return;
157
+
158
+ var wait = duration(panel);
159
+
160
+ if (!wait) {
161
+ el.open = false;
162
+ return;
163
+ }
164
+
165
+ closing = setTimeout(finishClosing, wait + 50);
166
+ panel.addEventListener('transitionend', onEnd);
167
+ panel.addEventListener('animationend', onEnd);
168
+ setAttr(el, 'data-gr-state', 'closing');
169
+ }
170
+
171
+ function onEnd(event) {
172
+ if (event.target === panel) finishClosing();
173
+ }
174
+
175
+ function settleClosing() {
176
+ if (!closing) return;
177
+
178
+ clearTimeout(closing);
179
+ closing = null;
180
+ panel.removeEventListener('transitionend', onEnd);
181
+ panel.removeEventListener('animationend', onEnd);
182
+ }
183
+
184
+ function finishClosing() {
185
+ if (!closing) return;
186
+
187
+ settleClosing();
188
+ el.open = false;
189
+ }
190
+
191
+ // Щелчок (и Enter/Space) по кнопке открытого <details>: платформа сняла бы
192
+ // open в тот же кадр — перехватывается и идёт через closing. Щелчок
193
+ // по закрывающемуся — передумали: панель остаётся.
194
+ function onTriggerClick(event) {
195
+ if (kind !== 'details' || !el.open) return;
196
+
197
+ event.preventDefault();
198
+
199
+ if (closing) open();
200
+ else close();
201
+ }
202
+
124
203
  // beforetoggle приходит синхронно, до показа: панель ещё без размера,
125
204
  // но для «снизу, по началу строки» размер и не нужен, а без этой
126
205
  // предварительной позиции она на один кадр вспыхивала бы в статическом
@@ -137,6 +216,14 @@
137
216
 
138
217
  function open() {
139
218
  cancelTimer();
219
+
220
+ // Открытие во время закрытия отменяет его: панель остаётся на месте.
221
+ if (closing) {
222
+ settleClosing();
223
+ setAttr(el, 'data-gr-state', 'open');
224
+ return;
225
+ }
226
+
140
227
  if (isOpen()) return;
141
228
 
142
229
  if (kind === 'details') el.open = true;
@@ -149,7 +236,7 @@
149
236
  cancelTimer();
150
237
  if (!isOpen()) return;
151
238
 
152
- if (kind === 'details') el.open = false;
239
+ if (kind === 'details') closeDetails();
153
240
  else if (kind === 'popover') {
154
241
  try { panel.hidePopover(); } catch (e) { sync(false); }
155
242
  } else sync(false);
@@ -286,6 +373,7 @@
286
373
 
287
374
  function destroy() {
288
375
  cancelTimer();
376
+ finishClosing();
289
377
  events.removeAll();
290
378
  if (kind === 'popover' && G.anchor) G.anchor.clear(panel);
291
379
  attrs.restore();
@@ -298,8 +386,10 @@
298
386
 
299
387
  roles();
300
388
 
301
- if (kind === 'details') listen(el, 'toggle', function () { sync(!!el.open); });
302
- else if (kind === 'popover') {
389
+ if (kind === 'details') {
390
+ listen(el, 'toggle', function () { sync(!!el.open); });
391
+ listen(trigger, 'click', onTriggerClick);
392
+ } else if (kind === 'popover') {
303
393
  listen(panel, 'beforetoggle', onBeforeToggle);
304
394
  listen(panel, 'toggle', function (event) { sync(event.newState === 'open'); });
305
395
  listen(document, 'scroll', onReflow, true);
@@ -27,7 +27,16 @@
27
27
  * закрывает и возвращает фокус на заголовок;
28
28
  * * закрытие щелчком мимо полосы и уходом фокуса;
29
29
  * * ajax-панели: data-gr-src на пункте грузится при первом открытии,
30
- * состояния loading → ready | error пишутся на пункт.
30
+ * состояния loading → ready | error пишутся на пункт;
31
+ * * закрытие с состоянием (Этап 48d): панель, чей уход тема анимирует,
32
+ * не пропадает в первом же кадре — на пункт ставится
33
+ * data-gr-state="closing", open снимается по transitionend/animationend
34
+ * самой панели или по таймауту в длительность --gr-transition плюс
35
+ * 50 мс. Щелчок по заголовку открытого пункта идёт тем же путём.
36
+ * Во всех браузерах, без развилки по ::details-content: Firefox 153
37
+ * поддерживает селектор и allow-discrete, но закрытие не анимирует —
38
+ * замер в browser/closing.spec.mjs (решение владельца 2026-09-21).
39
+ * CSS слоя переходов не задаёт — при 0s закрывается сразу.
31
40
  *
32
41
  * Пункт — <details>, не вложенный в другой <details> полосы, с панелью
33
42
  * после <summary>. Переключатель шапки (.gr-nav-toggle) панели не имеет
@@ -89,6 +98,18 @@
89
98
  return document.activeElement === node;
90
99
  }
91
100
 
101
+ // Длительность --gr-transition у узла, мс: «0.2s ease» → 200, «150ms» → 150,
102
+ // пусто или 0s → 0. Одно чтение вычисленного стиля при закрытии.
103
+ function duration(node) {
104
+ var value = '';
105
+
106
+ try { value = getComputedStyle(node).getPropertyValue('--gr-transition'); } catch (e) { /* без CSSOM */ }
107
+
108
+ var m = /(\d*\.?\d+)(m?s)/.exec(value);
109
+
110
+ return m ? parseFloat(m[1]) * (m[2] === 's' ? 1000 : 1) : 0;
111
+ }
112
+
92
113
  G.defineWidget('megamenu', { needs: ['media'] }, function (el, opts) {
93
114
  var o = G.merge(DEFAULTS, opts);
94
115
  var attrs = G.recorder();
@@ -159,6 +180,7 @@
159
180
  setAttr(item.summary, 'aria-expanded', item.el.open ? 'true' : 'false');
160
181
 
161
182
  listen(item.el, 'toggle', function () { sync(item); });
183
+ listen(item.summary, 'click', function (event) { onSummaryClick(event, item); });
162
184
  }
163
185
 
164
186
  function sync(item) {
@@ -166,10 +188,13 @@
166
188
 
167
189
  setAttr(item.summary, 'aria-expanded', isOpen ? 'true' : 'false');
168
190
 
169
- if (!isOpen) return;
191
+ if (!isOpen) {
192
+ settleClosing(item);
193
+ return;
194
+ }
170
195
 
171
196
  for (var i = 0; i < items.length; i++) {
172
- if (items[i] !== item && items[i].el.open) items[i].el.open = false;
197
+ if (items[i] !== item) close(items[i]);
173
198
  }
174
199
 
175
200
  load(item);
@@ -177,11 +202,69 @@
177
202
 
178
203
  function open(item) {
179
204
  cancelTimers();
180
- if (item && !item.el.open) item.el.open = true;
205
+ if (!item) return;
206
+
207
+ // Указатель вернулся на закрывающийся пункт — он остаётся открытым.
208
+ if (item.closing) {
209
+ settleClosing(item);
210
+ setAttr(item.el, 'data-gr-state', item.was);
211
+ }
212
+
213
+ if (!item.el.open) item.el.open = true;
181
214
  }
182
215
 
216
+ // --- Закрытие с состоянием closing ----------------------------------------
217
+
218
+ // open снимается не сразу: пункт получает data-gr-state="closing", и его
219
+ // панель уходит переходом темы; конец — transitionend/animationend самой
220
+ // панели (не потомка) или таймаут. Прежнее состояние пункта (ajax:
221
+ // ready | error) возвращается на место.
183
222
  function close(item) {
184
- if (item && item.el.open) item.el.open = false;
223
+ if (!item || !item.el.open || item.closing) return;
224
+
225
+ var wait = duration(item.panel);
226
+
227
+ if (!wait) {
228
+ item.el.open = false;
229
+ return;
230
+ }
231
+
232
+ item.was = item.el.getAttribute('data-gr-state');
233
+ item.onEnd = function (event) { if (event.target === item.panel) finishClosing(item); };
234
+ item.closing = setTimeout(function () { finishClosing(item); }, wait + 50);
235
+
236
+ item.panel.addEventListener('transitionend', item.onEnd);
237
+ item.panel.addEventListener('animationend', item.onEnd);
238
+ setAttr(item.el, 'data-gr-state', 'closing');
239
+ }
240
+
241
+ function settleClosing(item) {
242
+ if (!item.closing) return;
243
+
244
+ clearTimeout(item.closing);
245
+ item.closing = null;
246
+ item.panel.removeEventListener('transitionend', item.onEnd);
247
+ item.panel.removeEventListener('animationend', item.onEnd);
248
+ }
249
+
250
+ function finishClosing(item) {
251
+ if (!item.closing) return;
252
+
253
+ settleClosing(item);
254
+ setAttr(item.el, 'data-gr-state', item.was);
255
+ item.el.open = false;
256
+ }
257
+
258
+ // Щелчок (и Enter/Space) по заголовку открытого пункта: платформа сняла бы
259
+ // open в тот же кадр — перехватывается и идёт через closing. Щелчок
260
+ // по закрывающемуся пункту — передумали: он остаётся открытым.
261
+ function onSummaryClick(event, item) {
262
+ if (!item.el.open) return;
263
+
264
+ event.preventDefault();
265
+
266
+ if (item.closing) open(item);
267
+ else close(item);
185
268
  }
186
269
 
187
270
  function closeAll() {
@@ -284,6 +367,7 @@
284
367
  if (item === active || item === pending) {
285
368
  if (closeTimer) clearTimeout(closeTimer);
286
369
  closeTimer = null;
370
+ if (item.closing) open(item);
287
371
  return;
288
372
  }
289
373
 
@@ -456,6 +540,7 @@
456
540
 
457
541
  function destroy() {
458
542
  cancelTimers();
543
+ for (var i = 0; i < items.length; i++) finishClosing(items[i]);
459
544
  if (unmedia) unmedia();
460
545
  events.removeAll();
461
546
  attrs.restore();