@feugene/granularity 0.34.0 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{config-DGGaKf4R.js → config-CT4lP6S4.js} +1 -1
- package/dist/chunks/{config-DGGaKf4R.js.map → config-CT4lP6S4.js.map} +1 -1
- package/dist/chunks/{config-DBXCIb6r2.js → config-DVCNvhIG2.js} +1 -1
- package/dist/chunks/{config-DBXCIb6r2.js.map → config-DVCNvhIG2.js.map} +1 -1
- package/dist/chunks/{generated-Buw6W7Te.js → generated-Bk-f6NsN.js} +14 -0
- package/dist/chunks/{generated-Buw6W7Te.js.map → generated-Bk-f6NsN.js.map} +1 -1
- package/dist/chunks/{grPopoverStyles-DKwigt7b.js → grPopoverStyles-CYAeO3Ov.js} +20 -1
- package/dist/chunks/{grPopoverStyles-DKwigt7b.js.map → grPopoverStyles-CYAeO3Ov.js.map} +1 -1
- package/dist/chunks/{granular-provider-3LUawDPh.js → granular-provider-BAy4APOX.js} +2 -2
- package/dist/chunks/{granular-provider-3LUawDPh.js.map → granular-provider-BAy4APOX.js.map} +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-D1Iis7jf.js → GrColorPicker-CIgdPXHn.js} +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-D1Iis7jf.js.map → GrColorPicker-CIgdPXHn.js.map} +1 -1
- package/dist/components/GrColorPicker/index.js +1 -1
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-CWmgrzpi.js → GrContextMenu-DlFWm0eW.js} +1 -1
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-CWmgrzpi.js.map → GrContextMenu-DlFWm0eW.js.map} +1 -1
- package/dist/components/GrContextMenu/index.js +1 -1
- package/dist/components/GrDropdown/chunks/{GrDropdown-CDkPkvIJ.js → GrDropdown-7r772-Cq.js} +1 -1
- package/dist/components/GrDropdown/chunks/{GrDropdown-CDkPkvIJ.js.map → GrDropdown-7r772-Cq.js.map} +1 -1
- package/dist/components/GrDropdown/index.js +2 -2
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-DQrZ_yPV.js → GrDropdownMenu-DNurbsWc.js} +1 -1
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-DQrZ_yPV.js.map → GrDropdownMenu-DNurbsWc.js.map} +1 -1
- package/dist/components/GrDropdownMenu/index.js +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-CzeXMcvh.js → GrPopover-DEFxDffj.js} +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-CzeXMcvh.js.map → GrPopover-DEFxDffj.js.map} +1 -1
- package/dist/components/GrPopover/index.js +2 -2
- package/dist/composables/useFloating.js +31 -11
- package/dist/composables/useFloating.js.map +1 -1
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +7 -7
- package/dist/theme.js +1 -1
- package/dist/tokens.js +1 -1
- package/dist/types/components/GrPopover/grPopoverStyles.d.ts +20 -1
- package/dist/types/composables/useFloating.d.ts +15 -0
- package/dist/web-types.json +1 -1
- package/package.json +1 -1
package/dist/components/GrPopover/chunks/{GrPopover-CzeXMcvh.js.map → GrPopover-DEFxDffj.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrPopover-CzeXMcvh.js","names":[],"sources":["../../../../src/components/GrPopover/GrPopover.vue","../../../../src/components/GrPopover/GrPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * Чем открывается панель.\n *\n * `manual` — только программно (через `v-model:open`): нужно контекстному меню\n * и подтверждениям, где момент открытия решает потребитель. `hover` — по\n * наведению, с задержками из `openDelay` и `closeDelay`; клик и клавиатура в\n * этом режиме продолжают работать.\n */\n trigger?: 'click' | 'manual' | 'hover'\n /**\n * Задержка открытия по наведению, мс.\n *\n * Обе задержки нужны, и каждая по своей причине: без `openDelay` панель\n * выпрыгивает на любое пересечение курсором, без `closeDelay` её не удержать\n * при переходе с триггера на панель — между ними зазор `offsetPx`.\n */\n openDelay?: number\n /** Задержка закрытия после ухода курсора, мс. */\n closeDelay?: number\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n /**\n * Брать ширину у триггера: `true` — точно его ширина, `'min'` — не уже его,\n * дальше по содержимому. Панель у поля или у широкой кнопки, оказавшаяся уже\n * своего триггера, читается как ошибка вёрстки.\n *\n * С потолком `--gr-popover-max-width` сочетается, а не спорит: ширину задаёт\n * триггер, потолок её ограничивает — «шириной с триггер, но не шире\n * читаемого». Поэтому это отдельный проп, а не значение одного перечисления\n * вместе с потолком: оси независимы, и сочетание осмысленно.\n */\n matchWidth?: boolean | 'min'\n /**\n * Обёртка триггера занимает всю ширину родителя вместо того, чтобы обжимать\n * содержимое.\n *\n * Нужно форм-контролам: обёртка `inline-block` схлопывается по содержимому, и\n * `w-full` у самого триггера начинает резолвиться относительно неё, а не\n * относительно поля. Контрол, объявивший себя во всю ширину, рисуется по\n * содержимому — рядом с полем ввода это читается как сбитая вёрстка.\n *\n * Имя то же, что у `GrButton` и `GrSegmented`: в ядре это устоявшееся слово\n * для «во всю ширину».\n */\n block?: boolean\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n openDelay: 120,\n closeDelay: 160,\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n matchWidth: false,\n block: false,\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nlet hoverTimer: ReturnType<typeof setTimeout> | undefined\n\nfunction open(): void {\n if (props.disabled)\n return\n\n // Явное действие отменяет отложенное наведение: иначе таймер, заведённый\n // курсором, догонит и закроет панель, открытую кликом.\n clearTimeout(hoverTimer)\n setOpen(true)\n}\n\nfunction close(): void {\n clearTimeout(hoverTimer)\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n matchWidth: () => props.matchWidth,\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n *\n * Здесь и ARIA, и **клик**. ARIA — потому что `aria-expanded` обязан жить на том\n * узле, который получает фокус, а не на `div` вокруг него. Клик — потому что\n * обёртка ловит и клики по вложенным элементам: кнопка «Ещё» рядом с триггером,\n * ссылка в карточке-триггере, крестик на чипе открывали бы панель мимо намерения\n * пользователя. То же решение и по той же причине — в `GrDropdown`.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n 'onClick': onTriggerClick,\n}))\n\nfunction onTriggerClick(): void {\n // `hover` не отменяет клика: с клавиатуры и с тачскрина наведения не бывает,\n // и панель, открываемая только курсором, для них не существует вовсе.\n if (props.trigger === 'click' || props.trigger === 'hover')\n toggle()\n}\n\n/** Привязан ли `triggerProps`: `aria-expanded` он ставит всегда, при любой роли. */\nfunction hasBoundTrigger(): boolean {\n return rootEl.value?.querySelector('[aria-expanded]') != null\n}\n\n/**\n * Клик по обёртке — совместимость, а не механизм.\n *\n * Клик триггера живёт в `triggerProps`. Но слот, оставленный без\n * `v-bind=\"triggerProps\"`, до этой правки открывался кликом по обёртке, и молча\n * отнимать это у тех, кто на это опирался, нельзя. Поэтому обёртка срабатывает\n * ровно тогда, когда привязанного триггера внутри нет.\n *\n * Проверка на месте клика, а не при монтировании: содержимое слота реактивно, и\n * триггер может появиться позже. `querySelector` по одному узлу на клик дешевле\n * подписки на мутации.\n */\nfunction onWrapperClick(): void {\n if (hasBoundTrigger())\n return\n\n onTriggerClick()\n}\n\n/**\n * Открытие по наведению.\n *\n * Таймер один на обе стороны и сбрасывается при каждом событии: курсор,\n * прошедший триггер насквозь, не должен оставить за собой отложенное открытие.\n * Нулевая задержка выполняется сразу, а не через `setTimeout(0)`, — иначе\n * открытие уезжало бы на следующий тик и ломало тесты, которые ждут синхронности.\n */\nfunction scheduleHover(next: boolean, delayMs: number): void {\n clearTimeout(hoverTimer)\n\n if (delayMs <= 0) {\n setOpen(next)\n return\n }\n\n hoverTimer = setTimeout(setOpen, delayMs, next)\n}\n\nfunction onHoverEnter(): void {\n if (props.disabled || props.trigger !== 'hover')\n return\n\n scheduleHover(true, props.openDelay)\n}\n\nfunction onHoverLeave(): void {\n if (props.trigger !== 'hover')\n return\n\n scheduleHover(false, props.closeDelay)\n}\n\n// Триггер без `v-bind=\"triggerProps\"` работает по совместимости, но остаётся без\n// клавиатуры и без ARIA: панель для скринридера не объявлена, и с `Tab` до неё не\n// добраться. Молчать нельзя — снаружи это выглядит как «поповер почти работает».\nonMounted(() => {\n if (!__GR_DEV__ || !hasTrigger.value || hasBoundTrigger())\n return\n\n console.warn(\n '[GrPopover] триггер не привязан: добавьте `v-bind=\"triggerProps\"` на элемент '\n + 'внутри слота #trigger — <template #trigger=\"{ triggerProps }\">'\n + '<GrButton v-bind=\"triggerProps\">…</GrButton></template>. '\n + 'Клик по обёртке пока работает, но у триггера нет ни клавиатуры, ни '\n + 'aria-haspopup/aria-expanded, а клик по вложенным элементам открывает панель.',\n )\n})\n\nonBeforeUnmount(() => {\n clearTimeout(hoverTimer)\n})\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n :class=\"block ? 'block w-full' : 'inline-block max-w-full'\"\n @click=\"onWrapperClick\"\n @mouseenter=\"onHoverEnter\"\n @mouseleave=\"onHoverLeave\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\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=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @mouseenter=\"onHoverEnter\"\n @mouseleave=\"onHoverLeave\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * Чем открывается панель.\n *\n * `manual` — только программно (через `v-model:open`): нужно контекстному меню\n * и подтверждениям, где момент открытия решает потребитель. `hover` — по\n * наведению, с задержками из `openDelay` и `closeDelay`; клик и клавиатура в\n * этом режиме продолжают работать.\n */\n trigger?: 'click' | 'manual' | 'hover'\n /**\n * Задержка открытия по наведению, мс.\n *\n * Обе задержки нужны, и каждая по своей причине: без `openDelay` панель\n * выпрыгивает на любое пересечение курсором, без `closeDelay` её не удержать\n * при переходе с триггера на панель — между ними зазор `offsetPx`.\n */\n openDelay?: number\n /** Задержка закрытия после ухода курсора, мс. */\n closeDelay?: number\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n /**\n * Брать ширину у триггера: `true` — точно его ширина, `'min'` — не уже его,\n * дальше по содержимому. Панель у поля или у широкой кнопки, оказавшаяся уже\n * своего триггера, читается как ошибка вёрстки.\n *\n * С потолком `--gr-popover-max-width` сочетается, а не спорит: ширину задаёт\n * триггер, потолок её ограничивает — «шириной с триггер, но не шире\n * читаемого». Поэтому это отдельный проп, а не значение одного перечисления\n * вместе с потолком: оси независимы, и сочетание осмысленно.\n */\n matchWidth?: boolean | 'min'\n /**\n * Обёртка триггера занимает всю ширину родителя вместо того, чтобы обжимать\n * содержимое.\n *\n * Нужно форм-контролам: обёртка `inline-block` схлопывается по содержимому, и\n * `w-full` у самого триггера начинает резолвиться относительно неё, а не\n * относительно поля. Контрол, объявивший себя во всю ширину, рисуется по\n * содержимому — рядом с полем ввода это читается как сбитая вёрстка.\n *\n * Имя то же, что у `GrButton` и `GrSegmented`: в ядре это устоявшееся слово\n * для «во всю ширину».\n */\n block?: boolean\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n openDelay: 120,\n closeDelay: 160,\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n matchWidth: false,\n block: false,\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nlet hoverTimer: ReturnType<typeof setTimeout> | undefined\n\nfunction open(): void {\n if (props.disabled)\n return\n\n // Явное действие отменяет отложенное наведение: иначе таймер, заведённый\n // курсором, догонит и закроет панель, открытую кликом.\n clearTimeout(hoverTimer)\n setOpen(true)\n}\n\nfunction close(): void {\n clearTimeout(hoverTimer)\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n matchWidth: () => props.matchWidth,\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n *\n * Здесь и ARIA, и **клик**. ARIA — потому что `aria-expanded` обязан жить на том\n * узле, который получает фокус, а не на `div` вокруг него. Клик — потому что\n * обёртка ловит и клики по вложенным элементам: кнопка «Ещё» рядом с триггером,\n * ссылка в карточке-триггере, крестик на чипе открывали бы панель мимо намерения\n * пользователя. То же решение и по той же причине — в `GrDropdown`.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n 'onClick': onTriggerClick,\n}))\n\nfunction onTriggerClick(): void {\n // `hover` не отменяет клика: с клавиатуры и с тачскрина наведения не бывает,\n // и панель, открываемая только курсором, для них не существует вовсе.\n if (props.trigger === 'click' || props.trigger === 'hover')\n toggle()\n}\n\n/** Привязан ли `triggerProps`: `aria-expanded` он ставит всегда, при любой роли. */\nfunction hasBoundTrigger(): boolean {\n return rootEl.value?.querySelector('[aria-expanded]') != null\n}\n\n/**\n * Клик по обёртке — совместимость, а не механизм.\n *\n * Клик триггера живёт в `triggerProps`. Но слот, оставленный без\n * `v-bind=\"triggerProps\"`, до этой правки открывался кликом по обёртке, и молча\n * отнимать это у тех, кто на это опирался, нельзя. Поэтому обёртка срабатывает\n * ровно тогда, когда привязанного триггера внутри нет.\n *\n * Проверка на месте клика, а не при монтировании: содержимое слота реактивно, и\n * триггер может появиться позже. `querySelector` по одному узлу на клик дешевле\n * подписки на мутации.\n */\nfunction onWrapperClick(): void {\n if (hasBoundTrigger())\n return\n\n onTriggerClick()\n}\n\n/**\n * Открытие по наведению.\n *\n * Таймер один на обе стороны и сбрасывается при каждом событии: курсор,\n * прошедший триггер насквозь, не должен оставить за собой отложенное открытие.\n * Нулевая задержка выполняется сразу, а не через `setTimeout(0)`, — иначе\n * открытие уезжало бы на следующий тик и ломало тесты, которые ждут синхронности.\n */\nfunction scheduleHover(next: boolean, delayMs: number): void {\n clearTimeout(hoverTimer)\n\n if (delayMs <= 0) {\n setOpen(next)\n return\n }\n\n hoverTimer = setTimeout(setOpen, delayMs, next)\n}\n\nfunction onHoverEnter(): void {\n if (props.disabled || props.trigger !== 'hover')\n return\n\n scheduleHover(true, props.openDelay)\n}\n\nfunction onHoverLeave(): void {\n if (props.trigger !== 'hover')\n return\n\n scheduleHover(false, props.closeDelay)\n}\n\n// Триггер без `v-bind=\"triggerProps\"` работает по совместимости, но остаётся без\n// клавиатуры и без ARIA: панель для скринридера не объявлена, и с `Tab` до неё не\n// добраться. Молчать нельзя — снаружи это выглядит как «поповер почти работает».\nonMounted(() => {\n if (!__GR_DEV__ || !hasTrigger.value || hasBoundTrigger())\n return\n\n console.warn(\n '[GrPopover] триггер не привязан: добавьте `v-bind=\"triggerProps\"` на элемент '\n + 'внутри слота #trigger — <template #trigger=\"{ triggerProps }\">'\n + '<GrButton v-bind=\"triggerProps\">…</GrButton></template>. '\n + 'Клик по обёртке пока работает, но у триггера нет ни клавиатуры, ни '\n + 'aria-haspopup/aria-expanded, а клик по вложенным элементам открывает панель.',\n )\n})\n\nonBeforeUnmount(() => {\n clearTimeout(hoverTimer)\n})\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n :class=\"block ? 'block w-full' : 'inline-block max-w-full'\"\n @click=\"onWrapperClick\"\n @mouseenter=\"onHoverEnter\"\n @mouseleave=\"onHoverLeave\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\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=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @mouseenter=\"onHoverEnter\"\n @mouseleave=\"onHoverLeave\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrPopover-DEFxDffj.js","names":[],"sources":["../../../../src/components/GrPopover/GrPopover.vue","../../../../src/components/GrPopover/GrPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * Чем открывается панель.\n *\n * `manual` — только программно (через `v-model:open`): нужно контекстному меню\n * и подтверждениям, где момент открытия решает потребитель. `hover` — по\n * наведению, с задержками из `openDelay` и `closeDelay`; клик и клавиатура в\n * этом режиме продолжают работать.\n */\n trigger?: 'click' | 'manual' | 'hover'\n /**\n * Задержка открытия по наведению, мс.\n *\n * Обе задержки нужны, и каждая по своей причине: без `openDelay` панель\n * выпрыгивает на любое пересечение курсором, без `closeDelay` её не удержать\n * при переходе с триггера на панель — между ними зазор `offsetPx`.\n */\n openDelay?: number\n /** Задержка закрытия после ухода курсора, мс. */\n closeDelay?: number\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n /**\n * Брать ширину у триггера: `true` — точно его ширина, `'min'` — не уже его,\n * дальше по содержимому. Панель у поля или у широкой кнопки, оказавшаяся уже\n * своего триггера, читается как ошибка вёрстки.\n *\n * С потолком `--gr-popover-max-width` сочетается, а не спорит: ширину задаёт\n * триггер, потолок её ограничивает — «шириной с триггер, но не шире\n * читаемого». Поэтому это отдельный проп, а не значение одного перечисления\n * вместе с потолком: оси независимы, и сочетание осмысленно.\n */\n matchWidth?: boolean | 'min'\n /**\n * Обёртка триггера занимает всю ширину родителя вместо того, чтобы обжимать\n * содержимое.\n *\n * Нужно форм-контролам: обёртка `inline-block` схлопывается по содержимому, и\n * `w-full` у самого триггера начинает резолвиться относительно неё, а не\n * относительно поля. Контрол, объявивший себя во всю ширину, рисуется по\n * содержимому — рядом с полем ввода это читается как сбитая вёрстка.\n *\n * Имя то же, что у `GrButton` и `GrSegmented`: в ядре это устоявшееся слово\n * для «во всю ширину».\n */\n block?: boolean\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n openDelay: 120,\n closeDelay: 160,\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n matchWidth: false,\n block: false,\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nlet hoverTimer: ReturnType<typeof setTimeout> | undefined\n\nfunction open(): void {\n if (props.disabled)\n return\n\n // Явное действие отменяет отложенное наведение: иначе таймер, заведённый\n // курсором, догонит и закроет панель, открытую кликом.\n clearTimeout(hoverTimer)\n setOpen(true)\n}\n\nfunction close(): void {\n clearTimeout(hoverTimer)\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n matchWidth: () => props.matchWidth,\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n *\n * Здесь и ARIA, и **клик**. ARIA — потому что `aria-expanded` обязан жить на том\n * узле, который получает фокус, а не на `div` вокруг него. Клик — потому что\n * обёртка ловит и клики по вложенным элементам: кнопка «Ещё» рядом с триггером,\n * ссылка в карточке-триггере, крестик на чипе открывали бы панель мимо намерения\n * пользователя. То же решение и по той же причине — в `GrDropdown`.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n 'onClick': onTriggerClick,\n}))\n\nfunction onTriggerClick(): void {\n // `hover` не отменяет клика: с клавиатуры и с тачскрина наведения не бывает,\n // и панель, открываемая только курсором, для них не существует вовсе.\n if (props.trigger === 'click' || props.trigger === 'hover')\n toggle()\n}\n\n/** Привязан ли `triggerProps`: `aria-expanded` он ставит всегда, при любой роли. */\nfunction hasBoundTrigger(): boolean {\n return rootEl.value?.querySelector('[aria-expanded]') != null\n}\n\n/**\n * Клик по обёртке — совместимость, а не механизм.\n *\n * Клик триггера живёт в `triggerProps`. Но слот, оставленный без\n * `v-bind=\"triggerProps\"`, до этой правки открывался кликом по обёртке, и молча\n * отнимать это у тех, кто на это опирался, нельзя. Поэтому обёртка срабатывает\n * ровно тогда, когда привязанного триггера внутри нет.\n *\n * Проверка на месте клика, а не при монтировании: содержимое слота реактивно, и\n * триггер может появиться позже. `querySelector` по одному узлу на клик дешевле\n * подписки на мутации.\n */\nfunction onWrapperClick(): void {\n if (hasBoundTrigger())\n return\n\n onTriggerClick()\n}\n\n/**\n * Открытие по наведению.\n *\n * Таймер один на обе стороны и сбрасывается при каждом событии: курсор,\n * прошедший триггер насквозь, не должен оставить за собой отложенное открытие.\n * Нулевая задержка выполняется сразу, а не через `setTimeout(0)`, — иначе\n * открытие уезжало бы на следующий тик и ломало тесты, которые ждут синхронности.\n */\nfunction scheduleHover(next: boolean, delayMs: number): void {\n clearTimeout(hoverTimer)\n\n if (delayMs <= 0) {\n setOpen(next)\n return\n }\n\n hoverTimer = setTimeout(setOpen, delayMs, next)\n}\n\nfunction onHoverEnter(): void {\n if (props.disabled || props.trigger !== 'hover')\n return\n\n scheduleHover(true, props.openDelay)\n}\n\nfunction onHoverLeave(): void {\n if (props.trigger !== 'hover')\n return\n\n scheduleHover(false, props.closeDelay)\n}\n\n// Триггер без `v-bind=\"triggerProps\"` работает по совместимости, но остаётся без\n// клавиатуры и без ARIA: панель для скринридера не объявлена, и с `Tab` до неё не\n// добраться. Молчать нельзя — снаружи это выглядит как «поповер почти работает».\nonMounted(() => {\n if (!__GR_DEV__ || !hasTrigger.value || hasBoundTrigger())\n return\n\n console.warn(\n '[GrPopover] триггер не привязан: добавьте `v-bind=\"triggerProps\"` на элемент '\n + 'внутри слота #trigger — <template #trigger=\"{ triggerProps }\">'\n + '<GrButton v-bind=\"triggerProps\">…</GrButton></template>. '\n + 'Клик по обёртке пока работает, но у триггера нет ни клавиатуры, ни '\n + 'aria-haspopup/aria-expanded, а клик по вложенным элементам открывает панель.',\n )\n})\n\nonBeforeUnmount(() => {\n clearTimeout(hoverTimer)\n})\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n :class=\"block ? 'block w-full' : 'inline-block max-w-full'\"\n @click=\"onWrapperClick\"\n @mouseenter=\"onHoverEnter\"\n @mouseleave=\"onHoverLeave\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\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=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @mouseenter=\"onHoverEnter\"\n @mouseleave=\"onHoverLeave\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * Чем открывается панель.\n *\n * `manual` — только программно (через `v-model:open`): нужно контекстному меню\n * и подтверждениям, где момент открытия решает потребитель. `hover` — по\n * наведению, с задержками из `openDelay` и `closeDelay`; клик и клавиатура в\n * этом режиме продолжают работать.\n */\n trigger?: 'click' | 'manual' | 'hover'\n /**\n * Задержка открытия по наведению, мс.\n *\n * Обе задержки нужны, и каждая по своей причине: без `openDelay` панель\n * выпрыгивает на любое пересечение курсором, без `closeDelay` её не удержать\n * при переходе с триггера на панель — между ними зазор `offsetPx`.\n */\n openDelay?: number\n /** Задержка закрытия после ухода курсора, мс. */\n closeDelay?: number\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n /**\n * Брать ширину у триггера: `true` — точно его ширина, `'min'` — не уже его,\n * дальше по содержимому. Панель у поля или у широкой кнопки, оказавшаяся уже\n * своего триггера, читается как ошибка вёрстки.\n *\n * С потолком `--gr-popover-max-width` сочетается, а не спорит: ширину задаёт\n * триггер, потолок её ограничивает — «шириной с триггер, но не шире\n * читаемого». Поэтому это отдельный проп, а не значение одного перечисления\n * вместе с потолком: оси независимы, и сочетание осмысленно.\n */\n matchWidth?: boolean | 'min'\n /**\n * Обёртка триггера занимает всю ширину родителя вместо того, чтобы обжимать\n * содержимое.\n *\n * Нужно форм-контролам: обёртка `inline-block` схлопывается по содержимому, и\n * `w-full` у самого триггера начинает резолвиться относительно неё, а не\n * относительно поля. Контрол, объявивший себя во всю ширину, рисуется по\n * содержимому — рядом с полем ввода это читается как сбитая вёрстка.\n *\n * Имя то же, что у `GrButton` и `GrSegmented`: в ядре это устоявшееся слово\n * для «во всю ширину».\n */\n block?: boolean\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n openDelay: 120,\n closeDelay: 160,\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n matchWidth: false,\n block: false,\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nlet hoverTimer: ReturnType<typeof setTimeout> | undefined\n\nfunction open(): void {\n if (props.disabled)\n return\n\n // Явное действие отменяет отложенное наведение: иначе таймер, заведённый\n // курсором, догонит и закроет панель, открытую кликом.\n clearTimeout(hoverTimer)\n setOpen(true)\n}\n\nfunction close(): void {\n clearTimeout(hoverTimer)\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n matchWidth: () => props.matchWidth,\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n *\n * Здесь и ARIA, и **клик**. ARIA — потому что `aria-expanded` обязан жить на том\n * узле, который получает фокус, а не на `div` вокруг него. Клик — потому что\n * обёртка ловит и клики по вложенным элементам: кнопка «Ещё» рядом с триггером,\n * ссылка в карточке-триггере, крестик на чипе открывали бы панель мимо намерения\n * пользователя. То же решение и по той же причине — в `GrDropdown`.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n 'onClick': onTriggerClick,\n}))\n\nfunction onTriggerClick(): void {\n // `hover` не отменяет клика: с клавиатуры и с тачскрина наведения не бывает,\n // и панель, открываемая только курсором, для них не существует вовсе.\n if (props.trigger === 'click' || props.trigger === 'hover')\n toggle()\n}\n\n/** Привязан ли `triggerProps`: `aria-expanded` он ставит всегда, при любой роли. */\nfunction hasBoundTrigger(): boolean {\n return rootEl.value?.querySelector('[aria-expanded]') != null\n}\n\n/**\n * Клик по обёртке — совместимость, а не механизм.\n *\n * Клик триггера живёт в `triggerProps`. Но слот, оставленный без\n * `v-bind=\"triggerProps\"`, до этой правки открывался кликом по обёртке, и молча\n * отнимать это у тех, кто на это опирался, нельзя. Поэтому обёртка срабатывает\n * ровно тогда, когда привязанного триггера внутри нет.\n *\n * Проверка на месте клика, а не при монтировании: содержимое слота реактивно, и\n * триггер может появиться позже. `querySelector` по одному узлу на клик дешевле\n * подписки на мутации.\n */\nfunction onWrapperClick(): void {\n if (hasBoundTrigger())\n return\n\n onTriggerClick()\n}\n\n/**\n * Открытие по наведению.\n *\n * Таймер один на обе стороны и сбрасывается при каждом событии: курсор,\n * прошедший триггер насквозь, не должен оставить за собой отложенное открытие.\n * Нулевая задержка выполняется сразу, а не через `setTimeout(0)`, — иначе\n * открытие уезжало бы на следующий тик и ломало тесты, которые ждут синхронности.\n */\nfunction scheduleHover(next: boolean, delayMs: number): void {\n clearTimeout(hoverTimer)\n\n if (delayMs <= 0) {\n setOpen(next)\n return\n }\n\n hoverTimer = setTimeout(setOpen, delayMs, next)\n}\n\nfunction onHoverEnter(): void {\n if (props.disabled || props.trigger !== 'hover')\n return\n\n scheduleHover(true, props.openDelay)\n}\n\nfunction onHoverLeave(): void {\n if (props.trigger !== 'hover')\n return\n\n scheduleHover(false, props.closeDelay)\n}\n\n// Триггер без `v-bind=\"triggerProps\"` работает по совместимости, но остаётся без\n// клавиатуры и без ARIA: панель для скринридера не объявлена, и с `Tab` до неё не\n// добраться. Молчать нельзя — снаружи это выглядит как «поповер почти работает».\nonMounted(() => {\n if (!__GR_DEV__ || !hasTrigger.value || hasBoundTrigger())\n return\n\n console.warn(\n '[GrPopover] триггер не привязан: добавьте `v-bind=\"triggerProps\"` на элемент '\n + 'внутри слота #trigger — <template #trigger=\"{ triggerProps }\">'\n + '<GrButton v-bind=\"triggerProps\">…</GrButton></template>. '\n + 'Клик по обёртке пока работает, но у триггера нет ни клавиатуры, ни '\n + 'aria-haspopup/aria-expanded, а клик по вложенным элементам открывает панель.',\n )\n})\n\nonBeforeUnmount(() => {\n clearTimeout(hoverTimer)\n})\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n :class=\"block ? 'block w-full' : 'inline-block max-w-full'\"\n @click=\"onWrapperClick\"\n @mouseenter=\"onHoverEnter\"\n @mouseleave=\"onHoverLeave\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\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=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @mouseenter=\"onHoverEnter\"\n @mouseleave=\"onHoverLeave\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrPopover_default } from "./chunks/GrPopover-
|
|
2
|
-
import { n as grPopoverSafelist, t as grPopoverConfig } from "../../chunks/config-
|
|
1
|
+
import { t as GrPopover_default } from "./chunks/GrPopover-DEFxDffj.js";
|
|
2
|
+
import { n as grPopoverSafelist, t as grPopoverConfig } from "../../chunks/config-DVCNvhIG2.js";
|
|
3
3
|
export { GrPopover_default as GrPopover, GrPopover_default as default, grPopoverConfig, grPopoverSafelist };
|
|
@@ -3,6 +3,21 @@ 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
|
|
5
5
|
/**
|
|
6
|
+
* Кастомное свойство, в которое слой пишет доступное место по вертикали —
|
|
7
|
+
* расстояние от панели до края вьюпорта на той стороне, куда её в итоге
|
|
8
|
+
* поставило.
|
|
9
|
+
*
|
|
10
|
+
* Свойство ставится на сам плавающий элемент при каждом пересчёте позиции, в
|
|
11
|
+
* том числе на скролле и ресайзе. Слой **сообщает замер, а не применяет его**:
|
|
12
|
+
* ограничивать ли себя этим числом и что при этом скроллить — решает панель,
|
|
13
|
+
* потому что скролл принадлежит её содержимому.
|
|
14
|
+
*
|
|
15
|
+
* Ширину так публиковать незачем: её потолок статичен (`calc(100vw - 1rem)`) и
|
|
16
|
+
* замера не требует. Высота статичной быть не может — доступное место зависит
|
|
17
|
+
* от того, где стоит триггер.
|
|
18
|
+
*/
|
|
19
|
+
var floatingAvailableHeightVar = "--gr-floating-available-height";
|
|
20
|
+
/**
|
|
6
21
|
* Device pixel ratio плавающего элемента. Нужен, чтобы округлять координаты к сетке
|
|
7
22
|
* физических пикселей (см. `roundByDPR`) — иначе субпиксельные `left/top` дают размытый
|
|
8
23
|
* текст/границы панели на не-целочисленных позициях и HiDPI-экранах. Подход перенят у
|
|
@@ -90,19 +105,24 @@ function useFloating(reference, floatingEl, open, options = {}) {
|
|
|
90
105
|
shift({ padding: options.boundaryPadding ?? 8 })
|
|
91
106
|
];
|
|
92
107
|
const matchWidth = resolveMatchWidth();
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
middleware.push(size({
|
|
109
|
+
padding: options.boundaryPadding ?? 8,
|
|
110
|
+
apply({ rects, elements, availableHeight }) {
|
|
111
|
+
elements.floating.style.setProperty(floatingAvailableHeightVar, `${Math.max(0, Math.round(availableHeight))}px`);
|
|
112
|
+
if (!matchWidth) return;
|
|
113
|
+
if (matchWidth === "min") {
|
|
114
|
+
Object.assign(elements.floating.style, {
|
|
115
|
+
width: "max-content",
|
|
116
|
+
minWidth: `${rects.reference.width}px`
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
95
120
|
Object.assign(elements.floating.style, {
|
|
96
|
-
width:
|
|
97
|
-
minWidth:
|
|
121
|
+
width: `${rects.reference.width}px`,
|
|
122
|
+
minWidth: ""
|
|
98
123
|
});
|
|
99
|
-
return;
|
|
100
124
|
}
|
|
101
|
-
|
|
102
|
-
width: `${rects.reference.width}px`,
|
|
103
|
-
minWidth: ""
|
|
104
|
-
});
|
|
105
|
-
} }));
|
|
125
|
+
}));
|
|
106
126
|
const { x, y, placement } = await computePosition(reference, floating, {
|
|
107
127
|
placement: resolveRequestedPlacement(),
|
|
108
128
|
strategy: "fixed",
|
|
@@ -150,4 +170,4 @@ function useFloating(reference, floatingEl, open, options = {}) {
|
|
|
150
170
|
};
|
|
151
171
|
}
|
|
152
172
|
//#endregion
|
|
153
|
-
export { createFloatingAnchor, useFloating };
|
|
173
|
+
export { createFloatingAnchor, floatingAvailableHeightVar, useFloating };
|
|
@@ -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 { 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
|
+
{"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 * Кастомное свойство, в которое слой пишет доступное место по вертикали —\n * расстояние от панели до края вьюпорта на той стороне, куда её в итоге\n * поставило.\n *\n * Свойство ставится на сам плавающий элемент при каждом пересчёте позиции, в\n * том числе на скролле и ресайзе. Слой **сообщает замер, а не применяет его**:\n * ограничивать ли себя этим числом и что при этом скроллить — решает панель,\n * потому что скролл принадлежит её содержимому.\n *\n * Ширину так публиковать незачем: её потолок статичен (`calc(100vw - 1rem)`) и\n * замера не требует. Высота статичной быть не может — доступное место зависит\n * от того, где стоит триггер.\n */\nexport const floatingAvailableHeightVar = '--gr-floating-available-height'\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 middleware.push(\n sizeMiddleware({\n padding: options.boundaryPadding ?? 8,\n apply({ rects, elements, availableHeight }) {\n // Сколько места осталось до края вьюпорта на той стороне, куда панель\n // в итоге встала. Считается после `flip`, поэтому сторона уже\n // окончательная. Слой только сообщает замер — решает панель.\n elements.floating.style.setProperty(\n floatingAvailableHeightVar,\n `${Math.max(0, Math.round(availableHeight))}px`,\n )\n\n if (!matchWidth)\n return\n\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 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,2 +1,2 @@
|
|
|
1
|
-
import { a as granularityDefaultThemes, i as granularityComponentConfigs, n as GRANULARITY_PROVIDER_ID, o as granularityThemeNames, r as createGranularityProvider, t as granularityProvider } from "./chunks/granular-provider-
|
|
1
|
+
import { a as granularityDefaultThemes, i as granularityComponentConfigs, n as GRANULARITY_PROVIDER_ID, o as granularityThemeNames, r as createGranularityProvider, t as granularityProvider } from "./chunks/granular-provider-BAy4APOX.js";
|
|
2
2
|
export { GRANULARITY_PROVIDER_ID, createGranularityProvider, granularityProvider as default, granularityProvider, granularityComponentConfigs, granularityDefaultThemes, granularityThemeNames };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as granularityDefaultThemes, i as granularityComponentConfigs, n as GRANULARITY_PROVIDER_ID, o as granularityThemeNames, r as createGranularityProvider, t as granularityProvider } from "./chunks/granular-provider-
|
|
1
|
+
import { a as granularityDefaultThemes, i as granularityComponentConfigs, n as GRANULARITY_PROVIDER_ID, o as granularityThemeNames, r as createGranularityProvider, t as granularityProvider } from "./chunks/granular-provider-BAy4APOX.js";
|
|
2
2
|
export { GRANULARITY_PROVIDER_ID, createGranularityProvider, granularityProvider as default, granularityProvider, granularityComponentConfigs, granularityDefaultThemes, granularityThemeNames };
|
package/dist/index.js
CHANGED
|
@@ -63,10 +63,10 @@ import { n as GrCollapse_default, t as GrCollapseItem_default } from "./componen
|
|
|
63
63
|
import { n as grIconSafelist, t as grIconConfig } from "./chunks/config-3bQeQvqd.js";
|
|
64
64
|
import { t as GrInput_default } from "./components/GrInput/chunks/GrInput-BLywRZUN.js";
|
|
65
65
|
import { t as useFocusTrap } from "./chunks/useFocusTrap-D9bIPNF6.js";
|
|
66
|
-
import { t as GrPopover_default } from "./components/GrPopover/chunks/GrPopover-
|
|
66
|
+
import { t as GrPopover_default } from "./components/GrPopover/chunks/GrPopover-DEFxDffj.js";
|
|
67
67
|
import { useDragGesture } from "./composables/useDragGesture.js";
|
|
68
68
|
import { t as GrSlider_default } from "./components/GrSlider/chunks/GrSlider-DTTrZTHK.js";
|
|
69
|
-
import { t as GrColorPicker_default } from "./components/GrColorPicker/chunks/GrColorPicker-
|
|
69
|
+
import { t as GrColorPicker_default } from "./components/GrColorPicker/chunks/GrColorPicker-CIgdPXHn.js";
|
|
70
70
|
import { n as grColorPickerSafelist, t as grColorPickerConfig } from "./chunks/config-DIDz_E5m.js";
|
|
71
71
|
import { a as GR_KBD_TOKENS, o as findKbdToken, t as GrKbd_default } from "./components/GrKbd/chunks/GrKbd-D2ga1H9i.js";
|
|
72
72
|
import { t as GrModal_default } from "./components/GrModal/chunks/GrModal-_JrDhon-.js";
|
|
@@ -80,7 +80,7 @@ import { t as GrConfirmDialog_default } from "./components/GrConfirmDialog/chunk
|
|
|
80
80
|
import { t as grConfirmDialogConfig } from "./chunks/config-A-bBiotF.js";
|
|
81
81
|
import "./components/GrConfirmDialog/index.js";
|
|
82
82
|
import { a as GrDropdownMenuItem_default, c as GrDropdownMenuDivider_default, o as GrDropdownMenuGroup_default, s as GrDropdownMenuHeader_default, t as GrDropdownMenuList_default } from "./components/GrDropdownMenu/chunks/GrDropdownMenuList-B33ZzwfB.js";
|
|
83
|
-
import { t as GrContextMenu_default } from "./components/GrContextMenu/chunks/GrContextMenu-
|
|
83
|
+
import { t as GrContextMenu_default } from "./components/GrContextMenu/chunks/GrContextMenu-DlFWm0eW.js";
|
|
84
84
|
import { t as grContextMenuConfig } from "./chunks/config-CsCUuukB.js";
|
|
85
85
|
import { t as GrTable_default } from "./components/GrTable/chunks/GrTable-jhUTczjv.js";
|
|
86
86
|
import { t as useDragSort } from "./chunks/useDragSort-0l47h4yb.js";
|
|
@@ -106,10 +106,10 @@ import { n as grDividerSafelist, t as grDividerConfig } from "./chunks/config-D8
|
|
|
106
106
|
import { t as GrDivider_default } from "./components/GrDivider/chunks/GrDivider-BXavGcMX.js";
|
|
107
107
|
import { n as grDrawerSafelist, t as grDrawerConfig } from "./chunks/config-ChPg-BQv.js";
|
|
108
108
|
import { t as GrDrawer_default } from "./components/GrDrawer/chunks/GrDrawer-xfoy-MCj.js";
|
|
109
|
-
import { t as GrDropdown_default } from "./components/GrDropdown/chunks/GrDropdown-
|
|
110
|
-
import { n as grDropdownSafelist, t as grDropdownConfig } from "./chunks/config-
|
|
109
|
+
import { t as GrDropdown_default } from "./components/GrDropdown/chunks/GrDropdown-7r772-Cq.js";
|
|
110
|
+
import { n as grDropdownSafelist, t as grDropdownConfig } from "./chunks/config-CT4lP6S4.js";
|
|
111
111
|
import "./components/GrDropdown/index.js";
|
|
112
|
-
import { n as GrDropdownMenuColumn_default, r as GrDropdownMenu_default, t as GrDropdownMenuColumns_default } from "./components/GrDropdownMenu/chunks/GrDropdownMenu-
|
|
112
|
+
import { n as GrDropdownMenuColumn_default, r as GrDropdownMenu_default, t as GrDropdownMenuColumns_default } from "./components/GrDropdownMenu/chunks/GrDropdownMenu-DNurbsWc.js";
|
|
113
113
|
import { n as grDropdownMenuSafelist, t as grDropdownMenuConfig } from "./chunks/config-_jbgsFpw.js";
|
|
114
114
|
import { t as GrEmptyState_default } from "./components/GrEmptyState/chunks/GrEmptyState-RqsWvgI9.js";
|
|
115
115
|
import { n as grEmptyStateSafelist, t as grEmptyStateConfig } from "./chunks/config-CWtfxkM9.js";
|
|
@@ -152,7 +152,7 @@ import { t as GrNumberInput_default } from "./components/GrNumberInput/chunks/Gr
|
|
|
152
152
|
import { t as GrSelect_default } from "./components/GrSelect/chunks/GrSelect-CZqV1Eg4.js";
|
|
153
153
|
import { n as grPaginationSafelist, t as grPaginationConfig } from "./chunks/config-CjKpUcmy.js";
|
|
154
154
|
import { t as GrPagination_default } from "./components/GrPagination/chunks/GrPagination-gBoWVHk6.js";
|
|
155
|
-
import { n as grPopoverSafelist, t as grPopoverConfig } from "./chunks/config-
|
|
155
|
+
import { n as grPopoverSafelist, t as grPopoverConfig } from "./chunks/config-DVCNvhIG2.js";
|
|
156
156
|
import "./components/GrPopover/index.js";
|
|
157
157
|
import { n as grProgressBarSafelist, r as GR_TONES, t as grProgressBarConfig } from "./chunks/config-DWL81-aM.js";
|
|
158
158
|
import "./components/GrProgressBar/index.js";
|
package/dist/theme.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as grThemeTokens, n as grDerivedTokens, t as grComponentTokens } from "./chunks/generated-
|
|
1
|
+
import { a as grThemeTokens, n as grDerivedTokens, t as grComponentTokens } from "./chunks/generated-Bk-f6NsN.js";
|
|
2
2
|
//#region src/theme/color.ts
|
|
3
3
|
function parseHex(value) {
|
|
4
4
|
const match = value.trim().match(/^#([\da-f]{3}|[\da-f]{6})$/i);
|
package/dist/tokens.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as grThemeTokens, i as grThemeNames, n as grDerivedTokens, r as grFoundationTokens, t as grComponentTokens } from "./chunks/generated-
|
|
1
|
+
import { a as grThemeTokens, i as grThemeNames, n as grDerivedTokens, r as grFoundationTokens, t as grComponentTokens } from "./chunks/generated-Bk-f6NsN.js";
|
|
2
2
|
export { grComponentTokens, grDerivedTokens, grFoundationTokens, grThemeNames, grThemeTokens };
|
|
@@ -42,5 +42,24 @@ export declare const panelSizes: Record<GrPopoverSize, string>;
|
|
|
42
42
|
* сгенерированном CSS. Меню внутри поповера с `p-3` — визуально не меню.
|
|
43
43
|
*/
|
|
44
44
|
export declare const panelSizesFlush: Record<GrPopoverSize, string>;
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Высота — тоже `min()` из двух пределов, но устроена иначе, чем ширина.
|
|
47
|
+
*
|
|
48
|
+
* Потолок ширины статичен: `calc(100vw-1rem)` не зависит от того, где стоит
|
|
49
|
+
* триггер. Потолок высоты статичным быть не может — доступное место есть
|
|
50
|
+
* расстояние до края вьюпорта на той стороне, куда панель встала, и меняется
|
|
51
|
+
* оно на каждом скролле. Поэтому второй операнд здесь — не константа, а замер
|
|
52
|
+
* слоя (`--gr-floating-available-height`, см. `useFloating`), и он пересчитан
|
|
53
|
+
* уже **после** `flip`: сторона окончательная.
|
|
54
|
+
*
|
|
55
|
+
* `overflow-y: auto` идёт с потолком одной строкой намеренно. Потолок без
|
|
56
|
+
* скролла не ограничивает, а обрезает: содержимое молча уходит под нижний край
|
|
57
|
+
* панели, и добраться до него нельзя ничем. Пока потолок не упёрся, полоса не
|
|
58
|
+
* рисуется, так что для коротких панелей не меняется ничего.
|
|
59
|
+
*
|
|
60
|
+
* Хук `--gr-popover-max-height` по умолчанию `100vh` — то есть «мнения нет»,
|
|
61
|
+
* и высоту диктует замер. Он же служит фолбэком, пока позиция не посчитана
|
|
62
|
+
* (первый кадр, SSR): панель не схлопывается в ноль, а просто не ограничена.
|
|
63
|
+
*/
|
|
64
|
+
export declare const popoverPanelBaseClass = "rounded-[var(--gr-radius-xl)] border border-[var(--gr-brd)] bg-[var(--gr-popover)] text-[var(--gr-popover-fg)] shadow-[var(--gr-shadow-2)] max-w-[min(var(--gr-popover-max-width,22rem),calc(100vw-1rem))] max-h-[min(var(--gr-popover-max-height,100vh),var(--gr-floating-available-height,100vh))] overflow-y-auto focus:outline-none";
|
|
46
65
|
export declare function grPopoverPanelClass(size: GrPopoverSize, placement: Placement, extra?: string, padding?: GrPopoverPadding): string;
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import type { CSSProperties, Ref } from 'vue';
|
|
2
2
|
import type { Placement, VirtualElement } from '@floating-ui/dom';
|
|
3
3
|
export type UseFloatingPlacement = Placement;
|
|
4
|
+
/**
|
|
5
|
+
* Кастомное свойство, в которое слой пишет доступное место по вертикали —
|
|
6
|
+
* расстояние от панели до края вьюпорта на той стороне, куда её в итоге
|
|
7
|
+
* поставило.
|
|
8
|
+
*
|
|
9
|
+
* Свойство ставится на сам плавающий элемент при каждом пересчёте позиции, в
|
|
10
|
+
* том числе на скролле и ресайзе. Слой **сообщает замер, а не применяет его**:
|
|
11
|
+
* ограничивать ли себя этим числом и что при этом скроллить — решает панель,
|
|
12
|
+
* потому что скролл принадлежит её содержимому.
|
|
13
|
+
*
|
|
14
|
+
* Ширину так публиковать незачем: её потолок статичен (`calc(100vw - 1rem)`) и
|
|
15
|
+
* замера не требует. Высота статичной быть не может — доступное место зависит
|
|
16
|
+
* от того, где стоит триггер.
|
|
17
|
+
*/
|
|
18
|
+
export declare const floatingAvailableHeightVar = "--gr-floating-available-height";
|
|
4
19
|
/**
|
|
5
20
|
* Прямоугольник в координатах вьюпорта — якорь, которого нет в DOM.
|
|
6
21
|
*
|
package/dist/web-types.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
|
|
3
3
|
"name": "@feugene/granularity",
|
|
4
|
-
"version": "0.34.
|
|
4
|
+
"version": "0.34.1",
|
|
5
5
|
"framework": "vue",
|
|
6
6
|
"js-types-syntax": "typescript",
|
|
7
7
|
"description-markup": "markdown",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feugene/granularity",
|
|
3
3
|
"description": "Granularity design system package with Vue 3 components, consumed via the `@feugene/unocss-preset-granular` preset.",
|
|
4
|
-
"version": "0.34.
|
|
4
|
+
"version": "0.34.1",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Evgeniy Fureev",
|