@suphark/ui 0.33.0 → 0.34.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/CHANGELOG.md +8 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/components/sortable/sortable-list.tsx +50 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
|
|
4
4
|
|
|
5
|
+
## [0.34.0] — 2026-10-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`SortableProvider` ทำ hydration mismatch ทุกหน้าที่ SSR** (รายงานจาก Pi Config ตอนลากบนของจริง): ไม่ได้ส่ง `id` ให้ `DndContext` → dnd-kit สร้าง `aria-describedby="DndDescribedBy-N"` จากตัวนับที่เดินไม่ตรงกันระหว่าง server กับ client, React 19 ขึ้น error และ **ไม่ซ่อมให้** — attribute ค้างชี้ element ที่ไม่มีอยู่ ผู้ใช้ screen reader จึงไม่ได้ยินคำอธิบายวิธีลาก · ตอนนี้ค่าเริ่มต้นเป็น `useId()` (ตรงกันทั้งสองฝั่ง) และรับ prop **`id`** สำหรับ id ที่สื่อความหมาย — กระทบทุกแอปที่ใช้ `@suphark/ui/sortable` ใน Next.js
|
|
10
|
+
- `@suphark/ui/sortable`: เสียงประกาศระหว่างลากในกลุ่มเดียวกันบอกตำแหน่ง **เดิม** ซ้ำ ("ย้ายไปตำแหน่งที่ 1 จาก 2" ทั้งที่เลื่อนลงแล้ว) เพราะลำดับจริงเปลี่ยนตอนปล่อย — ตอนนี้ระหว่างลากประกาศตำแหน่งของเป้าหมาย และตอนปล่อยประกาศตำแหน่งจริงหลังย้าย (เจอระหว่างทดสอบลากด้วยคีย์บอร์ดตามที่ Pi Config ถาม; ลากด้วยคีย์บอร์ดเองทำงานถูกอยู่แล้ว)
|
|
11
|
+
- showcase: `/components/sortable-list` ย้ายแถว "ลากมาที่นี่เพื่อเอาออกจากกลุ่ม" ไปท้ายตาราง — เดิมโผล่เหนือกลุ่มตอนเริ่มลาก ดันแถวที่กำลังลากลง จนลากคีย์ในกลุ่มด้วยคีย์บอร์ดไม่ขยับ (README เพิ่มคำเตือนนี้ + เรื่อง `id`)
|
|
12
|
+
|
|
5
13
|
## [0.33.0] — 2026-10-06
|
|
6
14
|
|
|
7
15
|
### Fixed
|
package/README.md
CHANGED
|
@@ -217,7 +217,8 @@ const [order, setOrder] = useState(() =>
|
|
|
217
217
|
</SortableProvider>
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
-
- กลุ่มที่ไม่มีหัวกลุ่ม (เช่น "ยังไม่จัดกลุ่ม") ใช้ `useSortableDropZone(groupId)` แปะกับแถว placeholder ของตัวเอง และดูว่าควรแสดงเมื่อไหร่จาก `useSortableState().dragging`
|
|
220
|
+
- กลุ่มที่ไม่มีหัวกลุ่ม (เช่น "ยังไม่จัดกลุ่ม") ใช้ `useSortableDropZone(groupId)` แปะกับแถว placeholder ของตัวเอง และดูว่าควรแสดงเมื่อไหร่จาก `useSortableState().dragging` — **วางแถวนี้ไว้ใต้แถวที่ลากได้ทั้งหมด (ท้ายตาราง)** ถ้ามันโผล่ขึ้นเหนือแถวที่กำลังลาก layout จะเลื่อนตอนเริ่มลาก แล้วการลากด้วยคีย์บอร์ดจะไม่ขยับ
|
|
221
|
+
- ตั้ง `id` ของ `SortableProvider` ได้ (ไม่ตั้ง = `useId()`) — ต้องคงที่ระหว่าง SSR กับ client ไม่งั้น `aria-describedby` ของ dnd-kit จะ hydration mismatch
|
|
221
222
|
- `onCommit` ล้มเหลว (throw หรือ `{ ok: false, error }`) = คืนลำดับเดิม + toast; ถ้าอยากจัดการ error เองใส่ `onCommitError`
|
|
222
223
|
- `sortableOrderToPayload(order)` คืน `{ groups, items }` — `groups` คือลำดับของกลุ่ม, `items` คือรายการในแต่ละกลุ่มเรียงตามนั้น (ของที่ยังไม่มีกลุ่มมาก่อนด้วย `groupId: null`)
|
|
223
224
|
- `SortableProvider disabled` (เช่นระหว่างกรอง ซึ่งลำดับที่เห็นไม่ใช่ลำดับจริง) ทำให้ `DragHandle` จางลงและกดไม่ได้ — ไม่อยากให้เห็นเลยใส่ `hideWhenDisabled`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suphark/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Design system กลางของ Suphark apps: shadcn/ui (base-nova, Base UI, Tailwind v4) + shared components + token/ฟอนต์ Noto Sans Thai Looped — ส่งเป็น source ให้แอป Next transpile เอง",
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
type ReactNode,
|
|
42
42
|
useCallback,
|
|
43
43
|
useContext,
|
|
44
|
+
useId,
|
|
44
45
|
useMemo,
|
|
45
46
|
useRef,
|
|
46
47
|
useState,
|
|
@@ -131,6 +132,13 @@ export interface SortableProviderProps {
|
|
|
131
132
|
* ไม่ส่ง = แม่แบบจาก `UiText.sortable` (`{index}` / `{total}`)
|
|
132
133
|
*/
|
|
133
134
|
announcements?: SortableAnnouncements;
|
|
135
|
+
/**
|
|
136
|
+
* id ของ `DndContext` — ต้องคงที่ระหว่าง SSR กับ client ไม่งั้น `aria-describedby="DndDescribedBy-N"`
|
|
137
|
+
* ที่ dnd-kit สร้างจากตัวนับจะไม่ตรงกัน (hydration mismatch ที่ React ไม่ซ่อมให้ → screen reader
|
|
138
|
+
* ไม่ได้ยินคำอธิบายวิธีลาก) · ไม่ส่ง = `useId()` ซึ่งตรงกันทั้งสองฝั่งอยู่แล้ว · ส่ง id ที่สื่อความหมาย
|
|
139
|
+
* (เช่น `values-${environmentId}`) ช่วยตอนไล่ปัญหา
|
|
140
|
+
*/
|
|
141
|
+
id?: string;
|
|
134
142
|
children: ReactNode;
|
|
135
143
|
}
|
|
136
144
|
|
|
@@ -152,8 +160,10 @@ export function SortableProvider({
|
|
|
152
160
|
onCommitError,
|
|
153
161
|
screenReaderInstructions,
|
|
154
162
|
announcements,
|
|
163
|
+
id,
|
|
155
164
|
children,
|
|
156
165
|
}: SortableProviderProps) {
|
|
166
|
+
const fallbackId = useId();
|
|
157
167
|
const text = useUiText();
|
|
158
168
|
const [dragging, setDragging] = useState<SortableKind | null>(null);
|
|
159
169
|
const beforeDrag = useRef<SortableOrder | null>(null);
|
|
@@ -271,6 +281,31 @@ export function SortableProvider({
|
|
|
271
281
|
};
|
|
272
282
|
}, []);
|
|
273
283
|
|
|
284
|
+
/**
|
|
285
|
+
* ตำแหน่งที่ "จะไปวาง" ระหว่างลาก — ในกลุ่มเดียวกันลำดับจริงยังไม่เปลี่ยนจนกว่าจะปล่อย
|
|
286
|
+
* (เดิมประกาศตำแหน่งเดิมซ้ำ เช่น "ตำแหน่งที่ 1" ทั้งที่เลื่อนลงมาแล้ว) จึงใช้ตำแหน่งของเป้าหมายแทน
|
|
287
|
+
* ข้ามกลุ่มไม่ต้องทำอะไร — onDragOver ย้ายเข้ากลุ่มใหม่ใน order ไปแล้ว
|
|
288
|
+
*/
|
|
289
|
+
const describeTarget = useCallback(
|
|
290
|
+
(activeId: UniqueIdentifier, overId: UniqueIdentifier | null): SortableAnnouncementContext => {
|
|
291
|
+
const base = describe(activeId);
|
|
292
|
+
if (overId === null || overId === activeId) return base;
|
|
293
|
+
const current = latest.current;
|
|
294
|
+
if (base.kind === "group" && kindOf(overId) === "g") {
|
|
295
|
+
const at = current.groups.indexOf(rawOf(overId));
|
|
296
|
+
return at >= 0 ? { ...base, index: at + 1 } : base;
|
|
297
|
+
}
|
|
298
|
+
if (base.kind === "item" && kindOf(overId) === "i") {
|
|
299
|
+
const group = groupOfItem(current, rawOf(overId));
|
|
300
|
+
if (!group || group !== base.groupId) return base;
|
|
301
|
+
const at = (current.items[group] ?? []).indexOf(rawOf(overId));
|
|
302
|
+
return at >= 0 ? { ...base, index: at + 1 } : base;
|
|
303
|
+
}
|
|
304
|
+
return base;
|
|
305
|
+
},
|
|
306
|
+
[describe],
|
|
307
|
+
);
|
|
308
|
+
|
|
274
309
|
const dndAnnouncements = useMemo(() => {
|
|
275
310
|
const fill = (
|
|
276
311
|
template: string | undefined,
|
|
@@ -285,9 +320,19 @@ export function SortableProvider({
|
|
|
285
320
|
own: ((context: SortableAnnouncementContext) => string | undefined) | undefined,
|
|
286
321
|
template: string | undefined,
|
|
287
322
|
fallback: string,
|
|
323
|
+
// ระหว่างลาก = ตำแหน่งของเป้าหมาย; ตอนปล่อย order อัปเดตแล้ว จึงใช้ตำแหน่งจริงของตัวที่ลาก
|
|
324
|
+
useTarget = false,
|
|
288
325
|
) =>
|
|
289
|
-
({
|
|
290
|
-
|
|
326
|
+
({
|
|
327
|
+
active,
|
|
328
|
+
over,
|
|
329
|
+
}: {
|
|
330
|
+
active: { id: UniqueIdentifier };
|
|
331
|
+
over?: { id: UniqueIdentifier } | null;
|
|
332
|
+
}) => {
|
|
333
|
+
const context = useTarget
|
|
334
|
+
? describeTarget(active.id, over?.id ?? null)
|
|
335
|
+
: describe(active.id);
|
|
291
336
|
return own?.(context) ?? fill(template, fallback, context);
|
|
292
337
|
};
|
|
293
338
|
return {
|
|
@@ -300,6 +345,7 @@ export function SortableProvider({
|
|
|
300
345
|
announcements?.onDragOver,
|
|
301
346
|
text.sortable?.moved,
|
|
302
347
|
"ย้ายไปตำแหน่งที่ {index} จาก {total}",
|
|
348
|
+
true,
|
|
303
349
|
),
|
|
304
350
|
onDragEnd: announce(
|
|
305
351
|
announcements?.onDragEnd,
|
|
@@ -308,7 +354,7 @@ export function SortableProvider({
|
|
|
308
354
|
),
|
|
309
355
|
onDragCancel: announce(announcements?.onDragCancel, text.sortable?.cancelled, "ยกเลิกการลาก"),
|
|
310
356
|
};
|
|
311
|
-
}, [announcements,
|
|
357
|
+
}, [announcements, describeTarget, text.sortable]);
|
|
312
358
|
|
|
313
359
|
const groupIds = useMemo(() => order.groups.map((id) => dndId("group", id)), [order.groups]);
|
|
314
360
|
|
|
@@ -324,6 +370,7 @@ export function SortableProvider({
|
|
|
324
370
|
return (
|
|
325
371
|
<SortableStateContext.Provider value={state}>
|
|
326
372
|
<DndContext
|
|
373
|
+
id={id ?? fallbackId}
|
|
327
374
|
sensors={sensors}
|
|
328
375
|
collisionDetection={collision}
|
|
329
376
|
accessibility={{
|