@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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goplusvn/core",
3
3
  "description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
4
- "version": "0.1.95",
4
+ "version": "0.1.97",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org",
@@ -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 = normalizeVietnamese(label[i]);
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 = true,
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 true). Bắt buộc bật true khi nằm trong
29
- * Dialog / Sheet để tránh FocusTrap cướp focus khỏi ô tìm kiếm hỗ trợ chạm trên mobile.
28
+ * Chế độ modal cho Popover (mặc định false). Mặc định false giúp ô tìm kiếm
29
+ * nhận focus bật bàn phím ảo mượt trên mobile/sheet 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 = true,
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
- onClick={(e) => {
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
@@ -22,7 +22,7 @@
22
22
  "prepare": "husky || true"
23
23
  },
24
24
  "dependencies": {
25
- "@goerp/core": "npm:@goplusvn/core@^0.1.95",
25
+ "@goerp/core": "npm:@goplusvn/core@^0.1.97",
26
26
  "@hookform/resolvers": "3.9.0",
27
27
  "@prisma/adapter-pg": "^7.0.0",
28
28
  "@prisma/client": "^7.0.0",