mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
✨ feat(shell-tabs): improve collapse behavior with scroll and delay
- Add 1-second delay before collapsing when mouse moves away (desktop) - Add scroll-to-top detection: expand when page is near top (<50px) - Add scroll direction detection for tablet: expand on scroll up, collapse on scroll down - Scroll direction requires 50px threshold before triggering - Mouse proximity detection disabled on tablet devices
This commit is contained in:
@@ -58,12 +58,21 @@ Individual tab item component.
|
||||
|
||||
## Compact Mode
|
||||
|
||||
The tabs bar uses mouse proximity detection to automatically switch display modes:
|
||||
The tabs bar automatically switches between expanded and compact display modes based on user interaction.
|
||||
|
||||
- **Expanded mode** (mouse near): Full height tabs showing name and subtitle
|
||||
- **Compact mode** (mouse away): Reduced height tabs with hidden subtitle
|
||||
### Desktop Behavior
|
||||
|
||||
The proximity threshold is 50 pixels from the component edge.
|
||||
- **Expanded** when mouse is within 50px of the tabs bar
|
||||
- **Collapsed** with 1-second delay when mouse moves away
|
||||
- **Expanded** when page is scrolled to top (< 50px from top)
|
||||
|
||||
### Tablet Behavior (≤ 1279px viewport)
|
||||
|
||||
- **Expanded** when page is scrolled to top (< 50px from top)
|
||||
- **Expanded** when scrolling up (after 50px of upward scroll)
|
||||
- **Collapsed** when scrolling down
|
||||
|
||||
Mouse proximity detection is disabled on tablet devices.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import { Component, inject, ElementRef } from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
ElementRef,
|
||||
inject,
|
||||
OnDestroy,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { Router } from '@angular/router';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { ShellTabItemComponent } from './components/shell-tab-item.component';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { CarouselComponent } from '@isa/ui/carousel';
|
||||
import { IconButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaActionPlus } from '@isa/icons';
|
||||
import { TabsCollapsedService } from '@isa/shell/common';
|
||||
import { IconButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { CarouselComponent } from '@isa/ui/carousel';
|
||||
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { fromEvent, map, startWith } from 'rxjs';
|
||||
import { ShellTabItemComponent } from './components/shell-tab-item.component';
|
||||
|
||||
/**
|
||||
* Distance in pixels from the component edge within which
|
||||
@@ -16,6 +27,24 @@ import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
*/
|
||||
const PROXIMITY_THRESHOLD_PX = 50;
|
||||
|
||||
/**
|
||||
* Scroll position threshold in pixels. Tabs expand when
|
||||
* scroll position is below this value.
|
||||
*/
|
||||
const SCROLL_THRESHOLD_PX = 50;
|
||||
|
||||
/**
|
||||
* Delay in milliseconds before collapsing tabs after
|
||||
* the mouse moves away from the proximity zone.
|
||||
*/
|
||||
const COLLAPSE_DELAY_MS = 1000;
|
||||
|
||||
/**
|
||||
* Minimum scroll distance in pixels required to detect
|
||||
* a scroll direction change (tablet only).
|
||||
*/
|
||||
const SCROLL_DIRECTION_THRESHOLD_PX = 50;
|
||||
|
||||
/**
|
||||
* Shell tabs bar component that displays all open tabs in a horizontal carousel.
|
||||
*
|
||||
@@ -49,8 +78,9 @@ const PROXIMITY_THRESHOLD_PX = 50;
|
||||
'(document:mousemove)': 'onMouseMove($event)',
|
||||
},
|
||||
})
|
||||
export class ShellTabsComponent {
|
||||
export class ShellTabsComponent implements OnDestroy {
|
||||
#logger = logger({ component: 'ShellTabsComponent' });
|
||||
#document = inject(DOCUMENT);
|
||||
#tablet = breakpoint(Breakpoint.Tablet);
|
||||
|
||||
#tabService = inject(TabService);
|
||||
@@ -58,15 +88,83 @@ export class ShellTabsComponent {
|
||||
#elementRef = inject(ElementRef);
|
||||
#router = inject(Router);
|
||||
|
||||
/** Internal state: whether mouse is within proximity (desktop only). */
|
||||
#mouseNear = signal(false);
|
||||
|
||||
/** Internal state: whether page is scrolled near top. */
|
||||
#scrolledToTop = signal(true);
|
||||
|
||||
/** Internal state: whether user is scrolling up (tablet only). */
|
||||
#scrollingUp = signal(false);
|
||||
|
||||
/** Last scroll position to detect direction. */
|
||||
#lastScrollY = 0;
|
||||
|
||||
/** Anchor position when scroll direction changes (for threshold detection). */
|
||||
#scrollAnchorY = 0;
|
||||
|
||||
/** Current scroll direction: 'up' | 'down' | null */
|
||||
#currentDirection: 'up' | 'down' | null = null;
|
||||
|
||||
/** Timer ID for delayed collapse. */
|
||||
#collapseTimer: number | undefined;
|
||||
|
||||
/** Tracks the current scroll position of the window. */
|
||||
#scrollPosition = toSignal(
|
||||
fromEvent(this.#document.defaultView!, 'scroll', { passive: true }).pipe(
|
||||
startWith(null),
|
||||
map(() => this.#document.defaultView?.scrollY ?? 0),
|
||||
),
|
||||
{ initialValue: 0 },
|
||||
);
|
||||
|
||||
/**
|
||||
* Computed collapsed state based on combined conditions.
|
||||
* Desktop: Expanded when mouse is near OR scrolled to top.
|
||||
* Tablet: Expanded when scrolled to top OR scrolling up.
|
||||
*/
|
||||
#shouldCollapse = computed(() => {
|
||||
const isTablet = this.#tablet();
|
||||
const mouseCondition = isTablet ? false : this.#mouseNear();
|
||||
const scrollTopCondition = this.#scrolledToTop();
|
||||
// Scroll direction only matters on tablet
|
||||
const scrollUpCondition = isTablet ? this.#scrollingUp() : false;
|
||||
|
||||
return !(mouseCondition || scrollTopCondition || scrollUpCondition);
|
||||
});
|
||||
|
||||
/** All tabs from the TabService. */
|
||||
readonly tabs = this.#tabService.entities;
|
||||
|
||||
/** Whether tabs should display in compact mode (mouse is far from component). */
|
||||
/** Whether tabs should display in compact mode. */
|
||||
compact = this.#tabsCollapsedService.get;
|
||||
|
||||
constructor() {
|
||||
// Sync scroll position to scrolledToTop and track direction
|
||||
effect(() => {
|
||||
const scrollY = this.#scrollPosition();
|
||||
|
||||
// Track scroll direction with threshold (for tablet)
|
||||
this.#updateScrollDirection(scrollY);
|
||||
|
||||
// Track if at top
|
||||
this.#scrolledToTop.set(scrollY < SCROLL_THRESHOLD_PX);
|
||||
});
|
||||
|
||||
// Sync computed collapsed state to service
|
||||
effect(() => {
|
||||
this.#tabsCollapsedService.set(this.#shouldCollapse());
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.#clearCollapseTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse movement to detect proximity to the tabs bar.
|
||||
* Updates compact mode based on whether the mouse is near the component.
|
||||
* Expands immediately when near, collapses with delay when far.
|
||||
* Skipped on tablet devices (scroll-only behavior).
|
||||
*/
|
||||
onMouseMove(event: MouseEvent): void {
|
||||
if (this.#tablet()) {
|
||||
@@ -74,19 +172,75 @@ export class ShellTabsComponent {
|
||||
}
|
||||
|
||||
const rect = this.#elementRef.nativeElement.getBoundingClientRect();
|
||||
const mouseY = event.clientY;
|
||||
const mouseX = event.clientX;
|
||||
|
||||
const isWithinX = mouseX >= rect.left && mouseX <= rect.right;
|
||||
const isWithinX =
|
||||
event.clientX >= rect.left && event.clientX <= rect.right;
|
||||
const distanceY =
|
||||
mouseY < rect.top
|
||||
? rect.top - mouseY
|
||||
: mouseY > rect.bottom
|
||||
? mouseY - rect.bottom
|
||||
event.clientY < rect.top
|
||||
? rect.top - event.clientY
|
||||
: event.clientY > rect.bottom
|
||||
? event.clientY - rect.bottom
|
||||
: 0;
|
||||
|
||||
const isNear = isWithinX && distanceY <= PROXIMITY_THRESHOLD_PX;
|
||||
this.#tabsCollapsedService.set(!isNear);
|
||||
|
||||
if (isNear) {
|
||||
this.#clearCollapseTimer();
|
||||
this.#mouseNear.set(true);
|
||||
} else if (this.#mouseNear()) {
|
||||
this.#scheduleMouseLeave();
|
||||
}
|
||||
}
|
||||
|
||||
/** Schedules a delayed collapse when mouse leaves proximity zone. */
|
||||
#scheduleMouseLeave(): void {
|
||||
if (this.#collapseTimer !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#collapseTimer = window.setTimeout(() => {
|
||||
this.#mouseNear.set(false);
|
||||
this.#collapseTimer = undefined;
|
||||
}, COLLAPSE_DELAY_MS);
|
||||
}
|
||||
|
||||
/** Cancels any pending collapse timer. */
|
||||
#clearCollapseTimer(): void {
|
||||
if (this.#collapseTimer !== undefined) {
|
||||
clearTimeout(this.#collapseTimer);
|
||||
this.#collapseTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates scroll direction state with threshold detection.
|
||||
* Only triggers scrollingUp after scrolling up at least SCROLL_DIRECTION_THRESHOLD_PX.
|
||||
*/
|
||||
#updateScrollDirection(scrollY: number): void {
|
||||
// Detect direction based on movement from last position
|
||||
const movingUp = scrollY < this.#lastScrollY;
|
||||
const movingDown = scrollY > this.#lastScrollY;
|
||||
this.#lastScrollY = scrollY;
|
||||
|
||||
if (movingUp) {
|
||||
if (this.#currentDirection !== 'up') {
|
||||
// Direction changed to up, set new anchor
|
||||
this.#currentDirection = 'up';
|
||||
this.#scrollAnchorY = scrollY;
|
||||
}
|
||||
// Check if scrolled up enough from anchor
|
||||
const distanceFromAnchor = this.#scrollAnchorY - scrollY;
|
||||
if (distanceFromAnchor >= SCROLL_DIRECTION_THRESHOLD_PX) {
|
||||
this.#scrollingUp.set(true);
|
||||
}
|
||||
} else if (movingDown) {
|
||||
if (this.#currentDirection !== 'down') {
|
||||
// Direction changed to down, set new anchor
|
||||
this.#currentDirection = 'down';
|
||||
this.#scrollAnchorY = scrollY;
|
||||
}
|
||||
// Collapse immediately when scrolling down
|
||||
this.#scrollingUp.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/** Closes all open tabs and navigates to the root route. */
|
||||
|
||||
Reference in New Issue
Block a user