@feugene/granularity 0.31.0 → 0.32.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/README.md +0 -1
- package/dist/chunks/adapter-CAOxmn2G.js.map +1 -1
- package/dist/chunks/{config-BHcS5Cwm.js → config-87fszqnD.js} +1 -1
- package/dist/chunks/{config-BHcS5Cwm.js.map → config-87fszqnD.js.map} +1 -1
- package/dist/chunks/config-A-bBiotF.js.map +1 -1
- package/dist/chunks/config-B3J0alyY.js.map +1 -1
- package/dist/chunks/config-BhfNgyWL.js.map +1 -1
- package/dist/chunks/config-Bjn30smH.js.map +1 -1
- package/dist/chunks/config-BqT6PMzO.js.map +1 -1
- package/dist/chunks/config-C0Ea9ePP.js.map +1 -1
- package/dist/chunks/config-C0Eu8jpG.js.map +1 -1
- package/dist/chunks/config-CDat1wXf.js.map +1 -1
- package/dist/chunks/config-CQLNjc9X2.js.map +1 -1
- package/dist/chunks/config-CbEbLyDE.js.map +1 -1
- package/dist/chunks/{config-jyr6AIZt2.js → config-CuQmUkMw2.js} +2 -2
- package/dist/chunks/{config-jyr6AIZt2.js.map → config-CuQmUkMw2.js.map} +1 -1
- package/dist/chunks/config-DeyT9dJl.js.map +1 -1
- package/dist/chunks/config-DnF472Ox.js.map +1 -1
- package/dist/chunks/config-DwABJHEo.js.map +1 -1
- package/dist/chunks/{config-tiUtEn4Q.js → config-E1cTyRVV.js} +1 -1
- package/dist/chunks/{config-tiUtEn4Q.js.map → config-E1cTyRVV.js.map} +1 -1
- package/dist/chunks/config-MmPym-Z72.js.map +1 -1
- package/dist/chunks/config-T3-DMXYm.js.map +1 -1
- package/dist/chunks/config-Yy67uWHY.js.map +1 -1
- package/dist/chunks/config-_jbgsFpw.js.map +1 -1
- package/dist/chunks/config-g8-JGZ_Y.js.map +1 -1
- package/dist/chunks/config-mlheIMtV.js.map +1 -1
- package/dist/chunks/config-rLkc9Zn2.js.map +1 -1
- package/dist/chunks/config-uzUuosv7.js.map +1 -1
- package/dist/chunks/context-CTbuwcYs.js.map +1 -1
- package/dist/chunks/deltaTone-B5OoS1vo.js.map +1 -1
- package/dist/chunks/es-Do2CNrC2.js.map +1 -1
- package/dist/chunks/fileValidation-D5TFBhbv.js.map +1 -1
- package/dist/chunks/focusables-BkzK42SQ.js.map +1 -1
- package/dist/chunks/{generated-DJ2dJ__C.js → generated-CSyPsWtM.js} +1 -1
- package/dist/chunks/{generated-DJ2dJ__C.js.map → generated-CSyPsWtM.js.map} +1 -1
- package/dist/chunks/grBadgeStyles-D8Mz73B5.js.map +1 -1
- package/dist/chunks/{grButtonStyles-B_-YRE__.js → grButtonStyles-B3wRWW_i.js} +4 -4
- package/dist/chunks/grButtonStyles-B3wRWW_i.js.map +1 -0
- package/dist/chunks/grLinkStyles-FME_qMpi.js.map +1 -1
- package/dist/chunks/grLoadingStyles-CnQaf5ry.js.map +1 -1
- package/dist/chunks/{grRadioStyles-D4gnU65n.js → grRadioStyles-DGL2QLy6.js} +1 -1
- package/dist/chunks/{grRadioStyles-D4gnU65n.js.map → grRadioStyles-DGL2QLy6.js.map} +1 -1
- package/dist/chunks/grSelectStyles-Cdtd10eY.js.map +1 -1
- package/dist/chunks/{grSliderStyles-B5uNxNJq.js → grSliderStyles-kaz6VXZ3.js} +9 -1
- package/dist/chunks/grSliderStyles-kaz6VXZ3.js.map +1 -0
- package/dist/chunks/{granular-provider-yJNX1g1G.js → granular-provider-Da2CJO6j.js} +3 -3
- package/dist/chunks/{granular-provider-yJNX1g1G.js.map → granular-provider-Da2CJO6j.js.map} +1 -1
- package/dist/chunks/granularityI18n-BC5zsj_d.js.map +1 -1
- package/dist/chunks/icon-Dag2tIMs.js.map +1 -1
- package/dist/chunks/inert-DIHCCn_1.js.map +1 -1
- package/dist/chunks/keyboard-DE2yhz_b.js.map +1 -1
- package/dist/chunks/numericStep-Bq2lj4ew.js.map +1 -1
- package/dist/chunks/optionFilter-DYw9XPPx.js.map +1 -1
- package/dist/chunks/{overlayStack-C8cfsqXa.js → overlayStack-0CZS99Da.js} +40 -6
- package/dist/chunks/overlayStack-0CZS99Da.js.map +1 -0
- package/dist/chunks/portalRoot-CVhCgdG6.js.map +1 -1
- package/dist/chunks/renamedProp-BwcN-SJg.js.map +1 -1
- package/dist/chunks/{safelist-BOq5Li7p.js → safelist-DxkMj5IW.js} +1 -1
- package/dist/chunks/{safelist-BOq5Li7p.js.map → safelist-DxkMj5IW.js.map} +1 -1
- package/dist/chunks/slotNodes-DxNXpC7v.js.map +1 -1
- package/dist/chunks/useControlAddons-asUr10Pf.js.map +1 -1
- package/dist/chunks/useControlledOpen-DV3SqLTP.js.map +1 -1
- package/dist/chunks/useDragSort-0l47h4yb.js.map +1 -1
- package/dist/chunks/useFilePreviews-DnwgdyBs.js.map +1 -1
- package/dist/chunks/useFocusWithin-BwHE0rzm.js.map +1 -1
- package/dist/chunks/{useModalOverlay-Cv5hF63W.js → useModalOverlay-D__CqLGn.js} +3 -0
- package/dist/chunks/useModalOverlay-D__CqLGn.js.map +1 -0
- package/dist/chunks/usePortalTarget-C0p-mhaM.js.map +1 -1
- package/dist/chunks/validation-5HhpanPP.js.map +1 -1
- package/dist/components/GrAlert/chunks/GrAlert-FAVf-ksH.js.map +1 -1
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-X6RlmiJQ.js → GrAutocomplete-rYEY15UI.js} +1 -1
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-rYEY15UI.js.map +1 -0
- package/dist/components/GrAutocomplete/index.js +1 -1
- package/dist/components/GrAutocomplete/styles.css +1 -1
- package/dist/components/GrAvatar/chunks/GrAvatar-CNmLmgcr.js.map +1 -1
- package/dist/components/GrBadge/chunks/{GrBadge-BsoocpF-.js → GrBadge-D2MfThPz.js} +1 -1
- package/dist/components/GrBadge/chunks/GrBadge-D2MfThPz.js.map +1 -0
- package/dist/components/GrBadge/index.js +1 -1
- package/dist/components/GrBadge/styles.css +1 -1
- package/dist/components/GrBadgeWrap/chunks/GrBadgeWrap-B-A3PRkE.js.map +1 -1
- package/dist/components/GrBottomNav/chunks/GrBottomNav-wtFatLjB.js.map +1 -1
- package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-DRDNan_n.js.map +1 -1
- package/dist/components/GrButton/chunks/{GrButton-Bw2AH4uX.js → GrButton-7GP0zObj.js} +1 -1
- package/dist/components/GrButton/chunks/GrButton-7GP0zObj.js.map +1 -0
- package/dist/components/GrButton/index.js +3 -3
- package/dist/components/GrButtonGroup/chunks/GrButtonGroup-CKyiAV8K.js.map +1 -1
- package/dist/components/GrCard/chunks/GrCard-BttXz1pQ.js.map +1 -1
- package/dist/components/GrCheckbox/chunks/GrCheckbox-CEcHIsXM.js.map +1 -1
- package/dist/components/GrCheckboxGroup/chunks/GrCheckboxGroup-BOwrVcFr.js.map +1 -1
- package/dist/components/GrChip/chunks/GrChip-C0vU69tf.js.map +1 -1
- package/dist/components/GrChipGroup/chunks/GrChipGroup-C78Vcwos.js.map +1 -1
- package/dist/components/GrCodeBlock/chunks/{GrCodeBlock-Dkvkr91t.js → GrCodeBlock-Dp0eEBTZ.js} +1 -1
- package/dist/components/GrCodeBlock/chunks/GrCodeBlock-Dp0eEBTZ.js.map +1 -0
- package/dist/components/GrCodeBlock/index.js +1 -1
- package/dist/components/GrCollapse/chunks/GrCollapse-BlW6ybdL.js.map +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-C7ifyQ0R.js → GrColorPicker-p2XH_8cE.js} +2 -2
- package/dist/components/GrColorPicker/chunks/GrColorPicker-p2XH_8cE.js.map +1 -0
- package/dist/components/GrColorPicker/index.js +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-C5abL2Y7.js → GrCommandPalette-BXbh3q7O.js} +2 -2
- package/dist/components/GrCommandPalette/chunks/GrCommandPalette-BXbh3q7O.js.map +1 -0
- package/dist/components/GrCommandPalette/index.js +1 -1
- package/dist/components/GrCommandPalette/styles.css +1 -1
- package/dist/components/GrConfigProvider/chunks/GrConfigProvider-BF7R3hgg.js.map +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-B9mIU78E.js → GrConfirmDialog-CUP43q__.js} +3 -3
- package/dist/components/GrConfirmDialog/chunks/GrConfirmDialog-CUP43q__.js.map +1 -0
- package/dist/components/GrConfirmDialog/index.js +1 -1
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-_FKwQvuQ.js → GrContextMenu-ClkVjdcP.js} +1 -1
- package/dist/components/GrContextMenu/chunks/GrContextMenu-ClkVjdcP.js.map +1 -0
- package/dist/components/GrContextMenu/index.js +1 -1
- package/dist/components/GrDataTable/chunks/{GrDataTable-C8a9_nCg.js → GrDataTable-CAGgRk27.js} +5 -3
- package/dist/components/GrDataTable/chunks/GrDataTable-CAGgRk27.js.map +1 -0
- package/dist/components/GrDataTable/index.js +1 -1
- package/dist/components/GrDelta/chunks/GrDelta-BZwkgQW-.js.map +1 -1
- package/dist/components/GrDescriptionList/chunks/GrDescriptionList-BMeCgFV3.js.map +1 -1
- package/dist/components/GrDialog/chunks/{GrDialog-bOUDYfmh.js → GrDialog-BiMJ9_T4.js} +2 -2
- package/dist/components/GrDialog/chunks/{GrDialog-bOUDYfmh.js.map → GrDialog-BiMJ9_T4.js.map} +1 -1
- package/dist/components/GrDialog/index.js +1 -1
- package/dist/components/GrDialogService/chunks/{GrDialogService-6s2cQ-_6.js → GrDialogService-BxNK6crD.js} +4 -4
- package/dist/components/GrDialogService/chunks/GrDialogService-BxNK6crD.js.map +1 -0
- package/dist/components/GrDialogService/index.js +1 -1
- package/dist/components/GrDivider/chunks/GrDivider-BXavGcMX.js.map +1 -1
- package/dist/components/GrDrawer/chunks/{GrDrawer-B7PiAcbU.js → GrDrawer-Ce2l3zaG.js} +6 -5
- package/dist/components/GrDrawer/chunks/GrDrawer-Ce2l3zaG.js.map +1 -0
- package/dist/components/GrDrawer/index.js +1 -1
- package/dist/components/GrDropdown/chunks/GrDropdown-13Wt9eB_.js.map +1 -1
- package/dist/components/GrDropdownMenu/chunks/GrDropdownMenu-GOzuAXsi.js.map +1 -1
- package/dist/components/GrDropdownMenu/chunks/GrDropdownMenuList-B33ZzwfB.js.map +1 -1
- package/dist/components/GrFilePreview/chunks/GrFilePreview-C9Z7hMK8.js.map +1 -1
- package/dist/components/GrFileUpload/chunks/GrFileUpload-NsHkRRIa.js.map +1 -1
- package/dist/components/GrForm/chunks/GrForm-CrnfCsgj.js.map +1 -1
- package/dist/components/GrFormField/chunks/GrFormField-DSylU7IU.js.map +1 -1
- package/dist/components/GrFormFile/chunks/{GrFormFile-CYtXU9HV.js → GrFormFile-B1jGZyIj.js} +2 -2
- package/dist/components/GrFormFile/chunks/GrFormFile-B1jGZyIj.js.map +1 -0
- package/dist/components/GrFormFile/index.js +1 -1
- package/dist/components/GrFormFile/styles.css +1 -1
- package/dist/components/GrIcon/chunks/GrIcon-Dmf0c9ba.js.map +1 -1
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-CyVN6Zpc.js → GrImageViewer-CG4vUBg_.js} +4 -3
- package/dist/components/GrImageViewer/chunks/GrImageViewer-CG4vUBg_.js.map +1 -0
- package/dist/components/GrImageViewer/index.js +1 -1
- package/dist/components/GrInput/chunks/GrInput-BLywRZUN.js.map +1 -1
- package/dist/components/GrInputTag/chunks/GrInputTag-Cj4A9Xgs.js.map +1 -1
- package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-Cy2bknHO.js → GrJsonViewer-BCZ7lE2k.js} +2 -2
- package/dist/components/GrJsonViewer/chunks/GrJsonViewer-BCZ7lE2k.js.map +1 -0
- package/dist/components/GrJsonViewer/index.js +1 -1
- package/dist/components/GrKbd/chunks/GrKbd-D2ga1H9i.js.map +1 -1
- package/dist/components/GrLink/chunks/GrLink-DYXw3W9w.js.map +1 -1
- package/dist/components/GrList/chunks/{GrList-DmT4qzLO.js → GrList-B_CRJB4c.js} +1 -1
- package/dist/components/GrList/chunks/GrList-B_CRJB4c.js.map +1 -0
- package/dist/components/GrList/index.js +1 -1
- package/dist/components/GrList/styles.css +1 -1
- package/dist/components/GrModal/chunks/{GrModal-CCoWZuIl.js → GrModal-D0mf1SAx.js} +3 -2
- package/dist/components/GrModal/chunks/GrModal-D0mf1SAx.js.map +1 -0
- package/dist/components/GrModal/index.js +1 -1
- package/dist/components/GrNavbar/chunks/{GrNavbar-C5EjMKaC.js → GrNavbar-_rtHKwTf.js} +1 -1
- package/dist/components/GrNavbar/chunks/{GrNavbar-C5EjMKaC.js.map → GrNavbar-_rtHKwTf.js.map} +1 -1
- package/dist/components/GrNavbar/index.js +1 -1
- package/dist/components/GrNumberInput/chunks/GrNumberInput-DQ59YICu.js.map +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-Cw3R21dp.js → GrPagination-B_kAhoAV.js} +2 -2
- package/dist/components/GrPagination/chunks/{GrPagination-Cw3R21dp.js.map → GrPagination-B_kAhoAV.js.map} +1 -1
- package/dist/components/GrPagination/index.js +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-CofG-Tfe.js → GrPopover-faJanmGs.js} +1 -1
- package/dist/components/GrPopover/chunks/GrPopover-faJanmGs.js.map +1 -0
- package/dist/components/GrPopover/index.js +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-DLcl1xiq.js → GrPromptDialog-C6QGC7QJ.js} +4 -4
- package/dist/components/GrPromptDialog/chunks/GrPromptDialog-C6QGC7QJ.js.map +1 -0
- package/dist/components/GrPromptDialog/index.js +1 -1
- package/dist/components/GrRadio/chunks/{GrRadio-C_rESc5g.js → GrRadio-Aq3v8XgG.js} +1 -1
- package/dist/components/GrRadio/chunks/GrRadio-Aq3v8XgG.js.map +1 -0
- package/dist/components/GrRadio/index.js +2 -2
- package/dist/components/GrRadioGroup/chunks/{GrRadioGroup-CdQO4G-U.js → GrRadioGroup-y9LVdTWh.js} +1 -1
- package/dist/components/GrRadioGroup/chunks/GrRadioGroup-y9LVdTWh.js.map +1 -0
- package/dist/components/GrRadioGroup/index.js +1 -1
- package/dist/components/GrRating/chunks/GrRating-IEiTOdnF.js.map +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-C_skObtv.js → GrResponseErrorBanner-CBVRpgIG.js} +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-C_skObtv.js.map → GrResponseErrorBanner-CBVRpgIG.js.map} +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-DudGc9LO.js → GrResponseErrorBanner-DMeOWlzj.js} +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/GrResponseErrorBanner-DMeOWlzj.js.map +1 -0
- package/dist/components/GrResponseErrorBanner/index.js +2 -2
- package/dist/components/GrSegmented/chunks/GrSegmented-CB-VR-yL.js.map +1 -1
- package/dist/components/GrSelect/chunks/{GrSelect-BiUrSSxc.js → GrSelect-gCeRFgQo.js} +1 -1
- package/dist/components/GrSelect/chunks/GrSelect-gCeRFgQo.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-B8EQIlfW.js → GrSidebar-alkwBUzL.js} +1 -1
- package/dist/components/GrSidebar/chunks/GrSidebar-alkwBUzL.js.map +1 -0
- package/dist/components/GrSidebar/index.js +1 -1
- package/dist/components/GrSkeleton/chunks/GrSkeleton-CgfXxPQ2.js.map +1 -1
- package/dist/components/GrSlider/chunks/{GrSlider-DJfeEl4h.js → GrSlider-DTTrZTHK.js} +3 -3
- package/dist/components/GrSlider/chunks/GrSlider-DTTrZTHK.js.map +1 -0
- package/dist/components/GrSlider/index.js +2 -2
- package/dist/components/GrSortableList/chunks/GrSortableList-HyKX--rO.js.map +1 -1
- package/dist/components/GrSplitter/chunks/GrSplitter-MltjaADn.js.map +1 -1
- package/dist/components/GrStatistic/chunks/GrStatistic-qHVasK37.js.map +1 -1
- package/dist/components/GrSteps/chunks/GrSteps-C2jxpZoN.js.map +1 -1
- package/dist/components/GrSwitch/chunks/{GrSwitch-g_OOSkh7.js → GrSwitch-7Oe5QfQZ.js} +1 -1
- package/dist/components/GrSwitch/chunks/GrSwitch-7Oe5QfQZ.js.map +1 -0
- package/dist/components/GrSwitch/index.js +1 -1
- package/dist/components/GrTabPanels/chunks/GrTabPanels-BZQS8-JM.js.map +1 -1
- package/dist/components/GrTable/chunks/GrTable-Zcdz2St_.js.map +1 -1
- package/dist/components/GrTextarea/chunks/{GrTextarea-KREK75Bu.js → GrTextarea-DinXG12-.js} +10 -10
- package/dist/components/GrTextarea/chunks/GrTextarea-DinXG12-.js.map +1 -0
- package/dist/components/GrTextarea/index.js +1 -1
- package/dist/components/GrTimeline/chunks/GrTimeline-BiHpArsg.js.map +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-Dp7-4B8k.js → GrToaster-COXIBKzq.js} +2 -2
- package/dist/components/GrToaster/chunks/GrToaster-COXIBKzq.js.map +1 -0
- package/dist/components/GrToaster/index.js +1 -1
- package/dist/components/GrToaster/styles.css +1 -1
- package/dist/components/GrTooltip/chunks/GrTooltip-uYETdbCD.js.map +1 -1
- package/dist/components/GrTree/chunks/{GrTree-DC89I4AP.js → GrTree-Ci6CZfKR.js} +5 -5
- package/dist/components/GrTree/chunks/GrTree-Ci6CZfKR.js.map +1 -0
- package/dist/components/GrTree/index.js +1 -1
- package/dist/components/GrTree/styles.css +1 -1
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-iJnMl2t9.js → GrTreeSelect-BHwgGJcT.js} +1 -1
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect-BHwgGJcT.js.map +1 -0
- package/dist/components/GrTreeSelect/index.js +1 -1
- package/dist/composables/useAnnouncer.js.map +1 -1
- package/dist/composables/useComboboxNavigation.js.map +1 -1
- package/dist/composables/useDragGesture.js.map +1 -1
- package/dist/composables/useFloating.js +2 -22
- package/dist/composables/useFloating.js.map +1 -1
- package/dist/composables/useFocusTrap.js.map +1 -1
- package/dist/composables/useOverlayLayer.js +10 -3
- package/dist/composables/useOverlayLayer.js.map +1 -1
- package/dist/composables/useRovingFocus.js.map +1 -1
- package/dist/composables/useTheme.js.map +1 -1
- package/dist/composables/useToast.js.map +1 -1
- package/dist/composables/useVirtualList.js.map +1 -1
- package/dist/directives/autofocus.js.map +1 -1
- package/dist/directives/autosize.js.map +1 -1
- package/dist/directives/clickOutside.js.map +1 -1
- package/dist/directives/dropzone.js.map +1 -1
- package/dist/directives/hotkey.js.map +1 -1
- package/dist/directives/loading.js.map +1 -1
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +37 -37
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/dist/theme-apply.js.map +1 -1
- package/dist/theme.js +1 -1
- package/dist/theme.js.map +1 -1
- package/dist/tokens.js +1 -1
- package/dist/types/components/GrCodeBlock/tokenizeJson.d.ts +1 -3
- package/dist/types/components/GrImageViewer/GrImageViewer.vue.d.ts +1 -1
- package/dist/types/components/GrInput/GrInput.vue.d.ts +24 -24
- package/dist/types/components/GrSlider/grSliderStyles.d.ts +9 -1
- package/dist/types/components/GrTabPanels/GrTabPanel.vue.d.ts +3 -3
- package/dist/types/composables/internal/overlayStack.d.ts +30 -0
- package/dist/types/composables/internal/useModalOverlay.d.ts +14 -0
- package/dist/types/composables/useOverlayLayer.d.ts +8 -0
- package/dist/vue/index.js.map +1 -1
- package/dist/web-types.json +1 -1
- package/package.json +3 -3
- package/dist/chunks/grButtonStyles-B_-YRE__.js.map +0 -1
- package/dist/chunks/grSliderStyles-B5uNxNJq.js.map +0 -1
- package/dist/chunks/overlayStack-C8cfsqXa.js.map +0 -1
- package/dist/chunks/useModalOverlay-Cv5hF63W.js.map +0 -1
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-X6RlmiJQ.js.map +0 -1
- package/dist/components/GrBadge/chunks/GrBadge-BsoocpF-.js.map +0 -1
- package/dist/components/GrButton/chunks/GrButton-Bw2AH4uX.js.map +0 -1
- package/dist/components/GrCodeBlock/chunks/GrCodeBlock-Dkvkr91t.js.map +0 -1
- package/dist/components/GrColorPicker/chunks/GrColorPicker-C7ifyQ0R.js.map +0 -1
- package/dist/components/GrCommandPalette/chunks/GrCommandPalette-C5abL2Y7.js.map +0 -1
- package/dist/components/GrConfirmDialog/chunks/GrConfirmDialog-B9mIU78E.js.map +0 -1
- package/dist/components/GrContextMenu/chunks/GrContextMenu-_FKwQvuQ.js.map +0 -1
- package/dist/components/GrDataTable/chunks/GrDataTable-C8a9_nCg.js.map +0 -1
- package/dist/components/GrDialogService/chunks/GrDialogService-6s2cQ-_6.js.map +0 -1
- package/dist/components/GrDrawer/chunks/GrDrawer-B7PiAcbU.js.map +0 -1
- package/dist/components/GrFormFile/chunks/GrFormFile-CYtXU9HV.js.map +0 -1
- package/dist/components/GrImageViewer/chunks/GrImageViewer-CyVN6Zpc.js.map +0 -1
- package/dist/components/GrJsonViewer/chunks/GrJsonViewer-Cy2bknHO.js.map +0 -1
- package/dist/components/GrList/chunks/GrList-DmT4qzLO.js.map +0 -1
- package/dist/components/GrModal/chunks/GrModal-CCoWZuIl.js.map +0 -1
- package/dist/components/GrPopover/chunks/GrPopover-CofG-Tfe.js.map +0 -1
- package/dist/components/GrPromptDialog/chunks/GrPromptDialog-DLcl1xiq.js.map +0 -1
- package/dist/components/GrRadio/chunks/GrRadio-C_rESc5g.js.map +0 -1
- package/dist/components/GrRadioGroup/chunks/GrRadioGroup-CdQO4G-U.js.map +0 -1
- package/dist/components/GrResponseErrorBanner/chunks/GrResponseErrorBanner-DudGc9LO.js.map +0 -1
- package/dist/components/GrSelect/chunks/GrSelect-BiUrSSxc.js.map +0 -1
- package/dist/components/GrSidebar/chunks/GrSidebar-B8EQIlfW.js.map +0 -1
- package/dist/components/GrSlider/chunks/GrSlider-DJfeEl4h.js.map +0 -1
- package/dist/components/GrSwitch/chunks/GrSwitch-g_OOSkh7.js.map +0 -1
- package/dist/components/GrTextarea/chunks/GrTextarea-KREK75Bu.js.map +0 -1
- package/dist/components/GrToaster/chunks/GrToaster-Dp7-4B8k.js.map +0 -1
- package/dist/components/GrTree/chunks/GrTree-DC89I4AP.js.map +0 -1
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect-iJnMl2t9.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GrTreeSelect-BHwgGJcT.js","names":["$slots"],"sources":["../../../../src/components/GrTreeSelect/GrTreeSelect.vue","../../../../src/components/GrTreeSelect/GrTreeSelect.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"T extends Record<string, any> = any\">\nimport { computed, nextTick, ref, useId, useSlots, watch } from 'vue'\n\nimport { usePortalTarget } from '../../composables/usePortalTarget'\n\nimport { vClickOutside } from '../../directives'\nimport { useFloating } from '../../composables/useFloating'\nimport { useOverlayLayer } from '../../composables/useOverlayLayer'\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useGrComponentSize, useGrThemeAttrs } from '../GrConfigProvider/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTree, {\n type GrTreeInstance,\n type GrTreeKey,\n type GrTreeNode,\n} from '../GrTree'\nimport type { GrInputSize } from '../GrInput/GrInput.vue'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport type { GrTreeSelectModelValue, GrTreeSelectProps } from './grTreeSelectTypes'\nimport { grTreeSelectClass, grTreeSelectPanelClass, grTreeSelectStateClass, paddingX, trailingZoneWidth } from './grTreeSelectStyles'\n\nimport IconCheck from '~icons/lucide/check'\nimport IconChevronDown from '~icons/lucide/chevron-down'\nimport IconLoaderCircle from '~icons/lucide/loader-circle'\nimport IconX from '~icons/lucide/x'\n\nexport interface GrTreeSelectEmits<T extends Record<string, any> = any> {\n (e: 'update:modelValue', value: GrTreeSelectModelValue): void\n (e: 'change', value: GrTreeSelectModelValue): void\n /** Панель открылась/закрылась (`v-model:open`). */\n (e: 'update:open', value: boolean): void\n (e: 'clear'): void\n (e: 'nodeClick', data: T, node: GrTreeNode<T>): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrTreeSelectProps<T>>(),\n {\n props: () => ({\n children: 'children',\n label: 'label',\n }),\n nodeKey: 'id' as any,\n defaultExpandedKeys: () => [],\n disabled: false,\n placeholder: undefined,\n size: undefined,\n loading: false,\n invalid: false,\n readonly: false,\n required: false,\n ariaLabel: undefined,\n state: 'default',\n multiple: false,\n showCheckbox: false,\n checkStrictly: false,\n clearable: false,\n open: undefined,\n name: undefined,\n valueDisplay: 'label',\n filterable: false,\n filterPlaceholder: undefined,\n filterInputmode: undefined,\n filterNodeMethod: undefined,\n closeOnSelect: undefined,\n dropdownMaxHeight: 320,\n virtual: false,\n prefixMinWidth: undefined,\n prefixMaxWidth: undefined,\n suffixMinWidth: undefined,\n suffixMaxWidth: undefined,\n prefixFixed: false,\n suffixFixed: false,\n },\n)\n\nconst { t } = useGranularityTranslations()\n\nconst emit = defineEmits<GrTreeSelectEmits<T>>()\n\ndefineSlots<{\n /** Рендер значения внутри триггера (вместо дефолтного текста). */\n value?: (props: { value: GrTreeSelectModelValue, labels: string[], displayValue: string, pathLabels?: string[] }) => any\n /** Рендер строки дерева. */\n node?: (props: { node: GrTreeNode<T>, data: T, selected: boolean }) => any\n /** Содержимое пустого состояния (когда нет данных). */\n empty?: () => any\n /** Содержимое панели, пока данные едут. */\n loading?: () => any\n /** Аддон слева от значения: иконка, код валюты, метка. */\n prefix?: () => any\n /** Аддон справа от значения, перед крестиком и шевроном. */\n suffix?: () => any\n}>()\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst triggerEl = ref<HTMLInputElement | null>(null)\nconst treeId = `gr-tree-select-tree-${useId()}`\n\n// Контекст `GrFormField` + общий контракт форм-контрола.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nfunction focus(): void {\n triggerEl.value?.focus()\n}\n\nfunction blur(): void {\n triggerEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\nconst filterInputRef = ref<InstanceType<typeof GrInput> | null>(null)\nconst treeRef = ref<GrTreeInstance | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\n\n// Панель телепортирована в `body`, то есть лежит вне корня, но для пользователя\n// она часть контрола: без неё уход фокуса в панель читался бы как `blur`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n containers: () => [panelEl.value],\n})\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nlet hadPointerDownOnTrigger = false\n\n// Имя `open` сохранено: читатели работают с computed-Ref.\nconst { open, setOpen: setOpenState } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\nconst filterValue = ref('')\n\nconst { floatingStyle } = useFloating(rootEl, panelEl, open, {\n placement: 'bottom-start',\n matchWidth: true,\n zIndexVar: '--gr-z-dropdown',\n})\n\nconst closeOnSelectResolved = computed(() => {\n return props.closeOnSelect ?? !props.multiple\n})\n\nfunction getChildrenKey() {\n return props.props.children ?? 'children'\n}\n\nfunction getLabelKey() {\n return props.props.label ?? 'label'\n}\n\nfunction nodeLabel(data: T): string {\n const key = getLabelKey() as keyof T\n const v = data[key]\n return v == null ? '' : String(v)\n}\n\nfunction nodeChildren(data: T): T[] {\n const key = getChildrenKey() as keyof T\n const v = data[key]\n return Array.isArray(v) ? (v as any) : []\n}\n\nfunction nodeKeyOf(data: T, index: number, parentKey: GrTreeKey | undefined): GrTreeKey {\n const keyName = props.nodeKey as keyof T\n const v = data[keyName]\n if (v != null)\n return v as GrTreeKey\n\n return parentKey == null ? index : `${String(parentKey)}:${index}`\n}\n\nconst labelByKey = computed(() => {\n const map = new Map<GrTreeKey, string>()\n\n const walk = (items: T[], parentKey: GrTreeKey | undefined) => {\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n const key = nodeKeyOf(item, i, parentKey)\n map.set(key, nodeLabel(item))\n walk(nodeChildren(item), key)\n }\n }\n\n walk(props.data ?? [], undefined)\n return map\n})\n\nconst pathLabelsByKey = computed(() => {\n const map = new Map<GrTreeKey, string[]>()\n\n const walk = (items: T[], parentKey: GrTreeKey | undefined, parentPath: string[]) => {\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n const key = nodeKeyOf(item, i, parentKey)\n const label = nodeLabel(item)\n const path = [...parentPath, label]\n map.set(key, path)\n walk(nodeChildren(item), key, path)\n }\n }\n\n walk(props.data ?? [], undefined, [])\n return map\n})\n\nconst selectedKeys = computed<GrTreeKey[]>(() => {\n if (props.multiple) {\n return Array.isArray(props.modelValue) ? props.modelValue : []\n }\n\n const v = props.modelValue\n if (v == null || Array.isArray(v))\n return []\n return [v]\n})\n\nconst selectedKeySet = computed(() => new Set(selectedKeys.value))\n\nconst selectedLabels = computed(() => {\n const map = labelByKey.value\n return selectedKeys.value.map(k => map.get(k) ?? String(k))\n})\n\nconst selectedPathLabels = computed<string[] | undefined>(() => {\n if (props.multiple)\n return undefined\n\n const key = selectedKeys.value[0]\n if (key == null)\n return undefined\n\n const path = pathLabelsByKey.value.get(key)\n if (path?.length)\n return path\n\n const labels = selectedLabels.value\n return labels.length ? [labels[0]] : undefined\n})\n\nconst displayValue = computed(() => {\n const labels = selectedLabels.value\n if (labels.length === 0)\n return ''\n\n if (!props.multiple) {\n if (props.valueDisplay === 'path') {\n const path = selectedPathLabels.value\n if (path?.length)\n return path.join(' / ')\n }\n return labels[0]\n }\n\n if (labels.length === 1)\n return labels[0]\n return `${labels[0]} +${labels.length - 1}`\n})\n\nconst hasSelection = computed(() => selectedKeys.value.length > 0)\n\n/** Дерево рендерится только когда есть что показать — ссылаться иначе не на что. */\nconst hasTree = computed(() => !props.loading && (props.data?.length ?? 0) > 0)\n\nconst resolvedFilterPlaceholder = computed(() => {\n return props.filterPlaceholder ?? t('gr.treeSelect.filterPlaceholder', 'Search…')\n})\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTreeSelect' })\n\nconst className = computed(() => {\n return grTreeSelectClass({\n size: resolvedSize.value,\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n })\n})\n\nconst panelClasses = computed(() => {\n return grTreeSelectPanelClass\n})\n\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputSize, string> = {\n xs: '2rem',\n sm: '2.25rem',\n md: '2.5rem',\n lg: '3rem',\n}\n\nconst slots = useSlots()\n\nconst {\n hasPrefix,\n hasSuffix,\n prefixEl,\n suffixEl,\n prefixStyle,\n suffixStyle,\n fieldPadding: triggerStyle,\n} = useControlAddons(() => props, {\n defaultMinWidth: () => ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value],\n paddingX: () => paddingX[resolvedSize.value],\n // Место под крестик и шеврон уже отдано классом `pr-9`, но инлайн-паддинг\n // перекрывает класс целиком — с суффиксом эту зону приходится вернуть числом.\n trailingReserve: () => (slots.suffix ? trailingZoneWidth : '0px'),\n})\n\n// Слот `value` рисуется поверх поля и обязан жить в тех же границах, что и\n// текст под ним. Без аддонов границы задаёт класс — стиль остаётся пустым.\nconst valueOverlayStyle = computed(() => ({\n left: hasPrefix.value ? triggerStyle.value.paddingLeft : undefined,\n right: hasSuffix.value ? triggerStyle.value.paddingRight : undefined,\n}))\n\nfunction setOpen(next: boolean) {\n if (isDisabled.value || isReadonly.value)\n return\n\n setOpenState(next)\n}\n\nfunction onClickOutside(): void {\n setOpen(false)\n}\n\nfunction openDropdown(): void {\n setOpen(true)\n}\n\n/**\n * Стек слоёв возвращает фокус на триггер сразу после закрытия, а у триггера\n * открытие висит на `focus`. Без флага `Escape` из дерева закрывал бы панель и\n * тем же движением открывал её заново.\n */\nlet suppressFocusOpen = false\n\nfunction closeDropdown(): void {\n suppressFocusOpen = true\n setOpen(false)\n setTimeout(() => {\n suppressFocusOpen = false\n }, 0)\n}\n\nfunction toggleDropdown(): void {\n setOpen(!open.value)\n}\n\n// Клик по триггеру сам переключает панель через `@click`; флаг гасит открытие\n// по приходящему следом `focus`, иначе один клик открыл бы и тут же закрыл.\nfunction onTriggerPointerDown(): void {\n hadPointerDownOnTrigger = true\n setTimeout(() => {\n hadPointerDownOnTrigger = false\n }, 0)\n}\n\nfunction onTriggerFocus(): void {\n if (hadPointerDownOnTrigger || suppressFocusOpen)\n return\n openDropdown()\n}\n\n// Возврат фокуса на триггер — из контракта слоя, а не руками через `nextTick`.\nuseOverlayLayer(open, closeDropdown, { root: panelEl })\n\nwatch(\n open,\n async (isOpen) => {\n if (!isOpen) {\n if (filterValue.value.trim().length > 0)\n filterValue.value = ''\n return\n }\n\n // Подсветка дерева идёт за значением: панель могли открыть после того, как\n // значение сменили снаружи.\n if (!props.multiple) {\n const v = props.modelValue\n if (v != null && !Array.isArray(v))\n treeRef.value?.setCurrentKey(v)\n }\n\n if (props.filterable) {\n await nextTick()\n filterInputRef.value?.focus()\n }\n },\n { immediate: true },\n)\n\nwatch(\n () => props.modelValue,\n (v) => {\n if (props.multiple)\n return\n if (v == null || Array.isArray(v)) {\n treeRef.value?.setCurrentKey(undefined)\n return\n }\n treeRef.value?.setCurrentKey(v)\n },\n)\n\nwatch(\n filterValue,\n (v) => {\n treeRef.value?.filter(v)\n },\n)\n\nfunction emitModel(next: GrTreeSelectModelValue) {\n emit('update:modelValue', next)\n emit('change', next)\n}\n\nfunction clear(): void {\n if (isDisabled.value || isReadonly.value)\n return\n\n const next: GrTreeSelectModelValue = props.multiple ? [] : null\n emitModel(next)\n emit('clear')\n if (!props.multiple)\n treeRef.value?.setCurrentKey(undefined)\n}\n\n/**\n * Отдаёт клавиатуру дереву. Панель под `v-show`, поэтому дерево уже в DOM, но\n * скрытый элемент сфокусировать нельзя — сначала открытие, потом кадр, потом\n * фокус. Дальше работают клавиши `GrTree`, а `Escape` возвращает фокус на\n * триггер силами общего стека слоёв.\n */\nasync function focusTree(): Promise<boolean> {\n if (!open.value)\n return false\n\n await nextTick()\n return treeRef.value?.focus() ?? false\n}\n\nfunction onTriggerKeydown(e: KeyboardEvent): void {\n if (isDisabled.value)\n return\n\n if (e.key === 'Escape') {\n e.preventDefault()\n closeDropdown()\n return\n }\n\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n openDropdown()\n\n // При `filterable` фокус уходит в поле поиска (см. watch на `open`), и\n // дальше в дерево ведёт стрелка уже оттуда.\n if (!props.filterable)\n void focusTree()\n }\n}\n\n/** Стрелка из поля поиска — единственный путь из фильтра в дерево. */\nfunction onFilterKeydown(e: KeyboardEvent): void {\n if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp')\n return\n\n e.preventDefault()\n void focusTree()\n}\n\n/** `Tab` уводит из панели — оставлять её открытой за спиной незачем. */\nfunction onPanelKeydown(e: KeyboardEvent): void {\n if (e.key === 'Tab')\n closeDropdown()\n}\n\n// Чекбоксы осмысленны только в множественном выборе: одиночный уже подсвечен\n// текущим узлом.\nconst checkboxMode = computed(() => props.multiple && props.showCheckbox)\n\n// В режиме чекбоксов каскад по родителям и детям считает `GrTree`, а не копия\n// его логики здесь: значение приходит обратно одним `update:checkedKeys`.\nfunction onCheckedKeys(keys: GrTreeKey[]): void {\n if (isDisabled.value || isReadonly.value)\n return\n\n emitModel([...keys])\n}\n\nfunction onNodeClick(data: T, node: GrTreeNode<T>): void {\n emit('nodeClick', data, node)\n\n if (isDisabled.value)\n return\n\n if (checkboxMode.value) {\n // Клик по строке — тот же переключатель отметки, что и сам чекбокс;\n // значение придёт через `update:checkedKeys`, поэтому здесь не эмитим.\n treeRef.value?.setChecked(node.key, !selectedKeySet.value.has(node.key))\n return\n }\n\n if (props.multiple) {\n const next = new Set(selectedKeySet.value)\n if (next.has(node.key))\n next.delete(node.key)\n else\n next.add(node.key)\n emitModel([...next])\n\n if (closeOnSelectResolved.value) {\n closeDropdown()\n }\n return\n }\n\n emitModel(node.key)\n if (closeOnSelectResolved.value) {\n closeDropdown()\n }\n}\n\n// Телепорт включается только ПОСЛЕ монтирования: иначе первый клиентский\n// рендер не совпадает с серверным и ломается гидрация (см. композабл).\nconst { target: portalTarget, enabled: teleportEnabled } = usePortalTarget()\n\n// Тема поддерева на телепортированную панель: в DOM она уезжает в `body`, то\n// есть вне обёртки провайдера, и `data-theme` с неё не наследуется. В дереве\n// компонентов панель остаётся внутри — `inject` доходит, и тему она ставит себе\n// сама.\nconst themeAttrs = useGrThemeAttrs()\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n v-click-outside=\"{ handler: onClickOutside, enabled: open, exclude: clickOutsideExclude }\"\n data-gr-tree-select\n class=\"relative\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <!-- Нативная форма: hidden на каждый выбранный ключ. -->\n <template v-if=\"name\">\n <input\n v-for=\"key in selectedKeys\"\n :key=\"`hidden-${String(key)}`\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"String(key)\"\n >\n </template>\n <div class=\"relative\">\n <div\n v-if=\"hasPrefix\"\n ref=\"prefixEl\"\n data-gr-tree-select-prefix\n class=\"absolute inset-y-px left-px flex items-center justify-center rounded-l-[var(--gr-radius-control)] border-r border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :style=\"prefixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"prefix\" />\n </div>\n\n <input\n :id=\"fieldId\"\n ref=\"triggerEl\"\n data-testid=\"gr-tree-select-trigger\"\n data-gr-tree-select-trigger\n type=\"text\"\n :value=\"displayValue\"\n :placeholder=\"placeholder\"\n :disabled=\"isDisabled\"\n readonly\n role=\"combobox\"\n aria-readonly=\"true\"\n aria-haspopup=\"tree\"\n :aria-expanded=\"open ? 'true' : 'false'\"\n :aria-controls=\"open && hasTree ? treeId : undefined\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-describedby=\"describedBy\"\n :aria-label=\"ariaLabel\"\n class=\"w-full rounded-[var(--gr-radius-control)] border placeholder:text-[var(--gr-muted-fg)] transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n :class=\"[className, $slots.value ? 'text-transparent placeholder:text-transparent' : '']\"\n :style=\"triggerStyle\"\n @pointerdown=\"onTriggerPointerDown\"\n @click=\"toggleDropdown\"\n @focus=\"onTriggerFocus\"\n @keydown=\"onTriggerKeydown\"\n >\n\n <button\n v-if=\"clearable && hasSelection && !disabled && !isReadonly\"\n data-testid=\"gr-tree-select-clear\"\n data-gr-tree-select-clear\n type=\"button\"\n class=\"absolute top-1/2 -translate-y-1/2 right-3 h-6 w-6 inline-flex items-center justify-center rounded-[var(--gr-radius-control)] text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)] hover:bg-[color-mix(in_srgb,var(--gr-muted)_25%,transparent)]\"\n :aria-label=\"t('gr.common.clear', 'Clear')\"\n @click.stop=\"clear\"\n >\n <IconX class=\"inline-block h-4 w-4\" aria-hidden=\"true\" />\n </button>\n\n <span\n v-else\n data-testid=\"gr-tree-select-chevron\"\n data-gr-tree-select-chevron\n class=\"absolute top-1/2 -translate-y-1/2 right-3 text-[var(--gr-muted-fg)] pointer-events-none\"\n >\n <IconChevronDown\n class=\"inline-block h-4 w-4 transition-transform duration-[var(--gr-duration-fast)]\"\n :class=\"open ? 'rotate-180' : ''\"\n aria-hidden=\"true\"\n />\n </span>\n\n <div\n v-if=\"hasSuffix\"\n ref=\"suffixEl\"\n data-gr-tree-select-suffix\n class=\"absolute inset-y-px right-9 flex items-center justify-center border-l border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"suffixFixed ? '[direction:rtl]' : ''\"\n :style=\"suffixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"suffix\" />\n </div>\n\n <div\n v-if=\"$slots.value\"\n class=\"absolute inset-y-0 left-3 right-9 flex items-center pointer-events-none\"\n :style=\"valueOverlayStyle\"\n >\n <slot\n name=\"value\"\n :value=\"modelValue\"\n :labels=\"selectedLabels\"\n :display-value=\"displayValue\"\n :path-labels=\"selectedPathLabels\"\n />\n </div>\n </div>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n :enter-active-class=\"panelPopTransition.enter\"\n :enter-from-class=\"panelPopTransition.enterFrom\"\n :enter-to-class=\"panelPopTransition.enterTo\"\n :leave-active-class=\"panelPopTransition.leave\"\n :leave-from-class=\"panelPopTransition.leaveFrom\"\n :leave-to-class=\"panelPopTransition.leaveTo\"\n >\n <div\n v-show=\"open\"\n ref=\"panelEl\"\n data-testid=\"gr-tree-select-panel\"\n v-bind=\"themeAttrs\"\n data-gr-tree-select-panel\n data-gr-overlay-root\n :style=\"floatingStyle\"\n @keydown=\"onPanelKeydown\"\n >\n <div :class=\"panelClasses\">\n <div v-if=\"filterable\" class=\"p-2 border-b border-[var(--gr-brd)]\">\n <GrInput\n ref=\"filterInputRef\"\n v-model=\"filterValue\"\n data-testid=\"gr-tree-select-filter\"\n data-gr-tree-select-filter\n type=\"search\"\n :inputmode=\"filterInputmode\"\n :placeholder=\"resolvedFilterPlaceholder\"\n size=\"sm\"\n @keydown=\"onFilterKeydown\"\n />\n </div>\n\n <!--\n Скроллер один. При виртуализации его берёт на себя дерево: своя\n высота нужна ему, чтобы посчитать окно, а вложенный скроллер поверх\n дал бы вторую полосу прокрутки на том же списке.\n -->\n <div\n class=\"p-1\"\n :class=\"virtual ? '' : 'overflow-auto'\"\n :style=\"virtual ? undefined : { maxHeight: `${dropdownMaxHeight}px` }\"\n >\n <div\n v-if=\"loading\"\n data-gr-tree-select-loading\n role=\"status\"\n :class=\"grTreeSelectStateClass\"\n >\n <slot name=\"loading\">\n <IconLoaderCircle class=\"block h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n <span>{{ t('gr.treeSelect.loading', 'Loading…') }}</span>\n </slot>\n </div>\n\n <div v-else-if=\"(data?.length ?? 0) === 0\" :class=\"grTreeSelectStateClass\">\n <slot name=\"empty\">\n {{ t('gr.treeSelect.empty', 'No data') }}\n </slot>\n </div>\n\n <GrTree\n v-else\n :id=\"treeId\"\n ref=\"treeRef\"\n :size=\"resolvedSize\"\n :data=\"data\"\n :props=\"props.props\"\n :node-key=\"nodeKey as any\"\n :default-expanded-keys=\"defaultExpandedKeys\"\n :filter-node-method=\"filterNodeMethod\"\n :highlight-current=\"!multiple\"\n :virtual=\"virtual\"\n :max-height=\"dropdownMaxHeight\"\n :show-checkbox=\"checkboxMode\"\n :check-strictly=\"checkStrictly\"\n :checked-keys=\"selectedKeys\"\n @node-click=\"onNodeClick\"\n @update:checked-keys=\"onCheckedKeys\"\n >\n <template #default=\"{ node, data }\">\n <slot\n name=\"node\"\n :node=\"node\"\n :data=\"data\"\n :selected=\"selectedKeySet.has(node.key)\"\n >\n <div class=\"flex items-center gap-2 min-w-0\">\n <!-- Чекбокс дерева уже показывает отметку: вторая галочка\n рядом читалась бы как ошибка. -->\n <span\n v-if=\"!checkboxMode\"\n data-gr-tree-select-check\n class=\"inline-block h-4 w-4 shrink-0\"\n aria-hidden=\"true\"\n >\n <IconCheck v-if=\"selectedKeySet.has(node.key)\" class=\"block h-4 w-4 text-[var(--gr-primary-text)]\" />\n </span>\n <span class=\"truncate\">{{ node.label }}</span>\n </div>\n </slot>\n </template>\n </GrTree>\n </div>\n </div>\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n","<script setup lang=\"ts\" generic=\"T extends Record<string, any> = any\">\nimport { computed, nextTick, ref, useId, useSlots, watch } from 'vue'\n\nimport { usePortalTarget } from '../../composables/usePortalTarget'\n\nimport { vClickOutside } from '../../directives'\nimport { useFloating } from '../../composables/useFloating'\nimport { useOverlayLayer } from '../../composables/useOverlayLayer'\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useGrComponentSize, useGrThemeAttrs } from '../GrConfigProvider/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTree, {\n type GrTreeInstance,\n type GrTreeKey,\n type GrTreeNode,\n} from '../GrTree'\nimport type { GrInputSize } from '../GrInput/GrInput.vue'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport type { GrTreeSelectModelValue, GrTreeSelectProps } from './grTreeSelectTypes'\nimport { grTreeSelectClass, grTreeSelectPanelClass, grTreeSelectStateClass, paddingX, trailingZoneWidth } from './grTreeSelectStyles'\n\nimport IconCheck from '~icons/lucide/check'\nimport IconChevronDown from '~icons/lucide/chevron-down'\nimport IconLoaderCircle from '~icons/lucide/loader-circle'\nimport IconX from '~icons/lucide/x'\n\nexport interface GrTreeSelectEmits<T extends Record<string, any> = any> {\n (e: 'update:modelValue', value: GrTreeSelectModelValue): void\n (e: 'change', value: GrTreeSelectModelValue): void\n /** Панель открылась/закрылась (`v-model:open`). */\n (e: 'update:open', value: boolean): void\n (e: 'clear'): void\n (e: 'nodeClick', data: T, node: GrTreeNode<T>): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrTreeSelectProps<T>>(),\n {\n props: () => ({\n children: 'children',\n label: 'label',\n }),\n nodeKey: 'id' as any,\n defaultExpandedKeys: () => [],\n disabled: false,\n placeholder: undefined,\n size: undefined,\n loading: false,\n invalid: false,\n readonly: false,\n required: false,\n ariaLabel: undefined,\n state: 'default',\n multiple: false,\n showCheckbox: false,\n checkStrictly: false,\n clearable: false,\n open: undefined,\n name: undefined,\n valueDisplay: 'label',\n filterable: false,\n filterPlaceholder: undefined,\n filterInputmode: undefined,\n filterNodeMethod: undefined,\n closeOnSelect: undefined,\n dropdownMaxHeight: 320,\n virtual: false,\n prefixMinWidth: undefined,\n prefixMaxWidth: undefined,\n suffixMinWidth: undefined,\n suffixMaxWidth: undefined,\n prefixFixed: false,\n suffixFixed: false,\n },\n)\n\nconst { t } = useGranularityTranslations()\n\nconst emit = defineEmits<GrTreeSelectEmits<T>>()\n\ndefineSlots<{\n /** Рендер значения внутри триггера (вместо дефолтного текста). */\n value?: (props: { value: GrTreeSelectModelValue, labels: string[], displayValue: string, pathLabels?: string[] }) => any\n /** Рендер строки дерева. */\n node?: (props: { node: GrTreeNode<T>, data: T, selected: boolean }) => any\n /** Содержимое пустого состояния (когда нет данных). */\n empty?: () => any\n /** Содержимое панели, пока данные едут. */\n loading?: () => any\n /** Аддон слева от значения: иконка, код валюты, метка. */\n prefix?: () => any\n /** Аддон справа от значения, перед крестиком и шевроном. */\n suffix?: () => any\n}>()\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst triggerEl = ref<HTMLInputElement | null>(null)\nconst treeId = `gr-tree-select-tree-${useId()}`\n\n// Контекст `GrFormField` + общий контракт форм-контрола.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nfunction focus(): void {\n triggerEl.value?.focus()\n}\n\nfunction blur(): void {\n triggerEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\nconst filterInputRef = ref<InstanceType<typeof GrInput> | null>(null)\nconst treeRef = ref<GrTreeInstance | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\n\n// Панель телепортирована в `body`, то есть лежит вне корня, но для пользователя\n// она часть контрола: без неё уход фокуса в панель читался бы как `blur`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n containers: () => [panelEl.value],\n})\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nlet hadPointerDownOnTrigger = false\n\n// Имя `open` сохранено: читатели работают с computed-Ref.\nconst { open, setOpen: setOpenState } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\nconst filterValue = ref('')\n\nconst { floatingStyle } = useFloating(rootEl, panelEl, open, {\n placement: 'bottom-start',\n matchWidth: true,\n zIndexVar: '--gr-z-dropdown',\n})\n\nconst closeOnSelectResolved = computed(() => {\n return props.closeOnSelect ?? !props.multiple\n})\n\nfunction getChildrenKey() {\n return props.props.children ?? 'children'\n}\n\nfunction getLabelKey() {\n return props.props.label ?? 'label'\n}\n\nfunction nodeLabel(data: T): string {\n const key = getLabelKey() as keyof T\n const v = data[key]\n return v == null ? '' : String(v)\n}\n\nfunction nodeChildren(data: T): T[] {\n const key = getChildrenKey() as keyof T\n const v = data[key]\n return Array.isArray(v) ? (v as any) : []\n}\n\nfunction nodeKeyOf(data: T, index: number, parentKey: GrTreeKey | undefined): GrTreeKey {\n const keyName = props.nodeKey as keyof T\n const v = data[keyName]\n if (v != null)\n return v as GrTreeKey\n\n return parentKey == null ? index : `${String(parentKey)}:${index}`\n}\n\nconst labelByKey = computed(() => {\n const map = new Map<GrTreeKey, string>()\n\n const walk = (items: T[], parentKey: GrTreeKey | undefined) => {\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n const key = nodeKeyOf(item, i, parentKey)\n map.set(key, nodeLabel(item))\n walk(nodeChildren(item), key)\n }\n }\n\n walk(props.data ?? [], undefined)\n return map\n})\n\nconst pathLabelsByKey = computed(() => {\n const map = new Map<GrTreeKey, string[]>()\n\n const walk = (items: T[], parentKey: GrTreeKey | undefined, parentPath: string[]) => {\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n const key = nodeKeyOf(item, i, parentKey)\n const label = nodeLabel(item)\n const path = [...parentPath, label]\n map.set(key, path)\n walk(nodeChildren(item), key, path)\n }\n }\n\n walk(props.data ?? [], undefined, [])\n return map\n})\n\nconst selectedKeys = computed<GrTreeKey[]>(() => {\n if (props.multiple) {\n return Array.isArray(props.modelValue) ? props.modelValue : []\n }\n\n const v = props.modelValue\n if (v == null || Array.isArray(v))\n return []\n return [v]\n})\n\nconst selectedKeySet = computed(() => new Set(selectedKeys.value))\n\nconst selectedLabels = computed(() => {\n const map = labelByKey.value\n return selectedKeys.value.map(k => map.get(k) ?? String(k))\n})\n\nconst selectedPathLabels = computed<string[] | undefined>(() => {\n if (props.multiple)\n return undefined\n\n const key = selectedKeys.value[0]\n if (key == null)\n return undefined\n\n const path = pathLabelsByKey.value.get(key)\n if (path?.length)\n return path\n\n const labels = selectedLabels.value\n return labels.length ? [labels[0]] : undefined\n})\n\nconst displayValue = computed(() => {\n const labels = selectedLabels.value\n if (labels.length === 0)\n return ''\n\n if (!props.multiple) {\n if (props.valueDisplay === 'path') {\n const path = selectedPathLabels.value\n if (path?.length)\n return path.join(' / ')\n }\n return labels[0]\n }\n\n if (labels.length === 1)\n return labels[0]\n return `${labels[0]} +${labels.length - 1}`\n})\n\nconst hasSelection = computed(() => selectedKeys.value.length > 0)\n\n/** Дерево рендерится только когда есть что показать — ссылаться иначе не на что. */\nconst hasTree = computed(() => !props.loading && (props.data?.length ?? 0) > 0)\n\nconst resolvedFilterPlaceholder = computed(() => {\n return props.filterPlaceholder ?? t('gr.treeSelect.filterPlaceholder', 'Search…')\n})\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTreeSelect' })\n\nconst className = computed(() => {\n return grTreeSelectClass({\n size: resolvedSize.value,\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n })\n})\n\nconst panelClasses = computed(() => {\n return grTreeSelectPanelClass\n})\n\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputSize, string> = {\n xs: '2rem',\n sm: '2.25rem',\n md: '2.5rem',\n lg: '3rem',\n}\n\nconst slots = useSlots()\n\nconst {\n hasPrefix,\n hasSuffix,\n prefixEl,\n suffixEl,\n prefixStyle,\n suffixStyle,\n fieldPadding: triggerStyle,\n} = useControlAddons(() => props, {\n defaultMinWidth: () => ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value],\n paddingX: () => paddingX[resolvedSize.value],\n // Место под крестик и шеврон уже отдано классом `pr-9`, но инлайн-паддинг\n // перекрывает класс целиком — с суффиксом эту зону приходится вернуть числом.\n trailingReserve: () => (slots.suffix ? trailingZoneWidth : '0px'),\n})\n\n// Слот `value` рисуется поверх поля и обязан жить в тех же границах, что и\n// текст под ним. Без аддонов границы задаёт класс — стиль остаётся пустым.\nconst valueOverlayStyle = computed(() => ({\n left: hasPrefix.value ? triggerStyle.value.paddingLeft : undefined,\n right: hasSuffix.value ? triggerStyle.value.paddingRight : undefined,\n}))\n\nfunction setOpen(next: boolean) {\n if (isDisabled.value || isReadonly.value)\n return\n\n setOpenState(next)\n}\n\nfunction onClickOutside(): void {\n setOpen(false)\n}\n\nfunction openDropdown(): void {\n setOpen(true)\n}\n\n/**\n * Стек слоёв возвращает фокус на триггер сразу после закрытия, а у триггера\n * открытие висит на `focus`. Без флага `Escape` из дерева закрывал бы панель и\n * тем же движением открывал её заново.\n */\nlet suppressFocusOpen = false\n\nfunction closeDropdown(): void {\n suppressFocusOpen = true\n setOpen(false)\n setTimeout(() => {\n suppressFocusOpen = false\n }, 0)\n}\n\nfunction toggleDropdown(): void {\n setOpen(!open.value)\n}\n\n// Клик по триггеру сам переключает панель через `@click`; флаг гасит открытие\n// по приходящему следом `focus`, иначе один клик открыл бы и тут же закрыл.\nfunction onTriggerPointerDown(): void {\n hadPointerDownOnTrigger = true\n setTimeout(() => {\n hadPointerDownOnTrigger = false\n }, 0)\n}\n\nfunction onTriggerFocus(): void {\n if (hadPointerDownOnTrigger || suppressFocusOpen)\n return\n openDropdown()\n}\n\n// Возврат фокуса на триггер — из контракта слоя, а не руками через `nextTick`.\nuseOverlayLayer(open, closeDropdown, { root: panelEl })\n\nwatch(\n open,\n async (isOpen) => {\n if (!isOpen) {\n if (filterValue.value.trim().length > 0)\n filterValue.value = ''\n return\n }\n\n // Подсветка дерева идёт за значением: панель могли открыть после того, как\n // значение сменили снаружи.\n if (!props.multiple) {\n const v = props.modelValue\n if (v != null && !Array.isArray(v))\n treeRef.value?.setCurrentKey(v)\n }\n\n if (props.filterable) {\n await nextTick()\n filterInputRef.value?.focus()\n }\n },\n { immediate: true },\n)\n\nwatch(\n () => props.modelValue,\n (v) => {\n if (props.multiple)\n return\n if (v == null || Array.isArray(v)) {\n treeRef.value?.setCurrentKey(undefined)\n return\n }\n treeRef.value?.setCurrentKey(v)\n },\n)\n\nwatch(\n filterValue,\n (v) => {\n treeRef.value?.filter(v)\n },\n)\n\nfunction emitModel(next: GrTreeSelectModelValue) {\n emit('update:modelValue', next)\n emit('change', next)\n}\n\nfunction clear(): void {\n if (isDisabled.value || isReadonly.value)\n return\n\n const next: GrTreeSelectModelValue = props.multiple ? [] : null\n emitModel(next)\n emit('clear')\n if (!props.multiple)\n treeRef.value?.setCurrentKey(undefined)\n}\n\n/**\n * Отдаёт клавиатуру дереву. Панель под `v-show`, поэтому дерево уже в DOM, но\n * скрытый элемент сфокусировать нельзя — сначала открытие, потом кадр, потом\n * фокус. Дальше работают клавиши `GrTree`, а `Escape` возвращает фокус на\n * триггер силами общего стека слоёв.\n */\nasync function focusTree(): Promise<boolean> {\n if (!open.value)\n return false\n\n await nextTick()\n return treeRef.value?.focus() ?? false\n}\n\nfunction onTriggerKeydown(e: KeyboardEvent): void {\n if (isDisabled.value)\n return\n\n if (e.key === 'Escape') {\n e.preventDefault()\n closeDropdown()\n return\n }\n\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n openDropdown()\n\n // При `filterable` фокус уходит в поле поиска (см. watch на `open`), и\n // дальше в дерево ведёт стрелка уже оттуда.\n if (!props.filterable)\n void focusTree()\n }\n}\n\n/** Стрелка из поля поиска — единственный путь из фильтра в дерево. */\nfunction onFilterKeydown(e: KeyboardEvent): void {\n if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp')\n return\n\n e.preventDefault()\n void focusTree()\n}\n\n/** `Tab` уводит из панели — оставлять её открытой за спиной незачем. */\nfunction onPanelKeydown(e: KeyboardEvent): void {\n if (e.key === 'Tab')\n closeDropdown()\n}\n\n// Чекбоксы осмысленны только в множественном выборе: одиночный уже подсвечен\n// текущим узлом.\nconst checkboxMode = computed(() => props.multiple && props.showCheckbox)\n\n// В режиме чекбоксов каскад по родителям и детям считает `GrTree`, а не копия\n// его логики здесь: значение приходит обратно одним `update:checkedKeys`.\nfunction onCheckedKeys(keys: GrTreeKey[]): void {\n if (isDisabled.value || isReadonly.value)\n return\n\n emitModel([...keys])\n}\n\nfunction onNodeClick(data: T, node: GrTreeNode<T>): void {\n emit('nodeClick', data, node)\n\n if (isDisabled.value)\n return\n\n if (checkboxMode.value) {\n // Клик по строке — тот же переключатель отметки, что и сам чекбокс;\n // значение придёт через `update:checkedKeys`, поэтому здесь не эмитим.\n treeRef.value?.setChecked(node.key, !selectedKeySet.value.has(node.key))\n return\n }\n\n if (props.multiple) {\n const next = new Set(selectedKeySet.value)\n if (next.has(node.key))\n next.delete(node.key)\n else\n next.add(node.key)\n emitModel([...next])\n\n if (closeOnSelectResolved.value) {\n closeDropdown()\n }\n return\n }\n\n emitModel(node.key)\n if (closeOnSelectResolved.value) {\n closeDropdown()\n }\n}\n\n// Телепорт включается только ПОСЛЕ монтирования: иначе первый клиентский\n// рендер не совпадает с серверным и ломается гидрация (см. композабл).\nconst { target: portalTarget, enabled: teleportEnabled } = usePortalTarget()\n\n// Тема поддерева на телепортированную панель: в DOM она уезжает в `body`, то\n// есть вне обёртки провайдера, и `data-theme` с неё не наследуется. В дереве\n// компонентов панель остаётся внутри — `inject` доходит, и тему она ставит себе\n// сама.\nconst themeAttrs = useGrThemeAttrs()\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n v-click-outside=\"{ handler: onClickOutside, enabled: open, exclude: clickOutsideExclude }\"\n data-gr-tree-select\n class=\"relative\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <!-- Нативная форма: hidden на каждый выбранный ключ. -->\n <template v-if=\"name\">\n <input\n v-for=\"key in selectedKeys\"\n :key=\"`hidden-${String(key)}`\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"String(key)\"\n >\n </template>\n <div class=\"relative\">\n <div\n v-if=\"hasPrefix\"\n ref=\"prefixEl\"\n data-gr-tree-select-prefix\n class=\"absolute inset-y-px left-px flex items-center justify-center rounded-l-[var(--gr-radius-control)] border-r border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :style=\"prefixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"prefix\" />\n </div>\n\n <input\n :id=\"fieldId\"\n ref=\"triggerEl\"\n data-testid=\"gr-tree-select-trigger\"\n data-gr-tree-select-trigger\n type=\"text\"\n :value=\"displayValue\"\n :placeholder=\"placeholder\"\n :disabled=\"isDisabled\"\n readonly\n role=\"combobox\"\n aria-readonly=\"true\"\n aria-haspopup=\"tree\"\n :aria-expanded=\"open ? 'true' : 'false'\"\n :aria-controls=\"open && hasTree ? treeId : undefined\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-describedby=\"describedBy\"\n :aria-label=\"ariaLabel\"\n class=\"w-full rounded-[var(--gr-radius-control)] border placeholder:text-[var(--gr-muted-fg)] transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n :class=\"[className, $slots.value ? 'text-transparent placeholder:text-transparent' : '']\"\n :style=\"triggerStyle\"\n @pointerdown=\"onTriggerPointerDown\"\n @click=\"toggleDropdown\"\n @focus=\"onTriggerFocus\"\n @keydown=\"onTriggerKeydown\"\n >\n\n <button\n v-if=\"clearable && hasSelection && !disabled && !isReadonly\"\n data-testid=\"gr-tree-select-clear\"\n data-gr-tree-select-clear\n type=\"button\"\n class=\"absolute top-1/2 -translate-y-1/2 right-3 h-6 w-6 inline-flex items-center justify-center rounded-[var(--gr-radius-control)] text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)] hover:bg-[color-mix(in_srgb,var(--gr-muted)_25%,transparent)]\"\n :aria-label=\"t('gr.common.clear', 'Clear')\"\n @click.stop=\"clear\"\n >\n <IconX class=\"inline-block h-4 w-4\" aria-hidden=\"true\" />\n </button>\n\n <span\n v-else\n data-testid=\"gr-tree-select-chevron\"\n data-gr-tree-select-chevron\n class=\"absolute top-1/2 -translate-y-1/2 right-3 text-[var(--gr-muted-fg)] pointer-events-none\"\n >\n <IconChevronDown\n class=\"inline-block h-4 w-4 transition-transform duration-[var(--gr-duration-fast)]\"\n :class=\"open ? 'rotate-180' : ''\"\n aria-hidden=\"true\"\n />\n </span>\n\n <div\n v-if=\"hasSuffix\"\n ref=\"suffixEl\"\n data-gr-tree-select-suffix\n class=\"absolute inset-y-px right-9 flex items-center justify-center border-l border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"suffixFixed ? '[direction:rtl]' : ''\"\n :style=\"suffixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"suffix\" />\n </div>\n\n <div\n v-if=\"$slots.value\"\n class=\"absolute inset-y-0 left-3 right-9 flex items-center pointer-events-none\"\n :style=\"valueOverlayStyle\"\n >\n <slot\n name=\"value\"\n :value=\"modelValue\"\n :labels=\"selectedLabels\"\n :display-value=\"displayValue\"\n :path-labels=\"selectedPathLabels\"\n />\n </div>\n </div>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n :enter-active-class=\"panelPopTransition.enter\"\n :enter-from-class=\"panelPopTransition.enterFrom\"\n :enter-to-class=\"panelPopTransition.enterTo\"\n :leave-active-class=\"panelPopTransition.leave\"\n :leave-from-class=\"panelPopTransition.leaveFrom\"\n :leave-to-class=\"panelPopTransition.leaveTo\"\n >\n <div\n v-show=\"open\"\n ref=\"panelEl\"\n data-testid=\"gr-tree-select-panel\"\n v-bind=\"themeAttrs\"\n data-gr-tree-select-panel\n data-gr-overlay-root\n :style=\"floatingStyle\"\n @keydown=\"onPanelKeydown\"\n >\n <div :class=\"panelClasses\">\n <div v-if=\"filterable\" class=\"p-2 border-b border-[var(--gr-brd)]\">\n <GrInput\n ref=\"filterInputRef\"\n v-model=\"filterValue\"\n data-testid=\"gr-tree-select-filter\"\n data-gr-tree-select-filter\n type=\"search\"\n :inputmode=\"filterInputmode\"\n :placeholder=\"resolvedFilterPlaceholder\"\n size=\"sm\"\n @keydown=\"onFilterKeydown\"\n />\n </div>\n\n <!--\n Скроллер один. При виртуализации его берёт на себя дерево: своя\n высота нужна ему, чтобы посчитать окно, а вложенный скроллер поверх\n дал бы вторую полосу прокрутки на том же списке.\n -->\n <div\n class=\"p-1\"\n :class=\"virtual ? '' : 'overflow-auto'\"\n :style=\"virtual ? undefined : { maxHeight: `${dropdownMaxHeight}px` }\"\n >\n <div\n v-if=\"loading\"\n data-gr-tree-select-loading\n role=\"status\"\n :class=\"grTreeSelectStateClass\"\n >\n <slot name=\"loading\">\n <IconLoaderCircle class=\"block h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n <span>{{ t('gr.treeSelect.loading', 'Loading…') }}</span>\n </slot>\n </div>\n\n <div v-else-if=\"(data?.length ?? 0) === 0\" :class=\"grTreeSelectStateClass\">\n <slot name=\"empty\">\n {{ t('gr.treeSelect.empty', 'No data') }}\n </slot>\n </div>\n\n <GrTree\n v-else\n :id=\"treeId\"\n ref=\"treeRef\"\n :size=\"resolvedSize\"\n :data=\"data\"\n :props=\"props.props\"\n :node-key=\"nodeKey as any\"\n :default-expanded-keys=\"defaultExpandedKeys\"\n :filter-node-method=\"filterNodeMethod\"\n :highlight-current=\"!multiple\"\n :virtual=\"virtual\"\n :max-height=\"dropdownMaxHeight\"\n :show-checkbox=\"checkboxMode\"\n :check-strictly=\"checkStrictly\"\n :checked-keys=\"selectedKeys\"\n @node-click=\"onNodeClick\"\n @update:checked-keys=\"onCheckedKeys\"\n >\n <template #default=\"{ node, data }\">\n <slot\n name=\"node\"\n :node=\"node\"\n :data=\"data\"\n :selected=\"selectedKeySet.has(node.key)\"\n >\n <div class=\"flex items-center gap-2 min-w-0\">\n <!-- Чекбокс дерева уже показывает отметку: вторая галочка\n рядом читалась бы как ошибка. -->\n <span\n v-if=\"!checkboxMode\"\n data-gr-tree-select-check\n class=\"inline-block h-4 w-4 shrink-0\"\n aria-hidden=\"true\"\n >\n <IconCheck v-if=\"selectedKeySet.has(node.key)\" class=\"block h-4 w-4 text-[var(--gr-primary-text)]\" />\n </span>\n <span class=\"truncate\">{{ node.label }}</span>\n </div>\n </slot>\n </template>\n </GrTree>\n </div>\n </div>\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { n as grTreeSelectSafelist, t as grTreeSelectConfig } from "../../chunks/config-T3-DMXYm.js";
|
|
2
|
-
import { t as GrTreeSelect_default } from "./chunks/GrTreeSelect-
|
|
2
|
+
import { t as GrTreeSelect_default } from "./chunks/GrTreeSelect-BHwgGJcT.js";
|
|
3
3
|
export { GrTreeSelect_default as GrTreeSelect, GrTreeSelect_default as default, grTreeSelectConfig, grTreeSelectSafelist };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAnnouncer.js","names":[],"sources":["../../src/composables/useAnnouncer.ts"],"sourcesContent":["import { ensurePortalRoot } from './internal/portalRoot'\n\n/**\n * Общий живой регион пакета: один канал `aria-live` на документ.\n *\n * До него каждый компонент строил свой `role=\"status\"`, а сообщить что-то\n * скринридеру из кода приложения («скопировано», «строка удалена», «найдено N»)\n * было нечем вовсе. Регион виджета остаётся правильным решением, когда он\n * рендерит **состояние** этого виджета (счётчик символов, «загружается»,\n * «ничего не найдено»); объявитель — для **события**, которое произошло и\n * прошло, и своего места в разметке не имеет.\n *\n * ```ts\n * const { announce } = useAnnouncer()\n * announce('Ссылка скопирована')\n * announce('Не удалось сохранить', { politeness: 'assertive' })\n * ```\n *\n * Композабл не требует setup-контекста: его можно звать из обычного `.ts` —\n * из стора, обработчика, перехватчика запросов.\n */\n\nexport type GrAnnouncerPoliteness = 'polite' | 'assertive'\n\nexport interface GrAnnounceOptions {\n /**\n * `assertive` перебивает текущую речь и уместен только для того, что нельзя\n * пропустить (отказ операции, потеря соединения). По умолчанию `polite` —\n * диктор дочитает начатое.\n */\n politeness?: GrAnnouncerPoliteness\n /**\n * Через сколько мс стереть текст. `0` — не стирать.\n *\n * Регион остаётся в DOM, и пользователь виртуального курсора наткнётся на\n * его содержимое при обходе страницы — через минуту после события это уже\n * дезинформация. По умолчанию `7000`.\n */\n clearAfterMs?: number\n}\n\nexport interface GrAnnouncer {\n announce: (message: string, options?: GrAnnounceOptions) => void\n /** Стирает текст региона. Без аргумента — обоих. */\n clear: (politeness?: GrAnnouncerPoliteness) => void\n}\n\nconst HOST_ID = 'gr-announcer'\nconst DEFAULT_CLEAR_AFTER_MS = 7000\n\n/**\n * Визуальное скрытие — инлайновым стилем, а не классом `sr-only`.\n *\n * Узлы создаются императивно и в скан UnoCSS не попадают ни при каком safelist:\n * пресет сканирует `dist/components/**`, а этого класса там нет — CSS к нему не\n * сгенерируется, и регион окажется видимой строкой посреди страницы.\n *\n * Ни `display: none`, ни `visibility: hidden`: их содержимое AT не читает.\n */\nconst VISUALLY_HIDDEN = [\n 'position:absolute',\n 'width:1px',\n 'height:1px',\n 'margin:-1px',\n 'padding:0',\n 'overflow:hidden',\n 'clip:rect(0 0 0 0)',\n 'clip-path:inset(50%)',\n 'white-space:nowrap',\n 'border:0',\n].join(';')\n\ninterface AnnouncerHost {\n polite: HTMLElement\n assertive: HTMLElement\n}\n\nlet host: AnnouncerHost | null = null\nconst writeTimers = new Map<HTMLElement, ReturnType<typeof setTimeout>>()\nconst clearTimers = new Map<HTMLElement, ReturnType<typeof setTimeout>>()\n\nfunction createRegion(politeness: GrAnnouncerPoliteness): HTMLElement {\n const region = document.createElement('div')\n // `role` подразумевает нужную вежливость, но объявляем и её: связка\n // «роль + aria-live» — то же, что делают регионы компонентов пакета.\n region.setAttribute('role', politeness === 'assertive' ? 'alert' : 'status')\n region.setAttribute('aria-live', politeness)\n region.setAttribute('aria-atomic', 'true')\n region.setAttribute('data-gr-announcer-region', politeness)\n region.setAttribute('style', VISUALLY_HIDDEN)\n return region\n}\n\n/**\n * Создаёт хост при первом обращении. `null` на сервере.\n *\n * Узлы обязаны появиться в документе **раньше** текста: регион, который\n * возникает сразу с содержимым, часть AT не объявляет вовсе. Поэтому хост\n * ставится в момент `useAnnouncer()`, а текст пишется отложенно.\n */\nfunction ensureHost(): AnnouncerHost | null {\n if (typeof document === 'undefined')
|
|
1
|
+
{"version":3,"file":"useAnnouncer.js","names":[],"sources":["../../src/composables/useAnnouncer.ts"],"sourcesContent":["import { ensurePortalRoot } from './internal/portalRoot'\n\n/**\n * Общий живой регион пакета: один канал `aria-live` на документ.\n *\n * До него каждый компонент строил свой `role=\"status\"`, а сообщить что-то\n * скринридеру из кода приложения («скопировано», «строка удалена», «найдено N»)\n * было нечем вовсе. Регион виджета остаётся правильным решением, когда он\n * рендерит **состояние** этого виджета (счётчик символов, «загружается»,\n * «ничего не найдено»); объявитель — для **события**, которое произошло и\n * прошло, и своего места в разметке не имеет.\n *\n * ```ts\n * const { announce } = useAnnouncer()\n * announce('Ссылка скопирована')\n * announce('Не удалось сохранить', { politeness: 'assertive' })\n * ```\n *\n * Композабл не требует setup-контекста: его можно звать из обычного `.ts` —\n * из стора, обработчика, перехватчика запросов.\n */\n\nexport type GrAnnouncerPoliteness = 'polite' | 'assertive'\n\nexport interface GrAnnounceOptions {\n /**\n * `assertive` перебивает текущую речь и уместен только для того, что нельзя\n * пропустить (отказ операции, потеря соединения). По умолчанию `polite` —\n * диктор дочитает начатое.\n */\n politeness?: GrAnnouncerPoliteness\n /**\n * Через сколько мс стереть текст. `0` — не стирать.\n *\n * Регион остаётся в DOM, и пользователь виртуального курсора наткнётся на\n * его содержимое при обходе страницы — через минуту после события это уже\n * дезинформация. По умолчанию `7000`.\n */\n clearAfterMs?: number\n}\n\nexport interface GrAnnouncer {\n announce: (message: string, options?: GrAnnounceOptions) => void\n /** Стирает текст региона. Без аргумента — обоих. */\n clear: (politeness?: GrAnnouncerPoliteness) => void\n}\n\nconst HOST_ID = 'gr-announcer'\nconst DEFAULT_CLEAR_AFTER_MS = 7000\n\n/**\n * Визуальное скрытие — инлайновым стилем, а не классом `sr-only`.\n *\n * Узлы создаются императивно и в скан UnoCSS не попадают ни при каком safelist:\n * пресет сканирует `dist/components/**`, а этого класса там нет — CSS к нему не\n * сгенерируется, и регион окажется видимой строкой посреди страницы.\n *\n * Ни `display: none`, ни `visibility: hidden`: их содержимое AT не читает.\n */\nconst VISUALLY_HIDDEN = [\n 'position:absolute',\n 'width:1px',\n 'height:1px',\n 'margin:-1px',\n 'padding:0',\n 'overflow:hidden',\n 'clip:rect(0 0 0 0)',\n 'clip-path:inset(50%)',\n 'white-space:nowrap',\n 'border:0',\n].join(';')\n\ninterface AnnouncerHost {\n polite: HTMLElement\n assertive: HTMLElement\n}\n\nlet host: AnnouncerHost | null = null\nconst writeTimers = new Map<HTMLElement, ReturnType<typeof setTimeout>>()\nconst clearTimers = new Map<HTMLElement, ReturnType<typeof setTimeout>>()\n\nfunction createRegion(politeness: GrAnnouncerPoliteness): HTMLElement {\n const region = document.createElement('div')\n // `role` подразумевает нужную вежливость, но объявляем и её: связка\n // «роль + aria-live» — то же, что делают регионы компонентов пакета.\n region.setAttribute('role', politeness === 'assertive' ? 'alert' : 'status')\n region.setAttribute('aria-live', politeness)\n region.setAttribute('aria-atomic', 'true')\n region.setAttribute('data-gr-announcer-region', politeness)\n region.setAttribute('style', VISUALLY_HIDDEN)\n return region\n}\n\n/**\n * Создаёт хост при первом обращении. `null` на сервере.\n *\n * Узлы обязаны появиться в документе **раньше** текста: регион, который\n * возникает сразу с содержимым, часть AT не объявляет вовсе. Поэтому хост\n * ставится в момент `useAnnouncer()`, а текст пишется отложенно.\n */\nfunction ensureHost(): AnnouncerHost | null {\n if (typeof document === 'undefined')\n return null\n if (host?.polite.isConnected)\n return host\n\n const existing = document.getElementById(HOST_ID)\n existing?.remove()\n\n const container = document.createElement('div')\n container.id = HOST_ID\n // Пропуск для `inertableOutside`: под открытой модалкой хост обязан остаться\n // объявляемым, иначе объявитель молча замолкает ровно там, где нужнее всего.\n container.setAttribute('data-gr-live-region', '')\n\n const polite = createRegion('polite')\n const assertive = createRegion('assertive')\n container.append(polite, assertive)\n\n ;(ensurePortalRoot() ?? document.body).appendChild(container)\n\n host = { polite, assertive }\n return host\n}\n\nfunction cancel(map: Map<HTMLElement, ReturnType<typeof setTimeout>>, region: HTMLElement): void {\n const handle = map.get(region)\n if (handle === undefined)\n return\n clearTimeout(handle)\n map.delete(region)\n}\n\nexport function useAnnouncer(): GrAnnouncer {\n const resolved = ensureHost()\n\n function regionFor(politeness: GrAnnouncerPoliteness): HTMLElement | null {\n return politeness === 'assertive' ? resolved?.assertive ?? null : resolved?.polite ?? null\n }\n\n function clear(politeness?: GrAnnouncerPoliteness): void {\n const targets = politeness ? [regionFor(politeness)] : [regionFor('polite'), regionFor('assertive')]\n\n for (const region of targets) {\n if (!region)\n continue\n cancel(writeTimers, region)\n cancel(clearTimers, region)\n region.textContent = ''\n }\n }\n\n function announce(message: string, options: GrAnnounceOptions = {}): void {\n const region = regionFor(options.politeness ?? 'polite')\n if (!region || !message)\n return\n\n cancel(writeTimers, region)\n cancel(clearTimers, region)\n\n // Очищаем сейчас, пишем следующим тиком: без пустой фазы повтор того же\n // текста («Тег удалён» дважды подряд) не даёт мутации и не читается.\n region.textContent = ''\n\n writeTimers.set(region, setTimeout(() => {\n writeTimers.delete(region)\n region.textContent = message\n\n const clearAfterMs = options.clearAfterMs ?? DEFAULT_CLEAR_AFTER_MS\n if (clearAfterMs <= 0)\n return\n\n clearTimers.set(region, setTimeout(() => {\n clearTimers.delete(region)\n region.textContent = ''\n }, clearAfterMs))\n }, 0))\n }\n\n return { announce, clear }\n}\n\n/** Тестовая/служебная очистка: снимает хост и все отложенные записи. */\nexport function resetAnnouncer(): void {\n for (const map of [writeTimers, clearTimers]) {\n for (const handle of map.values()) clearTimeout(handle)\n map.clear()\n }\n\n if (typeof document !== 'undefined')\n document.getElementById(HOST_ID)?.remove()\n\n host = null\n}\n"],"mappings":";;AA+CA,IAAM,UAAU;AAChB,IAAM,yBAAyB;;;;;;;;;;AAW/B,IAAM,kBAAkB;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG;AAOV,IAAI,OAA6B;AACjC,IAAM,8BAAc,IAAI,IAAgD;AACxE,IAAM,8BAAc,IAAI,IAAgD;AAExE,SAAS,aAAa,YAAgD;CACpE,MAAM,SAAS,SAAS,cAAc,KAAK;CAG3C,OAAO,aAAa,QAAQ,eAAe,cAAc,UAAU,QAAQ;CAC3E,OAAO,aAAa,aAAa,UAAU;CAC3C,OAAO,aAAa,eAAe,MAAM;CACzC,OAAO,aAAa,4BAA4B,UAAU;CAC1D,OAAO,aAAa,SAAS,eAAe;CAC5C,OAAO;AACT;;;;;;;;AASA,SAAS,aAAmC;CAC1C,IAAI,OAAO,aAAa,aACtB,OAAO;CACT,IAAI,MAAM,OAAO,aACf,OAAO;CAGT,SAD0B,eAAe,OACzC,CAAA,EAAU,OAAO;CAEjB,MAAM,YAAY,SAAS,cAAc,KAAK;CAC9C,UAAU,KAAK;CAGf,UAAU,aAAa,uBAAuB,EAAE;CAEhD,MAAM,SAAS,aAAa,QAAQ;CACpC,MAAM,YAAY,aAAa,WAAW;CAC1C,UAAU,OAAO,QAAQ,SAAS;CAEjC,CAAC,iBAAiB,KAAK,SAAS,KAAA,CAAM,YAAY,SAAS;CAE5D,OAAO;EAAE;EAAQ;CAAU;CAC3B,OAAO;AACT;AAEA,SAAS,OAAO,KAAsD,QAA2B;CAC/F,MAAM,SAAS,IAAI,IAAI,MAAM;CAC7B,IAAI,WAAW,KAAA,GACb;CACF,aAAa,MAAM;CACnB,IAAI,OAAO,MAAM;AACnB;AAEA,SAAgB,eAA4B;CAC1C,MAAM,WAAW,WAAW;CAE5B,SAAS,UAAU,YAAuD;EACxE,OAAO,eAAe,cAAc,UAAU,aAAa,OAAO,UAAU,UAAU;CACxF;CAEA,SAAS,MAAM,YAA0C;EACvD,MAAM,UAAU,aAAa,CAAC,UAAU,UAAU,CAAC,IAAI,CAAC,UAAU,QAAQ,GAAG,UAAU,WAAW,CAAC;EAEnG,KAAK,MAAM,UAAU,SAAS;GAC5B,IAAI,CAAC,QACH;GACF,OAAO,aAAa,MAAM;GAC1B,OAAO,aAAa,MAAM;GAC1B,OAAO,cAAc;EACvB;CACF;CAEA,SAAS,SAAS,SAAiB,UAA6B,CAAC,GAAS;EACxE,MAAM,SAAS,UAAU,QAAQ,cAAc,QAAQ;EACvD,IAAI,CAAC,UAAU,CAAC,SACd;EAEF,OAAO,aAAa,MAAM;EAC1B,OAAO,aAAa,MAAM;EAI1B,OAAO,cAAc;EAErB,YAAY,IAAI,QAAQ,iBAAiB;GACvC,YAAY,OAAO,MAAM;GACzB,OAAO,cAAc;GAErB,MAAM,eAAe,QAAQ,gBAAgB;GAC7C,IAAI,gBAAgB,GAClB;GAEF,YAAY,IAAI,QAAQ,iBAAiB;IACvC,YAAY,OAAO,MAAM;IACzB,OAAO,cAAc;GACvB,GAAG,YAAY,CAAC;EAClB,GAAG,CAAC,CAAC;CACP;CAEA,OAAO;EAAE;EAAU;CAAM;AAC3B;;AAGA,SAAgB,iBAAuB;CACrC,KAAK,MAAM,OAAO,CAAC,aAAa,WAAW,GAAG;EAC5C,KAAK,MAAM,UAAU,IAAI,OAAO,GAAG,aAAa,MAAM;EACtD,IAAI,MAAM;CACZ;CAEA,IAAI,OAAO,aAAa,aACtB,SAAS,eAAe,OAAO,CAAC,EAAE,OAAO;CAE3C,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useComboboxNavigation.js","names":[],"sources":["../../src/composables/useComboboxNavigation.ts"],"sourcesContent":["import type { ComputedRef, Ref } from 'vue'\nimport { computed, ref, watch } from 'vue'\n\n/**\n * Активный элемент списка комбобокса (`aria-activedescendant`-паттерн).\n *\n * Одна и та же механика — activeIndex, циклический кламп, id активного, скролл\n * к нему, инициализация при открытии — была написана в пакете четырежды\n * (Select, Autocomplete, CommandPalette, частично Dropdown) и уже расходилась\n * до P1: Enter в Select активировал не тот элемент, что был подсвечен.\n * «Кто активен» обязан быть один на всех.\n *\n * Осознанно **не** обобщается активация: Enter/Tab/typeahead/Space — доменная\n * логика компонента (у Select — `add|option|fallback`, у палитры —\n * `selectItem`). Примитив отдаёт состояние и механические клавиши\n * (`handleNavigationKeys`), активацию компонент делает сам по `activeItem`.\n */\nexport interface UseComboboxNavigationOptions<TItem> {\n /** Навигируемые элементы (без disabled) в порядке отображения. */\n items: () => readonly TItem[]\n /** Открыт ли комбобокс — `activeDescendantId` существует только при открытом. */\n open: () => boolean\n /** DOM id элемента — цель `aria-activedescendant`. */\n idOf: (item: TItem, index: number) => string\n /**\n * Стартовый индекс для `init()` (обычно выбранное значение). Не задан или\n * вне диапазона — первый элемент, при пустом списке — `-1`.\n */\n initialIndex?: () => number\n /**\n * Донести активный элемент до экрана. Виртуализация и `scrollIntoView` —\n * забота компонента: только он знает свой контейнер и смещения окна.\n */\n scrollTo?: (item: TItem, index: number) => void | Promise<void>\n}\n\nexport interface UseComboboxNavigationReturn<TItem> {\n activeIndex: Ref<number>\n activeItem: ComputedRef<TItem | undefined>\n activeDescendantId: ComputedRef<string | undefined>\n /** Циклический кламп + прокрутка к элементу. */\n setActive: (index: number) => void\n /** Активировать стартовый элемент (вызывается при открытии). */\n init: () => void\n /** Сброс активного (вызывается при закрытии; палитра сбрасывает в `0`). */\n reset: (index?: number) => void\n /**\n * Механические клавиши: `ArrowDown`/`ArrowUp`/`Home`/`End`. Возвращает\n * `true`, если клавиша обработана (с `preventDefault`), — остальное\n * компонент разбирает сам.\n */\n handleNavigationKeys: (event: KeyboardEvent) => boolean\n}\n\nexport function useComboboxNavigation<TItem>(\n options: UseComboboxNavigationOptions<TItem>,\n): UseComboboxNavigationReturn<TItem> {\n const activeIndex = ref(-1)\n\n const activeItem = computed<TItem | undefined>(() =>\n activeIndex.value >= 0 ? options.items()[activeIndex.value] : undefined,\n )\n\n const activeDescendantId = computed(() => {\n if (!options.open())
|
|
1
|
+
{"version":3,"file":"useComboboxNavigation.js","names":[],"sources":["../../src/composables/useComboboxNavigation.ts"],"sourcesContent":["import type { ComputedRef, Ref } from 'vue'\nimport { computed, ref, watch } from 'vue'\n\n/**\n * Активный элемент списка комбобокса (`aria-activedescendant`-паттерн).\n *\n * Одна и та же механика — activeIndex, циклический кламп, id активного, скролл\n * к нему, инициализация при открытии — была написана в пакете четырежды\n * (Select, Autocomplete, CommandPalette, частично Dropdown) и уже расходилась\n * до P1: Enter в Select активировал не тот элемент, что был подсвечен.\n * «Кто активен» обязан быть один на всех.\n *\n * Осознанно **не** обобщается активация: Enter/Tab/typeahead/Space — доменная\n * логика компонента (у Select — `add|option|fallback`, у палитры —\n * `selectItem`). Примитив отдаёт состояние и механические клавиши\n * (`handleNavigationKeys`), активацию компонент делает сам по `activeItem`.\n */\nexport interface UseComboboxNavigationOptions<TItem> {\n /** Навигируемые элементы (без disabled) в порядке отображения. */\n items: () => readonly TItem[]\n /** Открыт ли комбобокс — `activeDescendantId` существует только при открытом. */\n open: () => boolean\n /** DOM id элемента — цель `aria-activedescendant`. */\n idOf: (item: TItem, index: number) => string\n /**\n * Стартовый индекс для `init()` (обычно выбранное значение). Не задан или\n * вне диапазона — первый элемент, при пустом списке — `-1`.\n */\n initialIndex?: () => number\n /**\n * Донести активный элемент до экрана. Виртуализация и `scrollIntoView` —\n * забота компонента: только он знает свой контейнер и смещения окна.\n */\n scrollTo?: (item: TItem, index: number) => void | Promise<void>\n}\n\nexport interface UseComboboxNavigationReturn<TItem> {\n activeIndex: Ref<number>\n activeItem: ComputedRef<TItem | undefined>\n activeDescendantId: ComputedRef<string | undefined>\n /** Циклический кламп + прокрутка к элементу. */\n setActive: (index: number) => void\n /** Активировать стартовый элемент (вызывается при открытии). */\n init: () => void\n /** Сброс активного (вызывается при закрытии; палитра сбрасывает в `0`). */\n reset: (index?: number) => void\n /**\n * Механические клавиши: `ArrowDown`/`ArrowUp`/`Home`/`End`. Возвращает\n * `true`, если клавиша обработана (с `preventDefault`), — остальное\n * компонент разбирает сам.\n */\n handleNavigationKeys: (event: KeyboardEvent) => boolean\n}\n\nexport function useComboboxNavigation<TItem>(\n options: UseComboboxNavigationOptions<TItem>,\n): UseComboboxNavigationReturn<TItem> {\n const activeIndex = ref(-1)\n\n const activeItem = computed<TItem | undefined>(() =>\n activeIndex.value >= 0 ? options.items()[activeIndex.value] : undefined,\n )\n\n const activeDescendantId = computed(() => {\n if (!options.open())\n return undefined\n const item = activeItem.value\n if (item === undefined)\n return undefined\n return options.idOf(item, activeIndex.value)\n })\n\n function clamp(index: number): number {\n const len = options.items().length\n if (len === 0)\n return -1\n return ((index % len) + len) % len\n }\n\n function setActive(index: number): void {\n activeIndex.value = clamp(index)\n\n const item = activeItem.value\n if (item !== undefined)\n void options.scrollTo?.(item, activeIndex.value)\n }\n\n function init(): void {\n const len = options.items().length\n if (len === 0) {\n activeIndex.value = -1\n return\n }\n\n const preferred = options.initialIndex?.() ?? -1\n activeIndex.value = preferred >= 0 && preferred < len ? preferred : 0\n }\n\n function reset(index = -1): void {\n activeIndex.value = index\n }\n\n // Список изменился (фильтрация, remote-ответ) — активный не должен указывать\n // мимо. За верхнюю границу — на последний; пустого активного при непустом\n // списке не бывает.\n watch(options.items, (items) => {\n if (!options.open())\n return\n if (activeIndex.value >= items.length)\n activeIndex.value = items.length - 1\n if (activeIndex.value < 0 && items.length)\n activeIndex.value = 0\n })\n\n function handleNavigationKeys(event: KeyboardEvent): boolean {\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n setActive(activeIndex.value + 1)\n return true\n case 'ArrowUp':\n event.preventDefault()\n setActive(activeIndex.value - 1)\n return true\n case 'Home':\n event.preventDefault()\n setActive(0)\n return true\n case 'End':\n event.preventDefault()\n setActive(options.items().length - 1)\n return true\n default:\n return false\n }\n }\n\n return { activeIndex, activeItem, activeDescendantId, setActive, init, reset, handleNavigationKeys }\n}\n"],"mappings":";;AAsDA,SAAgB,sBACd,SACoC;CACpC,MAAM,cAAc,IAAI,EAAE;CAE1B,MAAM,aAAa,eACjB,YAAY,SAAS,IAAI,QAAQ,MAAM,CAAC,CAAC,YAAY,SAAS,KAAA,CAChE;CAEA,MAAM,qBAAqB,eAAe;EACxC,IAAI,CAAC,QAAQ,KAAK,GAChB,OAAO,KAAA;EACT,MAAM,OAAO,WAAW;EACxB,IAAI,SAAS,KAAA,GACX,OAAO,KAAA;EACT,OAAO,QAAQ,KAAK,MAAM,YAAY,KAAK;CAC7C,CAAC;CAED,SAAS,MAAM,OAAuB;EACpC,MAAM,MAAM,QAAQ,MAAM,CAAC,CAAC;EAC5B,IAAI,QAAQ,GACV,OAAO;EACT,QAAS,QAAQ,MAAO,OAAO;CACjC;CAEA,SAAS,UAAU,OAAqB;EACtC,YAAY,QAAQ,MAAM,KAAK;EAE/B,MAAM,OAAO,WAAW;EACxB,IAAI,SAAS,KAAA,GACX,QAAa,WAAW,MAAM,YAAY,KAAK;CACnD;CAEA,SAAS,OAAa;EACpB,MAAM,MAAM,QAAQ,MAAM,CAAC,CAAC;EAC5B,IAAI,QAAQ,GAAG;GACb,YAAY,QAAQ;GACpB;EACF;EAEA,MAAM,YAAY,QAAQ,eAAe,KAAK;EAC9C,YAAY,QAAQ,aAAa,KAAK,YAAY,MAAM,YAAY;CACtE;CAEA,SAAS,MAAM,QAAQ,IAAU;EAC/B,YAAY,QAAQ;CACtB;CAKA,MAAM,QAAQ,QAAQ,UAAU;EAC9B,IAAI,CAAC,QAAQ,KAAK,GAChB;EACF,IAAI,YAAY,SAAS,MAAM,QAC7B,YAAY,QAAQ,MAAM,SAAS;EACrC,IAAI,YAAY,QAAQ,KAAK,MAAM,QACjC,YAAY,QAAQ;CACxB,CAAC;CAED,SAAS,qBAAqB,OAA+B;EAC3D,QAAQ,MAAM,KAAd;GACE,KAAK;IACH,MAAM,eAAe;IACrB,UAAU,YAAY,QAAQ,CAAC;IAC/B,OAAO;GACT,KAAK;IACH,MAAM,eAAe;IACrB,UAAU,YAAY,QAAQ,CAAC;IAC/B,OAAO;GACT,KAAK;IACH,MAAM,eAAe;IACrB,UAAU,CAAC;IACX,OAAO;GACT,KAAK;IACH,MAAM,eAAe;IACrB,UAAU,QAAQ,MAAM,CAAC,CAAC,SAAS,CAAC;IACpC,OAAO;GACT,SACE,OAAO;EACX;CACF;CAEA,OAAO;EAAE;EAAa;EAAY;EAAoB;EAAW;EAAM;EAAO;CAAqB;AACrG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDragGesture.js","names":[],"sources":["../../src/composables/useDragGesture.ts"],"sourcesContent":["import type { Ref } from 'vue'\nimport { onScopeDispose, ref } from 'vue'\n\n/**\n * Скелет указательного жеста: нажал, повёл, отпустил.\n *\n * Механика написана в пакете дважды дословно (`GrSlider`, `GrSplitter`) и\n * ещё раз иначе (`GrImageViewer`), а следующие потребители известны заранее —\n * swipe у `GrToaster`, перенос узла в `GrTree`, ресайз колонок `GrDataTable`.\n * Дублируется при этом не разметка, а **правило**: оборванный жест не\n * коммитит. Живя в двух копиях, оно разъезжается молча.\n *\n * Слушатели вешаются на `window`, а не на элемент: указатель уходит за\n * границы дорожки и разделителя постоянно, и жест обязан это переживать.\n * Захват (`setPointerCapture`) дал бы то же самое, но привязал бы жест к\n * живому DOM-узлу — а он в середине жеста может перерисоваться.\n *\n * У жеста **два разных исхода**, и это главное, ради чего примитив заведён:\n * `pointerup` — завершение (значение коммитится), `pointercancel` — обрыв\n * (браузер забрал указатель: системный жест, скролл, потеря окна), и тогда\n * состояние возвращается к тому, что было до нажатия.\n *\n * Осознанно **не** делает:\n * - не вызывает `preventDefault` — в слайдере и сплиттере это гашение\n * выделения текста, а в панорамировании картинки помешало бы; строка\n * дешевле опции, меняющей поведение невидимо;\n * - не считает координаты и дельты — это доменная арифметика компонента;\n * - не знает про порог начала жеста: он нужен сортировке, где нажатие ещё не\n * означает перенос, и появится вместе с ней.\n *\n * ```ts\n * const drag = useDragGesture({\n * disabled: () => isDisabled.value,\n * onStart: (event) => { activeThumb.value = nearestThumb(event) },\n * onMove: (event) => setValue(valueFromPointer(event), false),\n * onEnd: () => commit(),\n * onCancel: () => rollback(),\n * })\n *\n * <div @pointerdown=\"drag.start\" />\n * ```\n */\nexport interface UseDragGestureOptions {\n /** Жест не начинается: `disabled` / `readonly`. */\n disabled?: () => boolean\n /**\n * Нажатие прошло проверки. Вернуть `false` — жест не начинать: так\n * отсеивается попадание мимо цели, когда `disabled` тут ни при чём.\n */\n onStart?: (event: PointerEvent) => boolean | void\n onMove: (event: PointerEvent) => void\n /** Жест завершён отпусканием: значение коммитится. */\n onEnd?: (event?: PointerEvent) => void\n /** Жест оборван: откат к состоянию до нажатия. */\n onCancel?: () => void\n}\n\nexport interface UseDragGestureReturn {\n /** Идёт ли жест. Менять снаружи не нужно — состоянием владеет примитив. */\n isDragging: Ref<boolean>\n /** Вешается на `@pointerdown` цели. */\n start: (event: PointerEvent) => void\n /** Принудительное завершение. По умолчанию — как обрыв, без коммита. */\n stop: (commit?: boolean) => void\n}\n\nexport function useDragGesture(options: UseDragGestureOptions): UseDragGestureReturn {\n const isDragging = ref(false)\n\n // Флаг вместо проверки `isDragging`: `detach` зовётся и из `onScopeDispose`,\n // где жеста могло не быть вовсе, — а на сервере `window` трогать нельзя.\n let attached = false\n\n function onPointerMove(event: PointerEvent): void {\n if (!isDragging.value)
|
|
1
|
+
{"version":3,"file":"useDragGesture.js","names":[],"sources":["../../src/composables/useDragGesture.ts"],"sourcesContent":["import type { Ref } from 'vue'\nimport { onScopeDispose, ref } from 'vue'\n\n/**\n * Скелет указательного жеста: нажал, повёл, отпустил.\n *\n * Механика написана в пакете дважды дословно (`GrSlider`, `GrSplitter`) и\n * ещё раз иначе (`GrImageViewer`), а следующие потребители известны заранее —\n * swipe у `GrToaster`, перенос узла в `GrTree`, ресайз колонок `GrDataTable`.\n * Дублируется при этом не разметка, а **правило**: оборванный жест не\n * коммитит. Живя в двух копиях, оно разъезжается молча.\n *\n * Слушатели вешаются на `window`, а не на элемент: указатель уходит за\n * границы дорожки и разделителя постоянно, и жест обязан это переживать.\n * Захват (`setPointerCapture`) дал бы то же самое, но привязал бы жест к\n * живому DOM-узлу — а он в середине жеста может перерисоваться.\n *\n * У жеста **два разных исхода**, и это главное, ради чего примитив заведён:\n * `pointerup` — завершение (значение коммитится), `pointercancel` — обрыв\n * (браузер забрал указатель: системный жест, скролл, потеря окна), и тогда\n * состояние возвращается к тому, что было до нажатия.\n *\n * Осознанно **не** делает:\n * - не вызывает `preventDefault` — в слайдере и сплиттере это гашение\n * выделения текста, а в панорамировании картинки помешало бы; строка\n * дешевле опции, меняющей поведение невидимо;\n * - не считает координаты и дельты — это доменная арифметика компонента;\n * - не знает про порог начала жеста: он нужен сортировке, где нажатие ещё не\n * означает перенос, и появится вместе с ней.\n *\n * ```ts\n * const drag = useDragGesture({\n * disabled: () => isDisabled.value,\n * onStart: (event) => { activeThumb.value = nearestThumb(event) },\n * onMove: (event) => setValue(valueFromPointer(event), false),\n * onEnd: () => commit(),\n * onCancel: () => rollback(),\n * })\n *\n * <div @pointerdown=\"drag.start\" />\n * ```\n */\nexport interface UseDragGestureOptions {\n /** Жест не начинается: `disabled` / `readonly`. */\n disabled?: () => boolean\n /**\n * Нажатие прошло проверки. Вернуть `false` — жест не начинать: так\n * отсеивается попадание мимо цели, когда `disabled` тут ни при чём.\n */\n onStart?: (event: PointerEvent) => boolean | void\n onMove: (event: PointerEvent) => void\n /** Жест завершён отпусканием: значение коммитится. */\n onEnd?: (event?: PointerEvent) => void\n /** Жест оборван: откат к состоянию до нажатия. */\n onCancel?: () => void\n}\n\nexport interface UseDragGestureReturn {\n /** Идёт ли жест. Менять снаружи не нужно — состоянием владеет примитив. */\n isDragging: Ref<boolean>\n /** Вешается на `@pointerdown` цели. */\n start: (event: PointerEvent) => void\n /** Принудительное завершение. По умолчанию — как обрыв, без коммита. */\n stop: (commit?: boolean) => void\n}\n\nexport function useDragGesture(options: UseDragGestureOptions): UseDragGestureReturn {\n const isDragging = ref(false)\n\n // Флаг вместо проверки `isDragging`: `detach` зовётся и из `onScopeDispose`,\n // где жеста могло не быть вовсе, — а на сервере `window` трогать нельзя.\n let attached = false\n\n function onPointerMove(event: PointerEvent): void {\n if (!isDragging.value)\n return\n\n options.onMove(event)\n }\n\n function onPointerUp(event: PointerEvent): void {\n finish(true, event)\n }\n\n function onPointerCancel(): void {\n finish(false)\n }\n\n function detach(): void {\n if (!attached)\n return\n\n attached = false\n window.removeEventListener('pointermove', onPointerMove)\n window.removeEventListener('pointerup', onPointerUp)\n window.removeEventListener('pointercancel', onPointerCancel)\n }\n\n function finish(commit: boolean, event?: PointerEvent): void {\n if (!isDragging.value)\n return\n\n isDragging.value = false\n detach()\n\n if (commit)\n options.onEnd?.(event)\n else options.onCancel?.()\n }\n\n function start(event: PointerEvent): void {\n if (isDragging.value)\n return\n if (options.disabled?.())\n return\n // Только основная кнопка: правый и средний клик перетаскиванием не считаются.\n if (event.button !== 0)\n return\n if (options.onStart?.(event) === false)\n return\n\n isDragging.value = true\n attached = true\n window.addEventListener('pointermove', onPointerMove)\n window.addEventListener('pointerup', onPointerUp)\n window.addEventListener('pointercancel', onPointerCancel)\n }\n\n function stop(commit = false): void {\n finish(commit)\n }\n\n // Область умерла посреди жеста: слушатели снимаются, но обработчики не\n // зовутся — коммитить и откатывать уже некому и некуда.\n onScopeDispose(() => {\n detach()\n isDragging.value = false\n })\n\n return { isDragging, start, stop }\n}\n"],"mappings":";;AAkEA,SAAgB,eAAe,SAAsD;CACnF,MAAM,aAAa,IAAI,KAAK;CAI5B,IAAI,WAAW;CAEf,SAAS,cAAc,OAA2B;EAChD,IAAI,CAAC,WAAW,OACd;EAEF,QAAQ,OAAO,KAAK;CACtB;CAEA,SAAS,YAAY,OAA2B;EAC9C,OAAO,MAAM,KAAK;CACpB;CAEA,SAAS,kBAAwB;EAC/B,OAAO,KAAK;CACd;CAEA,SAAS,SAAe;EACtB,IAAI,CAAC,UACH;EAEF,WAAW;EACX,OAAO,oBAAoB,eAAe,aAAa;EACvD,OAAO,oBAAoB,aAAa,WAAW;EACnD,OAAO,oBAAoB,iBAAiB,eAAe;CAC7D;CAEA,SAAS,OAAO,QAAiB,OAA4B;EAC3D,IAAI,CAAC,WAAW,OACd;EAEF,WAAW,QAAQ;EACnB,OAAO;EAEP,IAAI,QACF,QAAQ,QAAQ,KAAK;OAClB,QAAQ,WAAW;CAC1B;CAEA,SAAS,MAAM,OAA2B;EACxC,IAAI,WAAW,OACb;EACF,IAAI,QAAQ,WAAW,GACrB;EAEF,IAAI,MAAM,WAAW,GACnB;EACF,IAAI,QAAQ,UAAU,KAAK,MAAM,OAC/B;EAEF,WAAW,QAAQ;EACnB,WAAW;EACX,OAAO,iBAAiB,eAAe,aAAa;EACpD,OAAO,iBAAiB,aAAa,WAAW;EAChD,OAAO,iBAAiB,iBAAiB,eAAe;CAC1D;CAEA,SAAS,KAAK,SAAS,OAAa;EAClC,OAAO,MAAM;CACf;CAIA,qBAAqB;EACnB,OAAO;EACP,WAAW,QAAQ;CACrB,CAAC;CAED,OAAO;EAAE;EAAY;EAAO;CAAK;AACnC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as floatingLayerZIndex } from "../chunks/overlayStack-0CZS99Da.js";
|
|
2
2
|
import { nextTick, onBeforeUnmount, ref, watch } from "vue";
|
|
3
3
|
import { autoUpdate, computePosition, flip, offset, shift, size } from "@floating-ui/dom";
|
|
4
4
|
//#region src/composables/useFloating.ts
|
|
@@ -9,26 +9,6 @@ import { autoUpdate, computePosition, flip, offset, shift, size } from "@floatin
|
|
|
9
9
|
* официального `@floating-ui/vue` (`useFloating` → `roundByDPR`), чтобы не тянуть весь
|
|
10
10
|
* пакет ради одной утилиты.
|
|
11
11
|
*/
|
|
12
|
-
/**
|
|
13
|
-
* Высота панели: своя шкала снаружи и подъём над окном внутри него.
|
|
14
|
-
*
|
|
15
|
-
* Панель телепортируется в общий портал, то есть становится **соседом** корня
|
|
16
|
-
* модального окна, а не его потомком: собственный stacking-контекст окна её не
|
|
17
|
-
* накрывает, и статический `--gr-z-dropdown` (1000) оставлял её под окном
|
|
18
|
-
* (`--gr-z-modal`, 1100). Поэтому высоту берём из стека слоёв — единственного
|
|
19
|
-
* места, которое знает, что сейчас открыто.
|
|
20
|
-
*
|
|
21
|
-
* Слагаемое — **число** модальных слоёв, а не шаг шкалы: вложенные окна дают
|
|
22
|
-
* +2, а полноэкранная загрузка (1150) и тосты (1200) остаются сверху.
|
|
23
|
-
*
|
|
24
|
-
* Обратный случай — «поповер снаружи не должен перекрывать окно» — не страдает:
|
|
25
|
-
* пока окно открыто, страница под ним в `inert`, и открыть там поповер нечем.
|
|
26
|
-
* Ненулевой счётчик означает ровно «панель открыта изнутри окна».
|
|
27
|
-
*/
|
|
28
|
-
function floatingZIndex(zIndexVar) {
|
|
29
|
-
const modals = openModalCount();
|
|
30
|
-
return modals > 0 ? `calc(var(--gr-z-modal) + ${modals})` : `var(${zIndexVar})`;
|
|
31
|
-
}
|
|
32
12
|
function getDpr(element) {
|
|
33
13
|
if (typeof window === "undefined") return 1;
|
|
34
14
|
return (element.ownerDocument?.defaultView ?? window).devicePixelRatio || 1;
|
|
@@ -133,7 +113,7 @@ function useFloating(reference, floatingEl, open, options = {}) {
|
|
|
133
113
|
position: "fixed",
|
|
134
114
|
left: `${roundByDpr(floating, x)}px`,
|
|
135
115
|
top: `${roundByDpr(floating, y)}px`,
|
|
136
|
-
zIndex:
|
|
116
|
+
zIndex: floatingLayerZIndex(options.zIndexVar ?? "--gr-z-dropdown")
|
|
137
117
|
};
|
|
138
118
|
}
|
|
139
119
|
function stop() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useFloating.js","names":[],"sources":["../../src/composables/useFloating.ts"],"sourcesContent":["import type { CSSProperties, Ref } from 'vue'\nimport type { Middleware, Placement, VirtualElement } from '@floating-ui/dom'\nimport { autoUpdate, computePosition, flip, offset as offsetMiddleware, shift, size as sizeMiddleware } from '@floating-ui/dom'\nimport { nextTick, onBeforeUnmount, ref, watch } from 'vue'\n\nimport { openModalCount } from './internal/overlayStack'\n\nexport type UseFloatingPlacement = Placement\n\n/**\n * Device pixel ratio плавающего элемента. Нужен, чтобы округлять координаты к сетке\n * физических пикселей (см. `roundByDPR`) — иначе субпиксельные `left/top` дают размытый\n * текст/границы панели на не-целочисленных позициях и HiDPI-экранах. Подход перенят у\n * официального `@floating-ui/vue` (`useFloating` → `roundByDPR`), чтобы не тянуть весь\n * пакет ради одной утилиты.\n */\n/**\n * Высота панели: своя шкала снаружи и подъём над окном внутри него.\n *\n * Панель телепортируется в общий портал, то есть становится **соседом** корня\n * модального окна, а не его потомком: собственный stacking-контекст окна её не\n * накрывает, и статический `--gr-z-dropdown` (1000) оставлял её под окном\n * (`--gr-z-modal`, 1100). Поэтому высоту берём из стека слоёв — единственного\n * места, которое знает, что сейчас открыто.\n *\n * Слагаемое — **число** модальных слоёв, а не шаг шкалы: вложенные окна дают\n * +2, а полноэкранная загрузка (1150) и тосты (1200) остаются сверху.\n *\n * Обратный случай — «поповер снаружи не должен перекрывать окно» — не страдает:\n * пока окно открыто, страница под ним в `inert`, и открыть там поповер нечем.\n * Ненулевой счётчик означает ровно «панель открыта изнутри окна».\n */\nfunction floatingZIndex(zIndexVar: string): string {\n const modals = openModalCount()\n\n return modals > 0 ? `calc(var(--gr-z-modal) + ${modals})` : `var(${zIndexVar})`\n}\n\nfunction getDpr(element: HTMLElement): number {\n if (typeof window === 'undefined')\n return 1\n\n const win = element.ownerDocument?.defaultView ?? window\n return win.devicePixelRatio || 1\n}\n\nfunction roundByDpr(element: HTMLElement, value: number): number {\n const dpr = getDpr(element)\n return Math.round(value * dpr) / dpr\n}\n\n/**\n * Прямоугольник в координатах вьюпорта — якорь, которого нет в DOM.\n *\n * Точка курсора описывается прямоугольником `0×0`. Размеры необязательны именно\n * поэтому: контекстное меню, открытое с клавиатуры, встаёт у **прямоугольника**\n * сфокусированной строки, а не у её угла, и переворачивается вместе с ней.\n */\nexport interface GrFloatingAnchorRect {\n x: number\n y: number\n /** По умолчанию 0. */\n width?: number\n /** По умолчанию 0. */\n height?: number\n}\n\n/** Ссылка позиционирования: элемент, виртуальный якорь или ничего. */\nexport type UseFloatingReference = HTMLElement | VirtualElement | null\n\n/**\n * Виртуальный якорь: объект создаётся один раз, координаты читает геттер на\n * каждом измерении.\n *\n * Стабильность объекта здесь не стилистика, а условие работы: `useFloating`\n * пересоздаёт подписку `autoUpdate` при смене самой ссылки, поэтому новый объект\n * на каждое движение курсора рвал бы и заводил её заново. С одним объектом смена\n * точки — это просто `update()`.\n */\nexport function createFloatingAnchor(\n get: () => GrFloatingAnchorRect | null | undefined,\n options: { contextElement?: () => Element | null | undefined } = {},\n): VirtualElement {\n return {\n getBoundingClientRect() {\n const rect = get() ?? { x: 0, y: 0 }\n const width = rect.width ?? 0\n const height = rect.height ?? 0\n\n return {\n x: rect.x,\n y: rect.y,\n width,\n height,\n top: rect.y,\n left: rect.x,\n right: rect.x + width,\n bottom: rect.y + height,\n }\n },\n get contextElement() {\n return options.contextElement?.() ?? undefined\n },\n }\n}\n\nexport interface UseFloatingOptions {\n /**\n * Предпочитаемое место панели относительно триггера. По умолчанию `bottom-start`.\n * Можно передать функцию-геттер, если место зависит от реактивного пропа\n * (например, `align`) — она вызывается заново при каждом пересчёте позиции.\n */\n placement?: UseFloatingPlacement | (() => UseFloatingPlacement)\n /** Зазор между триггером и панелью, px. По умолчанию `8`. */\n offsetPx?: number\n /** Минимальный отступ от края viewport при flip/shift, px. По умолчанию `8`. */\n boundaryPadding?: number\n /**\n * Согласовать ширину панели с шириной триггера (актуально для Select/TreeSelect).\n * Не годится для Dropdown/Tooltip, где ширина панели определяется контентом.\n * - `true` — точная ширина (`width = ширина триггера`);\n * - `'min'` — панель растёт по контенту (`width: max-content`), но не уже триггера\n * (`min-width = ширина триггера`) — режим `GrSelect view=\"link\"`;\n * - функция-геттер — как и `placement`, для реактивной зависимости от пропа.\n *\n * С виртуальным якорем не работает: ширина точки равна нулю.\n */\n matchWidth?: boolean | 'min' | (() => boolean | 'min')\n /**\n * CSS-переменная шкалы слоёв (см. `styles/tokens.css`). По умолчанию\n * `--gr-z-dropdown`. Действует, пока панель открыта вне модального окна: над\n * окном высоту задаёт стек слоёв (см. {@link floatingZIndex}).\n */\n zIndexVar?: string\n}\n\nexport interface UseFloatingReturn {\n /** Готовый объект для `:style` на плавающем элементе. */\n floatingStyle: Ref<CSSProperties>\n /**\n * Итоговое место панели ПОСЛЕ применения `flip` — может отличаться от запрошенного\n * `placement`, если места не хватило и панель перевернуло на противоположную сторону.\n * Полезно для выбора `transform-origin`/направления transition в потребителе.\n */\n resolvedPlacement: Ref<Placement>\n /** Принудительный пересчёт позиции (например, после изменения контента панели). */\n update: () => void\n}\n\n/**\n * Единый движок позиционирования floating-панелей (Dropdown/Select/TreeSelect/Tooltip)\n * поверх `@floating-ui/dom`: flip (переворот при нехватке места), shift (клэмп к границам\n * viewport), опциональный `size`-мидлвар для растягивания по ширине триггера.\n *\n * Пересчёт положения при скролле/ресайзе делает `autoUpdate` из `@floating-ui/dom` —\n * он сам использует `ResizeObserver`/`IntersectionObserver` и пассивные слушатели вместо\n * наивного враппера на каждый `scroll`/`resize`, поэтому отдельный rAF-throttling здесь\n * не нужен (в отличие от прежней ручной реализации в `GrSelect`/`GrDropdown`).\n *\n * Требует, чтобы `floatingEl` был примонтирован в DOM на момент открытия (обычно —\n * `<teleport to=\"body\">` + `v-show`, а не `v-if`): позиционирование не может измерить\n * элемент, которого нет в дереве.\n */\nexport function useFloating(\n // Объединение, а не `Ref<HTMLElement | VirtualElement | null>`: аксессорный\n // `Ref` проверяется контравариантно по записи, и расширение элементного типа\n // отняло бы у шести нынешних потребителей право передавать свой `Ref` как есть.\n reference: Ref<HTMLElement | null> | (() => UseFloatingReference),\n floatingEl: Ref<HTMLElement | null>,\n open: Ref<boolean>,\n options: UseFloatingOptions = {},\n): UseFloatingReturn {\n const readReference: () => UseFloatingReference\n = typeof reference === 'function' ? reference : () => reference.value\n\n const floatingStyle = ref<CSSProperties>({\n position: 'fixed',\n top: '0px',\n left: '0px',\n })\n function resolveRequestedPlacement(): Placement {\n return typeof options.placement === 'function' ? options.placement() : options.placement ?? 'bottom-start'\n }\n\n function resolveMatchWidth(): boolean | 'min' {\n return typeof options.matchWidth === 'function' ? options.matchWidth() : options.matchWidth ?? false\n }\n\n const resolvedPlacement = ref<Placement>(resolveRequestedPlacement())\n\n let stopAutoUpdate: (() => void) | null = null\n\n async function update(): Promise<void> {\n const reference = readReference()\n const floating = floatingEl.value\n if (!reference || !floating)\n return\n\n const middleware: Middleware[] = [\n offsetMiddleware(options.offsetPx ?? 8),\n flip({ padding: options.boundaryPadding ?? 8 }),\n shift({ padding: options.boundaryPadding ?? 8 }),\n ]\n\n const matchWidth = resolveMatchWidth()\n if (matchWidth) {\n middleware.push(\n sizeMiddleware({\n apply({ rects, elements }) {\n if (matchWidth === 'min') {\n Object.assign(elements.floating.style, {\n width: 'max-content',\n minWidth: `${rects.reference.width}px`,\n })\n return\n }\n\n Object.assign(elements.floating.style, {\n width: `${rects.reference.width}px`,\n minWidth: '',\n })\n },\n }),\n )\n }\n\n const { x, y, placement } = await computePosition(reference, floating, {\n placement: resolveRequestedPlacement(),\n strategy: 'fixed',\n middleware,\n })\n\n resolvedPlacement.value = placement\n floatingStyle.value = {\n position: 'fixed',\n left: `${roundByDpr(floating, x)}px`,\n top: `${roundByDpr(floating, y)}px`,\n zIndex: floatingZIndex(options.zIndexVar ?? '--gr-z-dropdown'),\n }\n }\n\n function stop(): void {\n stopAutoUpdate?.()\n stopAutoUpdate = null\n }\n\n function start(): void {\n stop()\n const reference = readReference()\n const floating = floatingEl.value\n if (!reference || !floating)\n return\n\n // `autoUpdate` подписывается на `ResizeObserver`/`IntersectionObserver` для\n // последующих пересчётов, но не гарантирует немедленный вызов `update` —\n // особенно в средах без этих API (например, jsdom в тестах). Считаем позицию\n // сразу, чтобы панель не «мигала» в 0,0 до первого scroll/resize.\n void update()\n stopAutoUpdate = autoUpdate(reference, floating, () => void update())\n }\n\n // На какую ссылку подписан текущий `autoUpdate`. Отличает смену самого якоря\n // (подписку пересоздать) от смены его координат (достаточно `update()`): у\n // виртуального якоря объект один на всё время жизни.\n let subscribedTo: UseFloatingReference = null\n\n watch(\n [open, () => readReference()],\n ([isOpen, nextReference]) => {\n if (!isOpen || !nextReference) {\n stop()\n subscribedTo = null\n return\n }\n\n if (nextReference === subscribedTo && stopAutoUpdate)\n return\n\n // `floatingEl` монтируется в том же тике, что и `open` переключается\n // (v-show уже в DOM, но teleport/переход могут ещё не отработать) —\n // ждём тик, чтобы `getBoundingClientRect` не читал нулевые размеры.\n // Перепроверка `open`: панель могли закрыть, пока `start()` ждал в\n // очереди, — `stop()` уже отработал, и снимать подписку было бы некому.\n void nextTick(() => {\n if (!open.value)\n return\n subscribedTo = readReference()\n start()\n })\n },\n { immediate: true },\n )\n\n onBeforeUnmount(stop)\n\n return {\n floatingStyle,\n resolvedPlacement,\n update: () => void update(),\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useFloating.js","names":[],"sources":["../../src/composables/useFloating.ts"],"sourcesContent":["import type { CSSProperties, Ref } from 'vue'\nimport type { Middleware, Placement, VirtualElement } from '@floating-ui/dom'\nimport { autoUpdate, computePosition, flip, offset as offsetMiddleware, shift, size as sizeMiddleware } from '@floating-ui/dom'\nimport { nextTick, onBeforeUnmount, ref, watch } from 'vue'\n\nimport { floatingLayerZIndex } from './internal/overlayStack'\n\nexport type UseFloatingPlacement = Placement\n\n/**\n * Device pixel ratio плавающего элемента. Нужен, чтобы округлять координаты к сетке\n * физических пикселей (см. `roundByDPR`) — иначе субпиксельные `left/top` дают размытый\n * текст/границы панели на не-целочисленных позициях и HiDPI-экранах. Подход перенят у\n * официального `@floating-ui/vue` (`useFloating` → `roundByDPR`), чтобы не тянуть весь\n * пакет ради одной утилиты.\n */\nfunction getDpr(element: HTMLElement): number {\n if (typeof window === 'undefined')\n return 1\n\n const win = element.ownerDocument?.defaultView ?? window\n return win.devicePixelRatio || 1\n}\n\nfunction roundByDpr(element: HTMLElement, value: number): number {\n const dpr = getDpr(element)\n return Math.round(value * dpr) / dpr\n}\n\n/**\n * Прямоугольник в координатах вьюпорта — якорь, которого нет в DOM.\n *\n * Точка курсора описывается прямоугольником `0×0`. Размеры необязательны именно\n * поэтому: контекстное меню, открытое с клавиатуры, встаёт у **прямоугольника**\n * сфокусированной строки, а не у её угла, и переворачивается вместе с ней.\n */\nexport interface GrFloatingAnchorRect {\n x: number\n y: number\n /** По умолчанию 0. */\n width?: number\n /** По умолчанию 0. */\n height?: number\n}\n\n/** Ссылка позиционирования: элемент, виртуальный якорь или ничего. */\nexport type UseFloatingReference = HTMLElement | VirtualElement | null\n\n/**\n * Виртуальный якорь: объект создаётся один раз, координаты читает геттер на\n * каждом измерении.\n *\n * Стабильность объекта здесь не стилистика, а условие работы: `useFloating`\n * пересоздаёт подписку `autoUpdate` при смене самой ссылки, поэтому новый объект\n * на каждое движение курсора рвал бы и заводил её заново. С одним объектом смена\n * точки — это просто `update()`.\n */\nexport function createFloatingAnchor(\n get: () => GrFloatingAnchorRect | null | undefined,\n options: { contextElement?: () => Element | null | undefined } = {},\n): VirtualElement {\n return {\n getBoundingClientRect() {\n const rect = get() ?? { x: 0, y: 0 }\n const width = rect.width ?? 0\n const height = rect.height ?? 0\n\n return {\n x: rect.x,\n y: rect.y,\n width,\n height,\n top: rect.y,\n left: rect.x,\n right: rect.x + width,\n bottom: rect.y + height,\n }\n },\n get contextElement() {\n return options.contextElement?.() ?? undefined\n },\n }\n}\n\nexport interface UseFloatingOptions {\n /**\n * Предпочитаемое место панели относительно триггера. По умолчанию `bottom-start`.\n * Можно передать функцию-геттер, если место зависит от реактивного пропа\n * (например, `align`) — она вызывается заново при каждом пересчёте позиции.\n */\n placement?: UseFloatingPlacement | (() => UseFloatingPlacement)\n /** Зазор между триггером и панелью, px. По умолчанию `8`. */\n offsetPx?: number\n /** Минимальный отступ от края viewport при flip/shift, px. По умолчанию `8`. */\n boundaryPadding?: number\n /**\n * Согласовать ширину панели с шириной триггера (актуально для Select/TreeSelect).\n * Не годится для Dropdown/Tooltip, где ширина панели определяется контентом.\n * - `true` — точная ширина (`width = ширина триггера`);\n * - `'min'` — панель растёт по контенту (`width: max-content`), но не уже триггера\n * (`min-width = ширина триггера`) — режим `GrSelect view=\"link\"`;\n * - функция-геттер — как и `placement`, для реактивной зависимости от пропа.\n *\n * С виртуальным якорем не работает: ширина точки равна нулю.\n */\n matchWidth?: boolean | 'min' | (() => boolean | 'min')\n /**\n * CSS-переменная шкалы слоёв (см. `styles/tokens.css`). По умолчанию\n * `--gr-z-dropdown`. Действует, пока панель открыта вне модального окна: над\n * окном высоту задаёт стек слоёв (см. {@link floatingZIndex}).\n */\n zIndexVar?: string\n}\n\nexport interface UseFloatingReturn {\n /** Готовый объект для `:style` на плавающем элементе. */\n floatingStyle: Ref<CSSProperties>\n /**\n * Итоговое место панели ПОСЛЕ применения `flip` — может отличаться от запрошенного\n * `placement`, если места не хватило и панель перевернуло на противоположную сторону.\n * Полезно для выбора `transform-origin`/направления transition в потребителе.\n */\n resolvedPlacement: Ref<Placement>\n /** Принудительный пересчёт позиции (например, после изменения контента панели). */\n update: () => void\n}\n\n/**\n * Единый движок позиционирования floating-панелей (Dropdown/Select/TreeSelect/Tooltip)\n * поверх `@floating-ui/dom`: flip (переворот при нехватке места), shift (клэмп к границам\n * viewport), опциональный `size`-мидлвар для растягивания по ширине триггера.\n *\n * Пересчёт положения при скролле/ресайзе делает `autoUpdate` из `@floating-ui/dom` —\n * он сам использует `ResizeObserver`/`IntersectionObserver` и пассивные слушатели вместо\n * наивного враппера на каждый `scroll`/`resize`, поэтому отдельный rAF-throttling здесь\n * не нужен (в отличие от прежней ручной реализации в `GrSelect`/`GrDropdown`).\n *\n * Требует, чтобы `floatingEl` был примонтирован в DOM на момент открытия (обычно —\n * `<teleport to=\"body\">` + `v-show`, а не `v-if`): позиционирование не может измерить\n * элемент, которого нет в дереве.\n */\nexport function useFloating(\n // Объединение, а не `Ref<HTMLElement | VirtualElement | null>`: аксессорный\n // `Ref` проверяется контравариантно по записи, и расширение элементного типа\n // отняло бы у шести нынешних потребителей право передавать свой `Ref` как есть.\n reference: Ref<HTMLElement | null> | (() => UseFloatingReference),\n floatingEl: Ref<HTMLElement | null>,\n open: Ref<boolean>,\n options: UseFloatingOptions = {},\n): UseFloatingReturn {\n const readReference: () => UseFloatingReference\n = typeof reference === 'function' ? reference : () => reference.value\n\n const floatingStyle = ref<CSSProperties>({\n position: 'fixed',\n top: '0px',\n left: '0px',\n })\n function resolveRequestedPlacement(): Placement {\n return typeof options.placement === 'function' ? options.placement() : options.placement ?? 'bottom-start'\n }\n\n function resolveMatchWidth(): boolean | 'min' {\n return typeof options.matchWidth === 'function' ? options.matchWidth() : options.matchWidth ?? false\n }\n\n const resolvedPlacement = ref<Placement>(resolveRequestedPlacement())\n\n let stopAutoUpdate: (() => void) | null = null\n\n async function update(): Promise<void> {\n const reference = readReference()\n const floating = floatingEl.value\n if (!reference || !floating)\n return\n\n const middleware: Middleware[] = [\n offsetMiddleware(options.offsetPx ?? 8),\n flip({ padding: options.boundaryPadding ?? 8 }),\n shift({ padding: options.boundaryPadding ?? 8 }),\n ]\n\n const matchWidth = resolveMatchWidth()\n if (matchWidth) {\n middleware.push(\n sizeMiddleware({\n apply({ rects, elements }) {\n if (matchWidth === 'min') {\n Object.assign(elements.floating.style, {\n width: 'max-content',\n minWidth: `${rects.reference.width}px`,\n })\n return\n }\n\n Object.assign(elements.floating.style, {\n width: `${rects.reference.width}px`,\n minWidth: '',\n })\n },\n }),\n )\n }\n\n const { x, y, placement } = await computePosition(reference, floating, {\n placement: resolveRequestedPlacement(),\n strategy: 'fixed',\n middleware,\n })\n\n resolvedPlacement.value = placement\n floatingStyle.value = {\n position: 'fixed',\n left: `${roundByDpr(floating, x)}px`,\n top: `${roundByDpr(floating, y)}px`,\n zIndex: floatingLayerZIndex(options.zIndexVar ?? '--gr-z-dropdown'),\n }\n }\n\n function stop(): void {\n stopAutoUpdate?.()\n stopAutoUpdate = null\n }\n\n function start(): void {\n stop()\n const reference = readReference()\n const floating = floatingEl.value\n if (!reference || !floating)\n return\n\n // `autoUpdate` подписывается на `ResizeObserver`/`IntersectionObserver` для\n // последующих пересчётов, но не гарантирует немедленный вызов `update` —\n // особенно в средах без этих API (например, jsdom в тестах). Считаем позицию\n // сразу, чтобы панель не «мигала» в 0,0 до первого scroll/resize.\n void update()\n stopAutoUpdate = autoUpdate(reference, floating, () => void update())\n }\n\n // На какую ссылку подписан текущий `autoUpdate`. Отличает смену самого якоря\n // (подписку пересоздать) от смены его координат (достаточно `update()`): у\n // виртуального якоря объект один на всё время жизни.\n let subscribedTo: UseFloatingReference = null\n\n watch(\n [open, () => readReference()],\n ([isOpen, nextReference]) => {\n if (!isOpen || !nextReference) {\n stop()\n subscribedTo = null\n return\n }\n\n if (nextReference === subscribedTo && stopAutoUpdate)\n return\n\n // `floatingEl` монтируется в том же тике, что и `open` переключается\n // (v-show уже в DOM, но teleport/переход могут ещё не отработать) —\n // ждём тик, чтобы `getBoundingClientRect` не читал нулевые размеры.\n // Перепроверка `open`: панель могли закрыть, пока `start()` ждал в\n // очереди, — `stop()` уже отработал, и снимать подписку было бы некому.\n void nextTick(() => {\n if (!open.value)\n return\n subscribedTo = readReference()\n start()\n })\n },\n { immediate: true },\n )\n\n onBeforeUnmount(stop)\n\n return {\n floatingStyle,\n resolvedPlacement,\n update: () => void update(),\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useFocusTrap.js","names":[],"sources":["../../src/composables/useFocusTrap.ts"],"sourcesContent":["import type { Ref } from 'vue'\nimport { nextTick, onScopeDispose, watch } from 'vue'\n\nimport { getFocusableElementsIn, isFocusable } from './internal/focusables'\n\nexport interface UseFocusTrapOptions {\n /**\n * Работает ли ловушка сейчас. Геттер, а не значение: модальное окно держит\n * фокус, только пока оно верхнее в стеке слоёв, — иначе нижнее окно отбирало\n * бы фокус у открытого поверх него.\n */\n active?: () => boolean\n /** Кому отдать фокус при активации. Не задан или не фокусируем — первый в контейнере. */\n initialFocus?: () => HTMLElement | null | undefined\n /**\n * Запасной приёмник фокуса, когда фокусировать внутри нечего: обычно сама\n * панель с `tabindex=\"-1\"`. Без него фокус остался бы снаружи, и Tab увёл бы\n * пользователя на страницу под окном.\n */\n fallbackFocus?: () => HTMLElement | null | undefined\n /**\n * Дополнительные корни, которые считаются частью слоя. Панель селекта,\n * открытого внутри окна, телепортирована в `body` и лежит вне поддерева\n * контейнера — без этого списка ловушка отбирала бы у неё фокус.\n */\n containers?: () => (HTMLElement | null | undefined)[]\n /**\n * Возвращать фокус туда, где он был до активации. По умолчанию — да.\n * Оверлеи пакета выключают: возврат делает стек слоёв по более строгому\n * правилу (только если фокус на момент закрытия ещё внутри слоя).\n */\n restoreFocus?: boolean\n}\n\n/**\n * Ловушка фокуса: пока активна, Tab ходит по кругу внутри слоя, а фокус,\n * утёкший наружу, возвращается обратно.\n *\n * Реализована на `keydown` и `focusin`, **без sentinel-узлов**: распространённый\n * подход с двумя focus-guard кнопками по краям кладёт в разметку интерактивные\n * элементы, а внутрь контейнера с ARIA-ролью их класть нельзя — роль объявляет\n * потомков презентационными (axe: `nested-interactive`).\n *\n * Слой из ловушки не выводится: порядок окон, Esc и `inert` — забота\n * `useOverlayLayer`. Композиция такая:\n *\n * ```ts\n * const layer = useOverlayLayer(open, close, { modal: true, root: panelRef })\n * useFocusTrap(panelRef, {\n * active: () => open.value && isTopmost.value,\n * containers: layer.rootsAbove,\n * })\n * ```\n */\nexport function useFocusTrap(\n container: Ref<HTMLElement | null>,\n options: UseFocusTrapOptions = {},\n): void {\n let previouslyFocused: HTMLElement | null = null\n let listening = false\n // Последний законный держатель фокуса: с него начинается возврат, когда\n // фокус ушёл в никуда (например, кликом по неинтерактивному фону).\n let lastFocusedInside: HTMLElement | null = null\n\n function roots(): HTMLElement[] {\n const extra = options.containers?.() ?? []\n return [container.value, ...extra].filter((root): root is HTMLElement => Boolean(root))\n }\n\n function contains(target: Node | null): boolean {\n if (!target)
|
|
1
|
+
{"version":3,"file":"useFocusTrap.js","names":[],"sources":["../../src/composables/useFocusTrap.ts"],"sourcesContent":["import type { Ref } from 'vue'\nimport { nextTick, onScopeDispose, watch } from 'vue'\n\nimport { getFocusableElementsIn, isFocusable } from './internal/focusables'\n\nexport interface UseFocusTrapOptions {\n /**\n * Работает ли ловушка сейчас. Геттер, а не значение: модальное окно держит\n * фокус, только пока оно верхнее в стеке слоёв, — иначе нижнее окно отбирало\n * бы фокус у открытого поверх него.\n */\n active?: () => boolean\n /** Кому отдать фокус при активации. Не задан или не фокусируем — первый в контейнере. */\n initialFocus?: () => HTMLElement | null | undefined\n /**\n * Запасной приёмник фокуса, когда фокусировать внутри нечего: обычно сама\n * панель с `tabindex=\"-1\"`. Без него фокус остался бы снаружи, и Tab увёл бы\n * пользователя на страницу под окном.\n */\n fallbackFocus?: () => HTMLElement | null | undefined\n /**\n * Дополнительные корни, которые считаются частью слоя. Панель селекта,\n * открытого внутри окна, телепортирована в `body` и лежит вне поддерева\n * контейнера — без этого списка ловушка отбирала бы у неё фокус.\n */\n containers?: () => (HTMLElement | null | undefined)[]\n /**\n * Возвращать фокус туда, где он был до активации. По умолчанию — да.\n * Оверлеи пакета выключают: возврат делает стек слоёв по более строгому\n * правилу (только если фокус на момент закрытия ещё внутри слоя).\n */\n restoreFocus?: boolean\n}\n\n/**\n * Ловушка фокуса: пока активна, Tab ходит по кругу внутри слоя, а фокус,\n * утёкший наружу, возвращается обратно.\n *\n * Реализована на `keydown` и `focusin`, **без sentinel-узлов**: распространённый\n * подход с двумя focus-guard кнопками по краям кладёт в разметку интерактивные\n * элементы, а внутрь контейнера с ARIA-ролью их класть нельзя — роль объявляет\n * потомков презентационными (axe: `nested-interactive`).\n *\n * Слой из ловушки не выводится: порядок окон, Esc и `inert` — забота\n * `useOverlayLayer`. Композиция такая:\n *\n * ```ts\n * const layer = useOverlayLayer(open, close, { modal: true, root: panelRef })\n * useFocusTrap(panelRef, {\n * active: () => open.value && isTopmost.value,\n * containers: layer.rootsAbove,\n * })\n * ```\n */\nexport function useFocusTrap(\n container: Ref<HTMLElement | null>,\n options: UseFocusTrapOptions = {},\n): void {\n let previouslyFocused: HTMLElement | null = null\n let listening = false\n // Последний законный держатель фокуса: с него начинается возврат, когда\n // фокус ушёл в никуда (например, кликом по неинтерактивному фону).\n let lastFocusedInside: HTMLElement | null = null\n\n function roots(): HTMLElement[] {\n const extra = options.containers?.() ?? []\n return [container.value, ...extra].filter((root): root is HTMLElement => Boolean(root))\n }\n\n function contains(target: Node | null): boolean {\n if (!target)\n return false\n return roots().some(root => root === target || root.contains(target))\n }\n\n function focusables(): HTMLElement[] {\n return getFocusableElementsIn(roots())\n }\n\n function focusInitial(): void {\n // Фокус уже внутри слоя — его поставили осознанно: диалог наводит на свою\n // кнопку, окно ввода — на поле, и делают это в тот же такт, когда панель\n // появилась. `initialFocus` — дефолт слоя, а не приказ, поэтому такую\n // установку он не перебивает.\n if (contains(document.activeElement))\n return\n\n const requested = options.initialFocus?.()\n if (requested && isFocusable(requested)) {\n requested.focus()\n return\n }\n\n const [first] = focusables()\n if (first) {\n first.focus()\n return\n }\n\n // Фокусировать нечего — забираем фокус на сам слой, иначе он останется\n // снаружи и Tab уведёт пользователя под окно.\n const fallback = options.fallbackFocus?.() ?? container.value\n fallback?.focus()\n }\n\n function onKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Tab')\n return\n if (!contains(document.activeElement))\n return\n\n const items = focusables()\n if (items.length === 0) {\n // Внутри некуда идти — держим фокус на слое, а не отпускаем на страницу.\n event.preventDefault()\n ;(options.fallbackFocus?.() ?? container.value)?.focus()\n return\n }\n\n const first = items[0]\n const last = items[items.length - 1]\n const active = document.activeElement as HTMLElement | null\n const index = active ? items.indexOf(active) : -1\n\n // Фокус на самой панели (`tabindex=\"-1\"`, в списке её нет): Tab обязан\n // войти внутрь, а не выпрыгнуть за слой.\n if (index === -1) {\n event.preventDefault()\n ;(event.shiftKey ? last : first).focus()\n return\n }\n\n if (event.shiftKey && active === first) {\n event.preventDefault()\n last.focus()\n return\n }\n\n if (!event.shiftKey && active === last) {\n event.preventDefault()\n first.focus()\n }\n }\n\n function onFocusIn(event: FocusEvent): void {\n const target = event.target as HTMLElement | null\n\n if (contains(target)) {\n lastFocusedInside = target\n return\n }\n\n // Фокус утёк наружу — программным `focus()`, кликом по фону или обходом\n // через адресную строку. Возвращаем его на последнее законное место.\n if (lastFocusedInside && contains(lastFocusedInside) && isFocusable(lastFocusedInside)) {\n lastFocusedInside.focus()\n return\n }\n\n focusInitial()\n }\n\n function activate(): void {\n if (listening)\n return\n if (typeof document === 'undefined')\n return\n listening = true\n\n previouslyFocused = (document.activeElement as HTMLElement) ?? null\n lastFocusedInside = null\n\n document.addEventListener('keydown', onKeydown, true)\n document.addEventListener('focusin', onFocusIn, true)\n\n // Ждём, пока Vue домонтирует содержимое слоя: до этого фокусировать нечего.\n void nextTick(() => {\n if (!listening)\n return\n focusInitial()\n })\n }\n\n function deactivate(): void {\n if (!listening)\n return\n listening = false\n\n document.removeEventListener('keydown', onKeydown, true)\n document.removeEventListener('focusin', onFocusIn, true)\n\n const target = previouslyFocused\n previouslyFocused = null\n lastFocusedInside = null\n\n if (options.restoreFocus === false || !target)\n return\n if (!target.isConnected)\n return\n target.focus?.()\n }\n\n watch(\n () => Boolean(options.active?.() ?? true) && Boolean(container.value),\n (isActive) => {\n if (isActive)\n activate()\n else deactivate()\n },\n { immediate: true },\n )\n\n onScopeDispose(deactivate)\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { nextTick, onUnmounted, watch } from "vue";
|
|
1
|
+
import { a as removeOverlayLayer, i as pushOverlayLayer, n as layerRootsAbove } from "../chunks/overlayStack-0CZS99Da.js";
|
|
2
|
+
import { nextTick, onUnmounted, ref, watch } from "vue";
|
|
3
3
|
//#region src/composables/useOverlayLayer.ts
|
|
4
4
|
/**
|
|
5
5
|
* Регистрирует оверлей в общем стеке слоёв, пока `open` истинно.
|
|
@@ -24,6 +24,7 @@ import { nextTick, onUnmounted, watch } from "vue";
|
|
|
24
24
|
function useOverlayLayer(open, onDismiss, options = {}) {
|
|
25
25
|
let layerId = null;
|
|
26
26
|
let previouslyFocused = null;
|
|
27
|
+
const depth = ref(0);
|
|
27
28
|
function register() {
|
|
28
29
|
if (layerId !== null) return;
|
|
29
30
|
if (typeof window === "undefined") return;
|
|
@@ -33,6 +34,9 @@ function useOverlayLayer(open, onDismiss, options = {}) {
|
|
|
33
34
|
shouldClose: () => options.closeOnEscape?.() ?? true,
|
|
34
35
|
close: onDismiss,
|
|
35
36
|
setTopmost: options.onTopmostChange,
|
|
37
|
+
setDepth: (value) => {
|
|
38
|
+
depth.value = value;
|
|
39
|
+
},
|
|
36
40
|
root: () => options.root?.value ?? null
|
|
37
41
|
});
|
|
38
42
|
}
|
|
@@ -59,7 +63,10 @@ function useOverlayLayer(open, onDismiss, options = {}) {
|
|
|
59
63
|
else unregister();
|
|
60
64
|
}, { immediate: true });
|
|
61
65
|
onUnmounted(unregister);
|
|
62
|
-
return {
|
|
66
|
+
return {
|
|
67
|
+
rootsAbove: () => layerId === null ? [] : layerRootsAbove(layerId),
|
|
68
|
+
depth
|
|
69
|
+
};
|
|
63
70
|
}
|
|
64
71
|
//#endregion
|
|
65
72
|
export { useOverlayLayer };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOverlayLayer.js","names":[],"sources":["../../src/composables/useOverlayLayer.ts"],"sourcesContent":["import type { Ref } from 'vue'\nimport { nextTick, onUnmounted, watch } from 'vue'\n\nimport { layerRootsAbove, pushOverlayLayer, removeOverlayLayer } from './internal/overlayStack'\n\nexport interface UseOverlayLayerOptions {\n /**\n * Модальный слой: блокирует страницу, и модалки под ним получают `inert`.\n * Немодальные (поповеры, меню, подсказки) участвуют только в очереди Esc.\n * По умолчанию `false`.\n *\n * Геттером — там, где модальность включается пропом (`GrDrawer`): значение\n * читается в момент регистрации, то есть при каждом открытии.\n */\n modal?: boolean | (() => boolean)\n /**\n * Закрывать ли слой по Esc. Геттер, а не значение, — чтобы читать реактивный\n * проп (`closeOnEsc`). По умолчанию закрывается всегда.\n *\n * Если геттер вернёт `false`, нажатие всё равно не пройдёт на слой ниже:\n * пользователь видит верхним этот слой, и Esc адресован ему.\n */\n closeOnEscape?: () => boolean\n /**\n * Уведомление «этот модальный слой сейчас верхний». Компонент вешает по нему\n * `inert` на свой корень, когда перестал быть верхним.\n */\n onTopmostChange?: (isTopmost: boolean) => void\n /**\n * Возвращать фокус элементу, который был активен до открытия. По умолчанию —\n * да. Отключать имеет смысл там, где фокус с триггера и не уходил: `GrSelect`\n * ведёт список через `aria-activedescendant`.\n */\n restoreFocus?: boolean\n /**\n * Корень слоя. Нужен дважды: для правила возврата фокуса (если на момент\n * закрытия фокус уже вне слоя, пользователь ушёл сам — забирать нельзя) и\n * для ловушки фокуса слоя под ним, которая обязана считать этот корень\n * своим (панель селекта внутри модалки телепортирована в `body`).\n */\n root?: Ref<HTMLElement | null>\n}\n\nexport interface OverlayLayerHandle {\n /**\n * Корни слоёв, открытых поверх этого. Передаётся в `useFocusTrap` как\n * `containers`: пока панель, открытая изнутри окна, жива, фокус в ней\n * законен.\n */\n rootsAbove: () => HTMLElement[]\n}\n\n/**\n * Регистрирует оверлей в общем стеке слоёв, пока `open` истинно.\n *\n * Один контракт на порядок Esc, `inert` и возврат фокуса — три механизма,\n * которые обязаны видеть один и тот же порядок слоёв. Что берёт на себя:\n *\n * - **Esc** — верхнему слою, вниз не проваливается;\n * - **`inert`** — через `onTopmostChange` у модальных слоёв;\n * - **возврат фокуса** — по единому правилу (см. ниже).\n *\n * Чего **не** делает: фокус-ловушку — это `useFocusTrap`, отдельный примитив.\n * Поповеру ловушка и не нужна: Tab обязан уводить фокус наружу и закрывать его,\n * поэтому связка «слой + ловушка» собирается в компоненте, а не тут.\n *\n * Правило возврата фокуса: восстанавливаем, **только если на момент закрытия\n * фокус всё ещё внутри слоя**. Если пользователь кликнул в другое поле, фокус\n * уже там — забирать его обратно нельзя. Эвристика «возвращать, если открыли с\n * клавиатуры» промахивается в обе стороны: мышью открытый слой фокус не вернёт,\n * а клавиатурный отберёт его у того, куда пользователь ушёл сам.\n */\nexport function useOverlayLayer(\n open: Ref<boolean>,\n onDismiss: () => void,\n options: UseOverlayLayerOptions = {},\n): OverlayLayerHandle {\n let layerId: number | null = null\n let previouslyFocused: HTMLElement | null = null\n\n function register(): void {\n if (layerId !== null)
|
|
1
|
+
{"version":3,"file":"useOverlayLayer.js","names":[],"sources":["../../src/composables/useOverlayLayer.ts"],"sourcesContent":["import type { Ref } from 'vue'\nimport { nextTick, onUnmounted, ref, watch } from 'vue'\n\nimport { layerRootsAbove, pushOverlayLayer, removeOverlayLayer } from './internal/overlayStack'\n\nexport interface UseOverlayLayerOptions {\n /**\n * Модальный слой: блокирует страницу, и модалки под ним получают `inert`.\n * Немодальные (поповеры, меню, подсказки) участвуют только в очереди Esc.\n * По умолчанию `false`.\n *\n * Геттером — там, где модальность включается пропом (`GrDrawer`): значение\n * читается в момент регистрации, то есть при каждом открытии.\n */\n modal?: boolean | (() => boolean)\n /**\n * Закрывать ли слой по Esc. Геттер, а не значение, — чтобы читать реактивный\n * проп (`closeOnEsc`). По умолчанию закрывается всегда.\n *\n * Если геттер вернёт `false`, нажатие всё равно не пройдёт на слой ниже:\n * пользователь видит верхним этот слой, и Esc адресован ему.\n */\n closeOnEscape?: () => boolean\n /**\n * Уведомление «этот модальный слой сейчас верхний». Компонент вешает по нему\n * `inert` на свой корень, когда перестал быть верхним.\n */\n onTopmostChange?: (isTopmost: boolean) => void\n /**\n * Возвращать фокус элементу, который был активен до открытия. По умолчанию —\n * да. Отключать имеет смысл там, где фокус с триггера и не уходил: `GrSelect`\n * ведёт список через `aria-activedescendant`.\n */\n restoreFocus?: boolean\n /**\n * Корень слоя. Нужен дважды: для правила возврата фокуса (если на момент\n * закрытия фокус уже вне слоя, пользователь ушёл сам — забирать нельзя) и\n * для ловушки фокуса слоя под ним, которая обязана считать этот корень\n * своим (панель селекта внутри модалки телепортирована в `body`).\n */\n root?: Ref<HTMLElement | null>\n}\n\nexport interface OverlayLayerHandle {\n /**\n * Корни слоёв, открытых поверх этого. Передаётся в `useFocusTrap` как\n * `containers`: пока панель, открытая изнутри окна, жива, фокус в ней\n * законен.\n */\n rootsAbove: () => HTMLElement[]\n /**\n * Позиция среди модальных слоёв, от нуля. По ней компонент считает высоту.\n *\n * Значение **не сбрасывается** при закрытии: слой уходит из стека сразу, а\n * его поддерево живёт до конца leave-анимации. Обнули глубину там — уходящее\n * окно нырнуло бы под соседнее прямо во время ухода.\n */\n depth: Ref<number>\n}\n\n/**\n * Регистрирует оверлей в общем стеке слоёв, пока `open` истинно.\n *\n * Один контракт на порядок Esc, `inert` и возврат фокуса — три механизма,\n * которые обязаны видеть один и тот же порядок слоёв. Что берёт на себя:\n *\n * - **Esc** — верхнему слою, вниз не проваливается;\n * - **`inert`** — через `onTopmostChange` у модальных слоёв;\n * - **возврат фокуса** — по единому правилу (см. ниже).\n *\n * Чего **не** делает: фокус-ловушку — это `useFocusTrap`, отдельный примитив.\n * Поповеру ловушка и не нужна: Tab обязан уводить фокус наружу и закрывать его,\n * поэтому связка «слой + ловушка» собирается в компоненте, а не тут.\n *\n * Правило возврата фокуса: восстанавливаем, **только если на момент закрытия\n * фокус всё ещё внутри слоя**. Если пользователь кликнул в другое поле, фокус\n * уже там — забирать его обратно нельзя. Эвристика «возвращать, если открыли с\n * клавиатуры» промахивается в обе стороны: мышью открытый слой фокус не вернёт,\n * а клавиатурный отберёт его у того, куда пользователь ушёл сам.\n */\nexport function useOverlayLayer(\n open: Ref<boolean>,\n onDismiss: () => void,\n options: UseOverlayLayerOptions = {},\n): OverlayLayerHandle {\n let layerId: number | null = null\n let previouslyFocused: HTMLElement | null = null\n const depth = ref(0)\n\n function register(): void {\n if (layerId !== null)\n return\n\n // Стек — браузерное понятие: Esc, `inert` и возврат фокуса на сервере\n // бессмысленны. Регистрируйся слой и там — он остался бы в модульном массиве\n // навсегда, потому что `onUnmounted` при `renderToString` не вызывается, и\n // каждый серверный рендер открытого оверлея копил бы замыкание со ссылками\n // на свои компоненты.\n if (typeof window === 'undefined')\n return\n\n if (options.restoreFocus !== false && typeof document !== 'undefined')\n previouslyFocused = (document.activeElement as HTMLElement) ?? null\n\n layerId = pushOverlayLayer({\n modal: typeof options.modal === 'function' ? options.modal() : options.modal ?? false,\n shouldClose: () => options.closeOnEscape?.() ?? true,\n close: onDismiss,\n setTopmost: options.onTopmostChange,\n setDepth: (value) => { depth.value = value },\n root: () => options.root?.value ?? null,\n })\n }\n\n function unregister(): void {\n if (layerId === null)\n return\n removeOverlayLayer(layerId)\n layerId = null\n\n restoreFocus()\n }\n\n function restoreFocus(): void {\n const target = previouslyFocused\n previouslyFocused = null\n\n if (options.restoreFocus === false || !target)\n return\n if (typeof document === 'undefined')\n return\n\n // Фокус уже вне слоя — пользователь ушёл сам, отбирать нельзя. Решение\n // принимаем сейчас, пока слой ещё в DOM, а применяем следующим тиком.\n const active = document.activeElement\n const root = options.root?.value\n if (root && active && active !== document.body && !root.contains(active))\n return\n\n // Возврат откладывается намеренно: ловушка фокуса того же слоя снимает свои\n // слушатели в этом же флаше, и сфокусируй мы триггер сразу — она затащила бы\n // фокус обратно в закрывающийся слой.\n void nextTick(() => {\n if (target.isConnected)\n target.focus?.()\n })\n }\n\n watch(\n open,\n (isOpen) => {\n if (isOpen)\n register()\n else unregister()\n },\n { immediate: true },\n )\n\n onUnmounted(unregister)\n\n return {\n rootsAbove: () => (layerId === null ? [] : layerRootsAbove(layerId)),\n depth,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAgFA,SAAgB,gBACd,MACA,WACA,UAAkC,CAAC,GACf;CACpB,IAAI,UAAyB;CAC7B,IAAI,oBAAwC;CAC5C,MAAM,QAAQ,IAAI,CAAC;CAEnB,SAAS,WAAiB;EACxB,IAAI,YAAY,MACd;EAOF,IAAI,OAAO,WAAW,aACpB;EAEF,IAAI,QAAQ,iBAAiB,SAAS,OAAO,aAAa,aACxD,oBAAqB,SAAS,iBAAiC;EAEjE,UAAU,iBAAiB;GACzB,OAAO,OAAO,QAAQ,UAAU,aAAa,QAAQ,MAAM,IAAI,QAAQ,SAAS;GAChF,mBAAmB,QAAQ,gBAAgB,KAAK;GAChD,OAAO;GACP,YAAY,QAAQ;GACpB,WAAW,UAAU;IAAE,MAAM,QAAQ;GAAM;GAC3C,YAAY,QAAQ,MAAM,SAAS;EACrC,CAAC;CACH;CAEA,SAAS,aAAmB;EAC1B,IAAI,YAAY,MACd;EACF,mBAAmB,OAAO;EAC1B,UAAU;EAEV,aAAa;CACf;CAEA,SAAS,eAAqB;EAC5B,MAAM,SAAS;EACf,oBAAoB;EAEpB,IAAI,QAAQ,iBAAiB,SAAS,CAAC,QACrC;EACF,IAAI,OAAO,aAAa,aACtB;EAIF,MAAM,SAAS,SAAS;EACxB,MAAM,OAAO,QAAQ,MAAM;EAC3B,IAAI,QAAQ,UAAU,WAAW,SAAS,QAAQ,CAAC,KAAK,SAAS,MAAM,GACrE;EAKF,eAAoB;GAClB,IAAI,OAAO,aACT,OAAO,QAAQ;EACnB,CAAC;CACH;CAEA,MACE,OACC,WAAW;EACV,IAAI,QACF,SAAS;OACN,WAAW;CAClB,GACA,EAAE,WAAW,KAAK,CACpB;CAEA,YAAY,UAAU;CAEtB,OAAO;EACL,kBAAmB,YAAY,OAAO,CAAC,IAAI,gBAAgB,OAAO;EAClE;CACF;AACF"}
|