@celestia-island/hikari 0.4.4 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +22 -7
- package/src/DemoApp.tsx +491 -0
- package/src/animation/registerAnimations.test.ts +94 -0
- package/src/animation/registerAnimations.ts +103 -0
- package/src/components/HkAboutModal.scss +95 -0
- package/src/components/HkAboutModal.tsx +114 -0
- package/src/components/HkAdaptiveDialog.scss +22 -0
- package/src/components/HkAdaptiveDialog.test.tsx +271 -0
- package/src/components/HkAdaptiveDialog.tsx +167 -0
- package/src/components/HkAdminHeader.test.tsx +185 -0
- package/src/components/HkAdminHeader.tsx +308 -0
- package/src/components/HkAdminShell.test.tsx +237 -0
- package/src/components/HkAdminShell.tsx +129 -0
- package/src/components/HkAdminTablePage.tsx +70 -0
- package/src/components/HkAdminWidgets.test.ts +89 -0
- package/src/components/HkAlert.tsx +1 -1
- package/src/components/HkAltSignIn.scss +55 -0
- package/src/components/HkAltSignIn.test.tsx +88 -0
- package/src/components/HkAltSignIn.tsx +92 -0
- package/src/components/HkAttachmentModal.scss +49 -0
- package/src/components/HkAttachmentModal.tsx +318 -0
- package/src/components/HkAuthCard.tsx +50 -0
- package/src/components/HkAuthSubmitButton.scss +12 -0
- package/src/components/HkAuthSubmitButton.tsx +94 -0
- package/src/components/HkAvatar.scss +93 -0
- package/src/components/HkAvatar.tsx +2 -2
- package/src/components/HkBlockingToast.scss +123 -0
- package/src/components/HkBlockingToast.test.tsx +221 -0
- package/src/components/HkBlockingToast.tsx +182 -0
- package/src/components/HkBreadcrumb.scss +624 -0
- package/src/components/HkBreadcrumb.tsx +2 -2
- package/src/components/HkButton.scss +16 -2
- package/src/components/HkCaptchaModal.scss +14 -0
- package/src/components/HkCaptchaModal.tsx +62 -0
- package/src/components/HkCaptchaWidget.scss +30 -0
- package/src/components/HkCaptchaWidget.tsx +181 -0
- package/src/components/HkCard.scss +1 -1
- package/src/components/HkChatTypes.ts +65 -0
- package/src/components/HkCheckbox.scss +4 -9
- package/src/components/HkCheckbox.tsx +18 -6
- package/src/components/HkColorPicker.scss +120 -3
- package/src/components/HkColorPicker.tsx +130 -19
- package/src/components/HkColorPickerTouch.test.tsx +145 -0
- package/src/components/HkColorSchemeDialog.scss +52 -0
- package/src/components/HkColorSchemeDialog.test.ts +201 -0
- package/src/components/HkColorSchemeDialog.tsx +96 -0
- package/src/components/HkColorSchemeEditor.test.ts +270 -0
- package/src/components/HkColorSchemeEditor.tsx +410 -0
- package/src/components/HkConfirmDialog.scss +43 -3
- package/src/components/HkConfirmDialog.tsx +7 -4
- package/src/components/HkConnectionInfo.ts +80 -0
- package/src/components/HkConnectionStatus.tsx +158 -0
- package/src/components/HkCookieConsent.scss +22 -0
- package/src/components/HkCookieConsent.test.tsx +70 -0
- package/src/components/HkCookieConsent.tsx +56 -0
- package/src/components/HkCountdownDigit.tsx +105 -0
- package/src/components/HkDatePicker.scss +361 -0
- package/src/components/HkDatePicker.test.ts +459 -0
- package/src/components/HkDatePicker.tsx +568 -0
- package/src/components/HkDateTimePicker.scss +65 -37
- package/src/components/HkDateTimePicker.test.ts +344 -0
- package/src/components/HkDateTimePicker.tsx +249 -82
- package/src/components/HkDraggableGrid.scss +4 -1
- package/src/components/HkDraggableList.scss +4 -1
- package/src/components/HkDrawer.scss +27 -13
- package/src/components/HkDrawer.test.tsx +73 -0
- package/src/components/HkDrawer.tsx +157 -10
- package/src/components/HkErrorBoundary.scss +5 -2
- package/src/components/HkErrorBoundary.tsx +7 -4
- package/src/components/HkExpansionPanel.scss +143 -0
- package/src/components/HkExpansionPanel.test.tsx +109 -0
- package/src/components/HkExpansionPanel.tsx +101 -0
- package/src/components/HkFab.scss +162 -0
- package/src/components/HkFab.test.tsx +217 -0
- package/src/components/HkFab.tsx +214 -0
- package/src/components/HkHoverRevealAction.scss +6 -0
- package/src/components/HkHoverRevealAction.test.ts +68 -0
- package/src/components/HkHoverRevealAction.tsx +84 -6
- package/src/components/HkIcon.scss +123 -0
- package/src/components/HkIcon.tsx +7 -23
- package/src/components/HkIconButton.scss +170 -0
- package/src/components/HkIconButton.test.tsx +75 -0
- package/src/components/HkIconButton.tsx +4 -3
- package/src/components/HkIconButtonVars.scss +180 -0
- package/src/components/HkInput.affix.test.ts +253 -0
- package/src/components/HkInput.enter.test.ts +88 -0
- package/src/components/HkInput.scss +127 -8
- package/src/components/HkInput.tsx +245 -13
- package/src/components/HkKeywordSearchModal.scss +13 -0
- package/src/components/HkKeywordSearchModal.tsx +46 -9
- package/src/components/HkLabel.scss +34 -0
- package/src/components/HkLabel.test.tsx +61 -0
- package/src/components/HkLabel.tsx +45 -0
- package/src/components/HkListTransition.scss +26 -0
- package/src/components/HkListTransition.tsx +57 -0
- package/src/components/HkLocalePicker.scss +37 -0
- package/src/components/HkLocalePicker.tsx +133 -0
- package/src/components/HkLocalePickerPopup.tsx +35 -19
- package/src/components/HkLocalizedInput.scss +277 -0
- package/src/components/HkLocalizedInput.test.tsx +693 -0
- package/src/components/HkLocalizedInput.tsx +495 -0
- package/src/components/HkLogWindow.scss +121 -0
- package/src/components/HkLogWindow.tsx +195 -0
- package/src/components/HkLogo.scss +1 -1
- package/src/components/HkLogo.tsx +1 -1
- package/src/components/HkMarkdownRenderer.scss +22 -2
- package/src/components/HkMarkdownRenderer.tsx +33 -3
- package/src/components/HkMediaControlBar.tsx +20 -13
- package/src/components/HkMenu.scss +200 -0
- package/src/components/HkMenu.test.ts +779 -0
- package/src/components/HkMenu.tsx +770 -0
- package/src/components/HkMenuActionItem.scss +58 -0
- package/src/components/HkMenuActionItem.tsx +41 -0
- package/src/components/HkMenuIdentityItem.scss +70 -0
- package/src/components/HkMenuIdentityItem.tsx +42 -0
- package/src/components/HkMenuPanel.scss +20 -0
- package/src/components/HkMenuPanel.test.tsx +118 -0
- package/src/components/HkMenuPanel.tsx +32 -0
- package/src/components/HkMinimap.tsx +16 -8
- package/src/components/HkModal.autofollow.test.ts +44 -0
- package/src/components/HkModal.scss +170 -53
- package/src/components/HkModal.sheetgap.test.ts +82 -0
- package/src/components/HkModal.test.tsx +334 -0
- package/src/components/HkModal.tsx +314 -68
- package/src/components/HkModalBreadcrumb.scss +4 -1
- package/src/components/HkModalBreadcrumb.test.tsx +111 -0
- package/src/components/HkModalBreadcrumb.tsx +34 -11
- package/src/components/HkModelCatalog.ts +98 -0
- package/src/components/HkModelTag.scss +130 -0
- package/src/components/HkModelTag.test.tsx +111 -0
- package/src/components/HkModelTag.tsx +190 -0
- package/src/components/HkNavItem.scss +13 -2
- package/src/components/HkNavItem.tsx +2 -1
- package/src/components/HkNavSidebar.tsx +99 -0
- package/src/components/HkNumberInput.scss +12 -0
- package/src/components/HkNumberInput.tsx +15 -8
- package/src/components/HkPageHeader.scss +79 -0
- package/src/components/HkPageHeader.test.ts +67 -0
- package/src/components/HkPageHeader.tsx +51 -0
- package/src/components/HkPasswordInput.scss +29 -17
- package/src/components/HkPasswordInput.test.ts +281 -0
- package/src/components/HkPasswordInput.tsx +337 -59
- package/src/components/HkPickerPane.scss +158 -0
- package/src/components/HkPlaceholderMarquee.scss +109 -0
- package/src/components/HkPlaceholderMarquee.test.ts +305 -0
- package/src/components/HkPlaceholderMarquee.tsx +170 -0
- package/src/components/HkPopover.scss +134 -10
- package/src/components/HkPopover.sheetdock.test.ts +34 -0
- package/src/components/HkPopover.surfacevars.test.ts +20 -0
- package/src/components/HkPopover.titleSuppress.test.tsx +265 -0
- package/src/components/HkPopover.tsx +225 -50
- package/src/components/HkPopoverSheet.test.tsx +130 -0
- package/src/components/HkPopupSelect.scss +30 -10
- package/src/components/HkPopupSelect.tsx +21 -1
- package/src/components/HkProgressDialog.scss +31 -0
- package/src/components/HkProgressDialog.tsx +37 -19
- package/src/components/HkProtocolModal.scss +26 -0
- package/src/components/HkProtocolModal.tsx +112 -0
- package/src/components/HkRadio.scss +1 -16
- package/src/components/HkRadio.tsx +5 -2
- package/src/components/HkRichInput.scss +162 -0
- package/src/components/HkRichInput.tsx +335 -0
- package/src/components/HkRollingNumber.test.ts +90 -0
- package/src/components/HkRollingNumber.tsx +39 -1
- package/src/components/HkScrollContainer.scss +71 -54
- package/src/components/HkScrollContainer.test.ts +160 -0
- package/src/components/HkScrollContainer.tsx +187 -160
- package/src/components/HkSearchInput.tsx +3 -3
- package/src/components/HkSecretRevealModal.scss +61 -0
- package/src/components/HkSecretRevealModal.tsx +91 -0
- package/src/components/HkSelect.scss +263 -12
- package/src/components/HkSelect.sheetdock.test.ts +30 -0
- package/src/components/HkSelect.test.tsx +88 -0
- package/src/components/HkSelect.tsx +59 -74
- package/src/components/HkSelectPanel.test.tsx +666 -0
- package/src/components/HkSelectPanel.tsx +595 -0
- package/src/components/HkSelectSheet.test.tsx +132 -0
- package/src/components/HkSelectionGrid.scss +1 -1
- package/src/components/HkSelectionGrid.tsx +2 -1
- package/src/components/HkSelectionWaterfall.scss +1 -1
- package/src/components/HkShareBar.scss +40 -0
- package/src/components/HkShareBar.tsx +50 -0
- package/src/components/HkSidebar.scss +15 -10
- package/src/components/HkSidebar.tsx +26 -2
- package/src/components/HkSignInCard.test.ts +154 -0
- package/src/components/HkSignInCard.tsx +155 -0
- package/src/components/HkSlider.scss +99 -0
- package/src/components/HkSlider.test.tsx +155 -0
- package/src/components/HkSlider.tsx +173 -0
- package/src/components/HkSpinner.scss +36 -1
- package/src/components/HkSpinner.test.tsx +63 -0
- package/src/components/HkSpinner.tsx +16 -1
- package/src/components/HkSplash.scss +1 -1
- package/src/components/HkSplash.tsx +3 -1
- package/src/components/HkStatCard.scss +73 -0
- package/src/components/HkStatCard.tsx +46 -0
- package/src/components/HkStatusBar.test.tsx +178 -0
- package/src/components/HkStatusBar.tsx +379 -0
- package/src/components/HkStatusPill.scss +73 -0
- package/src/components/HkStatusPill.tsx +41 -0
- package/src/components/HkStepFlow.scss +75 -0
- package/src/components/HkStepFlow.test.tsx +246 -0
- package/src/components/HkStepFlow.tsx +115 -0
- package/src/components/HkSwitch.scss +5 -17
- package/src/components/HkSwitch.tsx +6 -3
- package/src/components/HkTable.scss +20 -1
- package/src/components/HkTable.tsx +28 -3
- package/src/components/HkTabs.scss +222 -48
- package/src/components/HkTabs.test.ts +517 -0
- package/src/components/HkTabs.tsx +567 -56
- package/src/components/HkTag.tsx +2 -1
- package/src/components/HkTextarea.tsx +5 -0
- package/src/components/HkThemeToggle.scss +232 -0
- package/src/components/HkThemeToggle.test.ts +231 -0
- package/src/components/HkThemeToggle.tsx +289 -0
- package/src/components/HkTimeline.scss +134 -1
- package/src/components/HkTimeline.test.tsx +319 -0
- package/src/components/HkTimeline.tsx +303 -54
- package/src/components/HkToast.scss +34 -5
- package/src/components/HkToast.test.tsx +109 -0
- package/src/components/HkToast.tsx +57 -8
- package/src/components/HkTokenUsageBadge.scss +53 -0
- package/src/components/HkTokenUsageBadge.test.tsx +59 -0
- package/src/components/HkTokenUsageBadge.tsx +54 -0
- package/src/components/HkTokenUsagePanel.scss +95 -0
- package/src/components/HkTokenUsagePanel.tsx +140 -0
- package/src/components/HkToolBlock.scss +428 -0
- package/src/components/HkToolBlock.tsx +639 -0
- package/src/components/HkTooltip.scss +14 -2
- package/src/components/HkTooltip.tsx +25 -2
- package/src/components/HkTrendChart.tsx +27 -16
- package/src/components/HkVoiceInputPopup.scss +77 -0
- package/src/components/HkVoiceInputPopup.tsx +97 -0
- package/src/components/HkZoomToolbar.scss +4 -1
- package/src/components/HkZoomToolbar.tsx +7 -4
- package/src/components/_hikari-vars.scss +6 -0
- package/src/composables/highlightLanguages.ts +109 -0
- package/src/composables/iconRegistry.ts +127 -0
- package/src/composables/messaging/browserTransport.ts +96 -0
- package/src/composables/messaging/index.ts +204 -0
- package/src/composables/messaging/messaging.test.ts +256 -0
- package/src/composables/messaging/toastTransport.ts +26 -0
- package/src/composables/messaging/types.ts +115 -0
- package/src/composables/useActionBar.ts +66 -0
- package/src/composables/useAttachments.ts +154 -0
- package/src/composables/useConnectionProbe.ts +142 -0
- package/src/composables/useDeferredTransition.ts +16 -0
- package/src/composables/useEngineHealth.ts +69 -0
- package/src/composables/useHighlight.ts +124 -0
- package/src/composables/useMeasuredHighlight.ts +150 -0
- package/src/composables/useOverlayScrollbar.test.ts +221 -0
- package/src/composables/useOverlayScrollbar.ts +296 -0
- package/src/composables/usePageTitle.ts +37 -0
- package/src/composables/useReducedMotion.ts +34 -0
- package/src/composables/useResourceListModal.tsx +147 -0
- package/src/composables/useSafeArea.ts +137 -0
- package/src/composables/useSendShortcut.ts +35 -0
- package/src/composables/useSizeMorph.test.ts +156 -0
- package/src/composables/useSizeMorph.ts +149 -0
- package/src/composables/useSurfaceTransition.test.ts +96 -0
- package/src/composables/useSurfaceTransition.ts +71 -0
- package/src/composables/useZoomPan.ts +192 -0
- package/src/demo.scss +9 -1
- package/src/demo.ts +5 -1
- package/src/i18n/locales/ar/chat.json +38 -0
- package/src/i18n/locales/ar/components.json +141 -40
- package/src/i18n/locales/ar/components.toml +59 -0
- package/src/i18n/locales/ar/platform.json +65 -0
- package/src/i18n/locales/de/chat.json +38 -0
- package/src/i18n/locales/de/components.json +139 -38
- package/src/i18n/locales/de/components.toml +59 -0
- package/src/i18n/locales/de/platform.json +65 -0
- package/src/i18n/locales/en/chat.json +38 -0
- package/src/i18n/locales/en/components.json +104 -3
- package/src/i18n/locales/en/components.toml +59 -0
- package/src/i18n/locales/en/platform.json +65 -0
- package/src/i18n/locales/es/chat.json +38 -0
- package/src/i18n/locales/es/components.json +141 -40
- package/src/i18n/locales/es/components.toml +59 -0
- package/src/i18n/locales/es/platform.json +65 -0
- package/src/i18n/locales/fr/chat.json +38 -0
- package/src/i18n/locales/fr/components.json +137 -36
- package/src/i18n/locales/fr/components.toml +59 -0
- package/src/i18n/locales/fr/platform.json +65 -0
- package/src/i18n/locales/ja/chat.json +38 -0
- package/src/i18n/locales/ja/components.json +104 -3
- package/src/i18n/locales/ja/components.toml +59 -0
- package/src/i18n/locales/ja/platform.json +65 -0
- package/src/i18n/locales/ko/chat.json +38 -0
- package/src/i18n/locales/ko/components.json +104 -3
- package/src/i18n/locales/ko/components.toml +59 -0
- package/src/i18n/locales/ko/platform.json +65 -0
- package/src/i18n/locales/pt/chat.json +38 -0
- package/src/i18n/locales/pt/components.json +141 -40
- package/src/i18n/locales/pt/components.toml +59 -0
- package/src/i18n/locales/pt/platform.json +65 -0
- package/src/i18n/locales/ru/chat.json +38 -0
- package/src/i18n/locales/ru/components.json +141 -40
- package/src/i18n/locales/ru/components.toml +59 -0
- package/src/i18n/locales/ru/platform.json +65 -0
- package/src/i18n/locales/zh-Hans/chat.json +38 -0
- package/src/i18n/locales/zh-Hans/components.json +109 -4
- package/src/i18n/locales/zh-Hans/components.toml +59 -0
- package/src/i18n/locales/zh-Hans/platform.json +65 -0
- package/src/i18n/locales/zh-Hant/chat.json +38 -0
- package/src/i18n/locales/zh-Hant/components.json +109 -4
- package/src/i18n/locales/zh-Hant/components.toml +59 -0
- package/src/i18n/locales/zh-Hant/platform.json +65 -0
- package/src/index.ts +225 -6
- package/src/plugins/bootScriptsPlugin.ts +47 -0
- package/src/plugins/buildHashPlugin.ts +67 -0
- package/src/plugins/index.ts +2 -0
- package/src/render.ts +1 -1
- package/src/runtime/animationBus.test.ts +112 -0
- package/src/runtime/animationBus.ts +33 -2
- package/src/runtime/backStack.test.ts +334 -0
- package/src/runtime/backStack.ts +377 -0
- package/src/runtime/env.ts +4 -0
- package/src/runtime/index.ts +15 -3
- package/src/runtime/intervalBus.test.ts +118 -0
- package/src/runtime/intervalBus.ts +189 -0
- package/src/runtime/mobileViewport.test.ts +132 -0
- package/src/runtime/mobileViewport.ts +157 -0
- package/src/runtime/pageLifecycle.ts +91 -0
- package/src/runtime/useBlockingToast.ts +112 -0
- package/src/runtime/useClipboard.ts +28 -0
- package/src/runtime/useConfirm.ts +15 -7
- package/src/runtime/useMediaQuery.test.ts +67 -0
- package/src/runtime/useMediaQuery.ts +89 -0
- package/src/runtime/useOverlay.test.ts +160 -0
- package/src/runtime/useOverlay.ts +59 -12
- package/src/runtime/usePopupManager.test.ts +200 -0
- package/src/runtime/usePopupManager.ts +106 -10
- package/src/runtime/useToast.test.ts +90 -0
- package/src/runtime/useToast.ts +20 -2
- package/src/styles/admin-tokens.scss +744 -0
- package/src/styles/index.scss +5 -3
- package/src/styles/theme/_glass.scss +33 -0
- package/src/styles/theme/_layout.scss +123 -0
- package/src/styles/theme/_scrollbar.scss +73 -0
- package/src/styles/theme/_tokens.scss +69 -0
- package/src/styles/theme/base.scss +617 -0
- package/src/styles/theme/foundation.scss +219 -0
- package/src/styles/theme/mixins.scss +793 -0
- package/src/styles/theme/themes.scss +25 -0
- package/src/styles/theme/variables.scss +125 -0
- package/src/theme/_field.scss +116 -0
- package/src/theme/fontContext.test.ts +179 -0
- package/src/theme/fontContext.ts +169 -0
- package/src/theme/index.ts +19 -2
- package/src/theme/presets.ts +86 -18
- package/src/theme/theme.scss +61 -1
- package/src/theme/tokenGroups.test.ts +534 -0
- package/src/theme/tokenGroups.ts +552 -0
- package/src/theme/useSolarTime.test.ts +113 -0
- package/src/theme/useSolarTime.ts +95 -20
- package/src/theme/useTheme.test.ts +72 -0
- package/src/theme/useTheme.ts +144 -9
- package/src/tokens.scss +110 -0
- package/src/utils/base64.ts +31 -0
- package/src/utils/connectivity.ts +51 -0
- package/src/utils/dom.test.ts +131 -0
- package/src/utils/dom.ts +133 -0
- package/src/utils/download.ts +22 -0
- package/src/utils/fileType.ts +127 -0
- package/src/utils/format.ts +98 -0
- package/src/utils/fuzzy.ts +77 -0
- package/src/utils/fuzzySearch.test.ts +87 -0
- package/src/utils/healthProbe.ts +64 -0
- package/src/utils/objectPath.ts +59 -0
- package/src/utils/password.ts +53 -0
- package/src/utils/pow.test.ts +45 -0
- package/src/utils/pow.ts +226 -0
- package/src/utils/powNonce.ts +95 -0
- package/src/utils/validation.test.ts +78 -0
- package/src/DemoApp.vue +0 -400
- package/src/components/HkCollapse.tsx +0 -48
- package/src/components/HkMorphingTabs.scss +0 -141
- package/src/components/HkMorphingTabs.tsx +0 -113
- package/src/composables/useBreakpoint.ts +0 -41
- package/src/composables/useClipboard.ts +0 -37
- package/src/composables/useConfirm.ts +0 -35
- package/src/composables/useToast.ts +0 -2
- package/src/runtime/useAsyncData.ts +0 -30
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
onBeforeUnmount,
|
|
6
6
|
onMounted,
|
|
7
7
|
ref,
|
|
8
|
+
shallowRef,
|
|
8
9
|
Teleport,
|
|
9
10
|
Transition,
|
|
10
11
|
watch,
|
|
@@ -13,9 +14,16 @@ import {
|
|
|
13
14
|
|
|
14
15
|
import { useI18n } from "../i18n/context";
|
|
15
16
|
import "./HkModal.scss";
|
|
17
|
+
import { focusFirst, trapFocus } from "../utils/dom";
|
|
16
18
|
import { useOverlay } from "../runtime/useOverlay";
|
|
17
19
|
import { usePopupManager } from "../runtime/usePopupManager";
|
|
20
|
+
import { createBackGuard } from "../runtime/backStack";
|
|
21
|
+
import { scheduleFrame, type AnimationHandle } from "../runtime/animationBus";
|
|
22
|
+
import { attachOverlayScrollbars, type OverlayScrollbarHandle } from "../composables/useOverlayScrollbar";
|
|
23
|
+
import { useSurfaceTransition } from "../composables/useSurfaceTransition";
|
|
24
|
+
import { useSizeMorph } from "../composables/useSizeMorph";
|
|
18
25
|
import HButton from "./HkButton";
|
|
26
|
+
import HFab from "./HkFab";
|
|
19
27
|
import HSpinner from "./HkSpinner";
|
|
20
28
|
|
|
21
29
|
export interface ModalAction {
|
|
@@ -29,28 +37,6 @@ export interface ModalAction {
|
|
|
29
37
|
const FOCUSABLE_SELECTOR =
|
|
30
38
|
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
31
39
|
|
|
32
|
-
function trapFocus(container: HTMLElement, e: KeyboardEvent): void {
|
|
33
|
-
const focusable = container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
|
|
34
|
-
if (focusable.length === 0) return;
|
|
35
|
-
const first = focusable[0];
|
|
36
|
-
const last = focusable[focusable.length - 1];
|
|
37
|
-
if (e.shiftKey) {
|
|
38
|
-
if (document.activeElement === first) {
|
|
39
|
-
e.preventDefault();
|
|
40
|
-
last.focus();
|
|
41
|
-
}
|
|
42
|
-
} else {
|
|
43
|
-
if (document.activeElement === last) {
|
|
44
|
-
e.preventDefault();
|
|
45
|
-
first.focus();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function focusFirst(container: HTMLElement): void {
|
|
51
|
-
const el = container.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);
|
|
52
|
-
if (el) el.focus();
|
|
53
|
-
}
|
|
54
40
|
|
|
55
41
|
export default defineComponent({
|
|
56
42
|
name: "HkModal",
|
|
@@ -66,6 +52,14 @@ export default defineComponent({
|
|
|
66
52
|
windowed: { type: Boolean, default: false },
|
|
67
53
|
overscanScreens: { type: Number, default: 1 },
|
|
68
54
|
autoFollow: { type: Boolean, default: false },
|
|
55
|
+
/**
|
|
56
|
+
* Consume the browser/system back gesture while open (window-first
|
|
57
|
+
* back priority): a marked history entry is pushed on open so back
|
|
58
|
+
* closes the modal instead of leaving the page. Only meaningful
|
|
59
|
+
* together with `closable`; disable for surfaces that manage their
|
|
60
|
+
* own history entries.
|
|
61
|
+
*/
|
|
62
|
+
backGuard: { type: Boolean, default: true },
|
|
69
63
|
},
|
|
70
64
|
emits: {
|
|
71
65
|
"update:modelValue": (_value: boolean) => true,
|
|
@@ -74,11 +68,31 @@ export default defineComponent({
|
|
|
74
68
|
setup(props, { emit, slots }) {
|
|
75
69
|
const { t } = useI18n();
|
|
76
70
|
const manager = usePopupManager();
|
|
77
|
-
|
|
71
|
+
// Open/close motion reported into the unified animation context
|
|
72
|
+
// (animationBus) — scrim and content on separate tracks so each
|
|
73
|
+
// layer's report arms/cancels independently.
|
|
74
|
+
const surf = useSurfaceTransition(320);
|
|
75
|
+
const overlayHooks = surf.hooks("overlay");
|
|
76
|
+
const contentHooks = surf.hooks("content");
|
|
77
|
+
const overlay = useOverlay({
|
|
78
|
+
name: "hk-modal",
|
|
79
|
+
// A global closeAll() must be able to actually close this modal
|
|
80
|
+
// (not just untrack it) — route it through the same closable
|
|
81
|
+
// guard as the user-initiated paths.
|
|
82
|
+
onCloseRequested: () => { if (props.closable) close(); },
|
|
83
|
+
});
|
|
78
84
|
|
|
79
85
|
const handle = ref<{ id: string; zIndex: number } | null>(null);
|
|
80
86
|
const bodyRef = ref<HTMLElement>();
|
|
81
87
|
const contentRef = ref<HTMLElement>();
|
|
88
|
+
/** Natural-height probe inside the scroll container: the body's
|
|
89
|
+
* content wrapper, whose height is the content's intrinsic height
|
|
90
|
+
* regardless of scroll — the size morph measures this (see
|
|
91
|
+
* useSizeMorph). */
|
|
92
|
+
const innerRef = ref<HTMLElement>();
|
|
93
|
+
// Content-driven size morphing: the frame follows content growth with
|
|
94
|
+
// the height transition instead of snapping (see useSizeMorph).
|
|
95
|
+
const morph = useSizeMorph(contentRef, innerRef);
|
|
82
96
|
const shouldRender = ref(false);
|
|
83
97
|
let previouslyFocused: HTMLElement | null = null;
|
|
84
98
|
let unmounted = false;
|
|
@@ -95,9 +109,35 @@ export default defineComponent({
|
|
|
95
109
|
// Auto-follow state
|
|
96
110
|
let autoFollowMutationObserver: MutationObserver | null = null;
|
|
97
111
|
const isFollowing = ref(true);
|
|
98
|
-
|
|
112
|
+
/** Body actually scrolls (drives the "Auto" tag visibility). */
|
|
113
|
+
const hasOverflow = shallowRef(false);
|
|
114
|
+
/**
|
|
115
|
+
* Sticky user-intent flag. Set by wheel / touch / drag-press /
|
|
116
|
+
* scroll-key gestures and consumed by the scroll handler; cleared
|
|
117
|
+
* by every programmatic movement (which is never user intent).
|
|
118
|
+
* Fling momentum keeps firing scroll events without new touches,
|
|
119
|
+
* which is why the flag stays set until consumed or cleared.
|
|
120
|
+
*/
|
|
121
|
+
let gestureDirty = false;
|
|
122
|
+
/** performance.now() of the last gesture that raised the flag —
|
|
123
|
+
* drives the settle-frame quiet window below. */
|
|
124
|
+
let gestureAt = 0;
|
|
99
125
|
const scrollContainerRef = ref<HTMLElement>();
|
|
100
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Window-first back priority: while this modal is the topmost open
|
|
129
|
+
* window, the back gesture (mobile back button/gesture, desktop
|
|
130
|
+
* browser back) closes it instead of navigating the page. Disabled
|
|
131
|
+
* for non-closable modals — back must not be swallowed by a surface
|
|
132
|
+
* it cannot close.
|
|
133
|
+
*/
|
|
134
|
+
const backGuardEnabled = () => props.closable && props.backGuard;
|
|
135
|
+
const backGuard = createBackGuard({
|
|
136
|
+
onBack: () => {
|
|
137
|
+
if (backGuardEnabled()) close();
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
101
141
|
function close() {
|
|
102
142
|
emit("update:modelValue", false);
|
|
103
143
|
}
|
|
@@ -209,30 +249,145 @@ export default defineComponent({
|
|
|
209
249
|
}
|
|
210
250
|
|
|
211
251
|
// --- Auto-follow ---
|
|
252
|
+
//
|
|
253
|
+
// Follow policy: ONLY real user motion can cancel following.
|
|
254
|
+
// Streaming content is pinned with direct scrollTop assignments
|
|
255
|
+
// drained across a few animation frames (never smooth-scroll
|
|
256
|
+
// chains), and every programmatic movement clears the sticky
|
|
257
|
+
// gesture flag — so mid-flight scroll events cannot flip isFollowing
|
|
258
|
+
// back and forth (the old flicker).
|
|
259
|
+
|
|
260
|
+
const FOLLOW_THRESHOLD = 32;
|
|
261
|
+
const SETTLE_FRAMES = 4;
|
|
262
|
+
/** Pending pin frames yield to gestures younger than this. */
|
|
263
|
+
const GESTURE_QUIET_MS = 250;
|
|
264
|
+
|
|
265
|
+
let settleRemaining = 0;
|
|
266
|
+
let settleHandle: AnimationHandle | null = null;
|
|
267
|
+
|
|
268
|
+
function runSettleFrame() {
|
|
269
|
+
settleHandle = null;
|
|
270
|
+
// A cancelled follow must not be fought by pending pins: drained
|
|
271
|
+
// frames bail out immediately (resumeJumpLatest re-kicks). The
|
|
272
|
+
// reset above must stay first so kickSettle() never sees a stale,
|
|
273
|
+
// already-consumed handle — that would deadlock all future pins.
|
|
274
|
+
if (!isFollowing.value) {
|
|
275
|
+
settleRemaining = 0;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (gestureDirty && performance.now() - gestureAt < GESTURE_QUIET_MS) {
|
|
279
|
+
// A live gesture is in flight: yield this frame to it and let
|
|
280
|
+
// the scroll handler judge the resulting position. Stale flags
|
|
281
|
+
// older than the quiet window fall through and are consumed
|
|
282
|
+
// below — no stale-flag stall, no fight against the finger.
|
|
283
|
+
if (settleRemaining > 0) {
|
|
284
|
+
settleRemaining--;
|
|
285
|
+
settleHandle = scheduleFrame(runSettleFrame);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
gestureDirty = false;
|
|
290
|
+
pinToBottom();
|
|
291
|
+
updateOverflow();
|
|
292
|
+
if (settleRemaining > 0) {
|
|
293
|
+
settleRemaining--;
|
|
294
|
+
settleHandle = scheduleFrame(runSettleFrame);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
212
297
|
|
|
213
|
-
function
|
|
298
|
+
function kickSettle() {
|
|
299
|
+
// hasOverflow gates the "Auto" tag and the jump FAB. It is only
|
|
300
|
+
// recomputed inside settle frames and scroll events; with windowed
|
|
301
|
+
// lists the body can grow late (async chunks, virtualized mounts)
|
|
302
|
+
// without either firing — poll it alongside the settle drain so the
|
|
303
|
+
// affordances appear on the frame the overflow actually exists.
|
|
304
|
+
updateOverflow();
|
|
305
|
+
settleRemaining = SETTLE_FRAMES;
|
|
306
|
+
if (settleHandle) return;
|
|
307
|
+
settleHandle = scheduleFrame(runSettleFrame);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function cancelSettle() {
|
|
311
|
+
settleHandle?.disconnect();
|
|
312
|
+
settleHandle = null;
|
|
313
|
+
settleRemaining = 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Direct assignment — no smooth scrolling, no intermediate events. */
|
|
317
|
+
function pinToBottom() {
|
|
214
318
|
const el = scrollContainerRef.value;
|
|
215
319
|
if (!el) return;
|
|
216
|
-
el.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
320
|
+
el.scrollTop = el.scrollHeight;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function updateOverflow() {
|
|
324
|
+
const el = scrollContainerRef.value;
|
|
325
|
+
if (!el) return;
|
|
326
|
+
hasOverflow.value = el.scrollHeight > el.clientHeight + 4;
|
|
220
327
|
}
|
|
221
328
|
|
|
222
329
|
function onBodyScroll() {
|
|
223
330
|
if (!props.autoFollow) return;
|
|
224
331
|
const el = scrollContainerRef.value;
|
|
225
332
|
if (!el) return;
|
|
226
|
-
|
|
333
|
+
updateOverflow();
|
|
227
334
|
const distanceFromBottom =
|
|
228
335
|
el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
229
|
-
if (distanceFromBottom >
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
336
|
+
if (distanceFromBottom > FOLLOW_THRESHOLD) {
|
|
337
|
+
// Without a sticky gesture flag nothing cancels here — streaming
|
|
338
|
+
// pins no longer flash anything.
|
|
339
|
+
if (gestureDirty) {
|
|
340
|
+
gestureDirty = false;
|
|
341
|
+
isFollowing.value = false;
|
|
342
|
+
}
|
|
343
|
+
} else if (!isFollowing.value) {
|
|
344
|
+
// User returned near the bottom (or a pin landed): re-arm naturally.
|
|
345
|
+
gestureDirty = false;
|
|
234
346
|
isFollowing.value = true;
|
|
235
347
|
}
|
|
348
|
+
// Sub-threshold events while still following must NOT touch the
|
|
349
|
+
// flag: a slow upward drag emits many <32px scrolls, and eating
|
|
350
|
+
// the flag on any of them would make cancellation impossible.
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const GESTURE_KEYS = new Set([
|
|
354
|
+
"ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " ",
|
|
355
|
+
]);
|
|
356
|
+
|
|
357
|
+
function markGesture() {
|
|
358
|
+
gestureDirty = true;
|
|
359
|
+
gestureAt = performance.now();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function onGestureKeydown(e: KeyboardEvent) {
|
|
363
|
+
if (GESTURE_KEYS.has(e.key)) {
|
|
364
|
+
gestureDirty = true;
|
|
365
|
+
gestureAt = performance.now();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Press-and-drag covers mouse/touch before the drag starts; the
|
|
370
|
+
* flag stays sticky so post-fling momentum still cancels follow. */
|
|
371
|
+
function onGesturePointerdown(e: PointerEvent) {
|
|
372
|
+
if (e.button === 0) {
|
|
373
|
+
gestureDirty = true;
|
|
374
|
+
gestureAt = performance.now();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** A press-drag longer than the quiet window would otherwise have
|
|
379
|
+
* its flag purged mid-gesture (a mutation frame pins under the
|
|
380
|
+
* finger and the rest of the drag can no longer cancel). Moving
|
|
381
|
+
* while buttons are held re-stamps freshness; free hovering —
|
|
382
|
+
* e.buttons === 0 — must NOT keep a stale flag alive forever. */
|
|
383
|
+
function onGesturePointermove(e: PointerEvent) {
|
|
384
|
+
if (e.buttons !== 0) markGesture();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Touch drags emit touchmove without pointermove on some engines
|
|
388
|
+
* (and happy-dom tests synthesize either); belt and suspenders. */
|
|
389
|
+
function onGestureTouchmove() {
|
|
390
|
+
markGesture();
|
|
236
391
|
}
|
|
237
392
|
|
|
238
393
|
function setupAutoFollow() {
|
|
@@ -240,16 +395,24 @@ export default defineComponent({
|
|
|
240
395
|
teardownAutoFollow();
|
|
241
396
|
|
|
242
397
|
isFollowing.value = true;
|
|
243
|
-
|
|
244
|
-
|
|
398
|
+
gestureDirty = false;
|
|
399
|
+
pinToBottom();
|
|
400
|
+
updateOverflow();
|
|
401
|
+
kickSettle();
|
|
402
|
+
|
|
403
|
+
const el = scrollContainerRef.value!;
|
|
404
|
+
el.addEventListener("wheel", markGesture, { passive: true });
|
|
405
|
+
el.addEventListener("touchstart", markGesture, { passive: true });
|
|
406
|
+
el.addEventListener("touchmove", onGestureTouchmove, { passive: true });
|
|
407
|
+
el.addEventListener("keydown", onGestureKeydown);
|
|
408
|
+
el.addEventListener("pointerdown", onGesturePointerdown);
|
|
409
|
+
el.addEventListener("pointermove", onGesturePointermove, { passive: true });
|
|
245
410
|
|
|
246
411
|
autoFollowMutationObserver = new MutationObserver(() => {
|
|
247
|
-
if (isFollowing.value && !unmounted)
|
|
248
|
-
nextTick(() => scrollToBottom(true));
|
|
249
|
-
}
|
|
412
|
+
if (isFollowing.value && !unmounted) kickSettle();
|
|
250
413
|
});
|
|
251
414
|
|
|
252
|
-
autoFollowMutationObserver.observe(
|
|
415
|
+
autoFollowMutationObserver.observe(el, {
|
|
253
416
|
childList: true,
|
|
254
417
|
subtree: true,
|
|
255
418
|
characterData: true,
|
|
@@ -261,19 +424,53 @@ export default defineComponent({
|
|
|
261
424
|
autoFollowMutationObserver.disconnect();
|
|
262
425
|
autoFollowMutationObserver = null;
|
|
263
426
|
}
|
|
427
|
+
const el = scrollContainerRef.value;
|
|
428
|
+
if (el) {
|
|
429
|
+
el.removeEventListener("wheel", markGesture);
|
|
430
|
+
el.removeEventListener("touchstart", markGesture);
|
|
431
|
+
el.removeEventListener("touchmove", onGestureTouchmove);
|
|
432
|
+
el.removeEventListener("keydown", onGestureKeydown);
|
|
433
|
+
el.removeEventListener("pointerdown", onGesturePointerdown);
|
|
434
|
+
el.removeEventListener("pointermove", onGesturePointermove);
|
|
435
|
+
}
|
|
436
|
+
cancelSettle();
|
|
264
437
|
isFollowing.value = true;
|
|
265
|
-
|
|
438
|
+
hasOverflow.value = false;
|
|
439
|
+
gestureDirty = false;
|
|
266
440
|
}
|
|
267
441
|
|
|
268
|
-
function
|
|
442
|
+
function resumeJumpLatest() {
|
|
269
443
|
if (!props.autoFollow) return;
|
|
270
444
|
isFollowing.value = true;
|
|
271
|
-
|
|
272
|
-
|
|
445
|
+
pinToBottom();
|
|
446
|
+
kickSettle();
|
|
273
447
|
}
|
|
274
448
|
|
|
275
449
|
// --- Lifecycle ---
|
|
276
450
|
|
|
451
|
+
// Overlay scrollbar on the scrolling body (shared chrome). The body
|
|
452
|
+
// mounts with the modal surface (shouldRender) and survives through
|
|
453
|
+
// the leave transition; attach after the DOM lands, detach on close
|
|
454
|
+
// and unmount so nothing leaks inside the Teleport portal.
|
|
455
|
+
let bodyScrollbar: OverlayScrollbarHandle | null = null;
|
|
456
|
+
|
|
457
|
+
function detachBodyScrollbar(): void {
|
|
458
|
+
bodyScrollbar?.detach();
|
|
459
|
+
bodyScrollbar = null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
watch(shouldRender, (render) => {
|
|
463
|
+
if (render) {
|
|
464
|
+
void nextTick(() => {
|
|
465
|
+
if (!shouldRender.value || !scrollContainerRef.value) return;
|
|
466
|
+
detachBodyScrollbar();
|
|
467
|
+
bodyScrollbar = attachOverlayScrollbars(scrollContainerRef.value, { axis: "vertical" });
|
|
468
|
+
});
|
|
469
|
+
} else {
|
|
470
|
+
detachBodyScrollbar();
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
277
474
|
watch(
|
|
278
475
|
() => props.modelValue,
|
|
279
476
|
(val) => {
|
|
@@ -284,17 +481,36 @@ export default defineComponent({
|
|
|
284
481
|
manager.unregister(handle.value.id);
|
|
285
482
|
}
|
|
286
483
|
shouldRender.value = true;
|
|
484
|
+
// Windows always block, so the kind alone lists this layer in
|
|
485
|
+
// the modal-stack breadcrumb on every form factor.
|
|
287
486
|
handle.value = manager.register("modal", true, props.title);
|
|
288
487
|
overlay.open();
|
|
488
|
+
if (backGuardEnabled() && backGuard.entries === 0) {
|
|
489
|
+
backGuard.push();
|
|
490
|
+
}
|
|
289
491
|
} else {
|
|
290
492
|
// Close happens via Transition onAfterLeave,
|
|
291
493
|
// but if modelValue flips to false without Transition
|
|
292
494
|
// (e.g. immediate), clean up now.
|
|
495
|
+
overlay.close();
|
|
496
|
+
backGuard.release();
|
|
293
497
|
}
|
|
294
498
|
},
|
|
295
499
|
{ immediate: true },
|
|
296
500
|
);
|
|
297
501
|
|
|
502
|
+
// closable/backGuard may flip while open (submit flows disable
|
|
503
|
+
// closing): keep the owned entry in lockstep so back is never a
|
|
504
|
+
// dead gesture on a surface it can no longer close.
|
|
505
|
+
watch(
|
|
506
|
+
backGuardEnabled,
|
|
507
|
+
(enabled) => {
|
|
508
|
+
if (unmounted || !props.modelValue) return;
|
|
509
|
+
if (enabled && backGuard.entries === 0) backGuard.push();
|
|
510
|
+
else if (!enabled && backGuard.entries > 0) backGuard.release();
|
|
511
|
+
},
|
|
512
|
+
);
|
|
513
|
+
|
|
298
514
|
watch(
|
|
299
515
|
() => props.title,
|
|
300
516
|
(newTitle) => {
|
|
@@ -310,8 +526,10 @@ export default defineComponent({
|
|
|
310
526
|
|
|
311
527
|
onBeforeUnmount(() => {
|
|
312
528
|
unmounted = true;
|
|
529
|
+
detachBodyScrollbar();
|
|
313
530
|
teardownWindowed();
|
|
314
531
|
teardownAutoFollow();
|
|
532
|
+
backGuard.destroy();
|
|
315
533
|
if (handle.value) {
|
|
316
534
|
manager.unregister(handle.value.id);
|
|
317
535
|
handle.value = null;
|
|
@@ -358,7 +576,14 @@ export default defineComponent({
|
|
|
358
576
|
style={{ zIndex: overlayZ.value }}
|
|
359
577
|
onKeydown={onKeydown}
|
|
360
578
|
>
|
|
361
|
-
<Transition
|
|
579
|
+
<Transition
|
|
580
|
+
name="hk-modal-overlay"
|
|
581
|
+
appear
|
|
582
|
+
onBeforeEnter={overlayHooks.onBeforeEnter}
|
|
583
|
+
onAfterEnter={overlayHooks.onAfterEnter}
|
|
584
|
+
onBeforeLeave={overlayHooks.onBeforeLeave}
|
|
585
|
+
onAfterLeave={overlayHooks.onAfterLeave}
|
|
586
|
+
>
|
|
362
587
|
{props.modelValue && (
|
|
363
588
|
<div
|
|
364
589
|
class="hk-modal-overlay"
|
|
@@ -369,8 +594,23 @@ export default defineComponent({
|
|
|
369
594
|
<Transition
|
|
370
595
|
name="hk-modal-content"
|
|
371
596
|
appear
|
|
372
|
-
|
|
373
|
-
|
|
597
|
+
onBeforeEnter={contentHooks.onBeforeEnter}
|
|
598
|
+
onAfterEnter={() => {
|
|
599
|
+
contentHooks.onAfterEnter();
|
|
600
|
+
onAfterEnter();
|
|
601
|
+
// Size morphs arm once the open choreography finished —
|
|
602
|
+
// pinning during enter would override its height reveal.
|
|
603
|
+
morph.start();
|
|
604
|
+
}}
|
|
605
|
+
onBeforeLeave={() => {
|
|
606
|
+
contentHooks.onBeforeLeave();
|
|
607
|
+
// Release the pinned height so the leave owns the frame.
|
|
608
|
+
morph.stop();
|
|
609
|
+
}}
|
|
610
|
+
onAfterLeave={() => {
|
|
611
|
+
contentHooks.onAfterLeave();
|
|
612
|
+
onAfterLeave();
|
|
613
|
+
}}
|
|
374
614
|
>
|
|
375
615
|
{props.modelValue && (
|
|
376
616
|
<div
|
|
@@ -395,7 +635,7 @@ export default defineComponent({
|
|
|
395
635
|
{props.closable && (
|
|
396
636
|
<button
|
|
397
637
|
class="hk-modal-close"
|
|
398
|
-
aria-label={t("
|
|
638
|
+
aria-label={t("hikari::modal.close", "Close")}
|
|
399
639
|
onClick={close}
|
|
400
640
|
>
|
|
401
641
|
<svg
|
|
@@ -424,30 +664,36 @@ export default defineComponent({
|
|
|
424
664
|
class="hk-modal-body-scroll"
|
|
425
665
|
onScroll={onBodyScroll}
|
|
426
666
|
>
|
|
427
|
-
<div class="hk-modal-body-inner">
|
|
667
|
+
<div ref={innerRef} class="hk-modal-body-inner">
|
|
428
668
|
{props.windowed
|
|
429
669
|
? renderWindowedBody()
|
|
430
670
|
: slots.default?.()}
|
|
431
671
|
</div>
|
|
432
672
|
</div>
|
|
433
|
-
{props.autoFollow &&
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
<
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
673
|
+
{props.autoFollow && (
|
|
674
|
+
<>
|
|
675
|
+
{/* "Auto" marks ACTIVE follow: pinned at bottom
|
|
676
|
+
with a body that overflows. Cancelled follow
|
|
677
|
+
swaps it for the jump-back FAB instead. */}
|
|
678
|
+
{hasOverflow.value && isFollowing.value && (
|
|
679
|
+
<span
|
|
680
|
+
class="hk-modal-autofollow-tag"
|
|
681
|
+
aria-hidden="true"
|
|
682
|
+
>
|
|
683
|
+
{t("hikari::modal.auto", "Auto")}
|
|
684
|
+
</span>
|
|
685
|
+
)}
|
|
686
|
+
{!isFollowing.value && (
|
|
687
|
+
<HFab
|
|
688
|
+
class="hk-modal-jump"
|
|
689
|
+
positioning="absolute"
|
|
690
|
+
corner="bottom-right"
|
|
691
|
+
icon="ArrowDown"
|
|
692
|
+
ariaLabel={t("hikari::modal.jumpToLatest", "Back to latest")}
|
|
693
|
+
onClick={resumeJumpLatest}
|
|
694
|
+
/>
|
|
695
|
+
)}
|
|
696
|
+
</>
|
|
451
697
|
)}
|
|
452
698
|
</div>
|
|
453
699
|
{renderFooter()}
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
position: fixed;
|
|
3
3
|
left: 50%;
|
|
4
4
|
transform: translate(-50%, -50%);
|
|
5
|
-
|
|
5
|
+
/* Above every popup band's scrim (modal 1000+, dropdown 2000+; in-band
|
|
6
|
+
steps of 2 keep real stacks far below this) so multi-layer windows
|
|
7
|
+
never bury the strip; still below tooltip (3000) / toast (4000). */
|
|
8
|
+
z-index: var(--hk-breadcrumb-z, 2500);
|
|
6
9
|
display: flex;
|
|
7
10
|
align-items: center;
|
|
8
11
|
gap: var(--hi-space-4, 0.5rem);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createApp, nextTick } from "vue";
|
|
3
|
+
|
|
4
|
+
import HkModalBreadcrumb from "./HkModalBreadcrumb";
|
|
5
|
+
import { usePopupManager } from "../runtime/usePopupManager";
|
|
6
|
+
|
|
7
|
+
const mounts: ReturnType<typeof createApp>[] = [];
|
|
8
|
+
const containers: HTMLElement[] = [];
|
|
9
|
+
// The registry is a module singleton shared with the strip.
|
|
10
|
+
const manager = usePopupManager();
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
for (const app of mounts.splice(0)) app.unmount();
|
|
14
|
+
for (const el of containers.splice(0)) el.remove();
|
|
15
|
+
for (const entry of [...manager.registry.value.values()]) {
|
|
16
|
+
manager.unregister(entry.id);
|
|
17
|
+
}
|
|
18
|
+
document.body.style.overflow = "";
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
async function mountStrip(): Promise<void> {
|
|
22
|
+
const container = document.createElement("div");
|
|
23
|
+
document.body.appendChild(container);
|
|
24
|
+
containers.push(container);
|
|
25
|
+
const app = createApp(HkModalBreadcrumb);
|
|
26
|
+
mounts.push(app);
|
|
27
|
+
app.mount(container);
|
|
28
|
+
await nextTick();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function strip(): HTMLElement | null {
|
|
32
|
+
return document.querySelector("nav.hk-modal-breadcrumb");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function labels(): string[] {
|
|
36
|
+
const nav = strip();
|
|
37
|
+
if (!nav) return [];
|
|
38
|
+
return [...nav.querySelectorAll(".hk-modal-breadcrumb-item")].map(
|
|
39
|
+
(el) => el.textContent ?? "",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("HkModalBreadcrumb window-stack policy", () => {
|
|
44
|
+
it("treats an anchored desktop menu as a hidden level", async () => {
|
|
45
|
+
// Theme window + an untitled anchor-attached popover (blocking=false):
|
|
46
|
+
// the popover is not a window the user navigates, so a single-window
|
|
47
|
+
// situation stays strip-less.
|
|
48
|
+
manager.register("modal", true, "Custom theme");
|
|
49
|
+
manager.register("dropdown", false);
|
|
50
|
+
await mountStrip();
|
|
51
|
+
expect(strip()).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("lists a menu only while it blocks as a mobile bottom sheet", async () => {
|
|
55
|
+
manager.register("modal", true, "Custom theme");
|
|
56
|
+
const popup = manager.register("dropdown", false, "Pick a color", true);
|
|
57
|
+
await mountStrip();
|
|
58
|
+
expect(labels()).toEqual(["Custom theme", "Pick a color"]);
|
|
59
|
+
// The sheet paints above the window, so it is the current crumb.
|
|
60
|
+
expect(
|
|
61
|
+
strip()!.querySelector(".hk-modal-breadcrumb-item-current")!.textContent,
|
|
62
|
+
).toBe("Pick a color");
|
|
63
|
+
|
|
64
|
+
// Morphing back to an anchored popover (blocking=false) removes the
|
|
65
|
+
// level instead of leaving a stale crumb behind.
|
|
66
|
+
manager.setBlocking(popup.id, false);
|
|
67
|
+
await nextTick();
|
|
68
|
+
expect(strip()).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("never falls back to a bare Layer N label", async () => {
|
|
72
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
73
|
+
try {
|
|
74
|
+
// Untitled window + untitled blocking sheet: the strip must use the
|
|
75
|
+
// localized generic labels ("Window" / "Menu" in en), not "Layer N".
|
|
76
|
+
manager.register("modal", true);
|
|
77
|
+
manager.register("dropdown", false, undefined, true);
|
|
78
|
+
await mountStrip();
|
|
79
|
+
expect(labels()).toEqual(["Window", "Menu"]);
|
|
80
|
+
expect(labels().join(" ")).not.toMatch(/Layer/);
|
|
81
|
+
} finally {
|
|
82
|
+
warn.mockRestore();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("orders crumbs by z and names the strip for assistive tech", async () => {
|
|
87
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
88
|
+
try {
|
|
89
|
+
const win = manager.register("modal", true, "Settings");
|
|
90
|
+
const drawer = manager.register("drawer", true, "Details");
|
|
91
|
+
expect(drawer.zIndex).toBeGreaterThan(win.zIndex); // same band, later slot
|
|
92
|
+
await mountStrip();
|
|
93
|
+
// In-band order is open order: the later drawer sits on top.
|
|
94
|
+
expect(labels()).toEqual(["Settings", "Details"]);
|
|
95
|
+
expect(strip()!.getAttribute("aria-label")).toBe("Window layers");
|
|
96
|
+
} finally {
|
|
97
|
+
warn.mockRestore();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("exposes the registry entry shape the strip reads", async () => {
|
|
102
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
103
|
+
try {
|
|
104
|
+
const handle = manager.register("dropdown", false, "Sheet", true);
|
|
105
|
+
const entry = manager.registry.value.get(handle.id);
|
|
106
|
+
expect(entry).toMatchObject({ kind: "dropdown", title: "Sheet", blocking: true });
|
|
107
|
+
} finally {
|
|
108
|
+
warn.mockRestore();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|