@feugene/granularity 0.18.0 → 0.19.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.
- package/dist/chunks/chevron-left-D8AUBAM7.js +23 -0
- package/dist/chunks/chevron-left-D8AUBAM7.js.map +1 -0
- package/dist/chunks/{chevron-right-BfqMVpbB.js → chevron-right-C6Z01h0X.js} +1 -22
- package/dist/chunks/chevron-right-C6Z01h0X.js.map +1 -0
- package/dist/chunks/config-8Pcutu1j2.js.map +1 -0
- package/dist/chunks/{config-K0C2tcLB.js → config-9PYRZlKE.js} +1 -1
- package/dist/chunks/{config-K0C2tcLB.js.map → config-9PYRZlKE.js.map} +1 -1
- package/dist/chunks/{config-DEvsMTQE.js → config-BJ7Bv_G2.js} +1 -3
- package/dist/chunks/config-BJ7Bv_G2.js.map +1 -0
- package/dist/chunks/config-BKvX0q102.js.map +1 -0
- package/dist/chunks/{config-gvq_hWNM.js → config-Be9k9bOf.js} +1 -7
- package/dist/chunks/config-Be9k9bOf.js.map +1 -0
- package/dist/chunks/{config-DpqOnJiz2.js → config-CDat1wXf2.js} +1 -1
- package/dist/chunks/config-CDat1wXf2.js.map +1 -0
- package/dist/chunks/config-CY0_6cMi2.js.map +1 -0
- package/dist/chunks/config-D94Z9po52.js.map +1 -0
- package/dist/chunks/{config-D2aNP7Es.js → config-DAX8Deh1.js} +1 -3
- package/dist/chunks/config-DAX8Deh1.js.map +1 -0
- package/dist/chunks/config-DCFvmHEv.js.map +1 -0
- package/dist/chunks/config-DKdlqXiY.js.map +1 -1
- package/dist/chunks/config-Rw9OyW702.js.map +1 -0
- package/dist/chunks/generated-BRkbhyxk.js +3270 -0
- package/dist/chunks/generated-BRkbhyxk.js.map +1 -0
- package/dist/chunks/{granular-provider-B9nXQCMu.js → granular-provider-C6uAujdJ.js} +12 -12
- package/dist/chunks/{granular-provider-B9nXQCMu.js.map → granular-provider-C6uAujdJ.js.map} +1 -1
- package/dist/chunks/grip-vertical-lRha2S7q.js +16 -0
- package/dist/chunks/grip-vertical-lRha2S7q.js.map +1 -0
- package/dist/chunks/icon-Dag2tIMs.js +21 -0
- package/dist/chunks/icon-Dag2tIMs.js.map +1 -0
- package/dist/chunks/{portalRoot-CWD4zHBm.js → portalRoot-CVhCgdG6.js} +10 -1
- package/dist/chunks/{portalRoot-CWD4zHBm.js.map → portalRoot-CVhCgdG6.js.map} +1 -1
- package/dist/chunks/{useModalOverlay-BTq-0ImG.js → useModalOverlay-DrQVYwwA.js} +1 -1
- package/dist/chunks/{useModalOverlay-BTq-0ImG.js.map → useModalOverlay-DrQVYwwA.js.map} +1 -1
- package/dist/chunks/{usePortalTarget-RyzEgNTb.js → usePortalTarget-Cn-Wiz5d.js} +1 -1
- package/dist/chunks/{usePortalTarget-RyzEgNTb.js.map → usePortalTarget-Cn-Wiz5d.js.map} +1 -1
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-CPEQxmLZ.js → GrAutocomplete-gY4Xopza.js} +32 -24
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-gY4Xopza.js.map +1 -0
- package/dist/components/GrAutocomplete/index.js +1 -1
- package/dist/components/GrAutocomplete/styles.css +1 -1
- package/dist/components/GrBreadcrumbs/chunks/{GrBreadcrumbs-BCNGDS6k.js → GrBreadcrumbs-DS_-PBVt.js} +8 -7
- package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-DS_-PBVt.js.map +1 -0
- package/dist/components/GrBreadcrumbs/index.js +1 -1
- package/dist/components/GrCollapse/chunks/{GrCollapse-B2Fc6A9r.js.map → GrCollapse-Kk0DE8tb.js.map} +1 -1
- package/dist/components/GrCollapse/index.js +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-DAdQ-Gc3.js → GrColorPicker-CkBTzkqn.js} +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-DAdQ-Gc3.js.map → GrColorPicker-CkBTzkqn.js.map} +1 -1
- package/dist/components/GrColorPicker/index.js +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-DMeSqkEv.js → GrCommandPalette-BugTlGdj.js} +37 -11
- package/dist/components/GrCommandPalette/chunks/GrCommandPalette-BugTlGdj.js.map +1 -0
- package/dist/components/GrCommandPalette/index.js +2 -2
- package/dist/components/GrCommandPalette/styles.css +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-hUTi-GdJ.js → GrConfirmDialog-Ds5T0GiR.js} +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-hUTi-GdJ.js.map → GrConfirmDialog-Ds5T0GiR.js.map} +1 -1
- package/dist/components/GrConfirmDialog/index.js +1 -1
- package/dist/components/GrDataTable/chunks/{GrDataTable-DzaGLG3A.js → GrDataTable-CYe2A2WA.js} +9 -7
- package/dist/components/GrDataTable/chunks/GrDataTable-CYe2A2WA.js.map +1 -0
- package/dist/components/GrDataTable/index.js +2 -2
- package/dist/components/GrDialog/chunks/{GrDialog-MD9Fzy9C.js → GrDialog-C3tYaA8v.js} +1 -1
- package/dist/components/GrDialog/chunks/{GrDialog-MD9Fzy9C.js.map → GrDialog-C3tYaA8v.js.map} +1 -1
- package/dist/components/GrDialog/index.js +1 -1
- package/dist/components/GrDialogService/chunks/{GrDialogService-DtgUijMj.js → GrDialogService-CTBiiJDI.js} +3 -3
- package/dist/components/GrDialogService/chunks/{GrDialogService-DtgUijMj.js.map → GrDialogService-CTBiiJDI.js.map} +1 -1
- package/dist/components/GrDialogService/index.js +1 -1
- package/dist/components/GrDivider/chunks/{GrDivider-B2Xwc_QO.js → GrDivider-xxq1_fIl.js} +1 -1
- package/dist/components/GrDivider/chunks/{GrDivider-B2Xwc_QO.js.map → GrDivider-xxq1_fIl.js.map} +1 -1
- package/dist/components/GrDivider/index.js +2 -2
- package/dist/components/GrDrawer/chunks/{GrDrawer-DWHdZZzW.js → GrDrawer-BSSuiNHU.js} +2 -2
- package/dist/components/GrDrawer/chunks/{GrDrawer-DWHdZZzW.js.map → GrDrawer-BSSuiNHU.js.map} +1 -1
- package/dist/components/GrDrawer/index.js +2 -2
- package/dist/components/GrDropdown/chunks/{GrDropdown-0tJK01Kq.js → GrDropdown-Bh6UK2M5.js} +1 -1
- package/dist/components/GrDropdown/chunks/{GrDropdown-0tJK01Kq.js.map → GrDropdown-Bh6UK2M5.js.map} +1 -1
- package/dist/components/GrDropdown/index.js +1 -1
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-VPPrpCoA.js → GrDropdownMenu-LyfEFUbg.js} +2 -2
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-VPPrpCoA.js.map → GrDropdownMenu-LyfEFUbg.js.map} +1 -1
- package/dist/components/GrDropdownMenu/index.js +2 -2
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-DE7MBbF8.js → GrImageViewer-DfD4euH5.js} +3 -2
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-DE7MBbF8.js.map → GrImageViewer-DfD4euH5.js.map} +1 -1
- package/dist/components/GrImageViewer/index.js +1 -1
- package/dist/components/GrModal/chunks/{GrModal-BbhcsDQ8.js → GrModal-CbMMr6W1.js} +1 -1
- package/dist/components/GrModal/chunks/{GrModal-BbhcsDQ8.js.map → GrModal-CbMMr6W1.js.map} +1 -1
- package/dist/components/GrModal/index.js +1 -1
- package/dist/components/GrNumberInput/chunks/{GrNumberInput-CZOyPz4J.js → GrNumberInput-B5CHZDBj.js} +2 -1
- package/dist/components/GrNumberInput/chunks/{GrNumberInput-CZOyPz4J.js.map → GrNumberInput-B5CHZDBj.js.map} +1 -1
- package/dist/components/GrNumberInput/index.js +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-waKhxBP7.js → GrPagination-BM4FrD6Q.js} +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-waKhxBP7.js.map → GrPagination-BM4FrD6Q.js.map} +1 -1
- package/dist/components/GrPagination/index.js +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-DuqeoFaI.js → GrPopover-DMH4Jjst.js} +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-DuqeoFaI.js.map → GrPopover-DMH4Jjst.js.map} +1 -1
- package/dist/components/GrPopover/index.js +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-JVVqBsPY.js → GrPromptDialog-Pwr9NPxK.js} +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-JVVqBsPY.js.map → GrPromptDialog-Pwr9NPxK.js.map} +1 -1
- package/dist/components/GrPromptDialog/index.js +1 -1
- package/dist/components/GrRating/chunks/{GrRating-LarIiwJL.js → GrRating-DVFH3lYu.js} +8 -7
- package/dist/components/GrRating/chunks/GrRating-DVFH3lYu.js.map +1 -0
- package/dist/components/GrRating/index.js +1 -1
- package/dist/components/GrSelect/chunks/{GrSelect-DAdu044K.js → GrSelect-B-Voni7D.js} +65 -52
- package/dist/components/GrSelect/chunks/GrSelect-B-Voni7D.js.map +1 -0
- package/dist/components/GrSelect/index.js +1 -1
- package/dist/components/GrSelect/styles.css +1 -1
- package/dist/components/GrSidebar/chunks/{GrSidebar-DRGnnViw.js → GrSidebar-i7AxjhY_.js} +3 -2
- package/dist/components/GrSidebar/chunks/{GrSidebar-DRGnnViw.js.map → GrSidebar-i7AxjhY_.js.map} +1 -1
- package/dist/components/GrSidebar/index.js +2 -2
- package/dist/components/GrSkeleton/index.js +1 -1
- package/dist/components/GrSortableList/chunks/{GrSortableList-BefVfWKh.js → GrSortableList-XB7mXM1W.js} +6 -5
- package/dist/components/GrSortableList/chunks/GrSortableList-XB7mXM1W.js.map +1 -0
- package/dist/components/GrSortableList/index.js +2 -2
- package/dist/components/GrStatistic/chunks/{GrStatistic-J4qLs3TH.js → GrStatistic-DOsM6s5V.js} +54 -7
- package/dist/components/GrStatistic/chunks/GrStatistic-DOsM6s5V.js.map +1 -0
- package/dist/components/GrStatistic/index.js +2 -2
- package/dist/components/GrTabs/chunks/{GrTabs-TeXaU5Uh.js → GrTabs-f3-oQenu.js} +5 -4
- package/dist/components/GrTabs/chunks/GrTabs-f3-oQenu.js.map +1 -0
- package/dist/components/GrTabs/index.js +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-Ct-39hga.js → GrToaster-Dqja0Lf9.js} +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-Ct-39hga.js.map → GrToaster-Dqja0Lf9.js.map} +1 -1
- package/dist/components/GrToaster/index.js +1 -1
- package/dist/components/GrTooltip/chunks/{GrTooltip-xAcqs0bP.js → GrTooltip-Cn08heWz.js} +1 -1
- package/dist/components/GrTooltip/chunks/{GrTooltip-xAcqs0bP.js.map → GrTooltip-Cn08heWz.js.map} +1 -1
- package/dist/components/GrTooltip/index.js +1 -1
- package/dist/components/GrTree/chunks/{GrTree-CaYK8dGZ.js → GrTree-BR9a2pay.js} +25 -20
- package/dist/components/GrTree/chunks/GrTree-BR9a2pay.js.map +1 -0
- package/dist/components/GrTree/index.js +2 -2
- package/dist/components/GrTree/styles.css +1 -1
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-DZp7CqqW.js → GrTreeSelect--muUeB0v.js} +27 -13
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect--muUeB0v.js.map +1 -0
- package/dist/components/GrTreeSelect/index.js +2 -2
- package/dist/composables/useAnnouncer.js +1 -1
- package/dist/composables/usePortalTarget.js +1 -1
- package/dist/directives/loading.js +1 -1
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +41 -41
- package/dist/styles/themes/dark.css +6 -1
- package/dist/testing.js +199 -0
- package/dist/testing.js.map +1 -0
- package/dist/theme-apply.js +33 -0
- package/dist/theme-apply.js.map +1 -0
- package/dist/theme.js +501 -0
- package/dist/theme.js.map +1 -0
- package/dist/tokens.js +1 -3215
- package/dist/types/src/components/GrBreadcrumbs/grBreadcrumbsStyles.d.ts +5 -2
- package/dist/types/src/components/GrCommandPalette/GrCommandPalette.vue.d.ts +6 -6
- package/dist/types/src/components/GrCommandPalette/filtering.d.ts +6 -2
- package/dist/types/src/components/GrDataTable/grDataTableStyles.d.ts +0 -1
- package/dist/types/src/components/GrRating/GrRating.vue.d.ts +9 -4
- package/dist/types/src/components/GrSortableList/grSortableListStyles.d.ts +0 -1
- package/dist/types/src/components/GrStatistic/GrStatistic.vue.d.ts +12 -9
- package/dist/types/src/components/GrStatistic/grStatisticStyles.d.ts +0 -1
- package/dist/types/src/components/GrTabs/GrTabs.vue.d.ts +6 -2
- package/dist/types/src/components/GrTree/grTreeProps.d.ts +11 -4
- package/dist/types/src/components/GrTree/safelist.d.ts +1 -1
- package/dist/types/src/components/shared/icon.d.ts +13 -0
- package/dist/types/src/testing/announcer.d.ts +10 -0
- package/dist/types/src/testing/context.d.ts +36 -0
- package/dist/types/src/testing/env.d.ts +25 -0
- package/dist/types/src/testing/geometry.d.ts +23 -0
- package/dist/types/src/testing/index.d.ts +12 -0
- package/dist/types/src/testing/pointer.d.ts +31 -0
- package/dist/types/src/theme/apply.d.ts +27 -0
- package/dist/types/src/theme/color.d.ts +35 -0
- package/dist/types/src/theme/extendTheme.d.ts +63 -0
- package/dist/types/src/theme/index.d.ts +22 -0
- package/dist/types/src/theme/roles.d.ts +16 -0
- package/dist/types/src/theme/tone.d.ts +32 -0
- package/dist/types/src/theme/validate.d.ts +16 -0
- package/dist/types/src/tokens/types.d.ts +8 -0
- package/dist/web-types.json +14 -11
- package/package.json +17 -2
- package/tokens/themes/dark.json +2 -1
- package/dist/chunks/chevron-right-BfqMVpbB.js.map +0 -1
- package/dist/chunks/config-8Pcutu1j.js.map +0 -1
- package/dist/chunks/config-BKvX0q10.js.map +0 -1
- package/dist/chunks/config-CY0_6cMi.js.map +0 -1
- package/dist/chunks/config-D2aNP7Es.js.map +0 -1
- package/dist/chunks/config-D94Z9po5.js.map +0 -1
- package/dist/chunks/config-DCFvmHEv2.js.map +0 -1
- package/dist/chunks/config-DEvsMTQE.js.map +0 -1
- package/dist/chunks/config-DpqOnJiz2.js.map +0 -1
- package/dist/chunks/config-Rw9OyW70.js.map +0 -1
- package/dist/chunks/config-gvq_hWNM.js.map +0 -1
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-CPEQxmLZ.js.map +0 -1
- package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-BCNGDS6k.js.map +0 -1
- package/dist/components/GrCommandPalette/chunks/GrCommandPalette-DMeSqkEv.js.map +0 -1
- package/dist/components/GrDataTable/chunks/GrDataTable-DzaGLG3A.js.map +0 -1
- package/dist/components/GrRating/chunks/GrRating-LarIiwJL.js.map +0 -1
- package/dist/components/GrSelect/chunks/GrSelect-DAdu044K.js.map +0 -1
- package/dist/components/GrSortableList/chunks/GrSortableList-BefVfWKh.js.map +0 -1
- package/dist/components/GrStatistic/chunks/GrStatistic-J4qLs3TH.js.map +0 -1
- package/dist/components/GrTabs/chunks/GrTabs-TeXaU5Uh.js.map +0 -1
- package/dist/components/GrTree/chunks/GrTree-CaYK8dGZ.js.map +0 -1
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect-DZp7CqqW.js.map +0 -1
- package/dist/tokens.js.map +0 -1
- package/dist/chunks/{config-8Pcutu1j.js → config-8Pcutu1j2.js} +0 -0
- package/dist/chunks/{config-BKvX0q10.js → config-BKvX0q102.js} +0 -0
- package/dist/chunks/{config-CY0_6cMi.js → config-CY0_6cMi2.js} +0 -0
- package/dist/chunks/{config-D94Z9po5.js → config-D94Z9po52.js} +0 -0
- package/dist/chunks/{config-DCFvmHEv2.js → config-DCFvmHEv.js} +0 -0
- package/dist/chunks/{config-Rw9OyW70.js → config-Rw9OyW702.js} +0 -0
- package/dist/components/GrCollapse/chunks/{GrCollapse-B2Fc6A9r.js → GrCollapse-Kk0DE8tb.js} +1 -1
package/dist/testing.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { n as GR_CONFIG_KEY } from "./chunks/context-BSYR7RZs.js";
|
|
2
|
+
import { t as GRANULARITY_I18N_KEY } from "./chunks/adapter-CAOxmn2G.js";
|
|
3
|
+
import { n as resetPortalRoot } from "./chunks/portalRoot-CVhCgdG6.js";
|
|
4
|
+
import { resetAnnouncer } from "./composables/useAnnouncer.js";
|
|
5
|
+
import { useToast } from "./composables/useToast.js";
|
|
6
|
+
import { computed, ref } from "vue";
|
|
7
|
+
//#region src/testing/pointer.ts
|
|
8
|
+
/**
|
|
9
|
+
* Указательные жесты в тестах.
|
|
10
|
+
*
|
|
11
|
+
* Две вещи, которые ломают наивную запись жеста, и обе неочевидны:
|
|
12
|
+
*
|
|
13
|
+
* 1. **`pointerdown` идёт в элемент, а `pointermove`/`pointerup`/`pointercancel` — в `window`.**
|
|
14
|
+
* Так слушает `useDragGesture` (и всё, что на нём стоит: слайдер, сплиттер,
|
|
15
|
+
* перестановка, смахивание тоста): нажатие приходит от разметки, а движение
|
|
16
|
+
* ловится глобально, иначе жест обрывался бы на выходе курсора за элемент.
|
|
17
|
+
* Тест, отправивший движение в тот же элемент, ничего не проверяет — и молча.
|
|
18
|
+
* 2. **`PointerEvent` в jsdom нет**, а `wrapper.trigger('pointerdown', { clientY })`
|
|
19
|
+
* падает на попытке записать `clientY`. Поэтому события собираются
|
|
20
|
+
* `MouseEvent`-ом и отправляются через `dispatchEvent`.
|
|
21
|
+
*
|
|
22
|
+
* Плюс `button: 0` по умолчанию: жест игнорирует неосновную кнопку, и без этого
|
|
23
|
+
* «перетаскивание» в тесте не начинается вовсе.
|
|
24
|
+
*/
|
|
25
|
+
/** Событие указателя: `MouseEvent`, потому что `PointerEvent` в jsdom не реализован. */
|
|
26
|
+
function pointer(type, init = {}) {
|
|
27
|
+
return new MouseEvent(type, {
|
|
28
|
+
bubbles: true,
|
|
29
|
+
...init
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Нажатие: единственное событие жеста, которое адресуется элементу. */
|
|
33
|
+
function press(target, init = {}) {
|
|
34
|
+
target.dispatchEvent(pointer("pointerdown", {
|
|
35
|
+
button: 0,
|
|
36
|
+
...init
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
function move(init = {}) {
|
|
40
|
+
window.dispatchEvent(pointer("pointermove", init));
|
|
41
|
+
}
|
|
42
|
+
function release(init = {}) {
|
|
43
|
+
window.dispatchEvent(pointer("pointerup", init));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Обрыв жеста: система забрала указатель (системный жест, звонок, потеря окна).
|
|
47
|
+
* Отдельная ветка поведения у всех потребителей — смахивание возвращает тост,
|
|
48
|
+
* перестановка откатывает порядок, — поэтому проверяется отдельно от `release`.
|
|
49
|
+
*/
|
|
50
|
+
function cancelPointer(init = {}) {
|
|
51
|
+
window.dispatchEvent(pointer("pointercancel", init));
|
|
52
|
+
}
|
|
53
|
+
/** Жест целиком: нажали на элементе, провели, отпустили. */
|
|
54
|
+
function drag(target, from, to) {
|
|
55
|
+
press(target, from);
|
|
56
|
+
move(to);
|
|
57
|
+
release(to);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/testing/geometry.ts
|
|
61
|
+
/** Прямоугольник элемента. Незаданные стороны выводятся из заданных. */
|
|
62
|
+
function mockRect(target, rect) {
|
|
63
|
+
const left = rect.left ?? rect.x ?? 0;
|
|
64
|
+
const top = rect.top ?? rect.y ?? 0;
|
|
65
|
+
const width = rect.width ?? (rect.right ?? 0) - left;
|
|
66
|
+
const height = rect.height ?? (rect.bottom ?? 0) - top;
|
|
67
|
+
const value = {
|
|
68
|
+
x: left,
|
|
69
|
+
y: top,
|
|
70
|
+
left,
|
|
71
|
+
top,
|
|
72
|
+
width,
|
|
73
|
+
height,
|
|
74
|
+
right: rect.right ?? left + width,
|
|
75
|
+
bottom: rect.bottom ?? top + height,
|
|
76
|
+
toJSON: () => ({})
|
|
77
|
+
};
|
|
78
|
+
target.getBoundingClientRect = () => value;
|
|
79
|
+
}
|
|
80
|
+
/** Элементы подряд, встык, без зазоров: строки списка, ячейки шапки, деления шкалы. */
|
|
81
|
+
function stackRects(targets, options) {
|
|
82
|
+
const { size, axis = "vertical", start = 0, cross = 100 } = options;
|
|
83
|
+
let offset = start;
|
|
84
|
+
for (const target of targets) {
|
|
85
|
+
mockRect(target, axis === "vertical" ? {
|
|
86
|
+
top: offset,
|
|
87
|
+
height: size,
|
|
88
|
+
left: 0,
|
|
89
|
+
width: cross
|
|
90
|
+
} : {
|
|
91
|
+
left: offset,
|
|
92
|
+
width: size,
|
|
93
|
+
top: 0,
|
|
94
|
+
height: cross
|
|
95
|
+
});
|
|
96
|
+
offset += size;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/testing/env.ts
|
|
101
|
+
/**
|
|
102
|
+
* Уборка между тестами.
|
|
103
|
+
*
|
|
104
|
+
* `document.body.innerHTML = ''` сам по себе оставляет пакет в разобранном
|
|
105
|
+
* состоянии: корень портала и хост живого региона закэшированы модулями, и
|
|
106
|
+
* после очистки `body` они указывают на узлы **вне документа**. Следующий
|
|
107
|
+
* монтаж телепортирует туда оверлеи, тест ищет их в документе и не находит —
|
|
108
|
+
* без единой ошибки в консоли. Очередь тостов переживает размонтирование по
|
|
109
|
+
* замыслу, поэтому её тоже гасим.
|
|
110
|
+
*/
|
|
111
|
+
function resetGranularityDom() {
|
|
112
|
+
useToast().clear();
|
|
113
|
+
resetAnnouncer();
|
|
114
|
+
resetPortalRoot();
|
|
115
|
+
if (typeof document !== "undefined") document.body.innerHTML = "";
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Ответ `matchMedia` — например «пользователь просил меньше движения».
|
|
119
|
+
*
|
|
120
|
+
* Не для того, чтобы «не падало»: сам пакет зовёт `matchMedia` опционально и без
|
|
121
|
+
* него живёт. Нужно, когда проверяется поведение **под** медиазапросом.
|
|
122
|
+
* Возвращает функцию отката: глобальное состояние обязано уйти вместе с тестом.
|
|
123
|
+
*/
|
|
124
|
+
function stubMatchMedia(options = {}) {
|
|
125
|
+
const { reducedMotion = false, matches = false } = options;
|
|
126
|
+
const original = window.matchMedia;
|
|
127
|
+
window.matchMedia = ((query) => ({
|
|
128
|
+
media: query,
|
|
129
|
+
matches: query.includes("prefers-reduced-motion") ? reducedMotion : matches,
|
|
130
|
+
onchange: null,
|
|
131
|
+
addEventListener: () => {},
|
|
132
|
+
removeEventListener: () => {},
|
|
133
|
+
addListener: () => {},
|
|
134
|
+
removeListener: () => {},
|
|
135
|
+
dispatchEvent: () => false
|
|
136
|
+
}));
|
|
137
|
+
return () => {
|
|
138
|
+
window.matchMedia = original;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/testing/context.ts
|
|
143
|
+
/** Адаптер перевода для теста: словарь или своя функция. */
|
|
144
|
+
function i18nAdapter(options = {}) {
|
|
145
|
+
if (typeof options.t !== "function" && !("locale" in options) && !("te" in options)) {
|
|
146
|
+
const dictionary = options;
|
|
147
|
+
return {
|
|
148
|
+
t: (key) => dictionary[key] ?? key,
|
|
149
|
+
te: (key) => key in dictionary
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const { t, te, locale } = options;
|
|
153
|
+
return {
|
|
154
|
+
t: t ?? ((key) => key),
|
|
155
|
+
te,
|
|
156
|
+
locale: typeof locale === "string" ? ref(locale) : locale
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Окружение пакета для монтирования — то, что `<GrConfigProvider>` раздаёт
|
|
161
|
+
* через `provide`.
|
|
162
|
+
*
|
|
163
|
+
* Отдаётся объектом, а не компонентом-обёрткой, намеренно: провайдер тянет за
|
|
164
|
+
* собой лишний корневой элемент, а искать компонент внутри чужой разметки в
|
|
165
|
+
* каждом тесте дороже, чем передать `global`. Проверять **сам** провайдер
|
|
166
|
+
* (`data-gr-config-provider`, тема на корне) этим нельзя и не нужно — для этого
|
|
167
|
+
* его и монтируют.
|
|
168
|
+
*
|
|
169
|
+
* ```ts
|
|
170
|
+
* mount(GrSelect, { props, global: granularityGlobal({ size: 'sm' }) })
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
function granularityGlobal(options = {}) {
|
|
174
|
+
const config = {
|
|
175
|
+
size: computed(() => options.size),
|
|
176
|
+
componentDefaults: computed(() => options.componentDefaults ?? {}),
|
|
177
|
+
theme: computed(() => options.theme),
|
|
178
|
+
portalTarget: computed(() => options.portalTarget)
|
|
179
|
+
};
|
|
180
|
+
const provide = { [GR_CONFIG_KEY]: config };
|
|
181
|
+
if (options.i18n) provide[GRANULARITY_I18N_KEY] = "t" in options.i18n && typeof options.i18n.t === "function" ? options.i18n : i18nAdapter(options.i18n);
|
|
182
|
+
return { provide };
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/testing/announcer.ts
|
|
186
|
+
/**
|
|
187
|
+
* Текст живого региона.
|
|
188
|
+
*
|
|
189
|
+
* Ждать здесь обязательно: `announce` пишет в регион **отложенным** макротаском,
|
|
190
|
+
* и это не деталь реализации, а условие работы. Регион, в котором текст
|
|
191
|
+
* появился в том же кадре, что и сам регион, часть AT не объявляет вовсе;
|
|
192
|
+
* поэтому синхронное чтение сразу после действия покажет пустую строку.
|
|
193
|
+
*/
|
|
194
|
+
async function announced(politeness = "polite") {
|
|
195
|
+
await new Promise((resolve) => setTimeout(resolve, 2));
|
|
196
|
+
return document.querySelector(`[data-gr-announcer-region="${politeness}"]`)?.textContent ?? "";
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
export { announced, cancelPointer, drag, granularityGlobal, i18nAdapter, mockRect, move, pointer, press, release, resetGranularityDom, stackRects, stubMatchMedia };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","names":[],"sources":["../src/testing/pointer.ts","../src/testing/geometry.ts","../src/testing/env.ts","../src/testing/context.ts","../src/testing/announcer.ts"],"sourcesContent":["/**\n * Указательные жесты в тестах.\n *\n * Две вещи, которые ломают наивную запись жеста, и обе неочевидны:\n *\n * 1. **`pointerdown` идёт в элемент, а `pointermove`/`pointerup`/`pointercancel` — в `window`.**\n * Так слушает `useDragGesture` (и всё, что на нём стоит: слайдер, сплиттер,\n * перестановка, смахивание тоста): нажатие приходит от разметки, а движение\n * ловится глобально, иначе жест обрывался бы на выходе курсора за элемент.\n * Тест, отправивший движение в тот же элемент, ничего не проверяет — и молча.\n * 2. **`PointerEvent` в jsdom нет**, а `wrapper.trigger('pointerdown', { clientY })`\n * падает на попытке записать `clientY`. Поэтому события собираются\n * `MouseEvent`-ом и отправляются через `dispatchEvent`.\n *\n * Плюс `button: 0` по умолчанию: жест игнорирует неосновную кнопку, и без этого\n * «перетаскивание» в тесте не начинается вовсе.\n */\n\n/** Событие указателя: `MouseEvent`, потому что `PointerEvent` в jsdom не реализован. */\nexport function pointer(type: string, init: MouseEventInit = {}): MouseEvent {\n return new MouseEvent(type, { bubbles: true, ...init })\n}\n\n/** Нажатие: единственное событие жеста, которое адресуется элементу. */\nexport function press(target: Element, init: MouseEventInit = {}): void {\n target.dispatchEvent(pointer('pointerdown', { button: 0, ...init }))\n}\n\nexport function move(init: MouseEventInit = {}): void {\n window.dispatchEvent(pointer('pointermove', init))\n}\n\nexport function release(init: MouseEventInit = {}): void {\n window.dispatchEvent(pointer('pointerup', init))\n}\n\n/**\n * Обрыв жеста: система забрала указатель (системный жест, звонок, потеря окна).\n * Отдельная ветка поведения у всех потребителей — смахивание возвращает тост,\n * перестановка откатывает порядок, — поэтому проверяется отдельно от `release`.\n */\nexport function cancelPointer(init: MouseEventInit = {}): void {\n window.dispatchEvent(pointer('pointercancel', init))\n}\n\n/** Жест целиком: нажали на элементе, провели, отпустили. */\nexport function drag(target: Element, from: MouseEventInit, to: MouseEventInit): void {\n press(target, from)\n move(to)\n release(to)\n}\n","/**\n * Геометрия в jsdom.\n *\n * Раскладки в jsdom нет: `getBoundingClientRect` у любого элемента возвращает\n * нули. Всё, что считает попадание указателя — слайдер, сплиттер, перестановка,\n * ширины колонок таблицы, — на нулевом прямоугольнике проверять не на чем,\n * поэтому геометрия задаётся руками.\n */\n\nexport type MockRect = Partial<Omit<DOMRect, 'toJSON'>>\n\n/** Прямоугольник элемента. Незаданные стороны выводятся из заданных. */\nexport function mockRect(target: Element, rect: MockRect): void {\n const left = rect.left ?? rect.x ?? 0\n const top = rect.top ?? rect.y ?? 0\n const width = rect.width ?? (rect.right ?? 0) - left\n const height = rect.height ?? (rect.bottom ?? 0) - top\n\n const value: DOMRect = {\n x: left,\n y: top,\n left,\n top,\n width,\n height,\n right: rect.right ?? left + width,\n bottom: rect.bottom ?? top + height,\n toJSON: () => ({}),\n }\n\n target.getBoundingClientRect = () => value\n}\n\nexport interface StackRectsOptions {\n /** Размер элемента вдоль оси. */\n size: number\n /** Ось раскладки. По умолчанию вертикальная — так лежат списки и строки. */\n axis?: 'vertical' | 'horizontal'\n /** Смещение первого элемента. */\n start?: number\n /** Размер поперёк оси. */\n cross?: number\n}\n\n/** Элементы подряд, встык, без зазоров: строки списка, ячейки шапки, деления шкалы. */\nexport function stackRects(targets: Iterable<Element>, options: StackRectsOptions): void {\n const { size, axis = 'vertical', start = 0, cross = 100 } = options\n\n let offset = start\n\n for (const target of targets) {\n mockRect(target, axis === 'vertical'\n ? { top: offset, height: size, left: 0, width: cross }\n : { left: offset, width: size, top: 0, height: cross })\n\n offset += size\n }\n}\n","import { resetAnnouncer } from '../composables/useAnnouncer'\nimport { resetPortalRoot } from '../composables/internal/portalRoot'\nimport { useToast } from '../composables/useToast'\n\n/**\n * Уборка между тестами.\n *\n * `document.body.innerHTML = ''` сам по себе оставляет пакет в разобранном\n * состоянии: корень портала и хост живого региона закэшированы модулями, и\n * после очистки `body` они указывают на узлы **вне документа**. Следующий\n * монтаж телепортирует туда оверлеи, тест ищет их в документе и не находит —\n * без единой ошибки в консоли. Очередь тостов переживает размонтирование по\n * замыслу, поэтому её тоже гасим.\n */\nexport function resetGranularityDom(): void {\n useToast().clear()\n resetAnnouncer()\n resetPortalRoot()\n\n if (typeof document !== 'undefined')\n document.body.innerHTML = ''\n}\n\nexport interface StubMatchMediaOptions {\n /** Ответ на `(prefers-reduced-motion: reduce)`. */\n reducedMotion?: boolean\n /** Ответ на остальные запросы. */\n matches?: boolean\n}\n\n/**\n * Ответ `matchMedia` — например «пользователь просил меньше движения».\n *\n * Не для того, чтобы «не падало»: сам пакет зовёт `matchMedia` опционально и без\n * него живёт. Нужно, когда проверяется поведение **под** медиазапросом.\n * Возвращает функцию отката: глобальное состояние обязано уйти вместе с тестом.\n */\nexport function stubMatchMedia(options: StubMatchMediaOptions = {}): () => void {\n const { reducedMotion = false, matches = false } = options\n const original = window.matchMedia\n\n window.matchMedia = ((query: string) => ({\n media: query,\n matches: query.includes('prefers-reduced-motion') ? reducedMotion : matches,\n onchange: null,\n addEventListener: () => {},\n removeEventListener: () => {},\n addListener: () => {},\n removeListener: () => {},\n dispatchEvent: () => false,\n }))\n\n return () => {\n window.matchMedia = original\n }\n}\n","import { computed, ref, type Ref } from 'vue'\n\nimport { GR_CONFIG_KEY, type GrComponentDefaults, type GrComponentSize, type GrConfigContext } from '../components/GrConfigProvider/context'\nimport { GRANULARITY_I18N_KEY, type GranularityI18nAdapter, type GranularityI18nParams } from '../i18n/adapter'\n\nexport interface I18nAdapterOptions {\n /** Перевод по ключу. По умолчанию — сам ключ: видно, что спросили. */\n t?: (key: string, params?: GranularityI18nParams) => string\n te?: (key: string) => boolean\n locale?: string | Ref<string>\n}\n\n/** Адаптер перевода для теста: словарь или своя функция. */\nexport function i18nAdapter(options: I18nAdapterOptions | Record<string, string> = {}): GranularityI18nAdapter {\n const isDictionary = typeof (options as I18nAdapterOptions).t !== 'function'\n && !('locale' in options)\n && !('te' in options)\n\n if (isDictionary) {\n const dictionary = options as Record<string, string>\n\n return {\n t: key => dictionary[key] ?? key,\n te: key => key in dictionary,\n }\n }\n\n const { t, te, locale } = options as I18nAdapterOptions\n\n return {\n t: t ?? (key => key),\n te,\n locale: typeof locale === 'string' ? ref(locale) : locale,\n }\n}\n\nexport interface GranularityGlobalOptions {\n size?: GrComponentSize\n componentDefaults?: GrComponentDefaults\n theme?: string\n portalTarget?: string | HTMLElement\n /** Адаптер перевода: готовый, словарь `{ ключ: перевод }` или опции `i18nAdapter`. */\n i18n?: GranularityI18nAdapter | I18nAdapterOptions | Record<string, string>\n}\n\n/**\n * Окружение пакета для монтирования — то, что `<GrConfigProvider>` раздаёт\n * через `provide`.\n *\n * Отдаётся объектом, а не компонентом-обёрткой, намеренно: провайдер тянет за\n * собой лишний корневой элемент, а искать компонент внутри чужой разметки в\n * каждом тесте дороже, чем передать `global`. Проверять **сам** провайдер\n * (`data-gr-config-provider`, тема на корне) этим нельзя и не нужно — для этого\n * его и монтируют.\n *\n * ```ts\n * mount(GrSelect, { props, global: granularityGlobal({ size: 'sm' }) })\n * ```\n */\nexport function granularityGlobal(options: GranularityGlobalOptions = {}): { provide: Record<symbol, unknown> } {\n const config: GrConfigContext = {\n size: computed(() => options.size),\n componentDefaults: computed(() => options.componentDefaults ?? {}),\n theme: computed(() => options.theme),\n portalTarget: computed(() => options.portalTarget),\n }\n\n const provide: Record<symbol, unknown> = { [GR_CONFIG_KEY]: config }\n\n if (options.i18n)\n provide[GRANULARITY_I18N_KEY] = 't' in options.i18n && typeof options.i18n.t === 'function'\n ? options.i18n\n : i18nAdapter(options.i18n)\n\n return { provide }\n}\n","import type { GrAnnouncerPoliteness } from '../composables/useAnnouncer'\n\n/**\n * Текст живого региона.\n *\n * Ждать здесь обязательно: `announce` пишет в регион **отложенным** макротаском,\n * и это не деталь реализации, а условие работы. Регион, в котором текст\n * появился в том же кадре, что и сам регион, часть AT не объявляет вовсе;\n * поэтому синхронное чтение сразу после действия покажет пустую строку.\n */\nexport async function announced(politeness: GrAnnouncerPoliteness = 'polite'): Promise<string> {\n await new Promise(resolve => setTimeout(resolve, 2))\n\n return document.querySelector(`[data-gr-announcer-region=\"${politeness}\"]`)?.textContent ?? ''\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/theme/apply.ts
|
|
2
|
+
/**
|
|
3
|
+
* Подключение темы в рантайме — `@feugene/granularity/theme/apply`.
|
|
4
|
+
*
|
|
5
|
+
* Отдельный подпуть от `@feugene/granularity/theme` не по вкусу, а по весу:
|
|
6
|
+
* там справочник токенов (сотня килобайт данных), нужный, чтобы тему
|
|
7
|
+
* **собрать**. Чтобы её **подключить**, не нужно ничего, кроме готовой строки,
|
|
8
|
+
* — и приложению незачем платить за первое, если оно делает второе.
|
|
9
|
+
*
|
|
10
|
+
* Штатный путь всё же другой: собрать тему на сборке и отдать браузеру обычный
|
|
11
|
+
* CSS-файл. Этот модуль — для случаев, когда тема появляется только в браузере:
|
|
12
|
+
* редактор тем, превью, тема из пользовательских настроек.
|
|
13
|
+
*/
|
|
14
|
+
var ATTRIBUTE = "data-gr-theme";
|
|
15
|
+
/** Подключает CSS темы и возвращает функцию, которая его снимает. */
|
|
16
|
+
function applyTheme(css, options = {}) {
|
|
17
|
+
const { name = "custom", target } = options;
|
|
18
|
+
if (typeof document === "undefined") return () => {};
|
|
19
|
+
const host = target ?? document.head;
|
|
20
|
+
const existing = host.querySelector(`style[${ATTRIBUTE}="${name}"]`);
|
|
21
|
+
const style = existing ?? document.createElement("style");
|
|
22
|
+
style.setAttribute(ATTRIBUTE, name);
|
|
23
|
+
style.textContent = css;
|
|
24
|
+
if (!existing) host.append(style);
|
|
25
|
+
return () => style.remove();
|
|
26
|
+
}
|
|
27
|
+
/** Снимает тему, подключённую `applyTheme`. */
|
|
28
|
+
function removeTheme(name = "custom", target) {
|
|
29
|
+
if (typeof document === "undefined") return;
|
|
30
|
+
(target ?? document.head).querySelector(`style[${ATTRIBUTE}="${name}"]`)?.remove();
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { applyTheme, removeTheme };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-apply.js","names":[],"sources":["../src/theme/apply.ts"],"sourcesContent":["/**\n * Подключение темы в рантайме — `@feugene/granularity/theme/apply`.\n *\n * Отдельный подпуть от `@feugene/granularity/theme` не по вкусу, а по весу:\n * там справочник токенов (сотня килобайт данных), нужный, чтобы тему\n * **собрать**. Чтобы её **подключить**, не нужно ничего, кроме готовой строки,\n * — и приложению незачем платить за первое, если оно делает второе.\n *\n * Штатный путь всё же другой: собрать тему на сборке и отдать браузеру обычный\n * CSS-файл. Этот модуль — для случаев, когда тема появляется только в браузере:\n * редактор тем, превью, тема из пользовательских настроек.\n */\n\nconst ATTRIBUTE = 'data-gr-theme'\n\nexport interface ApplyThemeOptions {\n /**\n * Имя, под которым тема живёт в документе. Повторный вызов с тем же именем\n * заменяет её содержимое, а не добавляет второй `<style>`: иначе редактор\n * темы за минуту работы оставил бы в `<head>` сотню слоёв, и побеждал бы\n * последний — то есть результат зависел бы от истории правок.\n */\n name?: string\n /** Куда вставлять. По умолчанию `document.head`. */\n target?: HTMLElement | null\n}\n\n/** Подключает CSS темы и возвращает функцию, которая его снимает. */\nexport function applyTheme(css: string, options: ApplyThemeOptions = {}): () => void {\n const { name = 'custom', target } = options\n\n if (typeof document === 'undefined')\n return () => {}\n\n const host = target ?? document.head\n const existing = host.querySelector<HTMLStyleElement>(`style[${ATTRIBUTE}=\"${name}\"]`)\n const style = existing ?? document.createElement('style')\n\n style.setAttribute(ATTRIBUTE, name)\n style.textContent = css\n\n if (!existing) host.append(style)\n\n return () => style.remove()\n}\n\n/** Снимает тему, подключённую `applyTheme`. */\nexport function removeTheme(name = 'custom', target?: HTMLElement | null): void {\n if (typeof document === 'undefined') return\n\n const host = target ?? document.head\n host.querySelector(`style[${ATTRIBUTE}=\"${name}\"]`)?.remove()\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,IAAM,YAAY;;AAelB,SAAgB,WAAW,KAAa,UAA6B,CAAC,GAAe;CACnF,MAAM,EAAE,OAAO,UAAU,WAAW;CAEpC,IAAI,OAAO,aAAa,aACtB,aAAa,CAAC;CAEhB,MAAM,OAAO,UAAU,SAAS;CAChC,MAAM,WAAW,KAAK,cAAgC,SAAS,UAAU,IAAI,KAAK,GAAG;CACrF,MAAM,QAAQ,YAAY,SAAS,cAAc,OAAO;CAExD,MAAM,aAAa,WAAW,IAAI;CAClC,MAAM,cAAc;CAEpB,IAAI,CAAC,UAAU,KAAK,OAAO,KAAK;CAEhC,aAAa,MAAM,OAAO;AAC5B;;AAGA,SAAgB,YAAY,OAAO,UAAU,QAAmC;CAC9E,IAAI,OAAO,aAAa,aAAa;CAGrC,CADa,UAAU,SAAS,KAAA,CAC3B,cAAc,SAAS,UAAU,IAAI,KAAK,GAAG,CAAC,EAAE,OAAO;AAC9D"}
|