griffincss-ui 0.29.0 → 0.29.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.
package/scss/_table.scss CHANGED
@@ -254,6 +254,18 @@
254
254
  overscroll-behavior-x: contain;
255
255
  }
256
256
 
257
+ // Word и Google Docs кладут ширину таблицы в style — 100 %, пиксели,
258
+ // пункты. Блок растягивался по ней, а строки — анонимная таблица внутри —
259
+ // вставали по содержимому: рамка и фон уходили далеко за ячейки. Встроенный
260
+ // стиль сильнее любого правила без !important, поэтому здесь — единственное
261
+ // !important библиотеки, и только у таблицы прозы со встроенной шириной:
262
+ // у остальных таблиц ширину по-прежнему перебивает любой CSS сайта.
263
+ // Тема из подслоя ниже ui снимает его своим !important.
264
+ :where(.gr-prose table:not(.gr-table)[style*="width" i]) {
265
+ // stylelint-disable-next-line declaration-no-important
266
+ inline-size: max-content !important;
267
+ }
268
+
257
269
  // Подсказка прокрутки. Край колонки обрезает таблицу ровно по столбцу,
258
270
  // а полосы прокрутки macOS и телефонов видны только при касании — скрытые
259
271
  // столбцы легко не заметить. У края, за которым ещё есть содержимое,
@@ -279,6 +291,11 @@
279
291
  $gr-scroll-areas: '.gr-table-wrap, .gr-table-scroll, :where(.gr-prose table:not(.gr-table))';
280
292
 
281
293
  // Первые два слоя — шторки для Firefox (см. ниже); здесь они прозрачные.
294
+ //
295
+ // --gr-scroll-hint-inset — отступ подсказки от начала строки: у таблицы
296
+ // с липким первым столбцом тень «в начале ещё есть столбцы» лежит у его
297
+ // края, а не у края области, где её закрывает или (в строке без липкой
298
+ // ячейки) пятнает сам столбец. Сдвигаются начальная тень и шторка.
282
299
  #{$gr-scroll-areas} {
283
300
  background-image:
284
301
  linear-gradient(to right, var(--gr-scroll-cover, transparent) 30%, transparent),
@@ -286,7 +303,7 @@ $gr-scroll-areas: '.gr-table-wrap, .gr-table-scroll, :where(.gr-prose table:not(
286
303
  gr-scroll-shade(0, var(--gr-scroll-start)),
287
304
  gr-scroll-shade(100%, var(--gr-scroll-end));
288
305
  background-repeat: no-repeat;
289
- background-position: 0 50%, 100% 50%;
306
+ background-position: var(--gr-scroll-hint-inset, 0) 50%, 100% 50%;
290
307
  background-size: 2.5rem 100%, 2.5rem 100%, 1rem 100%, 1rem 100%;
291
308
  background-attachment: local, local, scroll, scroll;
292
309
  }
@@ -297,15 +314,18 @@ $gr-scroll-areas: '.gr-table-wrap, .gr-table-scroll, :where(.gr-prose table:not(
297
314
  animation-timeline: scroll(self inline);
298
315
  animation-range: 0 2rem, calc(100% - 2rem) 100%;
299
316
  }
317
+ }
300
318
 
301
- // В RTL прокрутка начинается справа: та же пара анимаций, но тень у левого
302
- // края гаснет к концу шкалы, а у правого появляется с её начала.
303
- .gr-table-wrap:dir(rtl),
304
- .gr-table-scroll:dir(rtl),
305
- :where(.gr-prose table:not(.gr-table):dir(rtl)) {
306
- animation-direction: reverse;
307
- animation-range: calc(100% - 2rem) 100%, 0 2rem;
308
- }
319
+ // В RTL строка начинается справа: отступ — у правого края, а та же пара
320
+ // анимаций идёт в обратную сторону — тень у левого края гаснет к концу
321
+ // шкалы, у правого появляется с её начала. Вне @supports: отступ нужен
322
+ // и шторке Firefox, а animation-* там без анимации ничего не делают.
323
+ .gr-table-wrap:dir(rtl),
324
+ .gr-table-scroll:dir(rtl),
325
+ :where(.gr-prose table:not(.gr-table):dir(rtl)) {
326
+ background-position: 0 50%, right var(--gr-scroll-hint-inset, 0) center;
327
+ animation-direction: reverse;
328
+ animation-range: calc(100% - 2rem) 100%, 0 2rem;
309
329
  }
310
330
 
311
331
  // Firefox шкалы прокрутки не знает: там тени у краёв стоят на полной силе,
@@ -95,6 +95,10 @@ $gr-megamenu-expand: map.get(bp.$gr-breakpoints, md);
95
95
  border-block-end: var(--gr-border-width) solid var(--gr-color-border);
96
96
  background-color: var(--gr-color-surface-overlay);
97
97
  color: var(--gr-color-text);
98
+
99
+ // Подложка — и шторке подсказки прокрутки (_table.scss): без шкалы прокрутки
100
+ // (Firefox) тень у края таблицы прячет шторка этого цвета.
101
+ --gr-scroll-hint-bg: var(--gr-color-surface-overlay);
98
102
  }
99
103
 
100
104
  // Ajax-панель, пока грузится: место под содержимое, чтобы полоса
@@ -1,10 +1,10 @@
1
1
  /*!
2
- * Griffincss UI — Runtime v0.29.0
2
+ * Griffincss UI — Runtime v0.29.2
3
3
  * Опциональный рантайм пакета компонентов. Делает ровно то, чего платформа
4
4
  * не даёт вовсе; всё, что умеют <details>, <dialog> и Popover API, остаётся
5
5
  * за ними. Без этого файла компоненты работают — просто без перечисленного.
6
6
  *
7
- * Здесь четыре вещи:
7
+ * Здесь пять вещей:
8
8
  *
9
9
  * 1. Закрытие модального окна и выдвижной панели щелчком по подложке.
10
10
  * <dialog> закрывается по Esc сам, но щелчок мимо окна ему безразличен,
@@ -27,6 +27,15 @@
27
27
  * для разметки с [data-gr-tabs]. Без скрипта та же разметка остаётся
28
28
  * списком ссылок-якорей и секциями подряд.
29
29
  *
30
+ * 5. Таблица в .gr-prose. Она прокручивается сама и потому — блок:
31
+ * Firefox считает такую таблицу без заголовков таблицей раскладки,
32
+ * и рантайм ставит ей role="table". Широкую он ставит в порядок Tab:
33
+ * Chromium с Firefox делают это сами, Safari — нет, а tabindex в тексте
34
+ * из редактора поставить некому; стала помещаться — снимает. Таблицы
35
+ * в поверхности редактора (contenteditable) не трогает. Разметке,
36
+ * вставленной позже, роль ставит повторный start(), фрагменту
37
+ * GriffinJS — событие griffin:load.
38
+ *
30
39
  * Всё, что умеют <details>, <dialog> и Popover API, остаётся за ними.
31
40
  */
32
41
  (function (factory) {
@@ -50,7 +59,7 @@
50
59
  })(function () {
51
60
  'use strict';
52
61
 
53
- var VERSION = '0.29.0';
62
+ var VERSION = '0.29.2';
54
63
 
55
64
  var OVERLAY_ATTR = 'data-gr-overlay-close';
56
65
 
@@ -73,12 +82,31 @@
73
82
 
74
83
  var TABS_ATTR = 'data-gr-tabs';
75
84
 
85
+ // Таблица прозы со своей прокруткой (_table.scss) и то, на чём внутри
86
+ // неё может остановиться Tab: он и так докручивает таблицу до своей
87
+ // остановки. Кандидаты отсеивает stopsTab(): tabindex="-1", отключённое
88
+ // и скрытое разметкой — не остановки. Ссылки и кнопки Safari обычным Tab
89
+ // пропускает, до них доходит Option+Tab.
90
+ var PROSE_TABLES = '.gr-prose table:not(.gr-table)';
91
+ var FOCUSABLE = 'a[href],button,input:not([type=hidden]),select,textarea,summary,iframe,' +
92
+ '[tabindex],[contenteditable]:not([contenteditable=false])';
93
+
94
+ // Скрыто разметкой: hidden (и until-found), inert, закрытый details вне
95
+ // своего summary. Проверка по дереву — раскладку не трогает.
96
+ var UNSHOWN = '[hidden],[inert],details:not([open])>:not(summary)';
97
+
76
98
  // Причина закрытия попадает в dialog.returnValue: обработчик close
77
99
  // на странице отличит щелчок мимо от кнопки «Сохранить».
78
100
  var OVERLAY_REASON = 'overlay';
79
101
 
80
102
  var started = false;
81
103
 
104
+ // Таблицы, которым рантайм поставил tabindex и роль: снимается только
105
+ // своё. Слабые наборы — таблицу, убранную из документа на время
106
+ // перерисовки, рантайм не держит и, вернувшуюся, не забывает.
107
+ var tabTables = new WeakSet();
108
+ var roleTables = new WeakSet();
109
+
82
110
  // Цель нажатия. Щелчок — это пара «нажал» и «отпустил», и закрывать окно
83
111
  // нужно, только если обе половины пришлись на подложку: выделение текста,
84
112
  // начатое в окне и законченное за его краем, щелчком по подложке не является.
@@ -611,14 +639,189 @@
611
639
  return Array.prototype.indexOf.call(list, node);
612
640
  }
613
641
 
642
+ // --- Таблица прозы: роль и порядок Tab ---------------------------------------
643
+
644
+ // Firefox решает, таблица это данных или раскладки, по числу столбцов
645
+ // у её рамки, а у таблицы со своей прокруткой рамка — блок без столбцов:
646
+ // без признаков данных она для читалки — раскладка, без «таблица, N строк»
647
+ // и перехода по ячейкам. Явная роль возвращает данные. В Chromium
648
+ // и WebKit своя прокрутка роли не отнимает, и роль там лишь подтверждает
649
+ // таблицу — а WebKit и сам гадает о раскладке: таблицу, где второй
650
+ // столбец даёт один rowspan, роль делает таблицей и для него. Как
651
+ // в Firefox у таблицы без своей прокрутки:
652
+ // от двух строк и двух столбцов по карте таблицы (colspan и rowspan),
653
+ // без своих признаков данных (th, thead, tfoot, col; у вложенной таблицы —
654
+ // её признаки). Пустой role — не роль. Таблица, ставшая редактируемой,
655
+ // теряет свои роль и tabindex: иначе они уйдут в HTML, который сохранит
656
+ // редактор.
657
+ function markRole(table) {
658
+ if (table.isContentEditable) {
659
+ unmark(table, tabTables, 'tabindex', '0');
660
+ return unmark(table, roleTables, 'role', 'table');
661
+ }
662
+ if (table.getAttribute('role') || table.tHead || table.tFoot || table.querySelector(':scope>col,:scope>colgroup')) return;
663
+
664
+ var rows = table.rows;
665
+ var spans = []; // ячейки с rowspan сверху: [colSpan, строк осталось]
666
+ var columns = false;
667
+
668
+ for (var i = 0; i < rows.length; i++) {
669
+ var cols = 0;
670
+
671
+ for (var k = spans.length; k--;) {
672
+ cols += spans[k][0];
673
+ if (!--spans[k][1]) spans.splice(k, 1);
674
+ }
675
+
676
+ for (var j = 0, cell; (cell = rows[i].cells[j]); j++) {
677
+ if (cell.tagName === 'TH') return;
678
+ cols += cell.colSpan;
679
+ if (cell.rowSpan > 1) spans.push([cell.colSpan, cell.rowSpan - 1]);
680
+ }
681
+
682
+ if (cols > 1) columns = true;
683
+ }
684
+
685
+ if (columns && rows.length > 1) {
686
+ table.setAttribute('role', 'table');
687
+ roleTables.add(table);
688
+ }
689
+ }
690
+
691
+ function markRoles() {
692
+ eachTable(PROSE_TABLES, markRole);
693
+ }
694
+
695
+ // Перед каждым Tab, в перехвате: порядок обхода браузер считает после
696
+ // обработчиков, и отметка, поставленная здесь, уже в нём. Так не нужны
697
+ // ни наблюдатели, ни замеры вне нажатия. Мерить все таблицы нельзя:
698
+ // в Chromium каждое чтение раскладки стоит пропорционально числу анимаций
699
+ // по прокрутке, и замер всех на каждый Tab — квадрат их числа. Поэтому —
700
+ // от фокуса по ходу Tab до первой широкой: дальше неё Tab не уйдёт.
701
+ // Фокус на body — откуда пойдёт Tab, неизвестно (щелчок в текст), и
702
+ // меряются все.
703
+ function onTabPress(event) {
704
+ if (event.key !== 'Tab') return;
705
+
706
+ var tables = document.querySelectorAll(PROSE_TABLES);
707
+ var from = document.activeElement;
708
+ var all = !from || from === document.body;
709
+ var step = all || !event.shiftKey ? 1 : -1;
710
+ var i = all ? 0 : firstAfter(tables, from) - (step < 0 ? 1 : 0);
711
+
712
+ // Shift+Tab с самой таблицы: она уже позади, замер — с предыдущей.
713
+ // Иначе отмеченная под фокусом останавливала его сразу, а предыдущая
714
+ // широкая оставалась без отметки — Safari её пропускал.
715
+ if (tables[i] === from) i += step;
716
+
717
+ // Встать можно только на таблице, до которой Tab дойдёт: невидимую
718
+ // широкую (закрытый details, inert) он пропустит — за ней идём дальше.
719
+ for (; i >= 0 && i < tables.length; i += step) {
720
+ markRole(tables[i]);
721
+ if (syncTabStop(tables[i]) && !all && shown(tables[i])) return;
722
+ }
723
+ }
724
+
725
+ // Tab до таблицы может дойти: она не скрыта разметкой и видна —
726
+ // checkVisibility (второе имя опции — у Chrome 105–120), а в Safari
727
+ // до 17.4, где его нет, — вычисленное visibility.
728
+ function shown(node) {
729
+ return !node.closest(UNSHOWN) && (node.checkVisibility
730
+ ? node.checkVisibility({ visibilityProperty: true, checkVisibilityCSS: true })
731
+ : getComputedStyle(node).visibility !== 'hidden');
732
+ }
733
+
734
+ // Остановка Tab внутри таблицы — только по дереву: в Chromium и чтение
735
+ // раскладки, и checkVisibility стоят по числу анимаций по прокрутке
736
+ // на странице, а таблиц со ссылками в тексте бывают сотни. Цена —
737
+ // ссылка, скрытая только стилями (visibility, display классом),
738
+ // считается остановкой. Хост правки tabIndex не даёт, но остановка.
739
+ function stopsTab(table) {
740
+ var found = table.querySelectorAll(FOCUSABLE);
741
+
742
+ for (var i = 0; i < found.length; i++) {
743
+ var node = found[i];
744
+
745
+ if ((node.tabIndex >= 0 || node.isContentEditable && !node.hasAttribute('tabindex')) &&
746
+ !node.matches(':disabled') && !node.closest(UNSHOWN)) return true;
747
+ }
748
+
749
+ return false;
750
+ }
751
+
752
+ // Первая таблица после узла в порядке документа — двоичным поиском:
753
+ // сравнение позиций раскладку не трогает. Таблица внутри узла — после
754
+ // него (4 — DOCUMENT_POSITION_FOLLOWING), таблица вокруг узла — до.
755
+ function firstAfter(tables, node) {
756
+ var lo = 0;
757
+ var hi = tables.length;
758
+
759
+ while (lo < hi) {
760
+ var mid = (lo + hi) >> 1;
761
+
762
+ if (node.compareDocumentPosition(tables[mid]) & 4) hi = mid;
763
+ else lo = mid + 1;
764
+ }
765
+
766
+ return lo;
767
+ }
768
+
769
+ // tabindex="0" — широкой таблице, где Tab больше не на чем остановиться;
770
+ // стала помещаться — снять. Своё — в наборе и сейчас "0": значение
771
+ // страницы (окно-ловушка фокуса ставит -1 на время, потом возвращает
772
+ // сохранённое) рантайм не снимает и не возвращает, а вернувшийся "0" —
773
+ // снова его. Атрибут сняли совсем (морфинг разметки) — это не значение
774
+ // страницы: отметка ставится заново. Остановки внутри — раньше ширины:
775
+ // по дереву они дешевле, а таблицу, где Tab и так остановится, мерить
776
+ // незачем. true — таблица теперь остановка Tab.
777
+ function syncTabStop(table) {
778
+ var mine = tabTables.has(table);
779
+ var value = table.getAttribute('tabindex');
780
+
781
+ if (mine && value === null) {
782
+ tabTables.delete(table);
783
+ mine = false;
784
+ }
785
+
786
+ if (mine ? value !== '0' : value !== null) return false;
787
+
788
+ var wide = !table.isContentEditable && !stopsTab(table) && table.scrollWidth > table.clientWidth;
789
+
790
+ if (wide && !mine) {
791
+ table.setAttribute('tabindex', '0');
792
+ tabTables.add(table);
793
+ } else if (!wide) {
794
+ unmark(table, tabTables, 'tabindex', '0');
795
+ }
796
+
797
+ return wide;
798
+ }
799
+
800
+ // Снять своё — атрибут, который поставил рантайм и с тех пор не сменили.
801
+ function unmark(table, set, name, value) {
802
+ if (set.has(table) && table.getAttribute(name) === value) table.removeAttribute(name);
803
+ set.delete(table);
804
+ }
805
+
806
+ function eachTable(selector, fn) {
807
+ var tables = document.querySelectorAll(selector);
808
+
809
+ for (var i = 0; i < tables.length; i++) fn(tables[i]);
810
+ }
811
+
614
812
  // Один делегированный обработчик на документ, а не по слушателю на окно:
615
813
  // окна появляются и исчезают вместе с разметкой, и переподписываться
616
- // на каждое пришлось бы вручную.
814
+ // на каждое пришлось бы вручную. Повторный вызов подписки не удваивает,
815
+ // а проходит таблицы прозы ещё раз: разметке, вставленной после старта
816
+ // (смена варианта товара, догруженный текст), роль нужна сразу — читалка
817
+ // идёт по тексту без Tab и прохода перед Tab может не дождаться.
617
818
  function start() {
618
- if (started) return publicApi;
819
+ if (!started) {
820
+ started = true;
821
+ listen('addEventListener');
822
+ }
619
823
 
620
- started = true;
621
- listen('addEventListener');
824
+ markRoles();
622
825
 
623
826
  return publicApi;
624
827
  }
@@ -630,13 +833,23 @@
630
833
  pressed = null;
631
834
  listen('removeEventListener');
632
835
 
836
+ // Все таблицы документа с такими значениями, а не только таблицы прозы:
837
+ // отмеченную могли вынести из прозы или дать ей класс .gr-table.
838
+ eachTable('table[tabindex="0"],table[role=table]', function (table) {
839
+ unmark(table, tabTables, 'tabindex', '0');
840
+ unmark(table, roleTables, 'role', 'table');
841
+ });
842
+
633
843
  return publicApi;
634
844
  }
635
845
 
636
846
  // Подписка и отписка — один список: диспетчер на каждый тип события
637
- // из реестра плюс перехват нажатия для щелчка по подложке.
847
+ // из реестра плюс перехват нажатия для щелчка по подложке и Tab
848
+ // для широких таблиц прозы и конец вставки фрагмента GriffinJS.
638
849
  function listen(method) {
639
850
  document[method]('pointerdown', onPointerDown, true);
851
+ document[method]('keydown', onTabPress, true);
852
+ document[method]('griffin:load', markRoles);
640
853
 
641
854
  for (var type in registry) {
642
855
  if (Object.prototype.hasOwnProperty.call(registry, type)) {
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * GriffinJS — Core v0.29.0
2
+ * GriffinJS — Core v0.29.2
3
3
  * Опциональный слой виджетов с состоянием поверх Griffincss: слайдер,
4
4
  * галерея, лайтбокс, параллакс, мегаменю. Подключается одной строкой
5
5
  * и одним файлом; страница без него — законное и рабочее состояние.
@@ -45,7 +45,7 @@
45
45
  })(function () {
46
46
  'use strict';
47
47
 
48
- var VERSION = '0.29.0';
48
+ var VERSION = '0.29.2';
49
49
 
50
50
  // Реестры. Модуль зовёт defineEngine/defineWidget; ядро — единственное
51
51
  // место, которое знает, как их применить.
@@ -232,9 +232,13 @@
232
232
 
233
233
  setAttr(el, 'data-gr-state', 'loading');
234
234
 
235
+ // griffin:load — конец вставки: его знает только окно. По нему
236
+ // рантайм компонентов ставит роль таблице прозы во фрагменте,
237
+ // а страница — оживляет свои скрипты.
235
238
  fragment(src).then(function (html) {
236
239
  target.replaceChildren(html);
237
240
  setAttr(el, 'data-gr-state', isOpen() ? 'open' : 'ready');
241
+ G.emit('griffin:load', el, { dialog: el });
238
242
  }, function (error) {
239
243
  G.warn('dialog: ' + src + ' not loaded: ' + (error && error.message));
240
244
  setAttr(el, 'data-gr-state', 'error');
@@ -329,9 +329,11 @@
329
329
 
330
330
  setAttr(item.el, 'data-gr-state', 'loading');
331
331
 
332
+ // griffin:load — конец вставки, как у окна.
332
333
  fragment(src).then(function (html) {
333
334
  item.panel.replaceChildren(html);
334
335
  setAttr(item.el, 'data-gr-state', 'ready');
336
+ G.emit('griffin:load', item.el, { item: item.el, panel: item.panel });
335
337
  }, function (error) {
336
338
  G.warn('megamenu: panel ' + src + ' not loaded: ' + (error && error.message));
337
339
  setAttr(item.el, 'data-gr-state', 'error');