@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
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { defineComponent, ref, computed, onMounted, onBeforeUnmount, watch, type PropType } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Overflow strategy for a placeholder that does not fit its input.
|
|
5
|
+
*
|
|
6
|
+
* - `marquee` (default): while the text fits, the overlay stays in the DOM
|
|
7
|
+
* only as an invisible measuring probe and the host input shows its NATIVE
|
|
8
|
+
* placeholder (crisp, accessible, zero animation). When the text overflows
|
|
9
|
+
* the input line, the overlay becomes visible, renders the text three
|
|
10
|
+
* times side by side inside a clipping window, and translates the strip
|
|
11
|
+
* leftward in a loop — the classic scrolling storefront sign. The strip
|
|
12
|
+
* content is identical in all three copies so the wrap-around is seamless.
|
|
13
|
+
*
|
|
14
|
+
* The motion is PURE CSS: one registered infinite keyframes animation
|
|
15
|
+
* (`hk-placeholder-marquee-scroll`, see animation/registerAnimations.ts)
|
|
16
|
+
* whose loop distance and duration live on inline custom properties
|
|
17
|
+
* (`--hk-marquee-shift` / `--hk-marquee-duration`), measured once per
|
|
18
|
+
* geometry change — never per frame. There is no JS/rAF animation and no
|
|
19
|
+
* reactive per-frame transform write, so scrolling the page (or a mobile
|
|
20
|
+
* bottom sheet) never competes with the strip for the main thread; the
|
|
21
|
+
* animation runs on the compositor. The animation-context switch
|
|
22
|
+
* (`setReducedMotion` / performance suspension via
|
|
23
|
+
* `html[data-css-animations="0"]`) parks the strip, and the strip also
|
|
24
|
+
* parks while the host input is focused (`data-parked`).
|
|
25
|
+
*
|
|
26
|
+
* The `overflowChange` event tells the host when to swap its native
|
|
27
|
+
* placeholder for the scrolling overlay.
|
|
28
|
+
* - `truncate`: hard-cut the text at the container edge with an ellipsis.
|
|
29
|
+
*/
|
|
30
|
+
export type PlaceholderVariant = "marquee" | "truncate";
|
|
31
|
+
|
|
32
|
+
/** Horizontal spacing between repeated copies — mirrors the scss copy
|
|
33
|
+
* padding-right; kept in sync so overflow detection measures reality. */
|
|
34
|
+
const COPY_SPACING = 24;
|
|
35
|
+
|
|
36
|
+
export const HkPlaceholderMarquee = defineComponent({
|
|
37
|
+
name: "HkPlaceholderMarquee",
|
|
38
|
+
props: {
|
|
39
|
+
text: { type: String, default: "" },
|
|
40
|
+
variant: { type: String as PropType<PlaceholderVariant>, default: "marquee" },
|
|
41
|
+
/** Pixels per second of strip travel. Negative scrolls rightward. */
|
|
42
|
+
speed: { type: Number, default: 24 },
|
|
43
|
+
},
|
|
44
|
+
emits: {
|
|
45
|
+
overflowChange: (_overflowing: boolean) => true,
|
|
46
|
+
},
|
|
47
|
+
setup(props, { emit, expose }) {
|
|
48
|
+
const hostEl = ref<HTMLElement | null>(null);
|
|
49
|
+
const firstCopyEl = ref<HTMLElement | null>(null);
|
|
50
|
+
const overflowing = ref(false);
|
|
51
|
+
const parked = ref(false);
|
|
52
|
+
/** One copy's laid-out width (incl. trailing spacing) — reactive so
|
|
53
|
+
* the loop geometry republishes when it changes even if the
|
|
54
|
+
* overflow state does not (zoom, font swap, text edit mid-overflow). */
|
|
55
|
+
const loopWidth = ref(0);
|
|
56
|
+
|
|
57
|
+
let ro: ResizeObserver | null = null;
|
|
58
|
+
|
|
59
|
+
const measure = () => {
|
|
60
|
+
const host = hostEl.value;
|
|
61
|
+
const copy = firstCopyEl.value;
|
|
62
|
+
if (!host || !copy) return;
|
|
63
|
+
// Measure ONE copy's laid-out width directly (it includes the 24px
|
|
64
|
+
// copy spacing as trailing padding). Reading the first copy element —
|
|
65
|
+
// instead of stripWidth / 3 — stays correct in the fitting case,
|
|
66
|
+
// where the strip holds a single copy, and through overflow flips,
|
|
67
|
+
// where the copy count changes under the same strip. The fractional
|
|
68
|
+
// bounding-rect width keeps the CSS loop shift bit-exact with the
|
|
69
|
+
// laid-out advance — an integer-rounded width would show a ≤0.5px
|
|
70
|
+
// seam at every wrap (the copy rides the animated strip, so the
|
|
71
|
+
// width itself is never affected by the strip's transform).
|
|
72
|
+
const copyWidth = copy.getBoundingClientRect().width;
|
|
73
|
+
loopWidth.value = copyWidth;
|
|
74
|
+
overflowing.value = copyWidth - COPY_SPACING > host.clientWidth;
|
|
75
|
+
emit("overflowChange", overflowing.value);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
onMounted(() => {
|
|
79
|
+
measure();
|
|
80
|
+
if (typeof ResizeObserver !== "undefined" && hostEl.value) {
|
|
81
|
+
ro = new ResizeObserver(() => measure());
|
|
82
|
+
ro.observe(hostEl.value);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
watch(() => props.text, () => {
|
|
87
|
+
// Re-measure after the DOM paints the new strip.
|
|
88
|
+
requestAnimationFrame(() => measure());
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
onBeforeUnmount(() => {
|
|
92
|
+
ro?.disconnect();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expose({
|
|
96
|
+
/** Called by the host input: focus parks the strip, blur resumes. */
|
|
97
|
+
setActive(active: boolean) {
|
|
98
|
+
parked.value = active;
|
|
99
|
+
},
|
|
100
|
+
measure,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/** Loop geometry, published as custom properties + a direction flag
|
|
104
|
+
* on the strip: the keyframes animate
|
|
105
|
+
* `translateX(0 → var(--hk-marquee-shift))` over
|
|
106
|
+
* `var(--hk-marquee-duration)` — both measured, never recomputed per
|
|
107
|
+
* frame. Duration = one copy width at the configured px/s speed;
|
|
108
|
+
* the sign of the speed flips the direction (reverse plays the
|
|
109
|
+
* keyframes backwards, which wraps seamlessly on the three copies).
|
|
110
|
+
* A zero speed parks the strip (no scroll class) — matching the
|
|
111
|
+
* pre-CSS behavior, where the JS loop simply never advanced
|
|
112
|
+
* (non-finite speeds fall back to the default rate). */
|
|
113
|
+
const stripVars = computed(() => {
|
|
114
|
+
if (!overflowing.value || loopWidth.value <= 0) return undefined;
|
|
115
|
+
const speed = Number.isFinite(props.speed) ? Math.abs(props.speed) : 24;
|
|
116
|
+
if (speed === 0) return undefined;
|
|
117
|
+
return {
|
|
118
|
+
"--hk-marquee-shift": `${-loopWidth.value}px`,
|
|
119
|
+
"--hk-marquee-duration": `${(loopWidth.value / speed).toFixed(3)}s`,
|
|
120
|
+
"--hk-marquee-direction": (props.speed ?? 24) < 0 ? "reverse" : "normal",
|
|
121
|
+
} as Record<string, string>;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return () => {
|
|
125
|
+
if (!props.text) return null;
|
|
126
|
+
if (props.variant === "truncate") {
|
|
127
|
+
return (
|
|
128
|
+
<span ref={hostEl} class="hk-placeholder-marquee hk-placeholder-marquee--truncate">
|
|
129
|
+
<span class="hk-placeholder-marquee__text">{props.text}</span>
|
|
130
|
+
</span>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return (
|
|
134
|
+
<span
|
|
135
|
+
ref={hostEl}
|
|
136
|
+
class={[
|
|
137
|
+
"hk-placeholder-marquee",
|
|
138
|
+
// While the text fits the overlay stays hidden (but laid out,
|
|
139
|
+
// so the probe keeps measuring) and the input's NATIVE
|
|
140
|
+
// placeholder does the showing.
|
|
141
|
+
overflowing.value ? "" : "hk-placeholder-marquee--hidden",
|
|
142
|
+
].filter(Boolean).join(" ")}
|
|
143
|
+
data-parked={parked.value || undefined}
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
>
|
|
146
|
+
<span
|
|
147
|
+
class={[
|
|
148
|
+
"hk-placeholder-marquee__strip",
|
|
149
|
+
// The sweep only starts once the loop geometry is known —
|
|
150
|
+
// overflowing text with a parked speed (0/non-finite) stays
|
|
151
|
+
// a static strip, matching the old JS behavior.
|
|
152
|
+
overflowing.value && stripVars.value ? "hk-placeholder-marquee__strip--scroll" : "",
|
|
153
|
+
].filter(Boolean).join(" ")}
|
|
154
|
+
style={stripVars.value}
|
|
155
|
+
>
|
|
156
|
+
<span ref={firstCopyEl} class="hk-placeholder-marquee__copy">{props.text}</span>
|
|
157
|
+
{overflowing.value && (
|
|
158
|
+
<>
|
|
159
|
+
<span class="hk-placeholder-marquee__copy">{props.text}</span>
|
|
160
|
+
<span class="hk-placeholder-marquee__copy">{props.text}</span>
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
</span>
|
|
164
|
+
</span>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
export default HkPlaceholderMarquee;
|
|
@@ -9,33 +9,45 @@
|
|
|
9
9
|
max-height: min(70vh, calc(100vh - 2 * 8px));
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/* Pop motion family tokens (--hk-pop-* in theme.scss) — the reference
|
|
13
|
+
* consumer. HkSelectPanel's desktop popout and HkTooltip use the same
|
|
14
|
+
* set so every anchored surface pops with one curve. */
|
|
12
15
|
.hk-popover-enter-active {
|
|
13
16
|
transition:
|
|
14
|
-
opacity
|
|
15
|
-
|
|
17
|
+
opacity var(--hk-pop-in-duration, 0.2s)
|
|
18
|
+
var(--hk-pop-in-opacity-ease, cubic-bezier(0.16, 1, 0.3, 1)),
|
|
19
|
+
transform var(--hk-pop-in-duration, 0.2s)
|
|
20
|
+
var(--hk-pop-in-transform-ease, cubic-bezier(0.34, 1.56, 0.64, 1));
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
.hk-popover-leave-active {
|
|
19
24
|
transition:
|
|
20
|
-
opacity
|
|
21
|
-
|
|
25
|
+
opacity var(--hk-pop-out-duration, 0.15s)
|
|
26
|
+
var(--hk-pop-out-ease, cubic-bezier(0.4, 0, 1, 1)),
|
|
27
|
+
transform var(--hk-pop-out-duration, 0.15s)
|
|
28
|
+
var(--hk-pop-out-ease, cubic-bezier(0.4, 0, 1, 1));
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
.hk-popover-enter-from {
|
|
25
32
|
opacity: 0;
|
|
26
|
-
transform: translateY(-
|
|
33
|
+
transform: translateY(calc(-1 * var(--hk-pop-offset, 4px)))
|
|
34
|
+
scale(var(--hk-pop-scale, 0.97));
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
.hk-popover-leave-to {
|
|
30
38
|
opacity: 0;
|
|
31
|
-
transform: translateY(4px)
|
|
39
|
+
transform: translateY(var(--hk-pop-offset, 4px))
|
|
40
|
+
scale(var(--hk-pop-scale, 0.97));
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
/* Glass dropdown content — used when `glass` prop is true
|
|
43
|
+
/* Glass dropdown content — used when `glass` prop is true.
|
|
44
|
+
--hk-popover-bg/--hk-popover-blur let a host app's surface-finish
|
|
45
|
+
preference (chest html[data-surface]) re-skin this layer; the defaults
|
|
46
|
+
keep the historical look. */
|
|
35
47
|
.hii-dropdown-content,
|
|
36
48
|
.hk-popover-panel.hii-dropdown-content {
|
|
37
|
-
background: rgb(var(--color-surface));
|
|
38
|
-
backdrop-filter: blur(var(--blur-lg)) saturate(1.2);
|
|
49
|
+
background: var(--hk-popover-bg, rgb(var(--color-surface)));
|
|
50
|
+
backdrop-filter: var(--hk-popover-blur, blur(var(--blur-lg)) saturate(1.2));
|
|
39
51
|
border: 1px solid rgb(var(--color-border) / 20%);
|
|
40
52
|
border-radius: var(--radius-sm);
|
|
41
53
|
box-shadow: var(--shadow-dropdown);
|
|
@@ -62,7 +74,9 @@
|
|
|
62
74
|
border-radius: var(--radius-sm);
|
|
63
75
|
}
|
|
64
76
|
|
|
65
|
-
|
|
77
|
+
/* Hover = background wash only, never on the active item (active >
|
|
78
|
+
hover precedence — see HkNavItem.scss for the spec). */
|
|
79
|
+
.hk-popup-menu-item:hover:not([data-active]) {
|
|
66
80
|
background: var(--c-primary-subtle);
|
|
67
81
|
}
|
|
68
82
|
|
|
@@ -80,3 +94,113 @@
|
|
|
80
94
|
font-size: var(--text-base);
|
|
81
95
|
line-height: 1;
|
|
82
96
|
}
|
|
97
|
+
|
|
98
|
+
// ------
|
|
99
|
+
// Mobile bottom-sheet mode (`sheetOnMobile`) — the panel docks to the
|
|
100
|
+
// bottom edge on <768px viewports instead of anchoring: a real scrim
|
|
101
|
+
// (click closes), rounded top corners only, slide-up reveal. Same visual
|
|
102
|
+
// contract as the modal mobile docking (top inset keeps the secondary-
|
|
103
|
+
// window breadcrumb strip visible).
|
|
104
|
+
// ------
|
|
105
|
+
.hk-popover-scrim {
|
|
106
|
+
position: fixed;
|
|
107
|
+
inset: 0;
|
|
108
|
+
pointer-events: auto;
|
|
109
|
+
background: rgb(0 0 0 / 40%);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.hk-popover-panel.hk-is-sheet {
|
|
113
|
+
border-radius: var(--hk-modal-radius, var(--hi-radius-lg, 12px))
|
|
114
|
+
var(--hk-modal-radius, var(--hi-radius-lg, 12px))
|
|
115
|
+
0 0;
|
|
116
|
+
border-left: 0;
|
|
117
|
+
border-right: 0;
|
|
118
|
+
border-bottom: 0;
|
|
119
|
+
max-height: min(
|
|
120
|
+
72vh,
|
|
121
|
+
calc(
|
|
122
|
+
100vh - var(--hk-sheet-top-inset, 4.25rem) -
|
|
123
|
+
var(--hk-sheet-bottom-gap, 0.375rem)
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
display: flex;
|
|
127
|
+
flex-direction: column;
|
|
128
|
+
overflow-y: auto;
|
|
129
|
+
overscroll-behavior: contain;
|
|
130
|
+
padding-top: 0;
|
|
131
|
+
/* The sheet's bottom-edge distance itself comes from the shared
|
|
132
|
+
* --hk-sheet-bottom-gap (inline in HkPopover's sheet style); here the
|
|
133
|
+
* content keeps the last row clear of the home bar: the safe-area
|
|
134
|
+
* inset stacked on a 12px family base, which also restores a bottom
|
|
135
|
+
* cushion over the glass dropdown chrome (equal specificity, earlier
|
|
136
|
+
* in source, whose own bottom padding the sheet rule would otherwise
|
|
137
|
+
* zero out). The modal sheet footer goes further (it adds
|
|
138
|
+
* --hk-sheet-footer-gap for its action buttons); this sheet has no
|
|
139
|
+
* footer, so this is the family baseline. */
|
|
140
|
+
padding-bottom: calc(var(--space-12, 0.75rem) + env(safe-area-inset-bottom, 0px));
|
|
141
|
+
|
|
142
|
+
/* Overlay scrollbar (useOverlayScrollbar, sheet mode only) — the
|
|
143
|
+
native chrome is hidden, never styled. */
|
|
144
|
+
scrollbar-width: none;
|
|
145
|
+
|
|
146
|
+
&::-webkit-scrollbar {
|
|
147
|
+
display: none;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.hk-popover-sheet-grabber {
|
|
152
|
+
flex-shrink: 0;
|
|
153
|
+
// Grabber zone: tap target tall enough for a thumb, bar visually small.
|
|
154
|
+
padding: 14px 0 20px;
|
|
155
|
+
margin: 0 auto;
|
|
156
|
+
width: 100%;
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
display: flex;
|
|
159
|
+
justify-content: center;
|
|
160
|
+
|
|
161
|
+
&::before {
|
|
162
|
+
content: "";
|
|
163
|
+
width: 2.25rem;
|
|
164
|
+
height: 4px;
|
|
165
|
+
border-radius: 999px;
|
|
166
|
+
background: rgb(var(--color-border, 100 100 100) / 40%);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.hk-popover-sheet-title {
|
|
171
|
+
flex-shrink: 0;
|
|
172
|
+
// Same heading grammar as the select sheet title: small, semibold,
|
|
173
|
+
// slightly letterspaced, inset to the content edge. Hosts tune the
|
|
174
|
+
// leading inset via --hk-sheet-title-inset.
|
|
175
|
+
padding: 0 var(--space-16, 1rem) var(--space-8, 0.5rem)
|
|
176
|
+
var(--hk-sheet-title-inset, var(--space-16, 1rem));
|
|
177
|
+
font-size: var(--text-xs, 0.75rem);
|
|
178
|
+
font-weight: 600;
|
|
179
|
+
letter-spacing: 0.02em;
|
|
180
|
+
color: rgb(var(--color-muted, 140 140 140));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.hk-popover-sheet-enter-active {
|
|
184
|
+
transition:
|
|
185
|
+
opacity 0.25s ease-out,
|
|
186
|
+
transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.hk-popover-sheet-leave-active {
|
|
190
|
+
transition:
|
|
191
|
+
opacity 0.2s ease-in,
|
|
192
|
+
transform 0.25s cubic-bezier(0.4, 0, 1, 1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.hk-popover-sheet-enter-from,
|
|
196
|
+
.hk-popover-sheet-leave-to {
|
|
197
|
+
opacity: 0;
|
|
198
|
+
transform: translateY(100%);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@media (prefers-reduced-motion: reduce) {
|
|
202
|
+
.hk-popover-sheet-enter-active,
|
|
203
|
+
.hk-popover-sheet-leave-active {
|
|
204
|
+
transition: none;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source contract for the popover sheet docking (2026-08-30 user
|
|
3
|
+
* report: the theme toggle's bottom-up sheet didn't read as part of
|
|
4
|
+
* the standard sheet family). HkPopover's sheetOnMobile mode must dock
|
|
5
|
+
* with the SAME host-tunable --hk-sheet-bottom-gap as the HkModal and
|
|
6
|
+
* HkSelectPanel dockings, and keep its content clear of the home bar
|
|
7
|
+
* via the safe-area inset. The inline style used to pin hard
|
|
8
|
+
* `bottom: "0"`.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const tsx = readFileSync(join(here, "HkPopover.tsx"), "utf-8");
|
|
17
|
+
const scss = readFileSync(join(here, "HkPopover.scss"), "utf-8");
|
|
18
|
+
|
|
19
|
+
describe("HkPopover mobile sheet docking contract", () => {
|
|
20
|
+
it("docks the sheet above the shared bottom gap (inline sheet style)", () => {
|
|
21
|
+
const block = tsx.slice(tsx.indexOf("panelStyle"));
|
|
22
|
+
expect(block).toContain("var(--hk-sheet-bottom-gap");
|
|
23
|
+
// Same default as the HkModal sheet contract.
|
|
24
|
+
expect(block).toMatch(/--hk-sheet-bottom-gap,\s*0\.375rem/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("keeps sheet content clear of the home bar via the safe-area inset", () => {
|
|
28
|
+
const block = scss.match(/\.hk-popover-panel\.hk-is-sheet\s*{[^}]*}/)?.[0] ?? "";
|
|
29
|
+
// Stacked on the family 12px base so the glass dropdown chrome's
|
|
30
|
+
// bottom padding survives on non-notched phones too.
|
|
31
|
+
expect(block).toContain("calc(var(--space-12");
|
|
32
|
+
expect(block).toContain("env(safe-area-inset-bottom");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source contract: popover glass content reads --hk-popover-bg/--hk-popover-blur
|
|
3
|
+
* so a host surface-finish preference (chest html[data-surface]) can re-skin
|
|
4
|
+
* floating dropdown layers without forking the component.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const src = readFileSync(join(here, "HkPopover.scss"), "utf-8");
|
|
13
|
+
|
|
14
|
+
describe("HkPopover glass layer surface hooks", () => {
|
|
15
|
+
it("keeps the glass background host-tunable with the historical default", () => {
|
|
16
|
+
const block = src.slice(src.indexOf(".hii-dropdown-content"));
|
|
17
|
+
expect(block).toContain("var(--hk-popover-bg, rgb(var(--color-surface)))");
|
|
18
|
+
expect(block).toContain("var(--hk-popover-blur,");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source contract: while an HkPopover is open, no native browser tooltip
|
|
3
|
+
* may fire over the custom popup — the anchor's `title` AND every
|
|
4
|
+
* descendant `title` are blanked and restored exactly (the model pill of
|
|
5
|
+
* hikari #338 was a descendant-title case). Restore must be lossless:
|
|
6
|
+
* elements without a title never gain one, `title=""` stays `title=""`,
|
|
7
|
+
* and unmounting while open must not leak a blanked title.
|
|
8
|
+
*/
|
|
9
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import { createApp, defineComponent, h, nextTick, ref } from "vue";
|
|
11
|
+
|
|
12
|
+
import HkPopover from "./HkPopover";
|
|
13
|
+
|
|
14
|
+
const mounts: ReturnType<typeof createApp>[] = [];
|
|
15
|
+
const containers: HTMLElement[] = [];
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const app of mounts.splice(0)) app.unmount();
|
|
19
|
+
for (const el of containers.splice(0)) el.remove();
|
|
20
|
+
// happy-dom never fires transitionend, so leave transitions never
|
|
21
|
+
// finish and teleported panels linger on <body> — strip them or the
|
|
22
|
+
// next test's querySelector sees this test's panel.
|
|
23
|
+
document.body
|
|
24
|
+
.querySelectorAll(".hk-popover-panel, .hk-popover-scrim")
|
|
25
|
+
.forEach((el) => el.remove());
|
|
26
|
+
vi.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function setViewport(width: number) {
|
|
30
|
+
window.innerWidth = width;
|
|
31
|
+
window.dispatchEvent(new Event("resize"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mounts an HkPopover whose anchor is a plain DOM node (NOT rendered by
|
|
36
|
+
* Vue) so tests can decorate it with arbitrary `title` attributes and
|
|
37
|
+
* observe attribute mutations without Vue's patching interfering.
|
|
38
|
+
*/
|
|
39
|
+
function mountPopover(opts: { open?: boolean; anchor?: HTMLElement } = {}) {
|
|
40
|
+
const container = document.createElement("div");
|
|
41
|
+
document.body.appendChild(container);
|
|
42
|
+
containers.push(container);
|
|
43
|
+
|
|
44
|
+
const anchor = opts.anchor ?? document.createElement("button");
|
|
45
|
+
// Only seed text on a fresh anchor — a caller-provided one may already
|
|
46
|
+
// carry the decorated descendants under test (textContent would wipe them).
|
|
47
|
+
if (!opts.anchor) anchor.textContent = "anchor";
|
|
48
|
+
container.appendChild(anchor);
|
|
49
|
+
|
|
50
|
+
const open = ref(opts.open ?? false);
|
|
51
|
+
const Wrapper = defineComponent({
|
|
52
|
+
setup() {
|
|
53
|
+
return () =>
|
|
54
|
+
h(HkPopover, {
|
|
55
|
+
modelValue: open.value,
|
|
56
|
+
"onUpdate:modelValue": (v: boolean) => { open.value = v; },
|
|
57
|
+
anchorRef: anchor,
|
|
58
|
+
}, { default: () => h("div", { class: "pop-content" }, "content") });
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const app = createApp(Wrapper);
|
|
62
|
+
mounts.push(app);
|
|
63
|
+
app.mount(container);
|
|
64
|
+
return { open, app, anchor };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("HkPopover native title suppression", () => {
|
|
68
|
+
it("blanks the anchor's and descendants' titles while open and restores both exactly on close", async () => {
|
|
69
|
+
setViewport(1200);
|
|
70
|
+
const anchor = document.createElement("button");
|
|
71
|
+
anchor.setAttribute("title", "anchor tip");
|
|
72
|
+
const inner = document.createElement("span");
|
|
73
|
+
inner.setAttribute("title", "inner tip");
|
|
74
|
+
const nested = document.createElement("em");
|
|
75
|
+
nested.setAttribute("title", "nested tip");
|
|
76
|
+
inner.appendChild(nested);
|
|
77
|
+
anchor.appendChild(inner);
|
|
78
|
+
|
|
79
|
+
const { open } = mountPopover({ anchor });
|
|
80
|
+
await nextTick();
|
|
81
|
+
expect(anchor.getAttribute("title")).toBe("anchor tip");
|
|
82
|
+
|
|
83
|
+
open.value = true;
|
|
84
|
+
await nextTick();
|
|
85
|
+
expect(anchor.getAttribute("title")).toBe("");
|
|
86
|
+
expect(inner.getAttribute("title")).toBe("");
|
|
87
|
+
expect(nested.getAttribute("title")).toBe("");
|
|
88
|
+
|
|
89
|
+
open.value = false;
|
|
90
|
+
await nextTick();
|
|
91
|
+
expect(anchor.getAttribute("title")).toBe("anchor tip");
|
|
92
|
+
expect(inner.getAttribute("title")).toBe("inner tip");
|
|
93
|
+
expect(nested.getAttribute("title")).toBe("nested tip");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("does not grant a title to an element that never had one", async () => {
|
|
97
|
+
setViewport(1200);
|
|
98
|
+
const anchor = document.createElement("button");
|
|
99
|
+
anchor.setAttribute("title", "anchor tip");
|
|
100
|
+
const plain = document.createElement("span");
|
|
101
|
+
anchor.appendChild(plain);
|
|
102
|
+
|
|
103
|
+
const { open } = mountPopover({ anchor });
|
|
104
|
+
open.value = true;
|
|
105
|
+
await nextTick();
|
|
106
|
+
open.value = false;
|
|
107
|
+
await nextTick();
|
|
108
|
+
|
|
109
|
+
expect(plain.hasAttribute("title")).toBe(false);
|
|
110
|
+
expect(plain.getAttribute("title")).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("leaves an existing title=\"\" as title=\"\" (not removed, not filled)", async () => {
|
|
114
|
+
setViewport(1200);
|
|
115
|
+
const anchor = document.createElement("button");
|
|
116
|
+
const empty = document.createElement("span");
|
|
117
|
+
empty.setAttribute("title", "");
|
|
118
|
+
anchor.appendChild(empty);
|
|
119
|
+
|
|
120
|
+
const { open } = mountPopover({ anchor });
|
|
121
|
+
open.value = true;
|
|
122
|
+
await nextTick();
|
|
123
|
+
expect(empty.hasAttribute("title")).toBe(true);
|
|
124
|
+
expect(empty.getAttribute("title")).toBe("");
|
|
125
|
+
|
|
126
|
+
open.value = false;
|
|
127
|
+
await nextTick();
|
|
128
|
+
expect(empty.hasAttribute("title")).toBe(true);
|
|
129
|
+
expect(empty.getAttribute("title")).toBe("");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("restores the anchor's title when unmounted while open", async () => {
|
|
133
|
+
setViewport(1200);
|
|
134
|
+
const anchor = document.createElement("button");
|
|
135
|
+
anchor.setAttribute("title", "unmount tip");
|
|
136
|
+
|
|
137
|
+
const { open, app } = mountPopover({ anchor });
|
|
138
|
+
open.value = true;
|
|
139
|
+
await nextTick();
|
|
140
|
+
expect(anchor.getAttribute("title")).toBe("");
|
|
141
|
+
|
|
142
|
+
app.unmount();
|
|
143
|
+
expect(anchor.getAttribute("title")).toBe("unmount tip");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("performs no title-related DOM mutations when the anchor has no titles anywhere", async () => {
|
|
147
|
+
setViewport(1200);
|
|
148
|
+
const anchor = document.createElement("button");
|
|
149
|
+
const plain = document.createElement("span");
|
|
150
|
+
anchor.appendChild(plain);
|
|
151
|
+
const setSpy = vi.spyOn(anchor, "setAttribute");
|
|
152
|
+
const removeSpy = vi.spyOn(anchor, "removeAttribute");
|
|
153
|
+
|
|
154
|
+
const { open } = mountPopover({ anchor });
|
|
155
|
+
open.value = true;
|
|
156
|
+
await nextTick();
|
|
157
|
+
open.value = false;
|
|
158
|
+
await nextTick();
|
|
159
|
+
|
|
160
|
+
expect(setSpy).not.toHaveBeenCalled();
|
|
161
|
+
expect(removeSpy).not.toHaveBeenCalled();
|
|
162
|
+
expect(anchor.hasAttribute("title")).toBe(false);
|
|
163
|
+
expect(anchor.querySelector("[title]")).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("restores the retired anchor and suppresses the new one on a mid-open anchorRef swap", async () => {
|
|
167
|
+
setViewport(1200);
|
|
168
|
+
const container = document.createElement("div");
|
|
169
|
+
document.body.appendChild(container);
|
|
170
|
+
containers.push(container);
|
|
171
|
+
|
|
172
|
+
const oldAnchor = document.createElement("button");
|
|
173
|
+
oldAnchor.setAttribute("title", "old tip");
|
|
174
|
+
const oldInner = document.createElement("span");
|
|
175
|
+
oldInner.setAttribute("title", "old inner tip");
|
|
176
|
+
oldAnchor.appendChild(oldInner);
|
|
177
|
+
const newAnchor = document.createElement("button");
|
|
178
|
+
newAnchor.setAttribute("title", "new tip");
|
|
179
|
+
container.append(oldAnchor, newAnchor);
|
|
180
|
+
|
|
181
|
+
const open = ref(false);
|
|
182
|
+
const anchorRef = ref<HTMLElement | null>(oldAnchor);
|
|
183
|
+
const Wrapper = defineComponent({
|
|
184
|
+
setup() {
|
|
185
|
+
return () =>
|
|
186
|
+
h(HkPopover, {
|
|
187
|
+
modelValue: open.value,
|
|
188
|
+
"onUpdate:modelValue": (v: boolean) => { open.value = v; },
|
|
189
|
+
anchorRef: anchorRef.value,
|
|
190
|
+
}, { default: () => h("div", { class: "pop-content" }, "content") });
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
const app = createApp(Wrapper);
|
|
194
|
+
mounts.push(app);
|
|
195
|
+
app.mount(container);
|
|
196
|
+
|
|
197
|
+
open.value = true;
|
|
198
|
+
await nextTick();
|
|
199
|
+
expect(oldAnchor.getAttribute("title")).toBe("");
|
|
200
|
+
expect(oldInner.getAttribute("title")).toBe("");
|
|
201
|
+
|
|
202
|
+
anchorRef.value = newAnchor;
|
|
203
|
+
await nextTick();
|
|
204
|
+
expect(oldAnchor.getAttribute("title")).toBe("old tip");
|
|
205
|
+
expect(oldInner.getAttribute("title")).toBe("old inner tip");
|
|
206
|
+
expect(newAnchor.getAttribute("title")).toBe("");
|
|
207
|
+
|
|
208
|
+
open.value = false;
|
|
209
|
+
await nextTick();
|
|
210
|
+
expect(newAnchor.getAttribute("title")).toBe("new tip");
|
|
211
|
+
expect(oldAnchor.getAttribute("title")).toBe("old tip");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("restores the retired subtree when opening and an anchor swap land in the same flush", async () => {
|
|
215
|
+
setViewport(1200);
|
|
216
|
+
const container = document.createElement("div");
|
|
217
|
+
document.body.appendChild(container);
|
|
218
|
+
containers.push(container);
|
|
219
|
+
|
|
220
|
+
const anchorA = document.createElement("button");
|
|
221
|
+
anchorA.setAttribute("title", "A tip");
|
|
222
|
+
const childA = document.createElement("span");
|
|
223
|
+
childA.setAttribute("title", "A child tip");
|
|
224
|
+
anchorA.appendChild(childA);
|
|
225
|
+
const anchorB = document.createElement("button");
|
|
226
|
+
anchorB.setAttribute("title", "B tip");
|
|
227
|
+
const childB = document.createElement("span");
|
|
228
|
+
childB.setAttribute("title", "B child tip");
|
|
229
|
+
anchorB.appendChild(childB);
|
|
230
|
+
container.append(anchorA, anchorB);
|
|
231
|
+
|
|
232
|
+
const open = ref(false);
|
|
233
|
+
const anchorRef = ref<HTMLElement | null>(anchorA);
|
|
234
|
+
const Wrapper = defineComponent({
|
|
235
|
+
setup() {
|
|
236
|
+
return () =>
|
|
237
|
+
h(HkPopover, {
|
|
238
|
+
modelValue: open.value,
|
|
239
|
+
"onUpdate:modelValue": (v: boolean) => { open.value = v; },
|
|
240
|
+
anchorRef: anchorRef.value,
|
|
241
|
+
}, { default: () => h("div", { class: "pop-content" }, "content") });
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
const app = createApp(Wrapper);
|
|
245
|
+
mounts.push(app);
|
|
246
|
+
app.mount(container);
|
|
247
|
+
|
|
248
|
+
// Same tick: the popover never observes "open while anchored to A" —
|
|
249
|
+
// open and the swap to B land in one flush together.
|
|
250
|
+
open.value = true;
|
|
251
|
+
anchorRef.value = anchorB;
|
|
252
|
+
await nextTick();
|
|
253
|
+
expect(anchorA.getAttribute("title")).toBe("A tip");
|
|
254
|
+
expect(childA.getAttribute("title")).toBe("A child tip");
|
|
255
|
+
expect(anchorB.getAttribute("title")).toBe("");
|
|
256
|
+
expect(childB.getAttribute("title")).toBe("");
|
|
257
|
+
|
|
258
|
+
open.value = false;
|
|
259
|
+
await nextTick();
|
|
260
|
+
expect(anchorA.getAttribute("title")).toBe("A tip");
|
|
261
|
+
expect(childA.getAttribute("title")).toBe("A child tip");
|
|
262
|
+
expect(anchorB.getAttribute("title")).toBe("B tip");
|
|
263
|
+
expect(childB.getAttribute("title")).toBe("B child tip");
|
|
264
|
+
});
|
|
265
|
+
});
|