From 1acd511ea323099c7ba9faee9df877f97de0781d Mon Sep 17 00:00:00 2001 From: Mike Tasset Date: Mon, 4 Aug 2025 18:44:50 +0200 Subject: [PATCH] Handle force closing logic on mobile --- .../components/viewer/AnchoredPoints.vue | 29 ++++++++++++- .../components/viewer/PreSetupWrapper.vue | 41 +++++++++++++++++-- .../components/viewer/controls/Bottom.vue | 22 +++++++++- .../components/viewer/controls/Left.vue | 19 +++++++++ .../components/viewer/selection/Sidebar.vue | 17 ++++++++ 5 files changed, 122 insertions(+), 6 deletions(-) diff --git a/packages/frontend-2/components/viewer/AnchoredPoints.vue b/packages/frontend-2/components/viewer/AnchoredPoints.vue index ce379b4f1..acd1acba2 100644 --- a/packages/frontend-2/components/viewer/AnchoredPoints.vue +++ b/packages/frontend-2/components/viewer/AnchoredPoints.vue @@ -144,17 +144,25 @@ import { useInjectedViewerState } from '~~/lib/viewer/composables/setup' import { useThreadUtilities, useFilterUtilities } from '~~/lib/viewer/composables/ui' +import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind' +import { useBreakpoints } from '@vueuse/core' + +const emit = defineEmits<{ + forceClosePanels: [] +}>() const parentEl = ref(null as Nullable) const { isLoggedIn } = useActiveUser() const viewerState = useInjectedViewerState() const { sessionId } = viewerState const { users } = useViewerUserActivityTracking({ parentEl }) -const { isOpenThread, open } = useThreadUtilities() +const { isOpenThread, open, closeAllThreads } = useThreadUtilities() const { filters: { hasAnyFiltersApplied } } = useFilterUtilities({ state: viewerState }) const canPostComment = useCheckViewerCommentingAccess() +const breakpoints = useBreakpoints(TailwindBreakpoints) +const isMobile = breakpoints.smaller('sm') const { isEnabled: isEmbedEnabled } = useEmbed() @@ -275,4 +283,23 @@ function setUserSpotlight(sessionId: string) { source: 'navbar' }) } + +const forceCloseThreads = async () => { + await closeAllThreads() +} + +// Watch for thread opening on mobile and emit event +watch( + () => openThread.value, + (newThread, oldThread) => { + // If a thread opened (wasn't open before) on mobile, emit event + if (newThread && !oldThread && isMobile.value) { + emit('forceClosePanels') + } + } +) + +defineExpose({ + forceCloseThreads +}) diff --git a/packages/frontend-2/components/viewer/PreSetupWrapper.vue b/packages/frontend-2/components/viewer/PreSetupWrapper.vue index 79da3a45a..2202fc8f1 100644 --- a/packages/frontend-2/components/viewer/PreSetupWrapper.vue +++ b/packages/frontend-2/components/viewer/PreSetupWrapper.vue @@ -41,7 +41,10 @@ enter-from-class="opacity-0" enter-active-class="transition duration-1000" > - + @@ -54,8 +57,14 @@ @@ -73,7 +82,11 @@ enter-from-class="opacity-0" enter-active-class="transition duration-1000" > - +
route.params.modelId as string) const projectId = writableAsyncComputed({ get: () => route.params.id as string, @@ -294,4 +312,19 @@ watch( }, { immediate: true } ) + +const closeAllPanels = (except?: 'left' | 'bottom' | 'selection' | 'threads') => { + if (except !== 'left' && leftControls.value?.forceClosePanels) { + leftControls.value.forceClosePanels() + } + if (except !== 'bottom' && bottomControls.value?.forceClosePanels) { + bottomControls.value.forceClosePanels() + } + if (except !== 'selection' && selectionSidebar.value?.forceClose) { + selectionSidebar.value.forceClose() + } + if (except !== 'threads' && anchoredPoints.value?.forceCloseThreads) { + anchoredPoints.value.forceCloseThreads() + } +} diff --git a/packages/frontend-2/components/viewer/controls/Bottom.vue b/packages/frontend-2/components/viewer/controls/Bottom.vue index 7a1ee082d..388818ce4 100644 --- a/packages/frontend-2/components/viewer/controls/Bottom.vue +++ b/packages/frontend-2/components/viewer/controls/Bottom.vue @@ -51,8 +51,9 @@ import { useViewerShortcuts, useFilterUtilities } from '~~/lib/viewer/composables/ui' -import { onKeyStroke } from '@vueuse/core' +import { onKeyStroke, useBreakpoints } from '@vueuse/core' import { useEmbed } from '~/lib/viewer/composables/setup/embed' +import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind' enum ActivePanel { none = 'none', @@ -63,6 +64,10 @@ enum ActivePanel { lightControls = 'lightControls' } +const emit = defineEmits<{ + forceClosePanels: [] +}>() + const { getShortcutDisplayText, shortcuts, registerShortcuts } = useViewerShortcuts() const { toggleSectionBox, resetSectionBox, closeSectionBox } = useSectionBoxUtilities() const { getActiveMeasurement, removeMeasurement, enableMeasurements } = @@ -70,8 +75,11 @@ const { getActiveMeasurement, removeMeasurement, enableMeasurements } = const { resetExplode } = useFilterUtilities() const { getTooltipProps } = useSmartTooltipDelay() const { isEnabled: isEmbedEnabled } = useEmbed() +const breakpoints = useBreakpoints(TailwindBreakpoints) +const isMobile = breakpoints.smaller('sm') const activePanel = ref(ActivePanel.none) + const panels = shallowRef({ [ActivePanel.measurements]: { id: ActivePanel.measurements, @@ -122,6 +130,10 @@ const showResetButton = computed(() => { const toggleActivePanel = (panel: ActivePanel) => { activePanel.value = activePanel.value === panel ? ActivePanel.none : panel + if (activePanel.value !== ActivePanel.none && isMobile.value) { + emit('forceClosePanels') + } + if (panel === ActivePanel.sectionBox) { toggleSectionBox() } @@ -161,6 +173,10 @@ registerShortcuts({ ToggleSectionBox: () => toggleSectionBox() }) +const forceClosePanels = () => { + activePanel.value = ActivePanel.none +} + onKeyStroke('Escape', () => { const isActiveMeasurement = getActiveMeasurement() @@ -175,4 +191,8 @@ onKeyStroke('Escape', () => { activePanel.value = ActivePanel.none } }) + +defineExpose({ + forceClosePanels +}) diff --git a/packages/frontend-2/components/viewer/controls/Left.vue b/packages/frontend-2/components/viewer/controls/Left.vue index 33bb2f1c6..e013dd86c 100644 --- a/packages/frontend-2/components/viewer/controls/Left.vue +++ b/packages/frontend-2/components/viewer/controls/Left.vue @@ -156,6 +156,10 @@ type ActivePanel = | 'filters' | 'devMode' +const emit = defineEmits<{ + forceClosePanels: [] +}>() + const width = ref(264) const scrollableControlsContainer = ref(null as Nullable) const height = ref(scrollableControlsContainer.value?.clientHeight) @@ -246,7 +250,17 @@ registerShortcuts({ }) const toggleActivePanel = (panel: ActivePanel) => { + const wasNone = activePanel.value === 'none' activePanel.value = activePanel.value === panel ? 'none' : panel + + // If a panel is being opened (not closed) on mobile, emit event to parent + if (wasNone && activePanel.value !== 'none' && isMobile.value) { + emit('forceClosePanels') + } +} + +const forceClosePanel = () => { + activePanel.value = 'none' } const openDocs = () => { @@ -261,4 +275,9 @@ onMounted(() => { watch(isSmallerOrEqualSm, (newVal) => { activePanel.value = newVal ? 'none' : 'models' }) + +defineExpose({ + forceClosePanel, + forceClosePanels: forceClosePanel +}) diff --git a/packages/frontend-2/components/viewer/selection/Sidebar.vue b/packages/frontend-2/components/viewer/selection/Sidebar.vue index bec18aa26..9f5eb324c 100644 --- a/packages/frontend-2/components/viewer/selection/Sidebar.vue +++ b/packages/frontend-2/components/viewer/selection/Sidebar.vue @@ -100,6 +100,10 @@ import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind' import type { ConcreteComponent } from 'vue' import type { LayoutMenuItem } from '~~/lib/layout/helpers/components' +const emit = defineEmits<{ + forceClosePanels: [] +}>() + enum ActionTypes { OpenInNewTab = 'open-in-new-tab' } @@ -119,6 +123,7 @@ const { hideObjects, showObjects, isolateObjects, unIsolateObjects } = const { isSmallerOrEqualSm } = useIsSmallerOrEqualThanBreakpoint() const breakpoints = useBreakpoints(TailwindBreakpoints) const isGreaterThanSm = breakpoints.greater('sm') +const isMobile = breakpoints.smaller('sm') const menuId = useId() const mp = useMixpanel() const { getTooltipProps } = useSmartTooltipDelay() @@ -248,6 +253,10 @@ const onClose = () => { trackAndClearSelection() } +const forceClose = () => { + sidebarOpen.value = false +} + onKeyStroke('Escape', () => { // Cleareance of any vis/iso state coming from here should happen in clearSelection() // Note: we're not using the trackAndClearSelection method beacuse @@ -267,6 +276,10 @@ watch( // Dont open sidebar if a comment is open if (newLength !== 0 && !focusedThreadId.value) { sidebarOpen.value = true + // Emit event when sidebar opens on mobile + if (isMobile.value) { + emit('forceClosePanels') + } } else if (newLength === 0) { sidebarOpen.value = false } @@ -295,4 +308,8 @@ watch( } } ) + +defineExpose({ + forceClose +})