@goplusvn/core 0.1.96 → 0.1.98

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.98 — Chuẩn hóa Modal FocusScope cho Combobox/MultiSelect khi nằm trong Dialog/Sheet/Modal
2
+
3
+ Khi Combobox hoặc MultiSelect nằm trong Modal/Dialog/Sheet (như Modal thanh toán POS hay Sheet bộ lọc nâng cao),
4
+ cơ chế FocusScope Stack của Radix yêu cầu `modal={true}` kết hợp với auto-focus vào `searchInputRef`
5
+ và loại bỏ hoàn toàn các rào cản `stopPropagation` trên `onFocus`/`onClick` của Input.
6
+
7
+ **Đổi**
8
+
9
+ - `ui/primitives/combobox.tsx` & `ui/forms/multi-select.tsx` — kích hoạt `modal={true}` mặc định,
10
+ tự động focus vào `searchInputRef` khi mở dropdown, bổ sung `onPointerDown` stopPropagation trên Input
11
+ để người dùng click/chạm vào ô tìm kiếm lập tức nhận con trỏ nhập liệu và bật bàn phím.
12
+
13
+ ## 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
14
+
15
+ Khi `modal={true}` được bật mặc định, Radix Popover kích hoạt FocusScope + ScrollLock
16
+ khiến thiết bị cảm ứng (Mobile/Tablet) và các trình duyệt bị xung đột:
17
+ 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.
18
+
19
+ **Đổi**
20
+
21
+ - `ui/primitives/combobox.tsx` & `ui/forms/multi-select.tsx` — chuyển `modal` về mặc định `false`,
22
+ loại bỏ `e.stopPropagation()` trên `onFocus`/`onClick` của `Input`, thêm `onPointerDown={(e) => e.stopPropagation()}`
23
+ để 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ị.
24
+ - `ui/forms/multi-select.tsx` — sửa hàm `findMatchRange` xử lý chính xác ký tự khoảng trắng khi highlight.
25
+
1
26
  ## 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
2
27
 
3
28
  Khi `Combobox` hoặc `MultiSelect` nằm trong `<Sheet>` (bộ lọc mobile) hoặc `<Dialog>`,
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.96",
4
+ "version": "0.1.98",
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);
@@ -726,10 +734,6 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
726
734
  e.preventDefault();
727
735
  searchInputRef.current?.focus();
728
736
  }}
729
- onFocusOutside={(e) => {
730
- // Ngăn Dialog / Sheet cha giật focus khỏi ô tìm kiếm của Popover portal
731
- e.preventDefault();
732
- }}
733
737
  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"
734
738
  style={{
735
739
  maxHeight:
@@ -749,9 +753,11 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
749
753
  placeholder={searchPlaceholder}
750
754
  value={searchValue}
751
755
  onChange={(e) => {
752
- e.stopPropagation();
753
756
  setSearchValue(e.target.value);
754
757
  }}
758
+ onPointerDown={(e) => {
759
+ e.stopPropagation();
760
+ }}
755
761
  onKeyDown={(e) => {
756
762
  // Prevent Dialog from intercepting keyboard events
757
763
  e.stopPropagation();
@@ -778,12 +784,6 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
778
784
  updateValue(value.slice(0, -1));
779
785
  }
780
786
  }}
781
- onClick={(e) => {
782
- e.stopPropagation();
783
- }}
784
- onFocus={(e) => {
785
- e.stopPropagation();
786
- }}
787
787
  className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-7 bg-transparent px-0 text-sm shadow-none"
788
788
  />
789
789
  {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 và hỗ trợ chạm trên mobile.
28
+ * Chế độ modal cho Popover (mặc định true). Bắt buộc true để kích hoạt
29
+ * FocusScope Stack của Radix khi nằm trong Dialog/Sheet/Modal.
30
30
  */
31
31
  modal?: boolean;
32
32
  /**
@@ -180,10 +180,6 @@ export function Combobox({
180
180
  e.preventDefault();
181
181
  searchInputRef.current?.focus();
182
182
  }}
183
- onFocusOutside={(e) => {
184
- // Ngăn Dialog / Sheet cha giật focus khỏi ô tìm kiếm của Popover portal
185
- e.preventDefault();
186
- }}
187
183
  className="z-[100] w-[var(--radix-popover-trigger-width)] min-w-[12rem] max-w-[calc(100vw-1rem)] p-0 flex flex-col overflow-hidden"
188
184
  style={{
189
185
  maxHeight: `min(${maxHeight}, var(--radix-popover-content-available-height, ${maxHeight}))`,
@@ -197,9 +193,11 @@ export function Combobox({
197
193
  placeholder={searchPlaceholder}
198
194
  value={searchValue}
199
195
  onChange={(e) => {
200
- e.stopPropagation();
201
196
  setSearchValue(e.target.value);
202
197
  }}
198
+ onPointerDown={(e) => {
199
+ e.stopPropagation();
200
+ }}
203
201
  onKeyDown={(e) => {
204
202
  // Prevent Dialog from intercepting keyboard events
205
203
  e.stopPropagation();
@@ -216,10 +214,7 @@ export function Combobox({
216
214
  }
217
215
  }
218
216
  }}
219
- onClick={(e) => {
220
- e.stopPropagation();
221
- }}
222
- className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-9 bg-transparent px-0"
217
+ className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-9 bg-transparent px-0 text-sm"
223
218
  />
224
219
  {searchValue && (
225
220
  <Button
@@ -22,7 +22,7 @@
22
22
  "prepare": "husky || true"
23
23
  },
24
24
  "dependencies": {
25
- "@goerp/core": "npm:@goplusvn/core@^0.1.96",
25
+ "@goerp/core": "npm:@goplusvn/core@^0.1.98",
26
26
  "@hookform/resolvers": "3.9.0",
27
27
  "@prisma/adapter-pg": "^7.0.0",
28
28
  "@prisma/client": "^7.0.0",