@mintplayer/ng-bootstrap 21.24.0 → 21.25.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.
@@ -318,12 +318,21 @@ const styles = unsafeCSS(`:host {
318
318
  padding: 0.5rem 1rem;
319
319
  margin: -0.5rem -1rem;
320
320
  touch-action: none;
321
+ user-select: none;
322
+ -webkit-user-select: none;
323
+ -webkit-touch-callout: none;
321
324
  }
322
325
 
323
326
  .dock-tab:active {
324
327
  cursor: grabbing;
325
328
  }
326
329
 
330
+ .dock-tab[data-pressing=true] {
331
+ background-color: var(--bs-secondary-bg-subtle, rgba(0, 0, 0, 0.05));
332
+ transform: scale(1.02);
333
+ transition: background-color 100ms ease-out, transform 100ms ease-out;
334
+ }
335
+
327
336
  .dock-stack__pane {
328
337
  position: relative;
329
338
  display: flex;
@@ -422,6 +431,13 @@ class MintDockManagerElement extends LitElement {
422
431
  return [...(super.observedAttributes ?? []), 'layout'];
423
432
  }
424
433
  static { this.instanceCounter = 0; }
434
+ // Touch tab-drag gesture: a finger that lands on a tab header must be able
435
+ // to scroll the tabstrip natively. We require a stationary hold before
436
+ // arming the drag, so a horizontal swipe scrolls instead of undocking.
437
+ // See docs/prd/dock-touch-long-press-drag.md.
438
+ static { this.TOUCH_LONG_PRESS_MS = 600; }
439
+ static { this.TOUCH_LONG_PRESS_SLOP_PX = 10; }
440
+ static { this.TOUCH_PRESS_FEEDBACK_DELAY_MS = 150; }
425
441
  renderSnapMarkersForCorner() {
426
442
  if (!this.showSnapMarkers)
427
443
  return;
@@ -1814,13 +1830,18 @@ class MintDockManagerElement extends LitElement {
1814
1830
  this.pendingTabDragMetrics = null;
1815
1831
  }
1816
1832
  /**
1817
- * Pointerdown handler arms a "may become a drag" gesture. Once the pointer
1818
- * moves past `threshold` pixels we promote it to an actual pane drag via
1819
- * {@link beginPaneDrag}; if the user releases first we just clear the
1820
- * pending tab metrics. All listeners self-clean on resolve so the gesture
1821
- * stays scoped to a single pointerdown.
1833
+ * Pointerdown handler arms a "may become a drag" gesture. Mouse / pen use a
1834
+ * 5 px distance threshold and arm immediately; touch dispatches to
1835
+ * {@link armPaneDragGestureTouch} which requires a 600 ms stationary hold
1836
+ * (so the user can scroll the tabstrip natively without undocking).
1837
+ * Once armed, both paths converge on {@link beginPaneDrag}. All listeners
1838
+ * self-clean on resolve so the gesture stays scoped to a single pointerdown.
1822
1839
  */
1823
1840
  armPaneDragGesture(startEvent, path, pane, stackEl) {
1841
+ if (startEvent.pointerType === 'touch') {
1842
+ this.armPaneDragGestureTouch(startEvent, path, pane, stackEl);
1843
+ return;
1844
+ }
1824
1845
  if (startEvent.pointerType === 'mouse' && startEvent.button !== 0)
1825
1846
  return;
1826
1847
  const win = this.windowRef;
@@ -1857,6 +1878,152 @@ class MintDockManagerElement extends LitElement {
1857
1878
  win.addEventListener('pointerup', onRelease, true);
1858
1879
  win.addEventListener('pointercancel', onRelease, true);
1859
1880
  }
1881
+ /**
1882
+ * Touch-specific gesture arming. With `.dock-tab` set to `touch-action:
1883
+ * none`, the browser never arbitrates the gesture itself, so JS owns it
1884
+ * from frame 1. Three outcomes from the pending state:
1885
+ *
1886
+ * - User holds within {@link TOUCH_LONG_PRESS_SLOP_PX} for
1887
+ * {@link TOUCH_LONG_PRESS_MS} → timer fires → drag arms via
1888
+ * {@link beginPaneDrag}.
1889
+ * - User moves past slop and the move is mostly horizontal, and the
1890
+ * strip's `<ul>` is scrollable → enter `scrolling` mode and drive
1891
+ * `ul.scrollLeft` from JS for the rest of the gesture (no drag, no
1892
+ * momentum — direct 1:1 finger follow).
1893
+ * - User moves past slop in any other direction, releases, or
1894
+ * pointercancel fires → abandoned, no drag, no scroll. Releases
1895
+ * under slop fire the synthesized click that drives `tab-activate`.
1896
+ *
1897
+ * `touch-action: pan-x` was the original PRD design but doesn't work:
1898
+ * the policy is frozen at touchstart and `setPointerCapture` doesn't
1899
+ * promote it, so first move after a long-press fires pointercancel and
1900
+ * strands the panel. JS-driven scroll is the only model that holds.
1901
+ */
1902
+ armPaneDragGestureTouch(startEvent, path, pane, stackEl) {
1903
+ const win = this.windowRef;
1904
+ if (!win)
1905
+ return;
1906
+ const startX = startEvent.clientX;
1907
+ const startY = startEvent.clientY;
1908
+ const pointerId = startEvent.pointerId;
1909
+ let latestX = startX;
1910
+ let latestY = startY;
1911
+ let lastScrollX = startX;
1912
+ let resolved = false;
1913
+ let scrolling = false;
1914
+ let pressFeedbackApplied = false;
1915
+ const tabSpan = startEvent.currentTarget instanceof HTMLElement
1916
+ ? startEvent.currentTarget
1917
+ : startEvent.target instanceof HTMLElement
1918
+ ? startEvent.target.closest('.dock-tab')
1919
+ : null;
1920
+ const stripUl = stackEl
1921
+ ? this.getStackStripEl(stackEl)?.querySelector('ul.nav.nav-tabs') ?? null
1922
+ : null;
1923
+ const cleanup = () => {
1924
+ if (resolved)
1925
+ return;
1926
+ resolved = true;
1927
+ win.clearTimeout(longPressTimeout);
1928
+ win.clearTimeout(pressFeedbackTimeout);
1929
+ win.removeEventListener('pointermove', onMove, true);
1930
+ win.removeEventListener('pointerup', onRelease, true);
1931
+ win.removeEventListener('pointercancel', onCancel, true);
1932
+ if (pressFeedbackApplied && tabSpan) {
1933
+ tabSpan.removeAttribute('data-pressing');
1934
+ }
1935
+ };
1936
+ const onMove = (event) => {
1937
+ if (resolved || event.pointerId !== pointerId)
1938
+ return;
1939
+ if (scrolling) {
1940
+ const dx = event.clientX - lastScrollX;
1941
+ lastScrollX = event.clientX;
1942
+ if (stripUl)
1943
+ stripUl.scrollLeft -= dx;
1944
+ return;
1945
+ }
1946
+ latestX = event.clientX;
1947
+ latestY = event.clientY;
1948
+ const dx = event.clientX - startX;
1949
+ const dy = event.clientY - startY;
1950
+ if (Math.hypot(dx, dy) <= MintDockManagerElement.TOUCH_LONG_PRESS_SLOP_PX) {
1951
+ return;
1952
+ }
1953
+ // Slop crossed before the long-press fired. If the move is mostly
1954
+ // horizontal and the strip can scroll, take over scroll in JS;
1955
+ // otherwise abandon (no drag — browser native UI is fully suppressed
1956
+ // by `touch-action: none` so no fallback is needed).
1957
+ const stripScrollable = !!stripUl && stripUl.scrollWidth > stripUl.clientWidth;
1958
+ if (Math.abs(dx) > Math.abs(dy) && stripScrollable) {
1959
+ win.clearTimeout(longPressTimeout);
1960
+ win.clearTimeout(pressFeedbackTimeout);
1961
+ if (pressFeedbackApplied && tabSpan) {
1962
+ tabSpan.removeAttribute('data-pressing');
1963
+ pressFeedbackApplied = false;
1964
+ }
1965
+ scrolling = true;
1966
+ lastScrollX = event.clientX;
1967
+ stripUl.scrollLeft -= dx;
1968
+ this.clearPendingTabDragMetrics();
1969
+ return;
1970
+ }
1971
+ cleanup();
1972
+ this.clearPendingTabDragMetrics();
1973
+ };
1974
+ const onRelease = (event) => {
1975
+ if (resolved || event.pointerId !== pointerId)
1976
+ return;
1977
+ cleanup();
1978
+ this.clearPendingTabDragMetrics();
1979
+ };
1980
+ const onCancel = (event) => {
1981
+ if (resolved || event.pointerId !== pointerId)
1982
+ return;
1983
+ cleanup();
1984
+ this.clearPendingTabDragMetrics();
1985
+ };
1986
+ const longPressTimeout = win.setTimeout(() => {
1987
+ if (resolved)
1988
+ return;
1989
+ cleanup();
1990
+ try {
1991
+ this.setPointerCapture(pointerId);
1992
+ }
1993
+ catch {
1994
+ // setPointerCapture throws if the pointer is no longer active
1995
+ // (e.g. timer raced a pointerup we didn't see). Drag will abort
1996
+ // naturally on the next event.
1997
+ }
1998
+ if (typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function') {
1999
+ try {
2000
+ navigator.vibrate(10);
2001
+ }
2002
+ catch {
2003
+ // ignore
2004
+ }
2005
+ }
2006
+ const synthEvent = new PointerEvent('pointermove', {
2007
+ pointerId,
2008
+ pointerType: 'touch',
2009
+ isPrimary: true,
2010
+ clientX: latestX,
2011
+ clientY: latestY,
2012
+ bubbles: false,
2013
+ cancelable: false,
2014
+ });
2015
+ this.beginPaneDrag(synthEvent, path, pane, stackEl);
2016
+ }, MintDockManagerElement.TOUCH_LONG_PRESS_MS);
2017
+ const pressFeedbackTimeout = win.setTimeout(() => {
2018
+ if (resolved || !tabSpan)
2019
+ return;
2020
+ pressFeedbackApplied = true;
2021
+ tabSpan.setAttribute('data-pressing', 'true');
2022
+ }, MintDockManagerElement.TOUCH_PRESS_FEEDBACK_DELAY_MS);
2023
+ win.addEventListener('pointermove', onMove, true);
2024
+ win.addEventListener('pointerup', onRelease, true);
2025
+ win.addEventListener('pointercancel', onCancel, true);
2026
+ }
1860
2027
  beginPaneDrag(event, path, pane, stackEl) {
1861
2028
  const { path: sourcePath, floatingIndex, pointerOffsetX, pointerOffsetY, } = this.preparePaneDragSource(path, pane, stackEl, event);
1862
2029
  // Capture header bounds for detecting when to convert to floating.
@@ -2420,7 +2587,32 @@ class MintDockManagerElement extends LitElement {
2420
2587
  const placeholderButton = placeholderTabId
2421
2588
  ? allTabButtons.find((b) => b.id === `${placeholderTabId}-header-button`) ?? null
2422
2589
  : null;
2423
- const targets = allTabButtons.filter((b) => b !== placeholderButton);
2590
+ // The dragged pane's content is `data-hidden`, but mp-tab-control's strip
2591
+ // re-render is async (its MutationObserver schedules a Lit microtask). When
2592
+ // beginPaneDrag calls updateDraggedFloatingPositionFromPoint synchronously
2593
+ // right after preparePaneDragSource, this runs before that re-render — so
2594
+ // the dragged button is still in the strip's shadow. We must exclude it
2595
+ // explicitly, otherwise the loop counts it as a real target and the
2596
+ // placeholder gets appended past the live tabs (visible on touch
2597
+ // long-press, where the finger doesn't move and the user sees the
2598
+ // mis-positioned placeholder for the duration of the hold).
2599
+ const draggedPane = this.dragState?.pane ?? null;
2600
+ let draggedTabId = null;
2601
+ if (draggedPane) {
2602
+ for (const child of Array.from(stack.children)) {
2603
+ if (child instanceof HTMLElement &&
2604
+ child.classList.contains('dock-tab') &&
2605
+ !child.hasAttribute('data-placeholder') &&
2606
+ child.dataset['pane'] === draggedPane) {
2607
+ draggedTabId = child.dataset['tabId'] ?? null;
2608
+ break;
2609
+ }
2610
+ }
2611
+ }
2612
+ const draggedButton = draggedTabId
2613
+ ? allTabButtons.find((b) => b.id === `${draggedTabId}-header-button`) ?? null
2614
+ : null;
2615
+ const targets = allTabButtons.filter((b) => b !== placeholderButton && b !== draggedButton);
2424
2616
  if (targets.length === 0) {
2425
2617
  return 0;
2426
2618
  }