@ttdk-design-system/library-react 0.0.0-stage → 1.0.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.
- package/AI_GUIDE.md +6518 -0
- package/llms.txt +6518 -0
- package/package.json +1098 -4
- package/src/core/index.ts +2 -0
- package/src/core/tailwind-preset.cjs +62 -0
- package/src/core/tailwind-preset.ts +85 -0
- package/src/core/theme/tokens.css +663 -0
- package/src/core/types.ts +836 -0
- package/src/index.ts +17 -0
- package/src/tailwind-preset.cjs +62 -0
- package/src/tailwind-preset.ts +85 -0
- package/src/types.ts +1 -0
- package/src/web/accordion/AI_GUIDE.md +149 -0
- package/src/web/accordion/accordion.css +1 -0
- package/src/web/accordion/accordion.tsx +430 -0
- package/src/web/accordion/index.ts +3 -0
- package/src/web/accordion/manifest.json +351 -0
- package/src/web/alert/AI_GUIDE.md +139 -0
- package/src/web/alert/alert.css +1 -0
- package/src/web/alert/alert.tsx +318 -0
- package/src/web/alert/index.ts +3 -0
- package/src/web/alert/manifest.json +240 -0
- package/src/web/avatar/AI_GUIDE.md +118 -0
- package/src/web/avatar/avatar.css +1 -0
- package/src/web/avatar/avatar.tsx +233 -0
- package/src/web/avatar/index.ts +3 -0
- package/src/web/avatar/manifest.json +195 -0
- package/src/web/badge/AI_GUIDE.md +94 -0
- package/src/web/badge/badge.css +1 -0
- package/src/web/badge/badge.tsx +140 -0
- package/src/web/badge/index.ts +2 -0
- package/src/web/badge/manifest.json +141 -0
- package/src/web/banner/AI_GUIDE.md +130 -0
- package/src/web/banner/banner.css +1 -0
- package/src/web/banner/banner.tsx +334 -0
- package/src/web/banner/index.ts +3 -0
- package/src/web/banner/manifest.json +240 -0
- package/src/web/breadcrumb/AI_GUIDE.md +120 -0
- package/src/web/breadcrumb/breadcrumb.css +1 -0
- package/src/web/breadcrumb/breadcrumb.tsx +306 -0
- package/src/web/breadcrumb/index.ts +3 -0
- package/src/web/breadcrumb/manifest.json +225 -0
- package/src/web/button/AI_GUIDE.md +121 -0
- package/src/web/button/button.css +1 -0
- package/src/web/button/button.tsx +287 -0
- package/src/web/button/index.ts +2 -0
- package/src/web/button/manifest.json +177 -0
- package/src/web/calendar/AI_GUIDE.md +151 -0
- package/src/web/calendar/calendar.css +1 -0
- package/src/web/calendar/calendar.tsx +1611 -0
- package/src/web/calendar/index.ts +3 -0
- package/src/web/calendar/manifest.json +470 -0
- package/src/web/card/AI_GUIDE.md +133 -0
- package/src/web/card/card.css +1 -0
- package/src/web/card/card.tsx +408 -0
- package/src/web/card/index.ts +4 -0
- package/src/web/card/manifest.json +357 -0
- package/src/web/checkbox/AI_GUIDE.md +146 -0
- package/src/web/checkbox/checkbox.css +1 -0
- package/src/web/checkbox/checkbox.tsx +307 -0
- package/src/web/checkbox/index.ts +2 -0
- package/src/web/checkbox/manifest.json +294 -0
- package/src/web/chip/AI_GUIDE.md +186 -0
- package/src/web/chip/chip.css +1 -0
- package/src/web/chip/chip.tsx +230 -0
- package/src/web/chip/index.ts +2 -0
- package/src/web/chip/manifest.json +186 -0
- package/src/web/dropdown/AI_GUIDE.md +167 -0
- package/src/web/dropdown/dropdown.css +1 -0
- package/src/web/dropdown/dropdown.tsx +1274 -0
- package/src/web/dropdown/index.ts +3 -0
- package/src/web/dropdown/manifest.json +576 -0
- package/src/web/flex/AI_GUIDE.md +113 -0
- package/src/web/flex/flex-item.tsx +97 -0
- package/src/web/flex/flex.css +1 -0
- package/src/web/flex/flex.tsx +108 -0
- package/src/web/flex/index.ts +5 -0
- package/src/web/flex/manifest.json +162 -0
- package/src/web/form/index.ts +7 -0
- package/src/web/grid/AI_GUIDE.md +112 -0
- package/src/web/grid/grid-item.tsx +112 -0
- package/src/web/grid/grid.css +1 -0
- package/src/web/grid/grid.tsx +99 -0
- package/src/web/grid/index.ts +5 -0
- package/src/web/grid/manifest.json +153 -0
- package/src/web/index.ts +86 -0
- package/src/web/input/AI_GUIDE.md +159 -0
- package/src/web/input/index.ts +3 -0
- package/src/web/input/input.css +1 -0
- package/src/web/input/input.tsx +537 -0
- package/src/web/input/manifest.json +465 -0
- package/src/web/input-number/AI_GUIDE.md +164 -0
- package/src/web/input-number/index.ts +3 -0
- package/src/web/input-number/input-number.css +1 -0
- package/src/web/input-number/input-number.tsx +1193 -0
- package/src/web/input-number/manifest.json +575 -0
- package/src/web/modal/AI_GUIDE.md +171 -0
- package/src/web/modal/index.ts +3 -0
- package/src/web/modal/manifest.json +375 -0
- package/src/web/modal/modal.css +1 -0
- package/src/web/modal/modal.tsx +571 -0
- package/src/web/pagination/AI_GUIDE.md +148 -0
- package/src/web/pagination/index.ts +21 -0
- package/src/web/pagination/manifest.json +432 -0
- package/src/web/pagination/pagination-content.tsx +45 -0
- package/src/web/pagination/pagination-ellipsis.tsx +54 -0
- package/src/web/pagination/pagination-first.tsx +155 -0
- package/src/web/pagination/pagination-item.tsx +45 -0
- package/src/web/pagination/pagination-last.tsx +155 -0
- package/src/web/pagination/pagination-link.tsx +121 -0
- package/src/web/pagination/pagination-next.tsx +158 -0
- package/src/web/pagination/pagination-previous.tsx +158 -0
- package/src/web/pagination/pagination.css +1 -0
- package/src/web/pagination/pagination.tsx +687 -0
- package/src/web/popover/AI_GUIDE.md +163 -0
- package/src/web/popover/index.ts +3 -0
- package/src/web/popover/manifest.json +396 -0
- package/src/web/popover/popover.css +1 -0
- package/src/web/popover/popover.tsx +702 -0
- package/src/web/radio/AI_GUIDE.md +155 -0
- package/src/web/radio/index.ts +2 -0
- package/src/web/radio/manifest.json +294 -0
- package/src/web/radio/radio.css +1 -0
- package/src/web/radio/radio.tsx +290 -0
- package/src/web/selectbox/AI_GUIDE.md +216 -0
- package/src/web/selectbox/index.ts +3 -0
- package/src/web/selectbox/manifest.json +477 -0
- package/src/web/selectbox/selectbox.css +1 -0
- package/src/web/selectbox/selectbox.tsx +1048 -0
- package/src/web/sonner/AI_GUIDE.md +98 -0
- package/src/web/sonner/index.ts +114 -0
- package/src/web/sonner/manifest.json +132 -0
- package/src/web/sonner/sonner.css +1 -0
- package/src/web/sonner/sonner.tsx +177 -0
- package/src/web/table/AI_GUIDE.md +145 -0
- package/src/web/table/index.ts +35 -0
- package/src/web/table/manifest.json +168 -0
- package/src/web/table/table-body.tsx +45 -0
- package/src/web/table/table-caption.tsx +45 -0
- package/src/web/table/table-cell.tsx +74 -0
- package/src/web/table/table-empty.tsx +108 -0
- package/src/web/table/table-footer.tsx +45 -0
- package/src/web/table/table-head.tsx +80 -0
- package/src/web/table/table-header.tsx +45 -0
- package/src/web/table/table-row.tsx +69 -0
- package/src/web/table/table.css +1 -0
- package/src/web/table/table.tsx +305 -0
- package/src/web/tabs/AI_GUIDE.md +150 -0
- package/src/web/tabs/index.ts +3 -0
- package/src/web/tabs/manifest.json +315 -0
- package/src/web/tabs/tabs.css +1 -0
- package/src/web/tabs/tabs.tsx +332 -0
- package/src/web/text/AI_GUIDE.md +129 -0
- package/src/web/text/index.ts +3 -0
- package/src/web/text/manifest.json +195 -0
- package/src/web/text/text.css +1 -0
- package/src/web/text/text.tsx +689 -0
- package/src/web/textarea/AI_GUIDE.md +141 -0
- package/src/web/textarea/index.ts +3 -0
- package/src/web/textarea/manifest.json +438 -0
- package/src/web/textarea/textarea.css +1 -0
- package/src/web/textarea/textarea.tsx +467 -0
- package/src/web/tooltip/AI_GUIDE.md +110 -0
- package/src/web/tooltip/index.ts +3 -0
- package/src/web/tooltip/manifest.json +222 -0
- package/src/web/tooltip/tooltip.css +1 -0
- package/src/web/tooltip/tooltip.tsx +423 -0
- package/src/webview/bottomsheet/AI_GUIDE.md +141 -0
- package/src/webview/bottomsheet/bottomsheet.css +1 -0
- package/src/webview/bottomsheet/bottomsheet.tsx +603 -0
- package/src/webview/bottomsheet/index.ts +4 -0
- package/src/webview/bottomsheet/manifest.json +377 -0
- package/src/webview/button/AI_GUIDE.md +79 -0
- package/src/webview/button/button-ios.css +1 -0
- package/src/webview/button/button-ios.tsx +138 -0
- package/src/webview/button/button-material.css +1 -0
- package/src/webview/button/button-material.tsx +138 -0
- package/src/webview/button/index.ts +7 -0
- package/src/webview/button/manifest.json +148 -0
- package/src/webview/index.ts +7 -0
- package/tailwind-preset.cjs +62 -0
- package/tailwind-preset.js +62 -0
- package/ttdk-manifest.json +10188 -0
- package/README.md +0 -3
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { useState, useRef, useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
export interface PopoverContentOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Sisi penempatan relatif terhadap elemen pemicu (trigger).
|
|
8
|
+
* @default 'bottom'
|
|
9
|
+
*/
|
|
10
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Penjajaran di sepanjang sumbu sisi yang dipilih.
|
|
14
|
+
* @default 'center'
|
|
15
|
+
*/
|
|
16
|
+
align?: "start" | "center" | "end";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Jarak offset dalam pixel dari elemen pemicu (trigger).
|
|
20
|
+
* @default 8
|
|
21
|
+
*/
|
|
22
|
+
sideOffset?: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Offset dalam pixel di sepanjang sumbu penjajaran (alignment axis).
|
|
26
|
+
* @default 0
|
|
27
|
+
*/
|
|
28
|
+
alignOffset?: number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Mencegah popover terpotong keluar dari batas viewport layar dengan memanfaatkan middleware flip dan shift.
|
|
32
|
+
* @default true
|
|
33
|
+
*/
|
|
34
|
+
avoidCollisions?: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Batas kontainer pendeteksi benturan viewport.
|
|
38
|
+
*/
|
|
39
|
+
collisionBoundary?: any;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Jarak padding minimal dalam pixel dari tepi layar saat terjadi benturan.
|
|
43
|
+
* @default 8
|
|
44
|
+
*/
|
|
45
|
+
collisionPadding?: number;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Jarak padding minimal dalam pixel untuk elemen panah (arrow) dari sudut popover.
|
|
49
|
+
* @default 8
|
|
50
|
+
*/
|
|
51
|
+
arrowPadding?: number;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Perilaku popover saat elemen pemicu tergulung keluar layar ('partial' | 'always').
|
|
55
|
+
*/
|
|
56
|
+
sticky?: "partial" | "always";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sembunyikan popover ketika pemicu terpisah atau tidak terlihat di viewport.
|
|
60
|
+
* @default false
|
|
61
|
+
*/
|
|
62
|
+
hideWhenDetached?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export type PopoverUiProps = UiProps;
|
|
65
|
+
export interface PopoverProps {
|
|
66
|
+
/**
|
|
67
|
+
* Status visibilitas popover (mode terkontrol / controlled state).
|
|
68
|
+
* Mendukung `v-model:open` pada Vue 3 dan controlled prop `open` pada React.
|
|
69
|
+
*/
|
|
70
|
+
open?: boolean;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Status visibilitas awal saat pertama kali dirender (mode tidak terkontrol / uncontrolled initial state).
|
|
74
|
+
* @default false
|
|
75
|
+
*/
|
|
76
|
+
defaultOpen?: boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Menampilkan elemen panah (arrow) penunjuk arah yang menunjuk ke elemen pemicu.
|
|
80
|
+
* @default false
|
|
81
|
+
*/
|
|
82
|
+
arrow?: boolean;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Mengizinkan popover ditutup saat pengguna mengeklik area di luar popover atau menekan tombol `Escape`.
|
|
86
|
+
* Jika bernilai `false`, popover tidak akan tertutup dan event `onClosePrevent` akan dipanggil.
|
|
87
|
+
* @default true
|
|
88
|
+
*/
|
|
89
|
+
dismissible?: boolean;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Mengaktifkan mode modal dengan menampilkan lapisan latar belakang redup (backdrop overlay)
|
|
93
|
+
* serta memblokir interaksi di luar popover dan mengunci scroll latar belakang.
|
|
94
|
+
* @default false
|
|
95
|
+
*/
|
|
96
|
+
modal?: boolean;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Mengunci pengguliran halaman web (body scroll lock) saat popover sedang terbuka,
|
|
100
|
+
* sehingga pengguna tidak dapat menggulir halaman di latar belakang.
|
|
101
|
+
* Otomatis aktif saat `modal={true}` kecuali dinyatakan `false`.
|
|
102
|
+
* @default false
|
|
103
|
+
*/
|
|
104
|
+
preventScroll?: boolean;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Alias untuk `preventScroll`.
|
|
108
|
+
* @default false
|
|
109
|
+
*/
|
|
110
|
+
lockScroll?: boolean;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Menonaktifkan pemicu sehingga popover tidak dapat dibuka.
|
|
114
|
+
* @default false
|
|
115
|
+
*/
|
|
116
|
+
disabled?: boolean;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Opsi konfigurasi penempatan dan benturan popover (side, align, sideOffset, dsb.)
|
|
120
|
+
* ATAU elemen konten popover secara langsung jika tidak menggunakan slot/children.
|
|
121
|
+
*/
|
|
122
|
+
content?: PopoverContentOptions | any;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Konten kustom panel popover (alias untuk konten popover jika `content` dipakai untuk konfigurasi).
|
|
126
|
+
* Menerima ReactNode, VNode, string, atau render function `({ close }) => ReactNode`.
|
|
127
|
+
*/
|
|
128
|
+
panel?: any;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Konten kustom panel popover (kompatibilitas selaras dengan Dropdown panelContent).
|
|
132
|
+
* Menerima ReactNode, VNode, string, atau render function `({ close }) => ReactNode`.
|
|
133
|
+
*/
|
|
134
|
+
panelContent?: any;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Pintasan posisi penempatan panel popover Floating UI ('top', 'bottom', 'left', 'right', dsb.)
|
|
138
|
+
* Selaras dengan konfigurasi content.side dan content.align.
|
|
139
|
+
* @default 'bottom'
|
|
140
|
+
*/
|
|
141
|
+
placement?:
|
|
142
|
+
| "top"
|
|
143
|
+
| "top-start"
|
|
144
|
+
| "top-end"
|
|
145
|
+
| "bottom"
|
|
146
|
+
| "bottom-start"
|
|
147
|
+
| "bottom-end"
|
|
148
|
+
| "left"
|
|
149
|
+
| "left-start"
|
|
150
|
+
| "left-end"
|
|
151
|
+
| "right"
|
|
152
|
+
| "right-start"
|
|
153
|
+
| "right-end";
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Jarak offset dalam pixel dari elemen pemicu (shorthand untuk content.sideOffset).
|
|
157
|
+
* @default 8
|
|
158
|
+
*/
|
|
159
|
+
offsetDistance?: number;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Menjadikan popover memenuhi lebar kontainer penuh (full width 100%).
|
|
163
|
+
* @default false
|
|
164
|
+
*/
|
|
165
|
+
block?: boolean;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Kustomisasi visual granular wrapper utama (UiProps).
|
|
169
|
+
*/
|
|
170
|
+
ui?: PopoverUiProps;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Style inline CSS native untuk wrapper luar.
|
|
174
|
+
*/
|
|
175
|
+
style?: React.CSSProperties;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Class CSS kustom untuk wrapper luar.
|
|
179
|
+
*/
|
|
180
|
+
className?: string;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Class CSS kustom untuk pemicu (trigger container).
|
|
184
|
+
*/
|
|
185
|
+
triggerClass?: string;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Style kustom untuk pemicu (UiProps).
|
|
189
|
+
*/
|
|
190
|
+
triggerStyle?: UiProps;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Class CSS kustom untuk panel popover konten.
|
|
194
|
+
*/
|
|
195
|
+
contentClass?: string;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Style kustom untuk panel popover konten (UiProps).
|
|
199
|
+
*/
|
|
200
|
+
contentStyle?: UiProps;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Class CSS kustom untuk elemen panah (arrow).
|
|
204
|
+
*/
|
|
205
|
+
arrowClass?: string;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Style kustom untuk elemen panah (UiProps).
|
|
209
|
+
*/
|
|
210
|
+
arrowStyle?: UiProps;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Class CSS kustom untuk lapisan latar belakang redup (modal overlay).
|
|
214
|
+
*/
|
|
215
|
+
overlayClass?: string;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Style kustom untuk lapisan latar belakang redup (UiProps).
|
|
219
|
+
*/
|
|
220
|
+
overlayStyle?: UiProps;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Callback event saat status terbuka/tertutup berubah (kompatibel dengan v-model:open pada Vue 3).
|
|
224
|
+
*/
|
|
225
|
+
onUpdateOpen?: (open: boolean) => void;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Callback event saat status terbuka/tertutup berubah (alias standar React).
|
|
229
|
+
*/
|
|
230
|
+
onOpenChange?: (open: boolean) => void;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Callback event saat popover terbuka.
|
|
234
|
+
*/
|
|
235
|
+
onOpen?: () => void;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Callback event saat popover ditutup.
|
|
239
|
+
*/
|
|
240
|
+
onClose?: () => void;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Callback event saat pengguna mencoba menutup popover tetapi dicegah karena dismissible={false}.
|
|
244
|
+
*/
|
|
245
|
+
onClosePrevent?: () => void;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Elemen pemicu (trigger), tombol, atau komponen anak lainnya.
|
|
249
|
+
*/
|
|
250
|
+
children?: any;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Komponen Popover TTDK Design System — Selaras dengan Spesifikasi Nuxt UI Popover (UPopover):
|
|
255
|
+
* - Mendukung mode 'click' dan mode 'hover' dengan configurable openDelay dan closeDelay
|
|
256
|
+
* - Floating UI otomatis (computePosition, autoUpdate, flip, shift, offset, arrow)
|
|
257
|
+
* - Posisi cerdas (top, bottom, left, right, start, end)
|
|
258
|
+
* - Elemen panah (arrow) penunjuk arah dinamis
|
|
259
|
+
* - Mode modal dengan backdrop overlay
|
|
260
|
+
* - Penanganan dismissible & closePrevent (Esc key, outside click)
|
|
261
|
+
* - Dukungan controlled state (v-model:open) dan uncontrolled defaultOpen
|
|
262
|
+
*/
|
|
263
|
+
|
|
264
|
+
import {
|
|
265
|
+
computePosition,
|
|
266
|
+
autoUpdate,
|
|
267
|
+
flip,
|
|
268
|
+
shift,
|
|
269
|
+
offset,
|
|
270
|
+
arrow as floatingArrow,
|
|
271
|
+
} from "@floating-ui/dom";
|
|
272
|
+
import { buildUiVars, UiProps } from "../../core/types";
|
|
273
|
+
|
|
274
|
+
let lockCount = 0;
|
|
275
|
+
let originalOverflow = "";
|
|
276
|
+
let originalPaddingRight = "";
|
|
277
|
+
|
|
278
|
+
function lockBodyScroll() {
|
|
279
|
+
if (typeof document === "undefined") return;
|
|
280
|
+
if (lockCount === 0) {
|
|
281
|
+
originalOverflow = document.body.style.overflow;
|
|
282
|
+
originalPaddingRight = document.body.style.paddingRight;
|
|
283
|
+
const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
284
|
+
document.body.style.overflow = "hidden";
|
|
285
|
+
if (scrollBarWidth > 0) {
|
|
286
|
+
document.body.style.paddingRight = scrollBarWidth + "px";
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
lockCount++;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function unlockBodyScroll() {
|
|
293
|
+
if (typeof document === "undefined") return;
|
|
294
|
+
lockCount = Math.max(0, lockCount - 1);
|
|
295
|
+
if (lockCount === 0) {
|
|
296
|
+
document.body.style.overflow = originalOverflow;
|
|
297
|
+
document.body.style.paddingRight = originalPaddingRight;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function Popover(props: PopoverProps) {
|
|
302
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
303
|
+
const triggerRef = useRef<HTMLDivElement>(null);
|
|
304
|
+
const popoverRef = useRef<HTMLDivElement>(null);
|
|
305
|
+
const arrowRef = useRef<HTMLDivElement>(null);
|
|
306
|
+
const [isOpen, setIsOpen] = useState(() =>
|
|
307
|
+
props.open !== undefined ? props.open : Boolean(props.defaultOpen)
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const cleanupAutoUpdateRef = useRef<any>(null);
|
|
311
|
+
|
|
312
|
+
function isOptionsConfig(val: any) {
|
|
313
|
+
if (!val || typeof val !== "object") return false;
|
|
314
|
+
return (
|
|
315
|
+
"side" in val ||
|
|
316
|
+
"align" in val ||
|
|
317
|
+
"sideOffset" in val ||
|
|
318
|
+
"alignOffset" in val ||
|
|
319
|
+
"avoidCollisions" in val ||
|
|
320
|
+
"collisionPadding" in val ||
|
|
321
|
+
"arrowPadding" in val
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getResolvedPlacement() {
|
|
326
|
+
if (props.placement) {
|
|
327
|
+
return props.placement;
|
|
328
|
+
}
|
|
329
|
+
if (
|
|
330
|
+
props.content &&
|
|
331
|
+
typeof props.content === "object" &&
|
|
332
|
+
isOptionsConfig(props.content)
|
|
333
|
+
) {
|
|
334
|
+
const side = props.content.side || "bottom";
|
|
335
|
+
const align = props.content.align || "center";
|
|
336
|
+
return align === "center" ? side : `${side}-${align}`;
|
|
337
|
+
}
|
|
338
|
+
return "bottom";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function getResolvedOffset() {
|
|
342
|
+
if (props.offsetDistance !== undefined && props.offsetDistance !== null) {
|
|
343
|
+
return props.offsetDistance;
|
|
344
|
+
}
|
|
345
|
+
if (
|
|
346
|
+
props.content &&
|
|
347
|
+
typeof props.content === "object" &&
|
|
348
|
+
isOptionsConfig(props.content)
|
|
349
|
+
) {
|
|
350
|
+
if (props.content.sideOffset !== undefined) {
|
|
351
|
+
return props.content.sideOffset;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return 8;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function updatePosition() {
|
|
358
|
+
if (!triggerRef.current || !popoverRef.current) return;
|
|
359
|
+
const placement = getResolvedPlacement();
|
|
360
|
+
const offsetVal = getResolvedOffset();
|
|
361
|
+
const avoidCollisions =
|
|
362
|
+
props.content &&
|
|
363
|
+
typeof props.content === "object" &&
|
|
364
|
+
isOptionsConfig(props.content)
|
|
365
|
+
? props.content.avoidCollisions !== false
|
|
366
|
+
: true;
|
|
367
|
+
const collisionPadding =
|
|
368
|
+
props.content &&
|
|
369
|
+
typeof props.content === "object" &&
|
|
370
|
+
isOptionsConfig(props.content)
|
|
371
|
+
? props.content.collisionPadding ?? 8
|
|
372
|
+
: 8;
|
|
373
|
+
const arrowPadding =
|
|
374
|
+
props.content &&
|
|
375
|
+
typeof props.content === "object" &&
|
|
376
|
+
isOptionsConfig(props.content)
|
|
377
|
+
? props.content.arrowPadding ?? 8
|
|
378
|
+
: 8;
|
|
379
|
+
const middlewares: any[] = [
|
|
380
|
+
offset(offsetVal),
|
|
381
|
+
...(avoidCollisions
|
|
382
|
+
? [
|
|
383
|
+
flip({
|
|
384
|
+
padding: collisionPadding,
|
|
385
|
+
fallbackPlacements: [
|
|
386
|
+
"top",
|
|
387
|
+
"bottom",
|
|
388
|
+
"left",
|
|
389
|
+
"right",
|
|
390
|
+
"top-start",
|
|
391
|
+
"top-end",
|
|
392
|
+
"bottom-start",
|
|
393
|
+
"bottom-end",
|
|
394
|
+
],
|
|
395
|
+
}),
|
|
396
|
+
shift({
|
|
397
|
+
padding: collisionPadding,
|
|
398
|
+
}),
|
|
399
|
+
]
|
|
400
|
+
: []),
|
|
401
|
+
];
|
|
402
|
+
if (props.arrow && arrowRef.current) {
|
|
403
|
+
middlewares.push(
|
|
404
|
+
floatingArrow({
|
|
405
|
+
element: arrowRef.current,
|
|
406
|
+
padding: arrowPadding,
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
computePosition(triggerRef.current, popoverRef.current, {
|
|
411
|
+
placement,
|
|
412
|
+
strategy: "absolute",
|
|
413
|
+
middleware: middlewares,
|
|
414
|
+
}).then(({ x, y, placement: finalPlacement, middlewareData }: any) => {
|
|
415
|
+
if (!popoverRef.current) return;
|
|
416
|
+
popoverRef.current.style.position = "absolute";
|
|
417
|
+
popoverRef.current.style.left = `${x}px`;
|
|
418
|
+
popoverRef.current.style.top = `${y}px`;
|
|
419
|
+
const [resolvedSide, resolvedAlign] = finalPlacement.split("-");
|
|
420
|
+
popoverRef.current.setAttribute("data-side", resolvedSide);
|
|
421
|
+
popoverRef.current.setAttribute("data-align", resolvedAlign || "center");
|
|
422
|
+
popoverRef.current.setAttribute("data-placement", finalPlacement);
|
|
423
|
+
if (props.arrow && middlewareData.arrow && arrowRef.current) {
|
|
424
|
+
const { x: arrowX, y: arrowY } = middlewareData.arrow;
|
|
425
|
+
const staticSideMap: Record<string, string> = {
|
|
426
|
+
top: "bottom",
|
|
427
|
+
right: "left",
|
|
428
|
+
bottom: "top",
|
|
429
|
+
left: "right",
|
|
430
|
+
};
|
|
431
|
+
const staticSide = staticSideMap[resolvedSide] || "top";
|
|
432
|
+
Object.assign(arrowRef.current.style, {
|
|
433
|
+
left: arrowX != null ? `${arrowX}px` : "",
|
|
434
|
+
top: arrowY != null ? `${arrowY}px` : "",
|
|
435
|
+
right: "",
|
|
436
|
+
bottom: "",
|
|
437
|
+
[staticSide]: "-4px",
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function startFloating() {
|
|
444
|
+
stopFloating();
|
|
445
|
+
if (triggerRef.current && popoverRef.current) {
|
|
446
|
+
cleanupAutoUpdateRef.current = autoUpdate(
|
|
447
|
+
triggerRef.current,
|
|
448
|
+
popoverRef.current,
|
|
449
|
+
() => {
|
|
450
|
+
updatePosition();
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function stopFloating() {
|
|
457
|
+
if (typeof cleanupAutoUpdateRef.current === "function") {
|
|
458
|
+
cleanupAutoUpdateRef.current();
|
|
459
|
+
cleanupAutoUpdateRef.current = null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function openPopover() {
|
|
464
|
+
if (props.disabled) return;
|
|
465
|
+
if (!isOpen) {
|
|
466
|
+
setIsOpen(true);
|
|
467
|
+
if (props.onOpen) props.onOpen();
|
|
468
|
+
if (props.onUpdateOpen) props.onUpdateOpen(true);
|
|
469
|
+
if (props.onOpenChange) props.onOpenChange(true);
|
|
470
|
+
}
|
|
471
|
+
setTimeout(() => {
|
|
472
|
+
startFloating();
|
|
473
|
+
updatePosition();
|
|
474
|
+
}, 0);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function closePopover(force?: boolean) {
|
|
478
|
+
if (props.dismissible === false && !force) {
|
|
479
|
+
if (props.onClosePrevent) {
|
|
480
|
+
props.onClosePrevent();
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
stopFloating();
|
|
485
|
+
if (isOpen) {
|
|
486
|
+
setIsOpen(false);
|
|
487
|
+
if (props.onClose) props.onClose();
|
|
488
|
+
if (props.onUpdateOpen) props.onUpdateOpen(false);
|
|
489
|
+
if (props.onOpenChange) props.onOpenChange(false);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function togglePopover() {
|
|
494
|
+
if (props.disabled) return;
|
|
495
|
+
if (isOpen) {
|
|
496
|
+
closePopover();
|
|
497
|
+
} else {
|
|
498
|
+
openPopover();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function handleTriggerClick() {
|
|
503
|
+
if (props.disabled) return;
|
|
504
|
+
togglePopover();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function handlePointerDown(event: any) {
|
|
508
|
+
if (!isOpen) return;
|
|
509
|
+
const target = event.target;
|
|
510
|
+
if (
|
|
511
|
+
rootRef.current &&
|
|
512
|
+
!rootRef.current.contains(target) &&
|
|
513
|
+
popoverRef.current &&
|
|
514
|
+
!popoverRef.current.contains(target)
|
|
515
|
+
) {
|
|
516
|
+
closePopover();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function handleKeyDown(event: any) {
|
|
521
|
+
if (!isOpen) return;
|
|
522
|
+
if (event.key === "Escape") {
|
|
523
|
+
closePopover();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function handleOverlayClick() {
|
|
528
|
+
closePopover();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function renderPanelContent() {
|
|
532
|
+
if (props.panel) {
|
|
533
|
+
return typeof props.panel === "function"
|
|
534
|
+
? props.panel({
|
|
535
|
+
close: () => closePopover(true),
|
|
536
|
+
})
|
|
537
|
+
: props.panel;
|
|
538
|
+
}
|
|
539
|
+
if (props.panelContent) {
|
|
540
|
+
return typeof props.panelContent === "function"
|
|
541
|
+
? props.panelContent({
|
|
542
|
+
close: () => closePopover(true),
|
|
543
|
+
})
|
|
544
|
+
: props.panelContent;
|
|
545
|
+
}
|
|
546
|
+
if (props.content && !isOptionsConfig(props.content)) {
|
|
547
|
+
return typeof props.content === "function"
|
|
548
|
+
? props.content({
|
|
549
|
+
close: () => closePopover(true),
|
|
550
|
+
})
|
|
551
|
+
: props.content;
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
useEffect(() => {
|
|
557
|
+
if (props.open !== undefined) {
|
|
558
|
+
setIsOpen(props.open);
|
|
559
|
+
}
|
|
560
|
+
}, [props.open]);
|
|
561
|
+
|
|
562
|
+
useEffect(() => {
|
|
563
|
+
const shouldLockScroll = Boolean(
|
|
564
|
+
props.preventScroll ||
|
|
565
|
+
props.lockScroll ||
|
|
566
|
+
(props.modal && props.preventScroll !== false && props.lockScroll !== false)
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
if (isOpen) {
|
|
570
|
+
if (shouldLockScroll) {
|
|
571
|
+
lockBodyScroll();
|
|
572
|
+
}
|
|
573
|
+
startFloating();
|
|
574
|
+
updatePosition();
|
|
575
|
+
|
|
576
|
+
const handlePointerDown = (event: any) => {
|
|
577
|
+
const target = event.target;
|
|
578
|
+
if (
|
|
579
|
+
rootRef.current &&
|
|
580
|
+
!rootRef.current.contains(target) &&
|
|
581
|
+
popoverRef.current &&
|
|
582
|
+
!popoverRef.current.contains(target)
|
|
583
|
+
) {
|
|
584
|
+
closePopover();
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const handleKeyDown = (event: any) => {
|
|
589
|
+
if (event.key === "Escape") {
|
|
590
|
+
closePopover();
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
document.addEventListener("pointerdown", handlePointerDown, true);
|
|
595
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
596
|
+
|
|
597
|
+
return () => {
|
|
598
|
+
if (shouldLockScroll) {
|
|
599
|
+
unlockBodyScroll();
|
|
600
|
+
}
|
|
601
|
+
stopFloating();
|
|
602
|
+
document.removeEventListener("pointerdown", handlePointerDown, true);
|
|
603
|
+
document.removeEventListener("keydown", handleKeyDown, true);
|
|
604
|
+
};
|
|
605
|
+
} else {
|
|
606
|
+
stopFloating();
|
|
607
|
+
}
|
|
608
|
+
}, [isOpen, props.dismissible, props.modal, props.preventScroll, props.lockScroll]);
|
|
609
|
+
|
|
610
|
+
useEffect(() => {
|
|
611
|
+
return () => {
|
|
612
|
+
stopFloating();
|
|
613
|
+
if (typeof document !== "undefined") {
|
|
614
|
+
document.removeEventListener(
|
|
615
|
+
"pointerdown",
|
|
616
|
+
(e: any) => handlePointerDown(e),
|
|
617
|
+
true
|
|
618
|
+
);
|
|
619
|
+
document.removeEventListener(
|
|
620
|
+
"keydown",
|
|
621
|
+
(e: any) => handleKeyDown(e),
|
|
622
|
+
true
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
}, []);
|
|
627
|
+
|
|
628
|
+
return (
|
|
629
|
+
<div
|
|
630
|
+
ref={rootRef}
|
|
631
|
+
className={`ttdk-popover-wrapper ${props.className || ""}`.trim()}
|
|
632
|
+
data-open={isOpen ? "true" : "false"}
|
|
633
|
+
data-disabled={props.disabled ? "true" : "false"}
|
|
634
|
+
data-block={props.block ? "true" : undefined}
|
|
635
|
+
style={{
|
|
636
|
+
...buildUiVars(props.ui),
|
|
637
|
+
...((props as any)["style"] || {}),
|
|
638
|
+
...props.style,
|
|
639
|
+
}}
|
|
640
|
+
>
|
|
641
|
+
<div
|
|
642
|
+
role="button"
|
|
643
|
+
aria-haspopup="dialog"
|
|
644
|
+
ref={triggerRef}
|
|
645
|
+
className={`ttdk-popover-trigger ${props.triggerClass || ""}`.trim()}
|
|
646
|
+
tabIndex={props.disabled ? -1 : 0}
|
|
647
|
+
aria-expanded={isOpen}
|
|
648
|
+
aria-disabled={props.disabled}
|
|
649
|
+
data-open={isOpen ? "true" : "false"}
|
|
650
|
+
data-disabled={props.disabled ? "true" : "false"}
|
|
651
|
+
style={{
|
|
652
|
+
...buildUiVars(props.triggerStyle),
|
|
653
|
+
}}
|
|
654
|
+
onClick={(event) => handleTriggerClick()}
|
|
655
|
+
onKeyDown={(e) => {
|
|
656
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
657
|
+
e.preventDefault();
|
|
658
|
+
handleTriggerClick();
|
|
659
|
+
}
|
|
660
|
+
}}
|
|
661
|
+
>
|
|
662
|
+
{props.children ? <>{props.children}</> : null}
|
|
663
|
+
</div>
|
|
664
|
+
{isOpen && props.modal ? (
|
|
665
|
+
<div
|
|
666
|
+
className={`ttdk-popover-overlay ${props.overlayClass || ""}`.trim()}
|
|
667
|
+
style={{
|
|
668
|
+
...buildUiVars(props.overlayStyle),
|
|
669
|
+
}}
|
|
670
|
+
onClick={(event) => handleOverlayClick()}
|
|
671
|
+
/>
|
|
672
|
+
) : null}
|
|
673
|
+
{isOpen ? (
|
|
674
|
+
<div
|
|
675
|
+
role="dialog"
|
|
676
|
+
ref={popoverRef}
|
|
677
|
+
className={`ttdk-popover-content ${props.contentClass || ""}`.trim()}
|
|
678
|
+
aria-modal={props.modal ? "true" : "false"}
|
|
679
|
+
tabIndex={-1}
|
|
680
|
+
style={{
|
|
681
|
+
...buildUiVars(props.contentStyle),
|
|
682
|
+
}}
|
|
683
|
+
>
|
|
684
|
+
{props.arrow ? (
|
|
685
|
+
<div
|
|
686
|
+
ref={arrowRef}
|
|
687
|
+
className={`ttdk-popover-arrow ${props.arrowClass || ""}`.trim()}
|
|
688
|
+
style={{
|
|
689
|
+
...buildUiVars(props.arrowStyle),
|
|
690
|
+
}}
|
|
691
|
+
/>
|
|
692
|
+
) : null}
|
|
693
|
+
{renderPanelContent()}
|
|
694
|
+
</div>
|
|
695
|
+
) : null}
|
|
696
|
+
</div>
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export default Popover;
|
|
701
|
+
|
|
702
|
+
export { Popover };
|