@goplusvn/core 0.1.95 → 0.1.97
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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
## 0.1.97 — Sửa triệt để tương tác chạm và nhập liệu cho ô tìm kiếm Combobox/MultiSelect
|
|
2
|
+
|
|
3
|
+
Khi `modal={true}` được bật mặc định, Radix Popover kích hoạt FocusScope + ScrollLock
|
|
4
|
+
khiến thiết bị cảm ứng (Mobile/Tablet) và các trình duyệt bị xung đột:
|
|
5
|
+
chạm vào ô tìm kiếm không mở được bàn phím ảo hoặc bị đóng dropdown / mất focus.
|
|
6
|
+
|
|
7
|
+
**Đổi**
|
|
8
|
+
|
|
9
|
+
- `ui/primitives/combobox.tsx` & `ui/forms/multi-select.tsx` — chuyển `modal` về mặc định `false`,
|
|
10
|
+
loại bỏ `e.stopPropagation()` trên `onFocus`/`onClick` của `Input`, thêm `onPointerDown={(e) => e.stopPropagation()}`
|
|
11
|
+
để input nhận focus tự nhiên và kích hoạt bàn phím ảo mượt mà trên mọi thiết bị.
|
|
12
|
+
- `ui/forms/multi-select.tsx` — sửa hàm `findMatchRange` xử lý chính xác ký tự khoảng trắng khi highlight.
|
|
13
|
+
|
|
14
|
+
## 0.1.96 — Sửa lỗi Focus Trap khiến ô tìm kiếm Combobox/MultiSelect mất focus khi lồng trong Sheet/Dialog
|
|
15
|
+
|
|
16
|
+
Khi `Combobox` hoặc `MultiSelect` nằm trong `<Sheet>` (bộ lọc mobile) hoặc `<Dialog>`,
|
|
17
|
+
Radix Dialog's `FocusScope` bắt sự kiện tương tác trên Portal và giật focus ngược lại về trigger,
|
|
18
|
+
khiến ô input tìm kiếm bị blur ngay lập tức và không thể gõ được trên một số thiết bị/trình duyệt.
|
|
19
|
+
|
|
20
|
+
**Đổi**
|
|
21
|
+
|
|
22
|
+
- `ui/primitives/combobox.tsx` & `ui/forms/multi-select.tsx` — thêm `onFocusOutside={(e) => e.preventDefault()}`
|
|
23
|
+
trên `PopoverContent`, ngăn Dialog/Sheet cha cướp focus khỏi ô tìm kiếm; kích hoạt focus trực tiếp vào
|
|
24
|
+
`searchInputRef` khi mở panel.
|
|
25
|
+
|
|
1
26
|
## 0.1.95 — Triệt tiêu Hydration Mismatch trên AvatarFallback của UserDropdown
|
|
2
27
|
|
|
3
28
|
Trong quá trình Server-Side Rendering (SSR), `useSafeAuthSession()` chưa có phiên đăng nhập
|
package/package.json
CHANGED
|
@@ -148,6 +148,14 @@ const normalizeVietnamese = (str: string) =>
|
|
|
148
148
|
.toLowerCase()
|
|
149
149
|
: "";
|
|
150
150
|
|
|
151
|
+
const normalizeVietnameseChar = (char: string) => {
|
|
152
|
+
return char
|
|
153
|
+
.normalize("NFD")
|
|
154
|
+
.replace(/[\u0300-\u036f\u031b\u0323]/g, "")
|
|
155
|
+
.replace(/[đĐ]/g, (m) => (m === "đ" ? "d" : "D"))
|
|
156
|
+
.toLowerCase();
|
|
157
|
+
};
|
|
158
|
+
|
|
151
159
|
/**
|
|
152
160
|
* Tìm vị trí khớp (không dấu, không phân biệt hoa thường, hỗ trợ đ/Đ) của query trong
|
|
153
161
|
* label GỐC — trả về [start, end) trên chuỗi gốc để highlight.
|
|
@@ -158,7 +166,7 @@ function findMatchRange(label: string, query: string): [number, number] | null {
|
|
|
158
166
|
let stripped = "";
|
|
159
167
|
const indexMap: number[] = [];
|
|
160
168
|
for (let i = 0; i < label.length; i++) {
|
|
161
|
-
const s =
|
|
169
|
+
const s = normalizeVietnameseChar(label[i]);
|
|
162
170
|
for (let j = 0; j < s.length; j++) {
|
|
163
171
|
stripped += s[j];
|
|
164
172
|
indexMap.push(i);
|
|
@@ -204,7 +212,7 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
204
212
|
loadingText = "Đang tải...",
|
|
205
213
|
className,
|
|
206
214
|
id,
|
|
207
|
-
modal =
|
|
215
|
+
modal = false,
|
|
208
216
|
},
|
|
209
217
|
ref,
|
|
210
218
|
) => {
|
|
@@ -724,16 +732,6 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
724
732
|
collisionPadding={8}
|
|
725
733
|
onOpenAutoFocus={(e) => {
|
|
726
734
|
e.preventDefault();
|
|
727
|
-
// Trên thiết bị cảm ứng / màn hình nhỏ, không ép mở bàn phím ảo để tránh che khuất danh sách
|
|
728
|
-
const isTouchOrMobile =
|
|
729
|
-
typeof window !== "undefined" &&
|
|
730
|
-
((typeof window.matchMedia === "function" &&
|
|
731
|
-
window.matchMedia("(pointer: coarse)").matches) ||
|
|
732
|
-
(typeof window.innerWidth === "number" &&
|
|
733
|
-
window.innerWidth < 640));
|
|
734
|
-
if (!isTouchOrMobile) {
|
|
735
|
-
searchInputRef.current?.focus();
|
|
736
|
-
}
|
|
737
735
|
}}
|
|
738
736
|
className="z-[100] flex w-[var(--radix-popover-trigger-width)] min-w-[15rem] max-w-[calc(100vw-1rem)] flex-col overflow-hidden rounded-md border bg-popover/95 p-0 text-popover-foreground shadow-lg shadow-black/5 backdrop-blur-md"
|
|
739
737
|
style={{
|
|
@@ -754,9 +752,11 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
754
752
|
placeholder={searchPlaceholder}
|
|
755
753
|
value={searchValue}
|
|
756
754
|
onChange={(e) => {
|
|
757
|
-
e.stopPropagation();
|
|
758
755
|
setSearchValue(e.target.value);
|
|
759
756
|
}}
|
|
757
|
+
onPointerDown={(e) => {
|
|
758
|
+
e.stopPropagation();
|
|
759
|
+
}}
|
|
760
760
|
onKeyDown={(e) => {
|
|
761
761
|
// Prevent Dialog from intercepting keyboard events
|
|
762
762
|
e.stopPropagation();
|
|
@@ -783,12 +783,6 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
783
783
|
updateValue(value.slice(0, -1));
|
|
784
784
|
}
|
|
785
785
|
}}
|
|
786
|
-
onClick={(e) => {
|
|
787
|
-
e.stopPropagation();
|
|
788
|
-
}}
|
|
789
|
-
onFocus={(e) => {
|
|
790
|
-
e.stopPropagation();
|
|
791
|
-
}}
|
|
792
786
|
className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-7 bg-transparent px-0 text-sm shadow-none"
|
|
793
787
|
/>
|
|
794
788
|
{searchValue && (
|
|
@@ -25,8 +25,8 @@ interface ComboboxProps {
|
|
|
25
25
|
className?: string;
|
|
26
26
|
id?: string;
|
|
27
27
|
/**
|
|
28
|
-
* Chế độ modal cho Popover (mặc định
|
|
29
|
-
*
|
|
28
|
+
* Chế độ modal cho Popover (mặc định false). Mặc định false giúp ô tìm kiếm
|
|
29
|
+
* nhận focus và bật bàn phím ảo mượt mà trên mobile/sheet mà không bị kẹt focus trap.
|
|
30
30
|
*/
|
|
31
31
|
modal?: boolean;
|
|
32
32
|
/**
|
|
@@ -56,7 +56,7 @@ export function Combobox({
|
|
|
56
56
|
disabled = false,
|
|
57
57
|
className,
|
|
58
58
|
id,
|
|
59
|
-
modal =
|
|
59
|
+
modal = false,
|
|
60
60
|
maxHeight = "20rem",
|
|
61
61
|
}: ComboboxProps) {
|
|
62
62
|
const [open, setOpen] = useState(false);
|
|
@@ -178,15 +178,6 @@ export function Combobox({
|
|
|
178
178
|
collisionPadding={8}
|
|
179
179
|
onOpenAutoFocus={(e) => {
|
|
180
180
|
e.preventDefault();
|
|
181
|
-
// Trên thiết bị cảm ứng / màn hình nhỏ, không ép mở bàn phím ảo để tránh che khuất danh sách
|
|
182
|
-
const isTouchOrMobile =
|
|
183
|
-
typeof window !== "undefined" &&
|
|
184
|
-
((typeof window.matchMedia === "function" &&
|
|
185
|
-
window.matchMedia("(pointer: coarse)").matches) ||
|
|
186
|
-
(typeof window.innerWidth === "number" && window.innerWidth < 640));
|
|
187
|
-
if (!isTouchOrMobile) {
|
|
188
|
-
searchInputRef.current?.focus();
|
|
189
|
-
}
|
|
190
181
|
}}
|
|
191
182
|
className="z-[100] w-[var(--radix-popover-trigger-width)] min-w-[12rem] max-w-[calc(100vw-1rem)] p-0 flex flex-col overflow-hidden"
|
|
192
183
|
style={{
|
|
@@ -201,9 +192,11 @@ export function Combobox({
|
|
|
201
192
|
placeholder={searchPlaceholder}
|
|
202
193
|
value={searchValue}
|
|
203
194
|
onChange={(e) => {
|
|
204
|
-
e.stopPropagation();
|
|
205
195
|
setSearchValue(e.target.value);
|
|
206
196
|
}}
|
|
197
|
+
onPointerDown={(e) => {
|
|
198
|
+
e.stopPropagation();
|
|
199
|
+
}}
|
|
207
200
|
onKeyDown={(e) => {
|
|
208
201
|
// Prevent Dialog from intercepting keyboard events
|
|
209
202
|
e.stopPropagation();
|
|
@@ -220,10 +213,7 @@ export function Combobox({
|
|
|
220
213
|
}
|
|
221
214
|
}
|
|
222
215
|
}}
|
|
223
|
-
|
|
224
|
-
e.stopPropagation();
|
|
225
|
-
}}
|
|
226
|
-
className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-9 bg-transparent px-0"
|
|
216
|
+
className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-9 bg-transparent px-0 text-sm"
|
|
227
217
|
/>
|
|
228
218
|
{searchValue && (
|
|
229
219
|
<Button
|