@ihabdevteam/core 0.252.0 → 0.253.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 +52 -0
- package/dist/components.cjs.js +174 -174
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +2 -2
- package/dist/i18n.cjs.js +2 -2
- package/dist/ihabdevteam-core.cjs.js +179 -179
- package/dist/ihabdevteam-core.d.ts +1 -1
- package/dist/packages/core/src/components/Table.css +27 -0
- package/dist/packages/core/src/components/Table.js +63 -33
- package/dist/packages/core/src/i18n/locales/en.json +2 -1
- package/dist/packages/core/src/i18n/locales/ko.json +2 -1
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +1 -1
- package/package.json +1 -1
|
@@ -1651,7 +1651,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1651
1651
|
*
|
|
1652
1652
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1653
1653
|
*/
|
|
1654
|
-
export declare const CORE_VERSION = "0.
|
|
1654
|
+
export declare const CORE_VERSION = "0.253.0";
|
|
1655
1655
|
|
|
1656
1656
|
/** @public */
|
|
1657
1657
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -131,6 +131,33 @@
|
|
|
131
131
|
background: var(--color-darkWhite);
|
|
132
132
|
border-top: 1px solid var(--color-whiteLine);
|
|
133
133
|
}
|
|
134
|
+
/* **상자 나무에서 빠진다**(말귀 요청 (56), 2026-10-02). 펼친 자리가 `role="row"` 가 되면서
|
|
135
|
+
안에 `role="cell"` 한 겹이 생겼다 — 그 겹이 보통 상자면 부르는 쪽이 넣은 조각들이 **한
|
|
136
|
+
덩어리 flex 아이**가 되어 `flex-wrap`·`gap` 이 통째로 죽는다. `display: contents` 면
|
|
137
|
+
그 겹은 자리를 안 차지하고 아이들이 전처럼 이 줄의 flex 아이로 선다.
|
|
138
|
+
※ 이 줄을 지우면 펼친 칸의 배치가 무너진다. 역할과 자리 잡기를 한 겹으로 함께 맡은 자리다. */
|
|
139
|
+
.table-row-expand__cell { display: contents; }
|
|
140
|
+
|
|
141
|
+
/* 기다리는 줄·빈 줄의 한 칸 — 여기도 상자 나무에서 빠진다. 줄이 이미 flex 로 가운데를
|
|
142
|
+
맞추므로, 상자를 가지면 그 겹이 flex 아이가 되어 한 겹 더 생길 뿐이다. */
|
|
143
|
+
.table-row__full-cell { display: contents; }
|
|
144
|
+
|
|
145
|
+
/* **`display: contents` 가 역할을 지우지 않는다** — 실측으로 확인했다(2026-10-02, 놀이터
|
|
146
|
+
`#table`, Chrome). 상자 나무에서 빠지는 마디라 접근성 나무에서도 빠질까 걱정했는데,
|
|
147
|
+
`role` 을 들고 있으면 그대로 선다: `row > cell > generic "Alice 님은 … 가입했습니다."`,
|
|
148
|
+
기다리는 줄은 `row > cell > status "불러오는 중…"`. 그래서 자리 잡기를 안 건드리고
|
|
149
|
+
역할만 더할 수 있었다 — 안 그랬으면 flex 설정을 안쪽 겹으로 옮겨야 했다. */
|
|
150
|
+
|
|
151
|
+
/* 줄을 누를 수 있는 표에만 서는 숨은 설명 — 눈에는 없고 `aria-describedby` 로만 읽힌다.
|
|
152
|
+
`display: none` 으로 감추면 **참조해도 안 읽힌다**(㊼ 에서 밟은 자리) — 잘라서 감춘다. */
|
|
153
|
+
.table-row-hint {
|
|
154
|
+
position: absolute;
|
|
155
|
+
width: 1px;
|
|
156
|
+
height: 1px;
|
|
157
|
+
overflow: hidden;
|
|
158
|
+
clip-path: inset(50%);
|
|
159
|
+
white-space: nowrap;
|
|
160
|
+
}
|
|
134
161
|
|
|
135
162
|
/* 높이 스케일 — Panel과 공유. header·fields·row 모두 적용. small 48 / medium 56 / large 64 */
|
|
136
163
|
.table-wrap--small .table-header,
|
|
@@ -12,17 +12,35 @@ import { useTranslation } from 'react-i18next';
|
|
|
12
12
|
import './Table.css';
|
|
13
13
|
import './TableCell.css';
|
|
14
14
|
import './TableField.css';
|
|
15
|
-
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
15
|
+
import { useState, useRef, useCallback, useEffect, useId } from 'react';
|
|
16
16
|
import ChevronUp from 'lucide-react/dist/esm/icons/chevron-up.mjs';
|
|
17
17
|
import ChevronDown from 'lucide-react/dist/esm/icons/chevron-down.mjs';
|
|
18
18
|
import Button from './Button';
|
|
19
19
|
import Icon from './Icon';
|
|
20
20
|
import Loader from './Loader';
|
|
21
21
|
import useMediaQuery from '../hooks/useMediaQuery';
|
|
22
|
+
/* 칸 하나 — `role="cell"` 이라야 낭독기가 **낱낱으로** 읽는다(말귀 요청 (56), 2026-10-02).
|
|
23
|
+
역할이 없던 때는 줄 전체가 한 덩어리 글로 읽혀 `홍길동 010-1234-5678 2026-09-30 활성`
|
|
24
|
+
이 한 호흡에 나갔다 — 어디까지가 이름이고 어디부터 전화인지 소리로는 알 수 없었다. */
|
|
22
25
|
function TableCell({ children, className, style }) {
|
|
23
|
-
return _jsx("div", { className: className ? `table-cell ${className}` : 'table-cell', style: style, children: children });
|
|
26
|
+
return _jsx("div", { role: "cell", className: className ? `table-cell ${className}` : 'table-cell', style: style, children: children });
|
|
24
27
|
}
|
|
25
|
-
|
|
28
|
+
/**
|
|
29
|
+
* 줄 하나 — **`role="row"` 다. 전에는 눌릴 수 있으면 `role="button"` 이었다**(말귀 요청 (56)).
|
|
30
|
+
*
|
|
31
|
+
* `role="button"` 은 두 가지를 망가뜨렸다.
|
|
32
|
+
* 1. 줄이 단추가 되면 안의 칸은 **그 단추의 이름 재료**일 뿐이라 낱낱으로 안 읽힌다.
|
|
33
|
+
* 2. 머리 칸의 `aria-sort` 가 통째로 버려진다 — `columnheader` 는 `row` 안에, 그 `row` 는
|
|
34
|
+
* `table` 안에 있어야 뜻이 생긴다. 줄이 단추면 그 사슬이 끊긴다(눈에는 화살표가
|
|
35
|
+
* 보이므로 오래 안 드러났다).
|
|
36
|
+
*
|
|
37
|
+
* **대신 "눌릴 수 있다" 는 말을 잃는다.** 고객 목록처럼 **줄 자체가 상세로 가는 유일한 길**
|
|
38
|
+
* 인 화면에서는 그 말이 실제로 필요하다(사장님 결정 2026-10-02: 그래도 (가) 로 간다 —
|
|
39
|
+
* 칸을 낱낱이 읽는 쪽이 먼저다). 그래서 `tabIndex`·Enter·Space 는 **그대로 두고**,
|
|
40
|
+
* `aria-describedby` 로 "줄을 누르면 자세히 본다" 를 붙여 메운다. 진짜 단추를 줄 안에
|
|
41
|
+
* 세우는 쪽은 다음 걸음이다 — 그때는 줄 이름을 알려 주는 prop 이 하나 필요하다.
|
|
42
|
+
*/
|
|
43
|
+
function TableRow({ variant = 'default', children, className, draggable, onClick, onMouseEnter, onMouseLeave, onDragStart, onDragEnd, expandedContent, colSpan, describedBy }) {
|
|
26
44
|
const handleKeyDown = useCallback((e) => {
|
|
27
45
|
if (!onClick)
|
|
28
46
|
return;
|
|
@@ -35,13 +53,18 @@ function TableRow({ variant = 'default', children, className, draggable, onClick
|
|
|
35
53
|
onClick();
|
|
36
54
|
}
|
|
37
55
|
}, [onClick]);
|
|
56
|
+
/* 기다리는 줄·빈 줄도 **줄이다** — 역할 없는 `div` 를 `rowgroup` 안에 두면 낭독기가
|
|
57
|
+
건너뛸 수 있어 "없습니다" 가 영 안 들린다. 가로를 다 차지하므로 칸 하나에 `aria-colspan`. */
|
|
38
58
|
if (variant === 'loading') {
|
|
39
|
-
return _jsx("div", { className: "table-row-wrap", children: _jsx("div", { className: "table-row table-row--loading", children: _jsx(Loader, { size: 28 }) }) });
|
|
59
|
+
return (_jsx("div", { role: "presentation", className: "table-row-wrap", children: _jsx("div", { role: "row", className: "table-row table-row--loading", children: _jsx("div", { role: "cell", "aria-colspan": colSpan, className: "table-row__full-cell", children: _jsx(Loader, { size: 28 }) }) }) }));
|
|
40
60
|
}
|
|
41
61
|
if (variant === 'empty') {
|
|
42
|
-
return _jsx("div", { className: "table-row-wrap", children: _jsx("div", { className: "table-row table-row--empty body-3", children: children }) });
|
|
62
|
+
return (_jsx("div", { role: "presentation", className: "table-row-wrap", children: _jsx("div", { role: "row", className: "table-row table-row--empty body-3", children: _jsx("div", { role: "cell", "aria-colspan": colSpan, className: "table-row__full-cell", children: children }) }) }));
|
|
43
63
|
}
|
|
44
|
-
return (
|
|
64
|
+
return (
|
|
65
|
+
/* 겉싸개는 **나무에서 지운다** — `table > row` 사슬 사이에 역할 없는 마디가 끼면
|
|
66
|
+
그 아래 `row` 가 표의 줄로 안 잡힌다. 자리 잡기(테두리·세로 쌓기)는 그대로다. */
|
|
67
|
+
_jsxs("div", { role: "presentation", className: "table-row-wrap", children: [_jsx("div", { className: className ? `table-row ${className}` : 'table-row', draggable: draggable, onClick: onClick, onKeyDown: onClick ? handleKeyDown : undefined, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, onDragStart: onDragStart, onDragEnd: onDragEnd, role: "row", tabIndex: onClick ? 0 : undefined, "aria-describedby": onClick ? describedBy : undefined, children: children }), expandedContent && (_jsx("div", { role: "row", className: "table-row-expand", children: _jsx("div", { role: "cell", "aria-colspan": colSpan, className: "table-row-expand__cell", children: expandedContent }) }))] }));
|
|
45
68
|
}
|
|
46
69
|
function SortArrow({ dir }) {
|
|
47
70
|
const Icon = dir === 'asc' ? ChevronUp : ChevronDown;
|
|
@@ -76,6 +99,10 @@ function TableField({ children, onClick, className, sortDir, ariaSort, onResizeS
|
|
|
76
99
|
/** @public */
|
|
77
100
|
export default function Table({ fields = [], headerGroups, items = [], renderRow, onRowClick, isRowClickable, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, mobileGridTemplateColumns, resizable = false, resizeMode = 'grow', resizeStorageKey, size = 'small', title, icon, count, collapsed = false, onToggleCollapse, headerActions, empty, loading, dragOver = false, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }) {
|
|
78
101
|
const { t } = useTranslation();
|
|
102
|
+
const uid = useId();
|
|
103
|
+
const 제목id = `table-title-${uid}`;
|
|
104
|
+
/** 줄을 눌러 들어가는 표에서만 쓰는 설명 id — `role="button"` 을 잃은 자리를 메운다 */
|
|
105
|
+
const 줄설명id = `table-row-hint-${uid}`;
|
|
79
106
|
/**
|
|
80
107
|
* **좁은 화면에서는 폭 조절을 통째로 끈다** — 손잡이·초기화 단추·저장값.
|
|
81
108
|
*
|
|
@@ -462,7 +489,7 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
462
489
|
? { '--table-grid-cols': effectiveGrid }
|
|
463
490
|
: undefined;
|
|
464
491
|
const hasHeader = title !== undefined;
|
|
465
|
-
return (
|
|
492
|
+
return (_jsxs("div", { ref: 통달기, className: `table-wrap table-wrap--${size}${폭조절 ? ' table-wrap--resizable' : ''}${dragOver ? ' table-wrap--dragover' : ''}${collapsed ? ' table-wrap--collapsed' : ''}`, style: gridStyle, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop,
|
|
466
493
|
/* **넘칠 때만 초점을 받는다**(말귀 요청 ㊿, 2026-10-01).
|
|
467
494
|
이 통은 `overflow-x: auto` 인데 초점을 못 받았다. 안에 초점 받을 것이 없으면
|
|
468
495
|
자판만 쓰는 사람은 **오른쪽 칸을 영영 못 본다** — Tab 이 왼쪽 칸을 벗어나지
|
|
@@ -473,30 +500,33 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
473
500
|
한 칸씩 는다. 그래서 `scrollWidth > clientWidth` 를 재서 켠다.
|
|
474
501
|
**이름 있는 `region`** 으로 둔다: 이름 없는 `region` 은 낭독기가 건너뛰고,
|
|
475
502
|
초점만 받는 `div` 는 "여기서 뭘 하라는 건지" 를 말해 주지 못한다. */
|
|
476
|
-
tabIndex: 넘치나 ? 0 : undefined, role: 넘치나 ? 'region' : undefined, "aria-label": 넘치나 ? (typeof title === 'string' ? title : t('table.scrollRegion', '가로로 굴릴 수 있는 표')) : undefined, children:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
onToggleCollapse()
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
503
|
+
tabIndex: 넘치나 ? 0 : undefined, role: 넘치나 ? 'region' : undefined, "aria-label": 넘치나 ? (typeof title === 'string' ? title : t('table.scrollRegion', '가로로 굴릴 수 있는 표')) : undefined, children: [!!onRowClick && _jsx("span", { id: 줄설명id, className: "table-row-hint", children: t('table.rowActionHint', '줄을 누르면 자세히 봅니다') }), _jsxs("div", { ref: 안달기, role: "table", "aria-labelledby": hasHeader && typeof title === 'string' ? 제목id : undefined, className: "table body-2", children: [hasHeader && (_jsxs("div", { className: "table-header", onClick: onToggleCollapse,
|
|
504
|
+
/* 접을 수 없는 머리는 **표의 설명(`caption`)** 이다 — `table` 의 아이로 허락된
|
|
505
|
+
역할이라 나무가 깨지지 않는다. 접을 수 있으면 그 자체가 단추라 그쪽이 먼저다. */
|
|
506
|
+
role: onToggleCollapse ? 'button' : 'caption', tabIndex: onToggleCollapse ? 0 : undefined, "aria-expanded": onToggleCollapse ? !collapsed : undefined,
|
|
507
|
+
// 머리 안 단추(headerActions)에서 누른 키는 그 단추 몫이다 — 가로채면 표가 접히고 단추 동작은 막혔다(TableRow 와 같게)
|
|
508
|
+
onKeyDown: onToggleCollapse ? (e) => { if (e.target !== e.currentTarget)
|
|
509
|
+
return; if (e.key === 'Enter' || e.key === ' ') {
|
|
510
|
+
e.preventDefault();
|
|
511
|
+
onToggleCollapse();
|
|
512
|
+
} } : undefined, children: [onToggleCollapse && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down', ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), className: "table-header__chevron" })), icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "table-header__icon" })), _jsx("span", { id: 제목id, className: "table-header__title h5b", children: title }), count !== undefined && _jsx("span", { className: "table-header__count body-3b", children: count }), headerActions && (_jsx("span", { className: "table-header__actions", onClick: e => e.stopPropagation(), children: headerActions }))] })), !collapsed && (_jsxs(_Fragment, { children: [headerGroups && headerGroups.length > 0 && (_jsx("div", { role: "row", className: "table-fields table-fields--groups", children: headerGroups.map((g, i) => (_jsx("div", { role: "columnheader", "aria-colspan": g.span, className: cx('table-field body-3', g.className), style: { gridColumn: `span ${g.span}` }, children: g.label }, i))) })), _jsxs("div", { role: "presentation", className: cx('table-fields-strip', 폭조절 && 'table-fields-strip--resizable'), children: [_jsx("div", { role: "row", className: "table-fields", ref: fieldsRef, children: fields.map((f, i) => (_jsx(TableField, { onClick: f.onClick, className: f.className, sortDir: f.sortDir, ariaSort: ariaSortOf(i),
|
|
513
|
+
/* fit 모드는 오른쪽 이웃에서 폭을 덜어 오므로, 덜어 올 컬럼이 없는 자리에는
|
|
514
|
+
손잡이를 두지 않는다 — 두면 그 컬럼만 grow처럼 넓어져 표가 밀린다. */
|
|
515
|
+
onResizeStart: 폭조절 && i < fields.length - 1 && f.resizable !== false
|
|
516
|
+
&& (resizeMode !== 'fit' || fields.some((g, j) => j > i && g.resizable !== false))
|
|
517
|
+
? (e) => startResize(i, e) : undefined, children: f.label }, i))) }), 폭조절 && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: "chevrons-left-right-ellipsis", ariaLabel: t('table.resetColumns', '칸 너비 초기화'), showTooltipInfo: t('table.resetColumns', '칸 너비 초기화'), className: "table-fields__reset", onClick: 자동맞춤 }))] }), _jsx("div", { role: "rowgroup", className: "table-body", children: loading ? (_jsx(TableRow, { variant: "loading", colSpan: fields.length || undefined })) : items.length === 0 && empty ? (_jsx(TableRow, { variant: "empty", colSpan: fields.length || undefined, children: empty })) : items.map((item, idx) => {
|
|
518
|
+
const isDragging = draggingId != null && item.id === draggingId;
|
|
519
|
+
const 눌림 = !!onRowClick && (isRowClickable ? isRowClickable(item) : true);
|
|
520
|
+
/* 글자로 견준다 — URL·폼에서 온 '5' 와 자료의 5 가 같은 줄인데 `===` 로는 안 펴졌다 */
|
|
521
|
+
const isExpanded = expandedId != null && item.id != null && (expandedId instanceof Set
|
|
522
|
+
? expandedId.has(item.id) || [...expandedId].some(x => String(x) === String(item.id))
|
|
523
|
+
: String(item.id) === String(expandedId));
|
|
524
|
+
return (_jsx(TableRow
|
|
525
|
+
// 타입까지 열쇠에 — id 없는 줄(idx 0)과 id:0, 1 과 '1' 이 같은 열쇠가 되어 목록을 바꾼 뒤 옛 줄이 남았다
|
|
526
|
+
, { className: `${눌림 ? ' table-row--clickable' : ''}${onRowDragStart ? ' table-row--draggable' : ''}${isDragging ? ' table-row--dragging' : ''}`.trim(), draggable: !!onRowDragStart,
|
|
527
|
+
// 옆의 핸들러들처럼 있을 때만 넘긴다. 항상 함수를 만들어 넘기면
|
|
528
|
+
// TableRow 가 "클릭 가능"으로 보고 role="button" · tabIndex=0 을 붙여,
|
|
529
|
+
// 클릭 동작이 없는 표의 모든 행이 버튼으로 읽히고 탭 정지점이 된다.
|
|
530
|
+
onClick: 눌림 ? () => onRowClick(item) : undefined, colSpan: fields.length || undefined, describedBy: 줄설명id, onMouseEnter: onRowMouseEnter ? (e) => onRowMouseEnter(item, e) : undefined, onMouseLeave: onRowMouseLeave ? (e) => onRowMouseLeave(item, e) : undefined, onDragStart: onRowDragStart ? (e) => onRowDragStart(item, e) : undefined, onDragEnd: onRowDragEnd ? (e) => onRowDragEnd(item, e) : undefined, expandedContent: isExpanded && renderExpandedRow ? renderExpandedRow(item) : undefined, children: renderRow(item).map((cell, i) => (_jsx(TableCell, { className: cell.className, style: cell.style, children: cell.content }, i))) }, item.id != null ? `${typeof item.id}:${item.id}` : `__idx:${idx}`));
|
|
531
|
+
}) })] }))] })] }));
|
|
502
532
|
}
|
|
@@ -1404,7 +1404,8 @@
|
|
|
1404
1404
|
},
|
|
1405
1405
|
"table": {
|
|
1406
1406
|
"resetColumns": "Reset column widths",
|
|
1407
|
-
"scrollRegion": "Horizontally scrollable table"
|
|
1407
|
+
"scrollRegion": "Horizontally scrollable table",
|
|
1408
|
+
"rowActionHint": "Press a row to open its details"
|
|
1408
1409
|
},
|
|
1409
1410
|
"wordScore": {
|
|
1410
1411
|
"panelTitle": "Word table scores",
|