@passui/react 0.1.45 → 0.1.47

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.
Files changed (141) hide show
  1. package/README.md +57 -46
  2. package/dist/types/src/components/accordion/accordion.d.ts +1 -1
  3. package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +1 -1
  4. package/dist/types/src/components/inline-disclosure/inline-disclosure.d.ts +1 -1
  5. package/dist/types/src/components/popover/anchored-surface.d.ts +5 -1
  6. package/dist/types/src/components/popover/index.d.ts +41 -0
  7. package/dist/types/src/components/popover/popover-content.d.ts +38 -0
  8. package/dist/types/src/components/popover/popover-root.d.ts +22 -0
  9. package/dist/types/src/components/popover/popover-trigger.d.ts +23 -0
  10. package/dist/types/src/components/stepper/rolling-digit.d.ts +1 -1
  11. package/dist/types/src/components/table/table.d.ts +1 -1
  12. package/dist/types/src/components/view-controller/view-controller.d.ts +2 -2
  13. package/dist/types/src/index.d.ts +1 -1
  14. package/dist/types/src/machines/popover.machine.d.ts +30 -0
  15. package/package.json +307 -307
  16. package/src/appearance/index.ts +2 -2
  17. package/src/appearance/platform.tsx +76 -60
  18. package/src/appearance/theme.tsx +409 -377
  19. package/src/components/accordion/accordion.tsx +58 -49
  20. package/src/components/ambient/ambient.tsx +25 -25
  21. package/src/components/ambient/index.ts +1 -1
  22. package/src/components/aspect-ratio/aspect-ratio.tsx +21 -16
  23. package/src/components/breadcrumb/breadcrumb.tsx +63 -57
  24. package/src/components/button/button-styles.ts +39 -32
  25. package/src/components/button/button.tsx +91 -84
  26. package/src/components/button/icon-button.tsx +27 -25
  27. package/src/components/button/pending-spinner.tsx +8 -8
  28. package/src/components/button/use-pending-indicator.ts +11 -11
  29. package/src/components/chip/chip.tsx +35 -30
  30. package/src/components/code-block/code-block.tsx +40 -39
  31. package/src/components/combo-box/combo-box.tsx +5 -5
  32. package/src/components/command/command.tsx +124 -94
  33. package/src/components/compact-date-picker/calendar-month.ts +75 -53
  34. package/src/components/compact-date-picker/compact-date-picker.tsx +308 -289
  35. package/src/components/compact-time-picker/compact-time-picker.tsx +288 -281
  36. package/src/components/compact-time-picker/time-value.ts +32 -29
  37. package/src/components/content-unavailable-view/content-unavailable-view.tsx +64 -48
  38. package/src/components/content-view/content-view.tsx +168 -159
  39. package/src/components/content-view/index.ts +1 -1
  40. package/src/components/context-menu/conceal-menu-source.ts +25 -25
  41. package/src/components/context-menu/context-menu-host.tsx +439 -402
  42. package/src/components/context-menu/context-menu-store.ts +125 -123
  43. package/src/components/context-menu/menu-navigation.ts +25 -18
  44. package/src/components/context-menu/menu-position.ts +33 -29
  45. package/src/components/context-menu/menu-reveal.ts +32 -32
  46. package/src/components/context-menu/use-context-menu.ts +63 -56
  47. package/src/components/conversation/conversation.tsx +265 -263
  48. package/src/components/conversation/index.ts +1 -1
  49. package/src/components/date-picker/date-picker.tsx +95 -94
  50. package/src/components/date-picker/date-value.ts +20 -10
  51. package/src/components/dialog/dialog-host.tsx +105 -103
  52. package/src/components/dialog/dialog-store.ts +37 -36
  53. package/src/components/dialog/dialog.tsx +192 -179
  54. package/src/components/dialog/index.ts +1 -1
  55. package/src/components/disclosure-group/disclosure-group.tsx +93 -86
  56. package/src/components/divider/divider.tsx +24 -22
  57. package/src/components/inline-disclosure/inline-disclosure.tsx +40 -36
  58. package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +23 -19
  59. package/src/components/labeled-content/labeled-content.tsx +68 -68
  60. package/src/components/locale-switcher/index.ts +1 -1
  61. package/src/components/locale-switcher/locale-switcher.tsx +278 -251
  62. package/src/components/menu/menu.tsx +75 -58
  63. package/src/components/morph/index.ts +2 -2
  64. package/src/components/morph/morph.tsx +227 -201
  65. package/src/components/morph/use-morph-disclosure.ts +19 -19
  66. package/src/components/one-time-code/one-time-code.tsx +216 -200
  67. package/src/components/picker/picker-trigger.ts +16 -13
  68. package/src/components/picker/picker.tsx +371 -345
  69. package/src/components/popover/anchored-surface.tsx +253 -229
  70. package/src/components/popover/index.ts +50 -0
  71. package/src/components/popover/popover-content.tsx +374 -0
  72. package/src/components/popover/popover-root.tsx +97 -0
  73. package/src/components/popover/popover-trigger.tsx +71 -0
  74. package/src/components/profile-image/profile-image.tsx +80 -64
  75. package/src/components/progress-view/progress-view.tsx +61 -59
  76. package/src/components/progress-view/use-liquid-progress.ts +38 -38
  77. package/src/components/radio-group/radio-group.tsx +71 -68
  78. package/src/components/search-field/expanding-search-field.tsx +191 -177
  79. package/src/components/search-field/search-field.tsx +59 -50
  80. package/src/components/search-list/search-list.tsx +246 -236
  81. package/src/components/section/section.tsx +58 -58
  82. package/src/components/secure-field/secure-field.tsx +3 -3
  83. package/src/components/segmented-control/segmented-control.tsx +136 -136
  84. package/src/components/sheet/modal-environment.ts +71 -64
  85. package/src/components/sheet/sheet-portal.tsx +58 -53
  86. package/src/components/sheet/sheet.tsx +146 -141
  87. package/src/components/sheet/use-sheet-focus.ts +97 -82
  88. package/src/components/sidebar/sidebar.tsx +78 -78
  89. package/src/components/skeleton/skeleton.tsx +30 -21
  90. package/src/components/slider/slider.tsx +113 -104
  91. package/src/components/slosh-slider/liquid-step.ts +32 -31
  92. package/src/components/slosh-slider/slosh-slider.tsx +265 -228
  93. package/src/components/stat-tile/index.ts +1 -1
  94. package/src/components/stat-tile/stat-tile.tsx +79 -68
  95. package/src/components/status-indicator/status-indicator.tsx +40 -37
  96. package/src/components/stepper/rolling-digit.tsx +82 -68
  97. package/src/components/stepper/stepper.tsx +122 -113
  98. package/src/components/swatch-picker/swatch-picker.tsx +120 -120
  99. package/src/components/tab-bar/tab-bar.tsx +149 -148
  100. package/src/components/tab-view/tab-view.tsx +140 -137
  101. package/src/components/table/table.tsx +117 -98
  102. package/src/components/text-editor/text-editor.tsx +34 -29
  103. package/src/components/text-field/text-field.tsx +182 -180
  104. package/src/components/toast/toast-store.ts +127 -115
  105. package/src/components/toast/toaster.tsx +361 -347
  106. package/src/components/toggle/toggle.tsx +297 -282
  107. package/src/components/tooltip/tooltip-host.tsx +157 -150
  108. package/src/components/view-controller/index.ts +1 -1
  109. package/src/components/view-controller/view-controller.tsx +64 -53
  110. package/src/components/wake-slider/wake-slider.tsx +220 -205
  111. package/src/hooks/scroll/minimize-decision.ts +11 -7
  112. package/src/hooks/use-anchored-disclosure.ts +19 -19
  113. package/src/hooks/use-anchored-placement.ts +29 -25
  114. package/src/hooks/use-dismiss.ts +33 -29
  115. package/src/hooks/use-floating-dismiss.ts +24 -22
  116. package/src/hooks/use-media-query.ts +67 -60
  117. package/src/hooks/use-minimize-on-scroll.ts +69 -60
  118. package/src/hooks/use-visual-viewport.ts +63 -63
  119. package/src/hooks/viewport/types.ts +16 -16
  120. package/src/hooks/viewport/viewport-metrics.ts +16 -10
  121. package/src/icons/icons.tsx +440 -328
  122. package/src/index.ts +91 -91
  123. package/src/lib/anchored-placement.ts +70 -65
  124. package/src/lib/class-names.ts +14 -13
  125. package/src/lib/clipboard.ts +8 -7
  126. package/src/lib/date.ts +61 -60
  127. package/src/lib/floating-layers.ts +39 -24
  128. package/src/lib/haptics.ts +10 -10
  129. package/src/lib/language.ts +9 -9
  130. package/src/lib/range-value.ts +27 -21
  131. package/src/lib/screen-edges.ts +33 -30
  132. package/src/lib/shapes.ts +38 -34
  133. package/src/machines/popover.machine.ts +51 -0
  134. package/src/motion/transitions.ts +46 -43
  135. package/src/styles/code-block.css +69 -43
  136. package/src/styles/motion-controls.css +393 -191
  137. package/src/tokens.ts +1 -1
  138. package/dist/types/src/components/popover/popover-fit.d.ts +0 -21
  139. package/dist/types/src/components/popover/popover.d.ts +0 -21
  140. package/src/components/popover/popover-fit.ts +0 -69
  141. package/src/components/popover/popover.tsx +0 -334
@@ -1,8 +1,11 @@
1
- import { type RefObject, useEffect } from "react"
2
- import { belongsToFloatingSurface, ownsFloatingLayer } from "../lib/floating-layers"
1
+ import { type RefObject, useEffect } from "react";
2
+ import {
3
+ belongsToFloatingSurface,
4
+ ownsFloatingLayer,
5
+ } from "../lib/floating-layers";
3
6
 
4
7
  /** Marks a floating layer (a pop-up list, a menu) whose clicks belong to what opened it. */
5
- export { FLOATING_LAYER } from "../lib/floating-layers"
8
+ export { FLOATING_LAYER } from "../lib/floating-layers";
6
9
 
7
10
  /**
8
11
  * Calls `onDismiss` when the user presses Escape or clicks outside `ref`
@@ -10,31 +13,32 @@ export { FLOATING_LAYER } from "../lib/floating-layers"
10
13
  * layer opened from inside, such as a pop-up button's list, don't count.
11
14
  */
12
15
  export function useDismiss(
13
- ref: RefObject<HTMLElement | null>,
14
- active: boolean,
15
- onDismiss: () => void
16
+ ref: RefObject<HTMLElement | null>,
17
+ active: boolean,
18
+ onDismiss: () => void,
16
19
  ): void {
17
- useEffect(() => {
18
- if (!active) return
19
- const onPointerDown = (event: PointerEvent) => {
20
- const target = event.target as Element
21
- const surface = ref.current
22
- if (surface && belongsToFloatingSurface(surface, target)) return
23
- onDismiss()
24
- }
25
- const onKeyDown = (event: KeyboardEvent) => {
26
- if (event.defaultPrevented) return
27
- if (ref.current && ownsFloatingLayer(ref.current, document.activeElement)) return
28
- if (event.key === "Escape") {
29
- event.preventDefault()
30
- onDismiss()
31
- }
32
- }
33
- document.addEventListener("pointerdown", onPointerDown)
34
- window.addEventListener("keydown", onKeyDown)
35
- return () => {
36
- document.removeEventListener("pointerdown", onPointerDown)
37
- window.removeEventListener("keydown", onKeyDown)
38
- }
39
- }, [ref, active, onDismiss])
20
+ useEffect(() => {
21
+ if (!active) return;
22
+ const onPointerDown = (event: PointerEvent) => {
23
+ const target = event.target as Element;
24
+ const surface = ref.current;
25
+ if (surface && belongsToFloatingSurface(surface, target)) return;
26
+ onDismiss();
27
+ };
28
+ const onKeyDown = (event: KeyboardEvent) => {
29
+ if (event.defaultPrevented) return;
30
+ if (ref.current && ownsFloatingLayer(ref.current, document.activeElement))
31
+ return;
32
+ if (event.key === "Escape") {
33
+ event.preventDefault();
34
+ onDismiss();
35
+ }
36
+ };
37
+ document.addEventListener("pointerdown", onPointerDown);
38
+ window.addEventListener("keydown", onKeyDown);
39
+ return () => {
40
+ document.removeEventListener("pointerdown", onPointerDown);
41
+ window.removeEventListener("keydown", onKeyDown);
42
+ };
43
+ }, [ref, active, onDismiss]);
40
44
  }
@@ -1,28 +1,30 @@
1
- import { type RefObject, useEffect } from "react"
2
- import { isOutsideFloatingSurface } from "../lib/floating-layers"
1
+ import { type RefObject, useEffect } from "react";
2
+ import { isOutsideFloatingSurface } from "../lib/floating-layers";
3
3
 
4
4
  /** Dismiss an anchored portal without stealing focus from outside pointer interactions. */
5
5
  export function useFloatingDismiss(
6
- surface: RefObject<HTMLElement | null>,
7
- anchor: HTMLElement,
8
- onDismiss: (restoreFocus: boolean) => void
6
+ surface: RefObject<HTMLElement | null>,
7
+ anchor: HTMLElement,
8
+ onDismiss: (restoreFocus: boolean) => void,
9
9
  ) {
10
- useEffect(() => {
11
- const onPointer = (event: PointerEvent) => {
12
- if (isOutsideFloatingSurface(surface.current, anchor, event.target)) onDismiss(false)
13
- }
14
- const onFocus = (event: FocusEvent) => {
15
- if (isOutsideFloatingSurface(surface.current, anchor, event.target)) onDismiss(false)
16
- }
17
- const onBlur = () => onDismiss(false)
10
+ useEffect(() => {
11
+ const onPointer = (event: PointerEvent) => {
12
+ if (isOutsideFloatingSurface(surface.current, anchor, event.target))
13
+ onDismiss(false);
14
+ };
15
+ const onFocus = (event: FocusEvent) => {
16
+ if (isOutsideFloatingSurface(surface.current, anchor, event.target))
17
+ onDismiss(false);
18
+ };
19
+ const onBlur = () => onDismiss(false);
18
20
 
19
- document.addEventListener("pointerdown", onPointer, true)
20
- document.addEventListener("focusin", onFocus, true)
21
- window.addEventListener("blur", onBlur)
22
- return () => {
23
- document.removeEventListener("pointerdown", onPointer, true)
24
- document.removeEventListener("focusin", onFocus, true)
25
- window.removeEventListener("blur", onBlur)
26
- }
27
- }, [surface, anchor, onDismiss])
21
+ document.addEventListener("pointerdown", onPointer, true);
22
+ document.addEventListener("focusin", onFocus, true);
23
+ window.addEventListener("blur", onBlur);
24
+ return () => {
25
+ document.removeEventListener("pointerdown", onPointer, true);
26
+ document.removeEventListener("focusin", onFocus, true);
27
+ window.removeEventListener("blur", onBlur);
28
+ };
29
+ }, [surface, anchor, onDismiss]);
28
30
  }
@@ -1,87 +1,94 @@
1
- import { useCallback, useSyncExternalStore } from "react"
1
+ import { useCallback, useSyncExternalStore } from "react";
2
2
 
3
3
  const BREAKPOINTS = {
4
- "2xl": 1536,
5
- "3xl": 1600,
6
- "4xl": 2000,
7
- lg: 1024,
8
- md: 800,
9
- sm: 640,
10
- xl: 1280,
11
- } as const
4
+ "2xl": 1536,
5
+ "3xl": 1600,
6
+ "4xl": 2000,
7
+ lg: 1024,
8
+ md: 800,
9
+ sm: 640,
10
+ xl: 1280,
11
+ } as const;
12
12
 
13
- export type Breakpoint = keyof typeof BREAKPOINTS
14
- export type BreakpointQuery = Breakpoint | `max-${Breakpoint}` | `${Breakpoint}:max-${Breakpoint}`
13
+ export type Breakpoint = keyof typeof BREAKPOINTS;
14
+ export type BreakpointQuery =
15
+ | Breakpoint
16
+ | `max-${Breakpoint}`
17
+ | `${Breakpoint}:max-${Breakpoint}`;
15
18
  function resolveMin(value: Breakpoint | number): string {
16
- const px = typeof value === "number" ? value : BREAKPOINTS[value]
17
- return `(min-width: ${px}px)`
19
+ const px = typeof value === "number" ? value : BREAKPOINTS[value];
20
+ return `(min-width: ${px}px)`;
18
21
  }
19
22
 
20
23
  function resolveMax(value: Breakpoint | number): string {
21
- const px = typeof value === "number" ? value : BREAKPOINTS[value]
22
- return `(max-width: ${px - 1}px)`
24
+ const px = typeof value === "number" ? value : BREAKPOINTS[value];
25
+ return `(max-width: ${px - 1}px)`;
23
26
  }
24
27
 
25
- function parseQuery(query: BreakpointQuery | MediaQueryInput | (string & {})): string {
26
- if (typeof query !== "string") {
27
- const parts: string[] = []
28
- if (query.min != null) parts.push(resolveMin(query.min))
29
- if (query.max != null) parts.push(resolveMax(query.max))
30
- if (query.pointer === "coarse") parts.push("(pointer: coarse)")
31
- if (query.pointer === "fine") parts.push("(pointer: fine)")
32
- if (parts.length === 0) return "(min-width: 0px)"
33
- return parts.join(" and ")
34
- }
28
+ function parseQuery(
29
+ query: BreakpointQuery | MediaQueryInput | (string & {}),
30
+ ): string {
31
+ if (typeof query !== "string") {
32
+ const parts: string[] = [];
33
+ if (query.min != null) parts.push(resolveMin(query.min));
34
+ if (query.max != null) parts.push(resolveMax(query.max));
35
+ if (query.pointer === "coarse") parts.push("(pointer: coarse)");
36
+ if (query.pointer === "fine") parts.push("(pointer: fine)");
37
+ if (parts.length === 0) return "(min-width: 0px)";
38
+ return parts.join(" and ");
39
+ }
35
40
 
36
- if (query.startsWith("(")) return query
41
+ if (query.startsWith("(")) return query;
37
42
 
38
- const parts: string[] = []
39
- for (const segment of query.split(":")) {
40
- if (segment.startsWith("max-")) {
41
- const bp = segment.slice(4)
42
- if (bp in BREAKPOINTS) parts.push(resolveMax(bp as Breakpoint))
43
- } else if (segment in BREAKPOINTS) {
44
- parts.push(resolveMin(segment as Breakpoint))
45
- }
46
- }
43
+ const parts: string[] = [];
44
+ for (const segment of query.split(":")) {
45
+ if (segment.startsWith("max-")) {
46
+ const bp = segment.slice(4);
47
+ if (bp in BREAKPOINTS) parts.push(resolveMax(bp as Breakpoint));
48
+ } else if (segment in BREAKPOINTS) {
49
+ parts.push(resolveMin(segment as Breakpoint));
50
+ }
51
+ }
47
52
 
48
- return parts.length > 0 ? parts.join(" and ") : query
53
+ return parts.length > 0 ? parts.join(" and ") : query;
49
54
  }
50
55
 
51
56
  function getServerSnapshot(): boolean {
52
- return false
57
+ return false;
53
58
  }
54
59
 
55
60
  export type MediaQueryInput = {
56
- min?: Breakpoint | number
57
- max?: Breakpoint | number
58
- /** Touch-like input (finger). Use "fine" for mouse/trackpad. */
59
- pointer?: PointerPrecision
60
- }
61
+ min?: Breakpoint | number;
62
+ max?: Breakpoint | number;
63
+ /** Touch-like input (finger). Use "fine" for mouse/trackpad. */
64
+ pointer?: PointerPrecision;
65
+ };
61
66
 
62
- export type PointerPrecision = "coarse" | "fine"
67
+ export type PointerPrecision = "coarse" | "fine";
63
68
 
64
- export function useMediaQuery(query: BreakpointQuery | MediaQueryInput | (string & {})): boolean {
65
- const mediaQuery = parseQuery(query)
69
+ export function useMediaQuery(
70
+ query: BreakpointQuery | MediaQueryInput | (string & {}),
71
+ ): boolean {
72
+ const mediaQuery = parseQuery(query);
66
73
 
67
- const subscribe = useCallback(
68
- (callback: () => void) => {
69
- if (typeof window === "undefined") return () => {}
70
- const mql = window.matchMedia(mediaQuery)
71
- mql.addEventListener("change", callback)
72
- return () => mql.removeEventListener("change", callback)
73
- },
74
- [mediaQuery]
75
- )
74
+ const subscribe = useCallback(
75
+ (callback: () => void) => {
76
+ if (typeof window === "undefined") return () => {};
77
+ const mql = window.matchMedia(mediaQuery);
78
+ mql.addEventListener("change", callback);
79
+ return () => mql.removeEventListener("change", callback);
80
+ },
81
+ [mediaQuery],
82
+ );
76
83
 
77
- const getSnapshot = useCallback(() => {
78
- if (typeof window === "undefined") return false
79
- return window.matchMedia(mediaQuery).matches
80
- }, [mediaQuery])
84
+ const getSnapshot = useCallback(() => {
85
+ if (typeof window === "undefined") return false;
86
+ return window.matchMedia(mediaQuery).matches;
87
+ }, [mediaQuery]);
81
88
 
82
- return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
89
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
83
90
  }
84
91
 
85
92
  export function useIsMobile(): boolean {
86
- return useMediaQuery("(max-width: 768px)")
93
+ return useMediaQuery("(max-width: 768px)");
87
94
  }
@@ -1,5 +1,5 @@
1
- import { useEffect, useState } from "react"
2
- import { minimizeDecision } from "./scroll/minimize-decision"
1
+ import { useEffect, useState } from "react";
2
+ import { minimizeDecision } from "./scroll/minimize-decision";
3
3
 
4
4
  /**
5
5
  * Minimized while you scroll down through content, restored when you scroll
@@ -7,62 +7,71 @@ import { minimizeDecision } from "./scroll/minimize-decision"
7
7
  * Listens to every scroll container, since each screen scrolls its own pane.
8
8
  */
9
9
  export function useMinimizeOnScroll() {
10
- const [minimized, setMinimized] = useState(false)
11
- useEffect(() => {
12
- const anchors = new WeakMap<Element, number>()
13
- let intent: number | null = null
14
- const onIntent = () => {
15
- intent = performance.now()
16
- }
17
- const onKey = (event: KeyboardEvent) => {
18
- if (
19
- ["ArrowDown", "ArrowUp", "PageDown", "PageUp", "Home", "End", " "].includes(
20
- event.key
21
- )
22
- )
23
- onIntent()
24
- }
25
- const onPointer = (event: PointerEvent) => {
26
- if (event.buttons === 1) onIntent()
27
- }
28
- const onScroll = (event: Event) => {
29
- const pane = event.target instanceof Element ? event.target : document.scrollingElement
30
- if (
31
- !pane ||
32
- pane.closest("[role=dialog], [data-floating-layer]") ||
33
- pane.scrollHeight <= pane.clientHeight + 1
34
- )
35
- return
36
- const top = pane.scrollTop
37
- const userDriven = intent !== null && performance.now() - intent < 2000
38
- const anchor = anchors.get(pane) ?? 0
39
- const next = minimizeDecision(top, anchor, userDriven)
40
- if (next !== null) setMinimized(next)
41
- if (next !== null || !userDriven) anchors.set(pane, top)
42
- }
43
- document.addEventListener("touchmove", onIntent, {
44
- passive: true,
45
- })
46
- document.addEventListener("wheel", onIntent, {
47
- passive: true,
48
- })
49
- document.addEventListener("pointermove", onPointer, {
50
- passive: true,
51
- })
52
- document.addEventListener("keydown", onKey)
53
- document.addEventListener("scroll", onScroll, {
54
- capture: true,
55
- passive: true,
56
- })
57
- return () => {
58
- document.removeEventListener("scroll", onScroll, {
59
- capture: true,
60
- })
61
- document.removeEventListener("touchmove", onIntent)
62
- document.removeEventListener("wheel", onIntent)
63
- document.removeEventListener("pointermove", onPointer)
64
- document.removeEventListener("keydown", onKey)
65
- }
66
- }, [])
67
- return [minimized, setMinimized] as const
10
+ const [minimized, setMinimized] = useState(false);
11
+ useEffect(() => {
12
+ const anchors = new WeakMap<Element, number>();
13
+ let intent: number | null = null;
14
+ const onIntent = () => {
15
+ intent = performance.now();
16
+ };
17
+ const onKey = (event: KeyboardEvent) => {
18
+ if (
19
+ [
20
+ "ArrowDown",
21
+ "ArrowUp",
22
+ "PageDown",
23
+ "PageUp",
24
+ "Home",
25
+ "End",
26
+ " ",
27
+ ].includes(event.key)
28
+ )
29
+ onIntent();
30
+ };
31
+ const onPointer = (event: PointerEvent) => {
32
+ if (event.buttons === 1) onIntent();
33
+ };
34
+ const onScroll = (event: Event) => {
35
+ const pane =
36
+ event.target instanceof Element
37
+ ? event.target
38
+ : document.scrollingElement;
39
+ if (
40
+ !pane ||
41
+ pane.closest("[role=dialog], [data-floating-layer]") ||
42
+ pane.scrollHeight <= pane.clientHeight + 1
43
+ )
44
+ return;
45
+ const top = pane.scrollTop;
46
+ const userDriven = intent !== null && performance.now() - intent < 2000;
47
+ const anchor = anchors.get(pane) ?? 0;
48
+ const next = minimizeDecision(top, anchor, userDriven);
49
+ if (next !== null) setMinimized(next);
50
+ if (next !== null || !userDriven) anchors.set(pane, top);
51
+ };
52
+ document.addEventListener("touchmove", onIntent, {
53
+ passive: true,
54
+ });
55
+ document.addEventListener("wheel", onIntent, {
56
+ passive: true,
57
+ });
58
+ document.addEventListener("pointermove", onPointer, {
59
+ passive: true,
60
+ });
61
+ document.addEventListener("keydown", onKey);
62
+ document.addEventListener("scroll", onScroll, {
63
+ capture: true,
64
+ passive: true,
65
+ });
66
+ return () => {
67
+ document.removeEventListener("scroll", onScroll, {
68
+ capture: true,
69
+ });
70
+ document.removeEventListener("touchmove", onIntent);
71
+ document.removeEventListener("wheel", onIntent);
72
+ document.removeEventListener("pointermove", onPointer);
73
+ document.removeEventListener("keydown", onKey);
74
+ };
75
+ }, []);
76
+ return [minimized, setMinimized] as const;
68
77
  }
@@ -1,67 +1,67 @@
1
- import { useLayoutEffect, useState } from "react"
2
- import { viewportMetrics } from "./viewport/viewport-metrics"
3
- import type { ViewportMetrics } from "./viewport/types"
4
- export type { ViewportMetrics } from "./viewport/types"
1
+ import { useLayoutEffect, useState } from "react";
2
+ import { viewportMetrics } from "./viewport/viewport-metrics";
3
+ import type { ViewportMetrics } from "./viewport/types";
4
+ export type { ViewportMetrics } from "./viewport/types";
5
5
 
6
6
  /** Measures once per frame, including Android resize and Safari viewport panning. */
7
7
  export function useVisualViewport(): ViewportMetrics | null {
8
- const [metrics, setMetrics] = useState<ViewportMetrics | null>(null)
9
- useLayoutEffect(() => {
10
- const viewport = window.visualViewport
11
- let baseline = {
12
- width: window.innerWidth,
13
- height: window.innerHeight,
14
- }
15
- let frame = 0
16
- const measure = () => {
17
- cancelAnimationFrame(frame)
18
- frame = requestAnimationFrame(() => {
19
- const active = document.activeElement
20
- const editing = !!active?.matches(
21
- "input:not([type=button]):not([type=checkbox]):not([type=radio]), textarea, [contenteditable=true]"
22
- )
23
- const width = viewport?.width ?? window.innerWidth
24
- const height = viewport?.height ?? window.innerHeight
25
- if (Math.abs(baseline.width - width) > 80 || !editing)
26
- baseline = {
27
- width,
28
- height: window.innerHeight,
29
- }
30
- const next = viewportMetrics(
31
- {
32
- width,
33
- height,
34
- layoutHeight: window.innerHeight,
35
- top: viewport?.offsetTop ?? 0,
36
- left: viewport?.offsetLeft ?? 0,
37
- editing,
38
- },
39
- baseline
40
- )
41
- setMetrics((previous) =>
42
- previous &&
43
- Object.entries(next).every(
44
- ([key, value]) => previous[key as keyof ViewportMetrics] === value
45
- )
46
- ? previous
47
- : next
48
- )
49
- })
50
- }
51
- measure()
52
- viewport?.addEventListener("resize", measure)
53
- viewport?.addEventListener("scroll", measure)
54
- window.addEventListener("resize", measure)
55
- document.addEventListener("focusin", measure)
56
- document.addEventListener("focusout", measure)
57
- return () => {
58
- cancelAnimationFrame(frame)
59
- viewport?.removeEventListener("resize", measure)
60
- viewport?.removeEventListener("scroll", measure)
61
- window.removeEventListener("resize", measure)
62
- document.removeEventListener("focusin", measure)
63
- document.removeEventListener("focusout", measure)
64
- }
65
- }, [])
66
- return metrics
8
+ const [metrics, setMetrics] = useState<ViewportMetrics | null>(null);
9
+ useLayoutEffect(() => {
10
+ const viewport = window.visualViewport;
11
+ let baseline = {
12
+ width: window.innerWidth,
13
+ height: window.innerHeight,
14
+ };
15
+ let frame = 0;
16
+ const measure = () => {
17
+ cancelAnimationFrame(frame);
18
+ frame = requestAnimationFrame(() => {
19
+ const active = document.activeElement;
20
+ const editing = !!active?.matches(
21
+ "input:not([type=button]):not([type=checkbox]):not([type=radio]), textarea, [contenteditable=true]",
22
+ );
23
+ const width = viewport?.width ?? window.innerWidth;
24
+ const height = viewport?.height ?? window.innerHeight;
25
+ if (Math.abs(baseline.width - width) > 80 || !editing)
26
+ baseline = {
27
+ width,
28
+ height: window.innerHeight,
29
+ };
30
+ const next = viewportMetrics(
31
+ {
32
+ width,
33
+ height,
34
+ layoutHeight: window.innerHeight,
35
+ top: viewport?.offsetTop ?? 0,
36
+ left: viewport?.offsetLeft ?? 0,
37
+ editing,
38
+ },
39
+ baseline,
40
+ );
41
+ setMetrics((previous) =>
42
+ previous &&
43
+ Object.entries(next).every(
44
+ ([key, value]) => previous[key as keyof ViewportMetrics] === value,
45
+ )
46
+ ? previous
47
+ : next,
48
+ );
49
+ });
50
+ };
51
+ measure();
52
+ viewport?.addEventListener("resize", measure);
53
+ viewport?.addEventListener("scroll", measure);
54
+ window.addEventListener("resize", measure);
55
+ document.addEventListener("focusin", measure);
56
+ document.addEventListener("focusout", measure);
57
+ return () => {
58
+ cancelAnimationFrame(frame);
59
+ viewport?.removeEventListener("resize", measure);
60
+ viewport?.removeEventListener("scroll", measure);
61
+ window.removeEventListener("resize", measure);
62
+ document.removeEventListener("focusin", measure);
63
+ document.removeEventListener("focusout", measure);
64
+ };
65
+ }, []);
66
+ return metrics;
67
67
  }
@@ -1,19 +1,19 @@
1
1
  export type ViewportInput = {
2
- width: number
3
- height: number
4
- top: number
5
- left: number
6
- layoutHeight: number
7
- editing: boolean
8
- }
2
+ width: number;
3
+ height: number;
4
+ top: number;
5
+ left: number;
6
+ layoutHeight: number;
7
+ editing: boolean;
8
+ };
9
9
  export type ViewportMetrics = {
10
- width: number
11
- height: number
12
- top: number
13
- left: number
14
- keyboardOpen: boolean
15
- }
10
+ width: number;
11
+ height: number;
12
+ top: number;
13
+ left: number;
14
+ keyboardOpen: boolean;
15
+ };
16
16
  export type ViewportBaseline = {
17
- width: number
18
- height: number
19
- }
17
+ width: number;
18
+ height: number;
19
+ };
@@ -1,13 +1,19 @@
1
- import type { ViewportBaseline, ViewportInput, ViewportMetrics } from "./types"
1
+ import type { ViewportBaseline, ViewportInput, ViewportMetrics } from "./types";
2
2
 
3
3
  /** A resized WebView and Safari's overlaid keyboard share one visible rectangle. */
4
- export function viewportMetrics(input: ViewportInput, baseline: ViewportBaseline): ViewportMetrics {
5
- const visibleHeight = Math.min(input.height, Math.max(0, input.layoutHeight - input.top))
6
- return {
7
- width: input.width,
8
- height: visibleHeight,
9
- top: input.top,
10
- left: input.left,
11
- keyboardOpen: input.editing && baseline.height - visibleHeight > 120,
12
- }
4
+ export function viewportMetrics(
5
+ input: ViewportInput,
6
+ baseline: ViewportBaseline,
7
+ ): ViewportMetrics {
8
+ const visibleHeight = Math.min(
9
+ input.height,
10
+ Math.max(0, input.layoutHeight - input.top),
11
+ );
12
+ return {
13
+ width: input.width,
14
+ height: visibleHeight,
15
+ top: input.top,
16
+ left: input.left,
17
+ keyboardOpen: input.editing && baseline.height - visibleHeight > 120,
18
+ };
13
19
  }