@passui/react 0.1.44 → 0.1.46

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 (140) 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 +32 -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 -398
  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 +279 -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 +334 -0
  72. package/src/components/popover/popover-fit.ts +58 -53
  73. package/src/components/popover/popover-root.tsx +97 -0
  74. package/src/components/popover/popover-trigger.tsx +71 -0
  75. package/src/components/profile-image/profile-image.tsx +80 -64
  76. package/src/components/progress-view/progress-view.tsx +61 -59
  77. package/src/components/progress-view/use-liquid-progress.ts +38 -38
  78. package/src/components/radio-group/radio-group.tsx +71 -68
  79. package/src/components/search-field/expanding-search-field.tsx +191 -177
  80. package/src/components/search-field/search-field.tsx +59 -50
  81. package/src/components/search-list/search-list.tsx +246 -236
  82. package/src/components/section/section.tsx +58 -58
  83. package/src/components/secure-field/secure-field.tsx +3 -3
  84. package/src/components/segmented-control/segmented-control.tsx +136 -136
  85. package/src/components/sheet/modal-environment.ts +71 -64
  86. package/src/components/sheet/sheet-portal.tsx +58 -53
  87. package/src/components/sheet/sheet.tsx +146 -141
  88. package/src/components/sheet/use-sheet-focus.ts +97 -82
  89. package/src/components/sidebar/sidebar.tsx +78 -78
  90. package/src/components/skeleton/skeleton.tsx +30 -21
  91. package/src/components/slider/slider.tsx +113 -104
  92. package/src/components/slosh-slider/liquid-step.ts +32 -31
  93. package/src/components/slosh-slider/slosh-slider.tsx +265 -228
  94. package/src/components/stat-tile/index.ts +1 -1
  95. package/src/components/stat-tile/stat-tile.tsx +79 -68
  96. package/src/components/status-indicator/status-indicator.tsx +40 -37
  97. package/src/components/stepper/rolling-digit.tsx +82 -68
  98. package/src/components/stepper/stepper.tsx +122 -113
  99. package/src/components/swatch-picker/swatch-picker.tsx +120 -120
  100. package/src/components/tab-bar/tab-bar.tsx +149 -148
  101. package/src/components/tab-view/tab-view.tsx +140 -137
  102. package/src/components/table/table.tsx +117 -98
  103. package/src/components/text-editor/text-editor.tsx +34 -29
  104. package/src/components/text-field/text-field.tsx +182 -180
  105. package/src/components/toast/toast-store.ts +127 -115
  106. package/src/components/toast/toaster.tsx +361 -347
  107. package/src/components/toggle/toggle.tsx +297 -282
  108. package/src/components/tooltip/tooltip-host.tsx +157 -150
  109. package/src/components/view-controller/index.ts +1 -1
  110. package/src/components/view-controller/view-controller.tsx +64 -53
  111. package/src/components/wake-slider/wake-slider.tsx +220 -205
  112. package/src/hooks/scroll/minimize-decision.ts +11 -7
  113. package/src/hooks/use-anchored-disclosure.ts +19 -19
  114. package/src/hooks/use-anchored-placement.ts +29 -25
  115. package/src/hooks/use-dismiss.ts +33 -29
  116. package/src/hooks/use-floating-dismiss.ts +24 -22
  117. package/src/hooks/use-media-query.ts +67 -60
  118. package/src/hooks/use-minimize-on-scroll.ts +69 -60
  119. package/src/hooks/use-visual-viewport.ts +63 -63
  120. package/src/hooks/viewport/types.ts +16 -16
  121. package/src/hooks/viewport/viewport-metrics.ts +16 -10
  122. package/src/icons/icons.tsx +440 -328
  123. package/src/index.ts +91 -91
  124. package/src/lib/anchored-placement.ts +70 -65
  125. package/src/lib/class-names.ts +14 -13
  126. package/src/lib/clipboard.ts +8 -7
  127. package/src/lib/date.ts +61 -60
  128. package/src/lib/floating-layers.ts +39 -24
  129. package/src/lib/haptics.ts +10 -10
  130. package/src/lib/language.ts +9 -9
  131. package/src/lib/range-value.ts +27 -21
  132. package/src/lib/screen-edges.ts +33 -30
  133. package/src/lib/shapes.ts +38 -34
  134. package/src/machines/popover.machine.ts +51 -0
  135. package/src/motion/transitions.ts +46 -43
  136. package/src/styles/code-block.css +69 -43
  137. package/src/styles/motion-controls.css +393 -191
  138. package/src/tokens.ts +1 -1
  139. package/dist/types/src/components/popover/popover.d.ts +0 -21
  140. package/src/components/popover/popover.tsx +0 -322
@@ -1,283 +1,311 @@
1
- import { getNativeLanguageName } from "../../lib/language"
2
- import { cn } from "../../lib/class-names"
3
- import { motion, useDragControls, useMotionValue } from "motion/react"
4
- import { useEffect, useRef, useState } from "react"
5
- import { useDismiss } from "../../hooks/use-dismiss"
6
- import { CheckIcon, GlobeIcon } from "../../icons/icons"
7
- import { Popover } from "../popover/popover"
1
+ import { getNativeLanguageName } from "../../lib/language";
2
+ import { cn } from "../../lib/class-names";
3
+ import { motion, useDragControls, useMotionValue } from "motion/react";
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { useDismiss } from "../../hooks/use-dismiss";
6
+ import { CheckIcon, GlobeIcon } from "../../icons/icons";
7
+ import { Popover } from "../popover";
8
8
 
9
- export type LocaleSwitcherSize = "sm" | "md" | "lg"
9
+ export type LocaleSwitcherSize = "sm" | "md" | "lg";
10
10
 
11
11
  const triggerSizes: Record<LocaleSwitcherSize, string> = {
12
- sm: "glass-menu flex h-9 min-w-16 touch-none cursor-grab items-center justify-center gap-1.5 rounded-full px-3 text-caption2 font-semibold text-label active:cursor-grabbing",
13
- md: "glass-menu flex h-12 min-w-20 touch-none cursor-grab items-center justify-center gap-2 rounded-full px-4 text-caption font-semibold text-label active:cursor-grabbing",
14
- lg: "glass-menu flex h-14 min-w-24 touch-none cursor-grab items-center justify-center gap-2.5 rounded-full px-5 text-subheadline font-semibold text-label active:cursor-grabbing",
15
- }
12
+ sm: "glass-menu flex h-9 min-w-16 touch-none cursor-grab items-center justify-center gap-1.5 rounded-full px-3 text-caption2 font-semibold text-label active:cursor-grabbing",
13
+ md: "glass-menu flex h-12 min-w-20 touch-none cursor-grab items-center justify-center gap-2 rounded-full px-4 text-caption font-semibold text-label active:cursor-grabbing",
14
+ lg: "glass-menu flex h-14 min-w-24 touch-none cursor-grab items-center justify-center gap-2.5 rounded-full px-5 text-subheadline font-semibold text-label active:cursor-grabbing",
15
+ };
16
16
  const iconSizes: Record<LocaleSwitcherSize, string> = {
17
- sm: "size-4",
18
- md: "size-5",
19
- lg: "size-6",
20
- }
17
+ sm: "size-4",
18
+ md: "size-5",
19
+ lg: "size-6",
20
+ };
21
21
  const popoverWidths: Record<LocaleSwitcherSize, string> = {
22
- sm: "w-44",
23
- md: "w-52",
24
- lg: "w-60",
25
- }
22
+ sm: "w-44",
23
+ md: "w-52",
24
+ lg: "w-60",
25
+ };
26
26
  const headerSizes: Record<LocaleSwitcherSize, string> = {
27
- sm: "flex min-h-9 w-full items-center justify-center gap-1.5 rounded-4xl bg-fill/50 text-caption2 font-semibold text-label",
28
- md: "flex min-h-12 w-full items-center justify-center gap-2 rounded-4xl bg-fill/50 text-caption font-semibold text-label",
29
- lg: "flex min-h-14 w-full items-center justify-center gap-2.5 rounded-4xl bg-fill/50 text-subheadline font-semibold text-label",
30
- }
27
+ sm: "flex min-h-9 w-full items-center justify-center gap-1.5 rounded-4xl bg-fill/50 text-caption2 font-semibold text-label",
28
+ md: "flex min-h-12 w-full items-center justify-center gap-2 rounded-4xl bg-fill/50 text-caption font-semibold text-label",
29
+ lg: "flex min-h-14 w-full items-center justify-center gap-2.5 rounded-4xl bg-fill/50 text-subheadline font-semibold text-label",
30
+ };
31
31
  const itemSizes: Record<LocaleSwitcherSize, string> = {
32
- sm: "flex min-h-9 items-center justify-between gap-2 rounded-4xl px-3 py-2 text-left text-caption text-label outline-none",
33
- md: "flex min-h-11 items-center justify-between gap-3 rounded-4xl px-3.5 py-2.5 text-left text-footnote text-label outline-none",
34
- lg: "flex min-h-13 items-center justify-between gap-3 rounded-4xl px-4 py-3 text-left text-subheadline text-label outline-none",
35
- }
32
+ sm: "flex min-h-9 items-center justify-between gap-2 rounded-4xl px-3 py-2 text-left text-caption text-label outline-none",
33
+ md: "flex min-h-11 items-center justify-between gap-3 rounded-4xl px-3.5 py-2.5 text-left text-footnote text-label outline-none",
34
+ lg: "flex min-h-13 items-center justify-between gap-3 rounded-4xl px-4 py-3 text-left text-subheadline text-label outline-none",
35
+ };
36
36
  const checkIconSizes: Record<LocaleSwitcherSize, string> = {
37
- sm: "size-3.5 shrink-0",
38
- md: "size-4 shrink-0",
39
- lg: "size-5 shrink-0",
40
- }
37
+ sm: "size-3.5 shrink-0",
38
+ md: "size-4 shrink-0",
39
+ lg: "size-5 shrink-0",
40
+ };
41
41
 
42
- type Side = "left" | "right"
43
- type Edge = "top" | "bottom"
42
+ type Side = "left" | "right";
43
+ type Edge = "top" | "bottom";
44
44
 
45
45
  type Anchor = {
46
- side: Side
47
- edge: Edge
48
- x: number
49
- y: number
50
- }
46
+ side: Side;
47
+ edge: Edge;
48
+ x: number;
49
+ y: number;
50
+ };
51
51
 
52
52
  const DEFAULT_ANCHOR: Anchor = {
53
- side: "right",
54
- edge: "bottom",
55
- x: 20,
56
- y: 20,
57
- }
53
+ side: "right",
54
+ edge: "bottom",
55
+ x: 20,
56
+ y: 20,
57
+ };
58
58
 
59
59
  function loadStoredAnchor(storageKey: string): Anchor | null {
60
- try {
61
- const raw = localStorage.getItem(storageKey)
62
- if (!raw) return null
63
- const parsed = JSON.parse(raw) as Partial<Anchor>
64
- if (
65
- (parsed.side === "left" || parsed.side === "right") &&
66
- (parsed.edge === "top" || parsed.edge === "bottom") &&
67
- typeof parsed.x === "number" &&
68
- typeof parsed.y === "number"
69
- ) {
70
- return {
71
- side: parsed.side,
72
- edge: parsed.edge,
73
- x: parsed.x,
74
- y: parsed.y,
75
- }
76
- }
77
- } catch {}
78
- return null
60
+ try {
61
+ const raw = localStorage.getItem(storageKey);
62
+ if (!raw) return null;
63
+ const parsed = JSON.parse(raw) as Partial<Anchor>;
64
+ if (
65
+ (parsed.side === "left" || parsed.side === "right") &&
66
+ (parsed.edge === "top" || parsed.edge === "bottom") &&
67
+ typeof parsed.x === "number" &&
68
+ typeof parsed.y === "number"
69
+ ) {
70
+ return {
71
+ side: parsed.side,
72
+ edge: parsed.edge,
73
+ x: parsed.x,
74
+ y: parsed.y,
75
+ };
76
+ }
77
+ } catch {}
78
+ return null;
79
79
  }
80
80
 
81
81
  function saveStoredAnchor(storageKey: string, anchor: Anchor) {
82
- try {
83
- localStorage.setItem(storageKey, JSON.stringify(anchor))
84
- } catch {}
82
+ try {
83
+ localStorage.setItem(storageKey, JSON.stringify(anchor));
84
+ } catch {}
85
85
  }
86
86
 
87
87
  export type LocaleSwitcherProps<L extends string> = {
88
- locales: readonly L[]
89
- locale: L
90
- onSelect: (locale: L) => void
91
- /** localStorage key for where the person dragged it. */
92
- storageKey: string
93
- label: string
94
- /** Trigger, header, list item and icon scale. @default "md" */
95
- size?: LocaleSwitcherSize
96
- }
88
+ locales: readonly L[];
89
+ locale: L;
90
+ onSelect: (locale: L) => void;
91
+ /** localStorage key for where the person dragged it. */
92
+ storageKey: string;
93
+ label: string;
94
+ /** Trigger, header, list item and icon scale. @default "md" */
95
+ size?: LocaleSwitcherSize;
96
+ };
97
97
 
98
98
  /** A draggable floating language picker that snaps to the nearest corner. */
99
99
  export function LocaleSwitcher<L extends string>({
100
- locales,
101
- locale,
102
- onSelect,
103
- storageKey,
104
- label,
105
- size = "md",
100
+ locales,
101
+ locale,
102
+ onSelect,
103
+ storageKey,
104
+ label,
105
+ size = "md",
106
106
  }: LocaleSwitcherProps<L>) {
107
- const containerRef = useRef<HTMLDivElement>(null)
108
- const triggerRef = useRef<HTMLButtonElement>(null)
109
- const dragControls = useDragControls()
110
- const x = useMotionValue(0)
111
- const y = useMotionValue(0)
112
- const [open, setOpen] = useState(false)
113
- const [anchor, setAnchor] = useState(DEFAULT_ANCHOR)
107
+ const containerRef = useRef<HTMLDivElement>(null);
108
+ const triggerRef = useRef<HTMLButtonElement>(null);
109
+ const dragControls = useDragControls();
110
+ const x = useMotionValue(0);
111
+ const y = useMotionValue(0);
112
+ const [open, setOpen] = useState(false);
113
+ const [anchor, setAnchor] = useState(DEFAULT_ANCHOR);
114
114
 
115
- const langs = Array.from(locales, (value) => ({
116
- value: value,
117
- label: getNativeLanguageName(value),
118
- }))
115
+ const langs = Array.from(locales, (value) => ({
116
+ value: value,
117
+ label: getNativeLanguageName(value),
118
+ }));
119
119
 
120
- useEffect(() => {
121
- const stored = loadStoredAnchor(storageKey)
122
- if (stored) setAnchor(stored)
123
- }, [storageKey])
120
+ useEffect(() => {
121
+ const stored = loadStoredAnchor(storageKey);
122
+ if (stored) setAnchor(stored);
123
+ }, [storageKey]);
124
124
 
125
- useDismiss(containerRef, open, () => setOpen(false))
126
- useEffect(() => {
127
- if (open)
128
- containerRef.current
129
- ?.querySelector<HTMLButtonElement>('[role="menuitemradio"][aria-checked="true"]')
130
- ?.focus()
131
- }, [open])
125
+ useDismiss(containerRef, open, () => setOpen(false));
126
+ useEffect(() => {
127
+ if (open)
128
+ containerRef.current
129
+ ?.querySelector<HTMLButtonElement>(
130
+ '[role="menuitemradio"][aria-checked="true"]',
131
+ )
132
+ ?.focus();
133
+ }, [open]);
132
134
 
133
- function commitDraggedPosition() {
134
- const el = containerRef.current
135
- if (!el) return
136
- const rect = el.getBoundingClientRect()
137
- const side: Side = rect.left + rect.width / 2 < window.innerWidth / 2 ? "left" : "right"
138
- const edge: Edge = rect.top + rect.height / 2 < window.innerHeight / 2 ? "top" : "bottom"
139
- const next: Anchor = {
140
- side,
141
- edge,
142
- x: side === "left" ? rect.left : window.innerWidth - rect.right,
143
- y: edge === "top" ? rect.top : window.innerHeight - rect.bottom,
144
- }
145
- setAnchor(next)
146
- saveStoredAnchor(storageKey, next)
147
- x.set(0)
148
- y.set(0)
149
- }
135
+ function commitDraggedPosition() {
136
+ const el = containerRef.current;
137
+ if (!el) return;
138
+ const rect = el.getBoundingClientRect();
139
+ const side: Side =
140
+ rect.left + rect.width / 2 < window.innerWidth / 2 ? "left" : "right";
141
+ const edge: Edge =
142
+ rect.top + rect.height / 2 < window.innerHeight / 2 ? "top" : "bottom";
143
+ const next: Anchor = {
144
+ side,
145
+ edge,
146
+ x: side === "left" ? rect.left : window.innerWidth - rect.right,
147
+ y: edge === "top" ? rect.top : window.innerHeight - rect.bottom,
148
+ };
149
+ setAnchor(next);
150
+ saveStoredAnchor(storageKey, next);
151
+ x.set(0);
152
+ y.set(0);
153
+ }
150
154
 
151
- const growsUp = anchor.edge === "bottom"
155
+ const growsUp = anchor.edge === "bottom";
152
156
 
153
- return (
154
- <motion.div
155
- ref={containerRef}
156
- drag
157
- dragControls={dragControls}
158
- dragListener={false}
159
- dragMomentum={false}
160
- dragElastic={0}
161
- style={{
162
- position: "fixed",
163
- x,
164
- y,
165
- ...(anchor.side === "left" ? { left: anchor.x } : { right: anchor.x }),
166
- ...(anchor.edge === "top" ? { top: anchor.y } : { bottom: anchor.y }),
167
- }}
168
- onDragEnd={commitDraggedPosition}
169
- onDragStart={() => setOpen(false)}
170
- onKeyDown={(event) => {
171
- if (!open) return
172
- if (event.key === "Escape") {
173
- event.preventDefault()
174
- setOpen(false)
175
- triggerRef.current?.focus()
176
- return
177
- }
178
- if (event.key === "Tab") {
179
- setOpen(false)
180
- return
181
- }
182
- if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return
183
- const options = Array.from(
184
- containerRef.current?.querySelectorAll<HTMLButtonElement>(
185
- '[role="menuitemradio"]'
186
- ) ?? []
187
- )
188
- if (!options.length) return
189
- event.preventDefault()
190
- const current = options.indexOf(document.activeElement as HTMLButtonElement)
191
- const next =
192
- event.key === "Home"
193
- ? 0
194
- : event.key === "End"
195
- ? options.length - 1
196
- : (current + (event.key === "ArrowDown" ? 1 : -1) + options.length) %
197
- options.length
198
- options[next]?.focus()
199
- }}
200
- className="z-60"
201
- >
202
- <button
203
- ref={triggerRef}
204
- type="button"
205
- aria-label={label}
206
- aria-haspopup="menu"
207
- aria-expanded={open}
208
- onPointerDown={(e) => dragControls.start(e)}
209
- onClick={() => setOpen((prev) => !prev)}
210
- onKeyDown={(event) => {
211
- if (!open && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
212
- event.preventDefault()
213
- setOpen(true)
214
- }
215
- }}
216
- className={triggerSizes[size]}
217
- >
218
- <GlobeIcon aria-hidden="true" className={iconSizes[size]} />
219
- {locale.toUpperCase()}
220
- </button>
221
- <Popover
222
- open={open}
223
- anchorRef={triggerRef}
224
- origin={anchor.side === "right" ? "top-right" : "top-left"}
225
- role="menu"
226
- aria-label={label}
227
- className={cn(
228
- "top-0 flex max-w-[calc(100vw-2.5rem)] gap-2 p-2",
229
- popoverWidths[size],
230
- anchor.side === "right" ? "right-0" : "left-0",
231
- growsUp ? "flex-col-reverse" : "flex-col"
232
- )}
233
- >
234
- <button
235
- type="button"
236
- aria-label={label}
237
- onClick={() => {
238
- setOpen(false)
239
- triggerRef.current?.focus()
240
- }}
241
- className={headerSizes[size]}
242
- >
243
- <GlobeIcon aria-hidden="true" className={iconSizes[size]} />
244
- {locale.toUpperCase()}
245
- </button>
246
- <div className="flex flex-col gap-1">
247
- {langs.map((item) => {
248
- const active = item.value === locale
249
- return (
250
- <button
251
- key={item.value}
252
- type="button"
253
- role="menuitemradio"
254
- aria-checked={active}
255
- tabIndex={-1}
256
- onClick={() => {
257
- setOpen(false)
258
- triggerRef.current?.focus()
259
- if (!active) {
260
- onSelect(item.value)
261
- }
262
- }}
263
- className={cn(
264
- itemSizes[size],
265
- "hover:bg-fill/65 focus-visible:bg-fill/65 transition-colors",
266
- active && "bg-accent/12 font-medium"
267
- )}
268
- >
269
- {item.label}
270
- {active && (
271
- <CheckIcon
272
- aria-hidden="true"
273
- className={checkIconSizes[size]}
274
- />
275
- )}
276
- </button>
277
- )
278
- })}
279
- </div>
280
- </Popover>
281
- </motion.div>
282
- )
157
+ return (
158
+ <motion.div
159
+ ref={containerRef}
160
+ drag
161
+ dragControls={dragControls}
162
+ dragListener={false}
163
+ dragMomentum={false}
164
+ dragElastic={0}
165
+ style={{
166
+ position: "fixed",
167
+ x,
168
+ y,
169
+ ...(anchor.side === "left" ? { left: anchor.x } : { right: anchor.x }),
170
+ ...(anchor.edge === "top" ? { top: anchor.y } : { bottom: anchor.y }),
171
+ }}
172
+ onDragEnd={commitDraggedPosition}
173
+ onDragStart={() => setOpen(false)}
174
+ onKeyDown={(event) => {
175
+ if (!open) return;
176
+ if (event.key === "Escape") {
177
+ event.preventDefault();
178
+ setOpen(false);
179
+ triggerRef.current?.focus();
180
+ return;
181
+ }
182
+ if (event.key === "Tab") {
183
+ setOpen(false);
184
+ return;
185
+ }
186
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key))
187
+ return;
188
+ const options = Array.from(
189
+ containerRef.current?.querySelectorAll<HTMLButtonElement>(
190
+ '[role="menuitemradio"]',
191
+ ) ?? [],
192
+ );
193
+ if (!options.length) return;
194
+ event.preventDefault();
195
+ const current = options.indexOf(
196
+ document.activeElement as HTMLButtonElement,
197
+ );
198
+ const next =
199
+ event.key === "Home"
200
+ ? 0
201
+ : event.key === "End"
202
+ ? options.length - 1
203
+ : (current +
204
+ (event.key === "ArrowDown" ? 1 : -1) +
205
+ options.length) %
206
+ options.length;
207
+ options[next]?.focus();
208
+ }}
209
+ className="z-60"
210
+ >
211
+ <Popover open={open} onOpenChange={setOpen}>
212
+ <Popover.Trigger
213
+ render={({
214
+ ref,
215
+ "aria-haspopup": ariaHasPopup,
216
+ "aria-expanded": ariaExpanded,
217
+ "aria-controls": ariaControls,
218
+ onClick,
219
+ onKeyDown,
220
+ }) => (
221
+ <button
222
+ ref={(node) => {
223
+ ref(node);
224
+ triggerRef.current = node;
225
+ }}
226
+ type="button"
227
+ aria-label={label}
228
+ aria-haspopup={ariaHasPopup}
229
+ aria-expanded={ariaExpanded}
230
+ aria-controls={ariaControls}
231
+ onPointerDown={(e) => dragControls.start(e)}
232
+ onClick={onClick}
233
+ onKeyDown={(event) => {
234
+ onKeyDown(event);
235
+ if (
236
+ !open &&
237
+ (event.key === "ArrowDown" || event.key === "ArrowUp")
238
+ ) {
239
+ event.preventDefault();
240
+ setOpen(true);
241
+ }
242
+ }}
243
+ className={triggerSizes[size]}
244
+ >
245
+ <GlobeIcon aria-hidden="true" className={iconSizes[size]} />
246
+ {locale.toUpperCase()}
247
+ </button>
248
+ )}
249
+ />
250
+ <Popover.Content
251
+ origin={anchor.side === "right" ? "top-right" : "top-left"}
252
+ role="menu"
253
+ label={label}
254
+ className={cn(
255
+ "top-0 flex max-w-[calc(100vw-2.5rem)] gap-2 p-2",
256
+ popoverWidths[size],
257
+ anchor.side === "right" ? "right-0" : "left-0",
258
+ growsUp ? "flex-col-reverse" : "flex-col",
259
+ )}
260
+ >
261
+ <button
262
+ type="button"
263
+ aria-label={label}
264
+ onClick={() => {
265
+ setOpen(false);
266
+ triggerRef.current?.focus();
267
+ }}
268
+ className={headerSizes[size]}
269
+ >
270
+ <GlobeIcon aria-hidden="true" className={iconSizes[size]} />
271
+ {locale.toUpperCase()}
272
+ </button>
273
+ <div className="flex flex-col gap-1">
274
+ {langs.map((item) => {
275
+ const active = item.value === locale;
276
+ return (
277
+ <button
278
+ key={item.value}
279
+ type="button"
280
+ role="menuitemradio"
281
+ aria-checked={active}
282
+ tabIndex={-1}
283
+ onClick={() => {
284
+ setOpen(false);
285
+ triggerRef.current?.focus();
286
+ if (!active) {
287
+ onSelect(item.value);
288
+ }
289
+ }}
290
+ className={cn(
291
+ itemSizes[size],
292
+ "hover:bg-fill/65 focus-visible:bg-fill/65 transition-colors",
293
+ active && "bg-accent/12 font-medium",
294
+ )}
295
+ >
296
+ {item.label}
297
+ {active && (
298
+ <CheckIcon
299
+ aria-hidden="true"
300
+ className={checkIconSizes[size]}
301
+ />
302
+ )}
303
+ </button>
304
+ );
305
+ })}
306
+ </div>
307
+ </Popover.Content>
308
+ </Popover>
309
+ </motion.div>
310
+ );
283
311
  }