@operato/data-grist 10.9.0 → 10.10.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/src/configure/column-builder.js +17 -2
  3. package/dist/src/configure/column-builder.js.map +1 -1
  4. package/dist/src/configure/config-builder.js +1 -1
  5. package/dist/src/configure/config-builder.js.map +1 -1
  6. package/dist/src/data-grid/data-grid-body-style.js +12 -1
  7. package/dist/src/data-grid/data-grid-body-style.js.map +1 -1
  8. package/dist/src/data-grid/data-grid-body.d.ts +24 -0
  9. package/dist/src/data-grid/data-grid-body.js +126 -39
  10. package/dist/src/data-grid/data-grid-body.js.map +1 -1
  11. package/dist/src/data-grid/data-grid-field.d.ts +2 -2
  12. package/dist/src/data-grid/data-grid-field.js +29 -29
  13. package/dist/src/data-grid/data-grid-field.js.map +1 -1
  14. package/dist/src/data-grid/data-grid-footer.js +1 -1
  15. package/dist/src/data-grid/data-grid-footer.js.map +1 -1
  16. package/dist/src/data-grid/data-grid-header.d.ts +12 -1
  17. package/dist/src/data-grid/data-grid-header.js +81 -8
  18. package/dist/src/data-grid/data-grid-header.js.map +1 -1
  19. package/dist/src/data-grid/data-grid.d.ts +9 -1
  20. package/dist/src/data-grid/data-grid.js +79 -27
  21. package/dist/src/data-grid/data-grid.js.map +1 -1
  22. package/dist/src/data-grist.d.ts +7 -0
  23. package/dist/src/data-grist.js +12 -8
  24. package/dist/src/data-grist.js.map +1 -1
  25. package/dist/src/editors/ox-grist-editor.d.ts +14 -1
  26. package/dist/src/editors/ox-grist-editor.js +27 -3
  27. package/dist/src/editors/ox-grist-editor.js.map +1 -1
  28. package/dist/src/editors/registry.d.ts +2 -2
  29. package/dist/src/editors/registry.js +2 -2
  30. package/dist/src/editors/registry.js.map +1 -1
  31. package/dist/src/filters/filter-input.js +15 -0
  32. package/dist/src/filters/filter-input.js.map +1 -1
  33. package/dist/src/filters/filter-range-date.js +9 -0
  34. package/dist/src/filters/filter-range-date.js.map +1 -1
  35. package/dist/src/filters/filter-range-number.js +9 -0
  36. package/dist/src/filters/filter-range-number.js.map +1 -1
  37. package/dist/src/gutters/gutter-row-selector.js +16 -0
  38. package/dist/src/gutters/gutter-row-selector.js.map +1 -1
  39. package/dist/src/index.d.ts +1 -0
  40. package/dist/src/index.js +1 -0
  41. package/dist/src/index.js.map +1 -1
  42. package/dist/src/renderers/ox-grist-renderer-boolean.js +8 -0
  43. package/dist/src/renderers/ox-grist-renderer-boolean.js.map +1 -1
  44. package/dist/src/types.d.ts +11 -1
  45. package/dist/src/types.js.map +1 -1
  46. package/dist/src/utils/column-text.d.ts +18 -0
  47. package/dist/src/utils/column-text.js +37 -0
  48. package/dist/src/utils/column-text.js.map +1 -0
  49. package/dist/src/utils/index.d.ts +1 -0
  50. package/dist/src/utils/index.js +1 -0
  51. package/dist/src/utils/index.js.map +1 -1
  52. package/dist/test/accessible-name.test.d.ts +2 -0
  53. package/dist/test/accessible-name.test.js +324 -0
  54. package/dist/test/accessible-name.test.js.map +1 -0
  55. package/dist/test/row-hover-visible.test.js +2 -2
  56. package/dist/test/row-hover-visible.test.js.map +1 -1
  57. package/dist/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +7 -6
  59. package/test/accessible-name.test.ts +408 -0
  60. package/test/row-hover-visible.test.ts +2 -2
  61. package/translations/en.json +8 -0
  62. package/translations/ja.json +8 -0
  63. package/translations/ko.json +7 -0
  64. package/translations/ms.json +8 -0
  65. package/translations/zh.json +8 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [10.10.0](https://github.com/hatiolab/operato/compare/v10.9.0...v10.10.0) (2026-09-06)
7
+
8
+
9
+ ### :memo: Documentation
10
+
11
+ * **webcomponents:** 주석의 지어낸 한국어를 고친다 ([2553211](https://github.com/hatiolab/operato/commit/255321196bcfc6dc2fce0a8b9806d1341a6a7ce3))
12
+ * 지어낸 말 「재보니·재봤음」을 걷는다 ([0b2a04d](https://github.com/hatiolab/operato/commit/0b2a04d874f2e6f3bc44be4d04f4b3eafde998ef))
13
+
14
+
15
+ ### :rocket: New Features
16
+
17
+ * **data-grist:** 표를 표라고 선언하고, 입력칸에 이름을 붙인다 ([e45922b](https://github.com/hatiolab/operato/commit/e45922b51c0bd38f220a618ddc387459a92bef20))
18
+
19
+
20
+
6
21
  ## [10.9.0](https://github.com/hatiolab/operato/compare/v10.8.1...v10.9.0) (2026-09-05)
7
22
 
8
23
 
@@ -13,15 +13,27 @@ import { getRenderer } from '../renderers/index.js';
13
13
  */
14
14
  const RIGHT_ALIGNED_TYPES = ['integer', 'float', 'number'];
15
15
  export const buildColumn = (column) => {
16
- var _a, _b;
16
+ var _a, _b, _c;
17
17
  var compiled = { ...column };
18
18
  if (column.type == 'gutter') {
19
19
  compiled = generateGutterColumn(column);
20
20
  }
21
21
  var { name, header, record, handlers, label, filter } = compiled;
22
22
  /* header */
23
+ /*
24
+ * 이름을 글자로도 남긴다 — renderer 하나만 두면 이름이 여기서 사라진다.
25
+ *
26
+ * 설정이 header: '상태' 처럼 문자열로 들어와도 여기를 지나면 () => '상태' 한 개만 남는다.
27
+ * 그 함수가 돌려주는 것은 화면에 그릴 조각(대개 lit template)이라 되읽을 수 없다. 그래서 화면 밖에서
28
+ * 이름이 필요한 곳 — 스크린 리더가 읽을 이름, filter 입력칸의 이름 — 이 이름을 구할 데가 없었고,
29
+ * 결국 각자 다시 적어 두는 수밖에 없었다. 한 사실에 출처가 둘이 되면 header 를 고쳐도 나머지가
30
+ * 그대로 남는다.
31
+ *
32
+ * 그리는 것과 이름을 함께 남긴다. columnText() 가 이것을 읽는다.
33
+ */
23
34
  if (typeof header == 'string') {
24
35
  compiled.header = {
36
+ text: header,
25
37
  renderer: () => header
26
38
  };
27
39
  }
@@ -29,6 +41,7 @@ export const buildColumn = (column) => {
29
41
  const text = compiled.header.renderer;
30
42
  compiled.header = {
31
43
  ...compiled.header,
44
+ text,
32
45
  renderer: () => text
33
46
  };
34
47
  }
@@ -38,11 +51,13 @@ export const buildColumn = (column) => {
38
51
  switch (type) {
39
52
  case 'boolean':
40
53
  compiled.label = {
41
- renderer: ((_b = compiled.header) === null || _b === void 0 ? void 0 : _b.renderer) || (() => name)
54
+ text: ((_b = compiled.header) === null || _b === void 0 ? void 0 : _b.text) || name,
55
+ renderer: ((_c = compiled.header) === null || _c === void 0 ? void 0 : _c.renderer) || (() => name)
42
56
  };
43
57
  break;
44
58
  case 'string':
45
59
  compiled.label = {
60
+ text: label,
46
61
  renderer: () => label
47
62
  };
48
63
  break;
@@ -1 +1 @@
1
- {"version":3,"file":"column-builder.js","sourceRoot":"","sources":["../../../src/configure/column-builder.ts"],"names":[],"mappings":"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","sourcesContent":["import { FilterConfigObject, FilterOperator } from '../index.js'\nimport { NOOP } from '../const.js'\nimport { getEditor } from '../editors/index.js'\nimport { generateGutterColumn } from '../gutters/index.js'\nimport { getGristEventHandler } from '../handlers/index.js'\nimport { getRenderer } from '../renderers/index.js'\nimport { ColumnConfig } from '../types.js'\n\n/**\n * 값을 오른쪽에 붙이는 칸.\n *\n * 자릿수가 세로로 맞아야 값을 견줄 수 있다. 왼쪽으로 두면 24 와 1,200 이 시작점만 같고 끝이 어긋나서\n * 어느 쪽이 큰지 눈으로 읽히지 않는다.\n *\n * progress 는 뺀다 — 막대로 그리므로 정렬이 뜻을 갖지 않는다.\n */\nconst RIGHT_ALIGNED_TYPES = ['integer', 'float', 'number']\n\nexport const buildColumn = (column: any): ColumnConfig => {\n var compiled: ColumnConfig = { ...column }\n\n if (column.type == 'gutter') {\n compiled = generateGutterColumn(column)\n }\n\n var { name, header, record, handlers, label, filter } = compiled\n\n /* header */\n\n if (typeof header == 'string') {\n compiled.header = {\n renderer: () => header\n }\n }\n\n if (typeof compiled.header?.renderer == 'string') {\n const text = compiled.header.renderer\n compiled.header = {\n ...compiled.header,\n renderer: () => text\n }\n }\n\n /* label */\n if (label) {\n let type = typeof label\n switch (type) {\n case 'boolean':\n compiled.label = {\n renderer: compiled.header?.renderer || (() => name)\n }\n break\n case 'string':\n compiled.label = {\n renderer: () => label\n }\n break\n default:\n compiled.label = label\n }\n } else {\n compiled.label = false\n }\n\n /*\n * record\n */\n var { renderer: recordRenderer, editor, editable, classifier } = record || {}\n\n recordRenderer = getRenderer(recordRenderer || column.type)\n\n if (editable !== false && typeof editor !== 'function') {\n editor = getEditor(column.type)\n }\n\n if (typeof classifier !== 'function') {\n classifier = NOOP\n }\n\n compiled.record = {\n /*\n * 숫자 칸의 기본 정렬. 화면이 align 을 주면 그것이 이긴다 — 자릿수를 견줄 값이 아닌 숫자\n * (전화번호 · 우편번호 같은 것)는 화면이 따로 정한다.\n *\n * 머리글도 따라온다. data-grid-header 가 열 제목을 그릴 때 record.align 을 본다.\n */\n ...(RIGHT_ALIGNED_TYPES.includes(column.type) ? { align: 'right' } : {}),\n ...record,\n classifier,\n renderer: recordRenderer,\n editor\n }\n\n /* filter */\n if (filter) {\n let type = typeof filter\n switch (type) {\n case 'boolean':\n compiled.filter = {\n type: column.type,\n operator: 'eq'\n }\n break\n case 'string':\n compiled.filter = {\n type: column.type,\n operator: filter as FilterOperator\n }\n break\n default:\n compiled.filter = {\n //@ts-ignore\n type: column.type,\n operator: 'eq',\n ...(filter as FilterConfigObject)\n }\n }\n }\n\n /* handler */\n var { click, dblclick, focus, contextmenu } = handlers || {}\n\n compiled.handlers = {\n click: click && getGristEventHandler(click),\n dblclick: dblclick && getGristEventHandler(dblclick),\n focus: focus && getGristEventHandler(focus),\n contextmenu: contextmenu && getGristEventHandler(contextmenu)\n }\n\n return compiled\n}\n"]}
1
+ {"version":3,"file":"column-builder.js","sourceRoot":"","sources":["../../../src/configure/column-builder.ts"],"names":[],"mappings":"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","sourcesContent":["import { FilterConfigObject, FilterOperator } from '../index.js'\nimport { NOOP } from '../const.js'\nimport { getEditor } from '../editors/index.js'\nimport { generateGutterColumn } from '../gutters/index.js'\nimport { getGristEventHandler } from '../handlers/index.js'\nimport { getRenderer } from '../renderers/index.js'\nimport { ColumnConfig } from '../types.js'\n\n/**\n * 값을 오른쪽에 붙이는 칸.\n *\n * 자릿수가 세로로 맞아야 값을 견줄 수 있다. 왼쪽으로 두면 24 와 1,200 이 시작점만 같고 끝이 어긋나서\n * 어느 쪽이 큰지 눈으로 읽히지 않는다.\n *\n * progress 는 뺀다 — 막대로 그리므로 정렬이 뜻을 갖지 않는다.\n */\nconst RIGHT_ALIGNED_TYPES = ['integer', 'float', 'number']\n\nexport const buildColumn = (column: any): ColumnConfig => {\n var compiled: ColumnConfig = { ...column }\n\n if (column.type == 'gutter') {\n compiled = generateGutterColumn(column)\n }\n\n var { name, header, record, handlers, label, filter } = compiled\n\n /* header */\n\n /*\n * 이름을 글자로도 남긴다 — renderer 하나만 두면 이름이 여기서 사라진다.\n *\n * 설정이 header: '상태' 처럼 문자열로 들어와도 여기를 지나면 () => '상태' 한 개만 남는다.\n * 그 함수가 돌려주는 것은 화면에 그릴 조각(대개 lit template)이라 되읽을 수 없다. 그래서 화면 밖에서\n * 이름이 필요한 곳 — 스크린 리더가 읽을 이름, filter 입력칸의 이름 — 이 이름을 구할 데가 없었고,\n * 결국 각자 다시 적어 두는 수밖에 없었다. 한 사실에 출처가 둘이 되면 header 를 고쳐도 나머지가\n * 그대로 남는다.\n *\n * 그리는 것과 이름을 함께 남긴다. columnText() 가 이것을 읽는다.\n */\n if (typeof header == 'string') {\n compiled.header = {\n text: header,\n renderer: () => header\n }\n }\n\n if (typeof compiled.header?.renderer == 'string') {\n const text = compiled.header.renderer\n compiled.header = {\n ...compiled.header,\n text,\n renderer: () => text\n }\n }\n\n /* label */\n if (label) {\n let type = typeof label\n switch (type) {\n case 'boolean':\n compiled.label = {\n text: compiled.header?.text || name,\n renderer: compiled.header?.renderer || (() => name)\n }\n break\n case 'string':\n compiled.label = {\n text: label as string,\n renderer: () => label\n }\n break\n default:\n compiled.label = label\n }\n } else {\n compiled.label = false\n }\n\n /*\n * record\n */\n var { renderer: recordRenderer, editor, editable, classifier } = record || {}\n\n recordRenderer = getRenderer(recordRenderer || column.type)\n\n if (editable !== false && typeof editor !== 'function') {\n editor = getEditor(column.type)\n }\n\n if (typeof classifier !== 'function') {\n classifier = NOOP\n }\n\n compiled.record = {\n /*\n * 숫자 칸의 기본 정렬. 화면이 align 을 주면 그것이 이긴다 — 자릿수를 견줄 값이 아닌 숫자\n * (전화번호 · 우편번호 같은 것)는 화면이 따로 정한다.\n *\n * 머리글도 따라온다. data-grid-header 가 열 제목을 그릴 때 record.align 을 본다.\n */\n ...(RIGHT_ALIGNED_TYPES.includes(column.type) ? { align: 'right' } : {}),\n ...record,\n classifier,\n renderer: recordRenderer,\n editor\n }\n\n /* filter */\n if (filter) {\n let type = typeof filter\n switch (type) {\n case 'boolean':\n compiled.filter = {\n type: column.type,\n operator: 'eq'\n }\n break\n case 'string':\n compiled.filter = {\n type: column.type,\n operator: filter as FilterOperator\n }\n break\n default:\n compiled.filter = {\n //@ts-ignore\n type: column.type,\n operator: 'eq',\n ...(filter as FilterConfigObject)\n }\n }\n }\n\n /* handler */\n var { click, dblclick, focus, contextmenu } = handlers || {}\n\n compiled.handlers = {\n click: click && getGristEventHandler(click),\n dblclick: dblclick && getGristEventHandler(dblclick),\n focus: focus && getGristEventHandler(focus),\n contextmenu: contextmenu && getGristEventHandler(contextmenu)\n }\n\n return compiled\n}\n"]}
@@ -24,7 +24,7 @@ export const buildConfig = (config) => {
24
24
  }
25
25
  /*
26
26
  * ── 선택 표식의 모양은 `rows.selectable` 이 정한다 (2026-08-31) ────────────
27
- * `row-selector` 거터는 **자기 열의 `multiple`** 을 보고 체크박스와 라디오를 가른다. 그런데
27
+ * `row-selector` 거터는 **자기 열의 `multiple`** 을 보고 체크박스와 라디오를 구분한다. 그런데
28
28
  * 「여러 건 고를 수 있나」는 원래 `rows.selectable.multiple` 이 정하는 것이다.
29
29
  *
30
30
  * 두 곳이 같은 것을 따로 정하고 있어서, 화면이 `rows.selectable.multiple: true` 만 주면
@@ -1 +1 @@
1
- {"version":3,"file":"config-builder.js","sourceRoot":"","sources":["../../../src/configure/config-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AAEjE,SAAS,0BAA0B,CAAC,GAAmB,EAAE,SAAuC;IAC9F,KAAK,IAAI,CAAC,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,CAAA;QACV,CAAC;IACH,CAAC;IACD,OAAO,CAAC,CAAC,CAAA;AACX,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAW,EAAe,EAAE;;IACtD,IAAI,EAAE,OAAO,GAAG,EAAE,EAAE,IAAI,GAAG,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,CAAA;IAExF,+CAA+C;IAC/C,MAAM,cAAc,GAAG,0BAA0B,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;IAC1E,IAAI,cAAc,IAAI,CAAC,EAAE,CAAC;QACxB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,cAAc,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAA;QACzB,CAAC;IACH,CAAC;IAED;;;;;;;;;;;OAWG;IACH,MAAM,gBAAgB,GAAG,MAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,UAAU,0CAAE,QAAQ,CAAA;IAEnD,OAAO;QACL,GAAG,MAAM;QACT,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,MAAW,EAAE,EAAE,CACnC,WAAW,CACT,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,UAAU,KAAK,cAAc,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS;YAC/F,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,gBAAgB,EAAE;YAC7C,CAAC,CAAC,MAAM,CACX,CACF;QACD,IAAI,EAAE,gBAAgB,CAAC,IAAI,CAAC;QAC5B,UAAU;QACV,OAAO;QACP,OAAO,EAAE,mBAAmB,CAAC,OAAO,CAAC;QACrC,IAAI,EAAE,gBAAgB,CAAC,IAAI,CAAC;QAC5B,IAAI,EAAE,gBAAgB,CAAC,IAAI,CAAC;QAC5B,IAAI,EAAE,gBAAgB,CAAC,IAAI,CAAC;KAC7B,CAAA;AACH,CAAC,CAAA","sourcesContent":["import { GristConfig, ColumnConfig } from '../types.js'\nimport { buildColumn } from './column-builder.js'\nimport { buildImexOptions } from './imex-option-builder.js'\nimport { buildListOptions } from './list-option-builder.js'\nimport { buildTreeOptions } from './tree-option-builder.js'\nimport { buildRowsOptions } from './rows-option-builder.js'\nimport { buildFiltersOptions } from './filters-option-builder.js'\n\nfunction findLastIndexWithCondition(arr: ColumnConfig[], condition: (x: ColumnConfig) => boolean) {\n for (let i = arr.length - 1; i >= 0; i--) {\n if (condition(arr[i])) {\n return i\n }\n }\n return -1\n}\n\nexport const buildConfig = (config: any): GristConfig => {\n var { columns = [], rows = {}, pagination, sorters, filters, list, tree, imex } = config\n\n /* 마지막 fixed 컬럼 앞의 모든 컬럼의 fixed를 true로 설정한다. */\n const lastFixedIndex = findLastIndexWithCondition(columns, x => !!x.fixed)\n if (lastFixedIndex >= 0) {\n for (let i = 0; i < lastFixedIndex; i++) {\n columns[i].fixed = true\n }\n }\n\n /*\n * ── 선택 표식의 모양은 `rows.selectable` 이 정한다 (2026-08-31) ────────────\n * `row-selector` 거터는 **자기 열의 `multiple`** 을 보고 체크박스와 라디오를 가른다. 그런데\n * 「여러 건 고를 수 있나」는 원래 `rows.selectable.multiple` 이 정하는 것이다.\n *\n * 두 곳이 같은 것을 따로 정하고 있어서, 화면이 `rows.selectable.multiple: true` 만 주면\n * **여러 건이 골라지는데 모양은 라디오**가 됐다 — 화면이 「하나만 된다」고 말하면서 여럿을 받는다.\n * 실제로 작업지시 목록이 그 상태였다.\n *\n * 거터 열이 스스로 `multiple` 을 적지 않았으면 `rows.selectable` 에서 이어받는다. 적었으면\n * 그 화면의 뜻이므로 그대로 둔다.\n */\n const selectorMultiple = rows?.selectable?.multiple\n\n return {\n ...config,\n columns: columns.map((column: any) =>\n buildColumn(\n column.type === 'gutter' && column.gutterName === 'row-selector' && column.multiple === undefined\n ? { ...column, multiple: !!selectorMultiple }\n : column\n )\n ),\n rows: buildRowsOptions(rows),\n pagination,\n sorters,\n filters: buildFiltersOptions(filters),\n list: buildListOptions(list),\n tree: buildTreeOptions(tree),\n imex: buildImexOptions(imex)\n }\n}\n"]}
1
+ {"version":3,"file":"config-builder.js","sourceRoot":"","sources":["../../../src/configure/config-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AAEjE,SAAS,0BAA0B,CAAC,GAAmB,EAAE,SAAuC;IAC9F,KAAK,IAAI,CAAC,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,CAAA;QACV,CAAC;IACH,CAAC;IACD,OAAO,CAAC,CAAC,CAAA;AACX,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAW,EAAe,EAAE;;IACtD,IAAI,EAAE,OAAO,GAAG,EAAE,EAAE,IAAI,GAAG,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,CAAA;IAExF,+CAA+C;IAC/C,MAAM,cAAc,GAAG,0BAA0B,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;IAC1E,IAAI,cAAc,IAAI,CAAC,EAAE,CAAC;QACxB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,cAAc,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAA;QACzB,CAAC;IACH,CAAC;IAED;;;;;;;;;;;OAWG;IACH,MAAM,gBAAgB,GAAG,MAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,UAAU,0CAAE,QAAQ,CAAA;IAEnD,OAAO;QACL,GAAG,MAAM;QACT,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,MAAW,EAAE,EAAE,CACnC,WAAW,CACT,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,UAAU,KAAK,cAAc,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS;YAC/F,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,gBAAgB,EAAE;YAC7C,CAAC,CAAC,MAAM,CACX,CACF;QACD,IAAI,EAAE,gBAAgB,CAAC,IAAI,CAAC;QAC5B,UAAU;QACV,OAAO;QACP,OAAO,EAAE,mBAAmB,CAAC,OAAO,CAAC;QACrC,IAAI,EAAE,gBAAgB,CAAC,IAAI,CAAC;QAC5B,IAAI,EAAE,gBAAgB,CAAC,IAAI,CAAC;QAC5B,IAAI,EAAE,gBAAgB,CAAC,IAAI,CAAC;KAC7B,CAAA;AACH,CAAC,CAAA","sourcesContent":["import { GristConfig, ColumnConfig } from '../types.js'\nimport { buildColumn } from './column-builder.js'\nimport { buildImexOptions } from './imex-option-builder.js'\nimport { buildListOptions } from './list-option-builder.js'\nimport { buildTreeOptions } from './tree-option-builder.js'\nimport { buildRowsOptions } from './rows-option-builder.js'\nimport { buildFiltersOptions } from './filters-option-builder.js'\n\nfunction findLastIndexWithCondition(arr: ColumnConfig[], condition: (x: ColumnConfig) => boolean) {\n for (let i = arr.length - 1; i >= 0; i--) {\n if (condition(arr[i])) {\n return i\n }\n }\n return -1\n}\n\nexport const buildConfig = (config: any): GristConfig => {\n var { columns = [], rows = {}, pagination, sorters, filters, list, tree, imex } = config\n\n /* 마지막 fixed 컬럼 앞의 모든 컬럼의 fixed를 true로 설정한다. */\n const lastFixedIndex = findLastIndexWithCondition(columns, x => !!x.fixed)\n if (lastFixedIndex >= 0) {\n for (let i = 0; i < lastFixedIndex; i++) {\n columns[i].fixed = true\n }\n }\n\n /*\n * ── 선택 표식의 모양은 `rows.selectable` 이 정한다 (2026-08-31) ────────────\n * `row-selector` 거터는 **자기 열의 `multiple`** 을 보고 체크박스와 라디오를 구분한다. 그런데\n * 「여러 건 고를 수 있나」는 원래 `rows.selectable.multiple` 이 정하는 것이다.\n *\n * 두 곳이 같은 것을 따로 정하고 있어서, 화면이 `rows.selectable.multiple: true` 만 주면\n * **여러 건이 골라지는데 모양은 라디오**가 됐다 — 화면이 「하나만 된다」고 말하면서 여럿을 받는다.\n * 실제로 작업지시 목록이 그 상태였다.\n *\n * 거터 열이 스스로 `multiple` 을 적지 않았으면 `rows.selectable` 에서 이어받는다. 적었으면\n * 그 화면의 뜻이므로 그대로 둔다.\n */\n const selectorMultiple = rows?.selectable?.multiple\n\n return {\n ...config,\n columns: columns.map((column: any) =>\n buildColumn(\n column.type === 'gutter' && column.gutterName === 'row-selector' && column.multiple === undefined\n ? { ...column, multiple: !!selectorMultiple }\n : column\n )\n ),\n rows: buildRowsOptions(rows),\n pagination,\n sorters,\n filters: buildFiltersOptions(filters),\n list: buildListOptions(list),\n tree: buildTreeOptions(tree),\n imex: buildImexOptions(imex)\n }\n}\n"]}
@@ -11,7 +11,7 @@ export const dataGridBodyStyle = css `
11
11
  *
12
12
  * 머리는 overflow: hidden 이라 스크롤바에 자리를 내주지 않고, 몸은 긴 목록에서 스크롤바 폭만큼
13
13
  * 좁아진다. 그래서 갈 수 있는 거리가 몸이 그만큼 더 길었고, 오른쪽 끝에서 몸만 더 갔다.
14
- * 재보니 정확히 --scrollbar-width 만큼(5px)이었다.
14
+ * 확인해 보니 정확히 --scrollbar-width 만큼(5px)이었다.
15
15
  *
16
16
  * 자리를 늘 비워 두면 목록 길이와 무관하게 두 폭이 같다. 목록이 길어질 때 열이 움찔하는 것도
17
17
  * 함께 없어진다 — 스크롤바가 생기는 순간 폭이 바뀌지 않는다.
@@ -23,6 +23,17 @@ export const dataGridBodyStyle = css `
23
23
  border-bottom: var(--grid-body-bottom-border);
24
24
  }
25
25
 
26
+ /*
27
+ * 줄 감싸개 — 상자를 만들지 않는다.
28
+ *
29
+ * role=row 를 담기 위한 요소일 뿐이라 배치에는 끼어들면 안 된다. display: contents 면 이 요소는
30
+ * 상자를 만들지 않고 자식이 그대로 바깥 grid 의 항목이 된다 — 열 폭 · 고정 열 · 묶음 머리의
31
+ * grid-column 배치가 그대로다.
32
+ */
33
+ div[grid-row] {
34
+ display: contents;
35
+ }
36
+
26
37
  ox-grid-field[odd] {
27
38
  background-color: var(--grid-record-odd-background-color);
28
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"data-grid-body-style.js","sourceRoot":"","sources":["../../../src/data-grid/data-grid-body-style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2InC,CAAA","sourcesContent":["import { css } from 'lit'\n\nexport const dataGridBodyStyle = css`\n :host {\n display: grid;\n grid-template-columns: var(--grid-template-columns);\n grid-auto-rows: var(--grid-record-height, min-content);\n\n overflow: auto;\n /*\n * 세로 스크롤바 자리를 늘 비워 둔다 — 머리와 가로 스크롤 범위를 같게 만들기 위해서다.\n *\n * 머리는 overflow: hidden 이라 스크롤바에 자리를 내주지 않고, 몸은 긴 목록에서 스크롤바 폭만큼\n * 좁아진다. 그래서 갈 수 있는 거리가 몸이 그만큼 더 길었고, 오른쪽 끝에서 몸만 더 갔다.\n * 재보니 정확히 --scrollbar-width 만큼(5px)이었다.\n *\n * 자리를 늘 비워 두면 목록 길이와 무관하게 두 폭이 같다. 목록이 길어질 때 열이 움찔하는 것도\n * 함께 없어진다 — 스크롤바가 생기는 순간 폭이 바뀌지 않는다.\n */\n scrollbar-gutter: stable;\n outline: none;\n color: var(--grid-record-color);\n position: relative;\n border-bottom: var(--grid-body-bottom-border);\n }\n\n ox-grid-field[odd] {\n background-color: var(--grid-record-odd-background-color);\n }\n\n ox-grid-field[disabled] {\n background-color: var(--grid-record-disabled-background-color, var(--grid-record-background-color));\n color: var(--grid-record-disabled-color, var(--grid-record-color));\n opacity: var(--grid-record-disabled-opacity, 1);\n }\n ox-grid-field[odd][disabled] {\n background-color: var(\n --grid-odd-record-disabled-background-color,\n var(--grid-record-disabled-background-color, var(--grid-record-odd-background-color))\n );\n color: var(--grid-odd-record-disabled-color, var(--grid-record-disabled-color, var(--grid-record-color)));\n opacity: var(--grid-odd-record-disabled-opacity, var(--grid-record-disabled-opacity, 1));\n }\n\n ox-grid-field[selected-row] {\n background-color: var(--grid-record-selected-background-color);\n color: var(--grid-record-selected-color);\n }\n\n /*\n * 마우스가 지나는 줄.\n *\n * 고른 줄보다 **훨씬 옅다.** 지나는 것은 사실이 아니고 눈이 어디 있는지 알려 주는 것뿐이라, 고른\n * 줄과 같은 무게로 그리면 무엇이 골라졌는지 헷갈린다. 그래서 고른 줄 규칙 뒤에 둔다 — 둘이 겹칠\n * 때는 고른 쪽이 이겨야 하므로 색을 옅게 두고 순서로 다투지 않는다.\n *\n * 값은 표가 정한다. 고치는 표에서는 비운다(data-grid.ts) — 거기서는 점선 셀 테두리가 이미 자리를\n * 말하고, 둘이 함께 있으면 시끄럽다.\n */\n @media (hover: hover) {\n ox-grid-field[hovered-row]:not([selected-row]) {\n background-color: var(--grid-record-hover-background-color, transparent);\n }\n }\n\n ox-grid-field[focused-row] {\n box-shadow: var(--grid-record-focused-box-shadow);\n font-weight: bold;\n color: var(--grid-record-focused-color);\n background-image: var(--focused-background-image);\n background-blend-mode: darken;\n }\n\n /*\n * 초점 셀 표식 — border 가 아니라 outline 으로 그린다.\n *\n * border 로 그리면 두 가지가 깨진다.\n * · 네 면 테두리가 셀 크기를 바꿔 줄이 어긋난다\n * · 아래 면이 **줄을 나누는 선 자리**를 차지해, 그 셀만 줄 선이 사라진다\n *\n * outline 은 자리를 차지하지 않는다. outline-offset: -1px 로 안쪽에 그려 옆 셀과 겹치지 않게 한다.\n * 표식을 감추고 싶은 화면은 토큰을 none 으로 두면 되고, 그래도 줄 선은 남는다.\n */\n ox-grid-field[focused] {\n outline: var(--grid-record-focused-cell-border);\n outline-offset: -1px;\n }\n\n ox-grid-field[emphasized-row],\n ox-grid-field[emphasized-row][focused] {\n background-color: var(--grid-record-emphasized-background-color);\n color: var(--grid-record-emphasized-color);\n }\n\n ox-grid-field[editing] {\n background-color: var(--grid-record-editing-background-color);\n }\n\n @media print {\n :host {\n grid-template-columns: var(--grid-template-print-columns);\n }\n ox-grid-field[focused] {\n outline: none;\n }\n\n ox-grid-field[selected-row] {\n background-color: transparent;\n }\n\n /* 종이에는 마우스가 없다. */\n ox-grid-field[hovered-row] {\n background-color: transparent;\n }\n\n ox-grid-field[emphasized-row] {\n background-color: transparent;\n color: initial;\n }\n\n ox-grid-field[focused-row] {\n background-color: transparent;\n color: initial;\n }\n\n ox-grid-field[editing] {\n background-color: transparent;\n }\n }\n\n @media (prefers-color-scheme: dark) {\n ox-grid-field[focused-row] {\n background-blend-mode: lighten;\n }\n }\n\n @media (prefers-color-scheme: light) {\n ox-grid-field[focused-row] {\n background-blend-mode: darken;\n }\n }\n`\n"]}
1
+ {"version":3,"file":"data-grid-body-style.js","sourceRoot":"","sources":["../../../src/data-grid/data-grid-body-style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsJnC,CAAA","sourcesContent":["import { css } from 'lit'\n\nexport const dataGridBodyStyle = css`\n :host {\n display: grid;\n grid-template-columns: var(--grid-template-columns);\n grid-auto-rows: var(--grid-record-height, min-content);\n\n overflow: auto;\n /*\n * 세로 스크롤바 자리를 늘 비워 둔다 — 머리와 가로 스크롤 범위를 같게 만들기 위해서다.\n *\n * 머리는 overflow: hidden 이라 스크롤바에 자리를 내주지 않고, 몸은 긴 목록에서 스크롤바 폭만큼\n * 좁아진다. 그래서 갈 수 있는 거리가 몸이 그만큼 더 길었고, 오른쪽 끝에서 몸만 더 갔다.\n * 확인해 보니 정확히 --scrollbar-width 만큼(5px)이었다.\n *\n * 자리를 늘 비워 두면 목록 길이와 무관하게 두 폭이 같다. 목록이 길어질 때 열이 움찔하는 것도\n * 함께 없어진다 — 스크롤바가 생기는 순간 폭이 바뀌지 않는다.\n */\n scrollbar-gutter: stable;\n outline: none;\n color: var(--grid-record-color);\n position: relative;\n border-bottom: var(--grid-body-bottom-border);\n }\n\n /*\n * 줄 감싸개 — 상자를 만들지 않는다.\n *\n * role=row 를 담기 위한 요소일 뿐이라 배치에는 끼어들면 안 된다. display: contents 면 이 요소는\n * 상자를 만들지 않고 자식이 그대로 바깥 grid 의 항목이 된다 — 열 폭 · 고정 열 · 묶음 머리의\n * grid-column 배치가 그대로다.\n */\n div[grid-row] {\n display: contents;\n }\n\n ox-grid-field[odd] {\n background-color: var(--grid-record-odd-background-color);\n }\n\n ox-grid-field[disabled] {\n background-color: var(--grid-record-disabled-background-color, var(--grid-record-background-color));\n color: var(--grid-record-disabled-color, var(--grid-record-color));\n opacity: var(--grid-record-disabled-opacity, 1);\n }\n ox-grid-field[odd][disabled] {\n background-color: var(\n --grid-odd-record-disabled-background-color,\n var(--grid-record-disabled-background-color, var(--grid-record-odd-background-color))\n );\n color: var(--grid-odd-record-disabled-color, var(--grid-record-disabled-color, var(--grid-record-color)));\n opacity: var(--grid-odd-record-disabled-opacity, var(--grid-record-disabled-opacity, 1));\n }\n\n ox-grid-field[selected-row] {\n background-color: var(--grid-record-selected-background-color);\n color: var(--grid-record-selected-color);\n }\n\n /*\n * 마우스가 지나는 줄.\n *\n * 고른 줄보다 **훨씬 옅다.** 지나는 것은 사실이 아니고 눈이 어디 있는지 알려 주는 것뿐이라, 고른\n * 줄과 같은 무게로 그리면 무엇이 골라졌는지 헷갈린다. 그래서 고른 줄 규칙 뒤에 둔다 — 둘이 겹칠\n * 때는 고른 쪽이 이겨야 하므로 색을 옅게 두고 순서로 다투지 않는다.\n *\n * 값은 표가 정한다. 고치는 표에서는 비운다(data-grid.ts) — 거기서는 점선 셀 테두리가 이미 자리를\n * 말하고, 둘이 함께 있으면 시끄럽다.\n */\n @media (hover: hover) {\n ox-grid-field[hovered-row]:not([selected-row]) {\n background-color: var(--grid-record-hover-background-color, transparent);\n }\n }\n\n ox-grid-field[focused-row] {\n box-shadow: var(--grid-record-focused-box-shadow);\n font-weight: bold;\n color: var(--grid-record-focused-color);\n background-image: var(--focused-background-image);\n background-blend-mode: darken;\n }\n\n /*\n * 초점 셀 표식 — border 가 아니라 outline 으로 그린다.\n *\n * border 로 그리면 두 가지가 깨진다.\n * · 네 면 테두리가 셀 크기를 바꿔 줄이 어긋난다\n * · 아래 면이 **줄을 나누는 선 자리**를 차지해, 그 셀만 줄 선이 사라진다\n *\n * outline 은 자리를 차지하지 않는다. outline-offset: -1px 로 안쪽에 그려 옆 셀과 겹치지 않게 한다.\n * 표식을 감추고 싶은 화면은 토큰을 none 으로 두면 되고, 그래도 줄 선은 남는다.\n */\n ox-grid-field[focused] {\n outline: var(--grid-record-focused-cell-border);\n outline-offset: -1px;\n }\n\n ox-grid-field[emphasized-row],\n ox-grid-field[emphasized-row][focused] {\n background-color: var(--grid-record-emphasized-background-color);\n color: var(--grid-record-emphasized-color);\n }\n\n ox-grid-field[editing] {\n background-color: var(--grid-record-editing-background-color);\n }\n\n @media print {\n :host {\n grid-template-columns: var(--grid-template-print-columns);\n }\n ox-grid-field[focused] {\n outline: none;\n }\n\n ox-grid-field[selected-row] {\n background-color: transparent;\n }\n\n /* 종이에는 마우스가 없다. */\n ox-grid-field[hovered-row] {\n background-color: transparent;\n }\n\n ox-grid-field[emphasized-row] {\n background-color: transparent;\n color: initial;\n }\n\n ox-grid-field[focused-row] {\n background-color: transparent;\n color: initial;\n }\n\n ox-grid-field[editing] {\n background-color: transparent;\n }\n }\n\n @media (prefers-color-scheme: dark) {\n ox-grid-field[focused-row] {\n background-blend-mode: lighten;\n }\n }\n\n @media (prefers-color-scheme: light) {\n ox-grid-field[focused-row] {\n background-blend-mode: darken;\n }\n }\n`\n"]}
@@ -13,6 +13,13 @@ export declare class DataGridBody extends LitElement {
13
13
  row: number;
14
14
  column: number;
15
15
  };
16
+ /**
17
+ * 이 판의 첫 줄이 표 전체에서 몇 번째인가(1부터).
18
+ *
19
+ * 머리줄 수와 페이지 자리를 합한 값이라 ox-grid 가 계산해서 내려 준다 — 여기서 다시 세면
20
+ * 머리가 한 줄인지 두 줄인지(묶음 머리)를 몸이 알아야 한다.
21
+ */
22
+ ariaRowIndexBase: number;
16
23
  editTarget: {
17
24
  row: number;
18
25
  column: number;
@@ -47,6 +54,16 @@ export declare class DataGridBody extends LitElement {
47
54
  private _hoveredRow;
48
55
  /** 지나는 줄을 옮긴다. 같은 줄이면 아무것도 하지 않는다. */
49
56
  private setHoveredRow;
57
+ /**
58
+ * 줄 하나 — 그 줄의 칸을 담고 있는 요소.
59
+ *
60
+ * ── 자식을 세지 않는다 (2026-09-06) ────────────────────────────────────
61
+ * 전에는 `renderRoot.children` 을 `줄번호 * (칸수 + 1)` 로 셌다. 「+1」은 남는 폭을 메우는
62
+ * 빈 칸이고, 그 앞의 주석은 그 셈이 왜 필요한지를 설명하고 있었다 — 셈이 맞아야만 도는 코드였다.
63
+ *
64
+ * 지금은 줄이 **실제 요소**다(role=row). 셀 필요가 없다.
65
+ */
66
+ private rowElementAt;
50
67
  private markRow;
51
68
  getFieldByIndex(rowIndex: number, columnIndex: number, residue?: boolean): DataGridField | undefined;
52
69
  /**
@@ -72,6 +89,13 @@ export declare class DataGridBody extends LitElement {
72
89
  paste(): Promise<void>;
73
90
  setSelectBlock(start?: DataGridField, end?: DataGridField): void;
74
91
  buildAccumulatorRecord(): GristRecord;
92
+ /**
93
+ * IME buffer 가 지금 어느 칸을 대신하고 있는지 — 스크린 리더가 읽을 이름.
94
+ *
95
+ * 이름의 출처는 column 정의 하나다(columnText). 포커스가 아직 아무 칸에도 없거나 이름을 구할 수
96
+ * 없으면 「칸 입력」으로 물러난다 — 이름을 지어내지 않고, 그렇다고 이름을 비우지도 않는다.
97
+ */
98
+ private _imeBufferLabel;
75
99
  /** IME buffer를 포커스된 셀 위에 위치시킨다 */
76
100
  private _positionIMEBuffer;
77
101
  /** IME buffer 값을 field-change 이벤트로 커밋한다 */
@@ -6,9 +6,11 @@ import { customElement, property, query, state } from 'lit/decorators.js';
6
6
  import { ifDefined } from 'lit/directives/if-defined.js';
7
7
  import debounce from 'lodash-es/debounce.js';
8
8
  import isEmpty from 'lodash-es/isEmpty.js';
9
+ import { i18next } from '@operato/i18n';
9
10
  import { sleep, parseToNumberOrNull, closestElement } from '@operato/utils';
10
11
  import { ZERO_CONFIG, ZERO_DATA } from '../configure/zero-config.js';
11
12
  import { RecordViewHandler } from '../record-view/record-view-handler.js';
13
+ import { columnText } from '../utils/column-text.js';
12
14
  // import { supportsPassive } from '../utils/index.js'
13
15
  import { dataGridBodyStyle } from './data-grid-body-style.js';
14
16
  import { dataGridBodyClickHandler } from './event-handlers/data-grid-body-click-handler.js';
@@ -86,6 +88,13 @@ let DataGridBody = class DataGridBody extends LitElement {
86
88
  this.columns = [];
87
89
  this.data = ZERO_DATA;
88
90
  this.focused = ZERO_FOCUS;
91
+ /**
92
+ * 이 판의 첫 줄이 표 전체에서 몇 번째인가(1부터).
93
+ *
94
+ * 머리줄 수와 페이지 자리를 합한 값이라 ox-grid 가 계산해서 내려 준다 — 여기서 다시 세면
95
+ * 머리가 한 줄인지 두 줄인지(묶음 머리)를 몸이 알아야 한다.
96
+ */
97
+ this.ariaRowIndexBase = 2;
89
98
  this.editTarget = null;
90
99
  this.from = -1;
91
100
  this.to = -1;
@@ -123,6 +132,14 @@ let DataGridBody = class DataGridBody extends LitElement {
123
132
  var { records } = data;
124
133
  var { appendable, classifier, accumulator } = this.config.rows;
125
134
  const { start, end } = this._selectBlock || {};
135
+ /*
136
+ * 줄을 고를 수 있는 표에서만 aria-selected 를 낸다.
137
+ *
138
+ * 이 속성은 「고를 수 있는데 지금은 안 골랐다」는 뜻이라, 고를 수 없는 표의 모든 줄에 false 를
139
+ * 달면 있지도 않은 기능을 있다고 말하게 된다. 고르는 장치가 있는지는 표가 스스로 안다 —
140
+ * 줄 선택 거터가 있느냐다.
141
+ */
142
+ const selectable = columns.some(column => column.gutterName === 'row-selector');
126
143
  /*
127
144
  * 레코드를 추가할 수 있는 경우에는 항상 추가 레코드를 보여준다.
128
145
  * 만약, 이전 방식처럼, 커서를 옮겨야만 새로운 레코드가 보이게 하고 싶다면, 조건부를 다음의 코드로 대체한다.
@@ -134,6 +151,16 @@ let DataGridBody = class DataGridBody extends LitElement {
134
151
  if (accumulator) {
135
152
  var accumRecord = this.buildAccumulatorRecord();
136
153
  }
154
+ /*
155
+ * ── 줄이 몇 번째인가 ──────────────────────────────────────────────────
156
+ *
157
+ * aria-rowindex 는 **표 전체에서의 자리**다 — 지금 페이지 안의 자리가 아니다. 2페이지 첫 줄은
158
+ * 21번째 줄이지 이 페이지의 1번째 줄이 아니다. 그렇게 읽어 주지 않으면 페이지를 넘길 때마다
159
+ * 「1번째 줄」로 돌아가서 어디까지 봤는지 알 수 없다.
160
+ *
161
+ * 머리줄도 센다(1부터). 묶음 머리가 있으면 머리가 두 줄이라 본문은 3번부터 시작한다.
162
+ */
163
+ const rowIndexBase = this.ariaRowIndexBase;
137
164
  return html `
138
165
  ${records.map((record, idxRow) => {
139
166
  var attrFocusedRow = idxRow === focusedRow;
@@ -142,8 +169,17 @@ let DataGridBody = class DataGridBody extends LitElement {
142
169
  var dirtyFields = record['__dirtyfields__'] || {};
143
170
  var { emphasized } = classifier.call(null, record, idxRow) || {};
144
171
  return html `
172
+ <div
173
+ grid-row
174
+ role="row"
175
+ aria-rowindex=${rowIndexBase + idxRow}
176
+ aria-selected=${ifDefined(selectable ? !!attrSelected : undefined)}
177
+ >
145
178
  ${columns.map((column, idxColumn) => html `
146
179
  <ox-grid-field
180
+ id=${'cell-' + idxRow + '-' + idxColumn}
181
+ role="gridcell"
182
+ aria-colindex=${idxColumn + 1}
147
183
  .data=${data}
148
184
  .type=${column.type}
149
185
  .rowIndex=${idxRow}
@@ -165,7 +201,12 @@ let DataGridBody = class DataGridBody extends LitElement {
165
201
  fixed=${ifDefined(this.fixedLefts[idxColumn])}
166
202
  ></ox-grid-field>
167
203
  `)}
204
+ <!--
205
+ 남는 폭을 메우는 칸 — 읽을 것이 없다. 줄 안에는 두되(색이 함께 바뀌어야 한다) 표의
206
+ 칸으로는 세지 않는다.
207
+ -->
168
208
  <ox-grid-field
209
+ role="presentation"
169
210
  .data=${data}
170
211
  .rowIndex=${idxRow}
171
212
  .columnIndex=${-1}
@@ -175,31 +216,54 @@ let DataGridBody = class DataGridBody extends LitElement {
175
216
  ?focused-row=${attrFocusedRow}
176
217
  ?selected-row=${attrSelected}
177
218
  ></ox-grid-field>
219
+ </div>
178
220
  `;
179
221
  })}
180
222
  ${accumulator
181
223
  ? html `
182
- ${columns.map((column, idxColumn) => html `
183
- <ox-grid-accum-field
184
- .data=${data}
185
- .columnIndex=${idxColumn}
186
- .rowIndex=${records.length}
187
- .column=${column}
188
- .record=${accumRecord}
189
- .value=${accumRecord[column.name]}
190
- fixed=${ifDefined(this.fixedLefts[idxColumn])}
191
- ></ox-grid-accum-field>
192
- `)}
193
- <ox-grid-accum-field
194
- .data=${data}
195
- .columnIndex=${-1}
196
- .rowIndex=${records.length}
197
- .record=${accumRecord}
198
- ></ox-grid-accum-field>
224
+ <div grid-row role="row" aria-rowindex=${rowIndexBase + records.length}>
225
+ ${columns.map((column, idxColumn) => html `
226
+ <ox-grid-accum-field
227
+ role="gridcell"
228
+ aria-colindex=${idxColumn + 1}
229
+ .data=${data}
230
+ .columnIndex=${idxColumn}
231
+ .rowIndex=${records.length}
232
+ .column=${column}
233
+ .record=${accumRecord}
234
+ .value=${accumRecord[column.name]}
235
+ fixed=${ifDefined(this.fixedLefts[idxColumn])}
236
+ ></ox-grid-accum-field>
237
+ `)}
238
+ <ox-grid-accum-field
239
+ role="presentation"
240
+ .data=${data}
241
+ .columnIndex=${-1}
242
+ .rowIndex=${records.length}
243
+ .record=${accumRecord}
244
+ ></ox-grid-accum-field>
245
+ </div>
199
246
  `
200
247
  : nothing}
201
248
  ${start && end && start !== end ? html ` <div select-block></div> ` : html ``}
202
- <input id="ime-buffer" type="text" />
249
+ <!--
250
+ 이 grid 의 키 입력은 이 입력칸이 받는다 — 칸을 눌러 편집을 열기 전까지 포커스가 여기에 있고,
251
+ 한글 IME 의 조합 중간 글자도 여기에 쌓인다. 곧 **포커스가 실제로 놓이는 자리**다.
252
+
253
+ 전에는 이름이 없었다. 눈에 보이지 않으니(opacity: 0) 문제가 아니라고 볼 수 있는데, 그렇지 않다.
254
+ 보이지 않는 것과 없는 것은 다르다 — 포커스를 받고 글자를 받는 입력칸이므로 스크린 리더는 이것을
255
+ 읽는다. 이름이 없으면 「편집 텍스트」 하나로만 읽히고, grid 안에서 화살표로 아무리 옮겨 다녀도
256
+ 읽히는 것이 바뀌지 않는다. 어느 칸에 서 있는지 알 방법이 없다.
257
+
258
+ aria-hidden 으로 감추는 것은 답이 아니다. 포커스를 받는 것을 보조기술에서 지우면 포커스가
259
+ 허공으로 사라진 것처럼 된다. 감추는 대신 **지금 서 있는 칸의 이름**을 말한다.
260
+ -->
261
+ <input
262
+ id="ime-buffer"
263
+ type="text"
264
+ aria-label=${this._imeBufferLabel(columns[focusedColumn])}
265
+ aria-activedescendant=${ifDefined(focusedRow >= 0 && focusedColumn >= 0 ? `cell-${focusedRow}-${focusedColumn}` : undefined)}
266
+ />
203
267
  <slot></slot>
204
268
  `;
205
269
  }
@@ -362,7 +426,7 @@ let DataGridBody = class DataGridBody extends LitElement {
362
426
  * 표를 벗어나면 지운다 — 남겨 두면 마지막 줄이 골라진 것처럼 보인다.
363
427
  *
364
428
  * **요소에 붙인다.** renderRoot 는 ShadowRoot 이고 pointerleave 는 요소에 가는 사건이라 거기서는
365
- * 안 먹었다. [재봤음] 표를 벗어난 뒤에도 표식 8개가 남아 있었다. 2026-09-02
429
+ * 안 먹었다. [확인함] 표를 벗어난 뒤에도 표식 8개가 남아 있었다. 2026-09-02
366
430
  */
367
431
  this.addEventListener('pointerleave', () => this.setHoveredRow(-1));
368
432
  this.renderRoot.addEventListener('click', dataGridBodyClickHandler.bind(this));
@@ -407,37 +471,45 @@ let DataGridBody = class DataGridBody extends LitElement {
407
471
  this.markRow(rowIndex, true);
408
472
  this._hoveredRow = rowIndex;
409
473
  }
410
- markRow(rowIndex, on) {
474
+ /**
475
+ * 줄 하나 — 그 줄의 칸을 담고 있는 요소.
476
+ *
477
+ * ── 자식을 세지 않는다 (2026-09-06) ────────────────────────────────────
478
+ * 전에는 `renderRoot.children` 을 `줄번호 * (칸수 + 1)` 로 셌다. 「+1」은 남는 폭을 메우는
479
+ * 빈 칸이고, 그 앞의 주석은 그 셈이 왜 필요한지를 설명하고 있었다 — 셈이 맞아야만 도는 코드였다.
480
+ *
481
+ * 지금은 줄이 **실제 요소**다(role=row). 셀 필요가 없다.
482
+ */
483
+ rowElementAt(rowIndex) {
411
484
  var _a;
412
485
  if (rowIndex < 0) {
486
+ return null;
487
+ }
488
+ return (_a = this.renderRoot) === null || _a === void 0 ? void 0 : _a.querySelectorAll('div[grid-row]').item(rowIndex);
489
+ }
490
+ markRow(rowIndex, on) {
491
+ const row = this.rowElementAt(rowIndex);
492
+ if (!row) {
413
493
  return;
414
494
  }
415
- const columns = this.columns.filter(column => !column.hidden).length;
416
495
  /*
417
- * 한 줄은 칸 수보다 **하나 많은** 자식을 갖는다. 마지막 하나는 남는 폭을 메우는 빈 칸이다
418
- * (`columnIndex` 가 -1 인 `ox-grid-field` — 위 render 참조).
419
- *
420
- * 전에는 칸 수만큼만 돌아서 **그 빈 칸만 색이 안 바뀌었다.** 표가 화면보다 좁으면 줄 오른쪽이
421
- * 잘려 보인다.
496
+ * 줄 안의 칸을 모두 바꾼다 — 남는 폭을 메우는 빈 칸까지.
422
497
  *
423
- * `getFieldByIndex` 로는 그 칸에 닿을 수 없다 — 열 번호를 `% columns` 로 감기 때문에 칸 수를
424
- * 넘기면 0번으로 돌아온다. 그래서 자식을 직접 센다.
498
+ * 전에는 그 빈 칸이 빠져서, 표가 화면보다 좁으면 줄 오른쪽이 잘려 보였다.
425
499
  */
426
- const cells = columns + 1;
427
- const first = rowIndex * cells;
428
- for (let index = 0; index < cells; index++) {
429
- (_a = this.renderRoot.children.item(first + index)) === null || _a === void 0 ? void 0 : _a.toggleAttribute('hovered-row', on);
430
- }
500
+ Array.from(row.children).forEach(cell => cell.toggleAttribute('hovered-row', on));
431
501
  }
432
502
  getFieldByIndex(rowIndex, columnIndex, residue = true) {
433
- if (rowIndex < 0) {
503
+ const row = this.rowElementAt(rowIndex);
504
+ if (!row) {
434
505
  return;
435
506
  }
436
507
  var columns = this.columns.filter(column => !column.hidden).length;
437
508
  if (!residue && columnIndex >= columns) {
438
509
  return;
439
510
  }
440
- return this.renderRoot.children.item(rowIndex * (columns + 1) /* 1 means last dummy column */ + ((columnIndex + columns) % columns));
511
+ /* 열 번호는 감는다 — 끝에서 오른쪽으로 가면 첫 열로 돌아온다(residue). */
512
+ return row.children.item((columnIndex + columns) % columns);
441
513
  }
442
514
  /**
443
515
  * 칸의 **팝업 표식**을 눌렀다 — 그 칸의 편집을 연다.
@@ -759,7 +831,7 @@ let DataGridBody = class DataGridBody extends LitElement {
759
831
  this.focusedField.isWorking = false;
760
832
  }
761
833
  }
762
- /* 엑셀식 타일링: 타겟 선택 영역이 소스 크기의 정수배이면 그 차원으로 반복, 아니면 1회 (차원별 독립 판정) */
834
+ /* 엑셀식 타일링: 타겟 선택 영역이 소스 크기의 정수배이면 그 차원으로 반복하고, 아니면 한 번만 붙여 넣는다 (차원마다 따로 판단) */
763
835
  const trs = Array.from(table.querySelectorAll('tr')).filter((r) => r instanceof HTMLTableRowElement);
764
836
  if (!trs.length) {
765
837
  return;
@@ -879,7 +951,7 @@ let DataGridBody = class DataGridBody extends LitElement {
879
951
  }
880
952
  else if (type === 'text/plain') {
881
953
  /* TSV 포맷 파싱: 줄바꿈으로 행 분리, 탭으로 열 분리 (엑셀 등 외부 소스 붙여넣기 지원) */
882
- /* CRLF 정규화 + 마지막 빈 줄 제거 (엑셀 TSV는 개행으로 끝남 — 빈 행이 소스 크기 판정을 깨뜨림) */
954
+ /* CRLF 정규화 + 마지막 빈 줄 제거 (엑셀 TSV 는 개행으로 끝난다. 빈 행이 소스 크기 판단을 깨뜨린다) */
883
955
  const tsvRows = content
884
956
  .replace(/\r\n/g, '\n')
885
957
  .replace(/\n$/, '')
@@ -888,7 +960,7 @@ let DataGridBody = class DataGridBody extends LitElement {
888
960
  if (!tsvRows.length) {
889
961
  return;
890
962
  }
891
- /* 엑셀식 타일링: 타겟 선택 영역이 소스 크기의 정수배이면 그 차원으로 반복, 아니면 1회 (차원별 독립 판정) */
963
+ /* 엑셀식 타일링: 타겟 선택 영역이 소스 크기의 정수배이면 그 차원으로 반복하고, 아니면 한 번만 붙여 넣는다 (차원마다 따로 판단) */
892
964
  const srcRows = tsvRows.length;
893
965
  const srcCols = Math.max(...tsvRows.map(r => r.length));
894
966
  const repeatR = srcRows > 0 && tgtRows % srcRows === 0 ? tgtRows / srcRows : 1;
@@ -977,6 +1049,18 @@ let DataGridBody = class DataGridBody extends LitElement {
977
1049
  return record;
978
1050
  }, {});
979
1051
  }
1052
+ /**
1053
+ * IME buffer 가 지금 어느 칸을 대신하고 있는지 — 스크린 리더가 읽을 이름.
1054
+ *
1055
+ * 이름의 출처는 column 정의 하나다(columnText). 포커스가 아직 아무 칸에도 없거나 이름을 구할 수
1056
+ * 없으면 「칸 입력」으로 물러난다 — 이름을 지어내지 않고, 그렇다고 이름을 비우지도 않는다.
1057
+ */
1058
+ _imeBufferLabel(column) {
1059
+ const name = columnText(column);
1060
+ return name
1061
+ ? i18next.t('text.grid-cell-input', { name, defaultValue: name })
1062
+ : i18next.t('text.grid-cell-input-plain', { defaultValue: 'cell input' });
1063
+ }
980
1064
  /** IME buffer를 포커스된 셀 위에 위치시킨다 */
981
1065
  _positionIMEBuffer() {
982
1066
  var _a;
@@ -1129,7 +1213,7 @@ DataGridBody.styles = [
1129
1213
  }
1130
1214
 
1131
1215
  @media (pointer: coarse) {
1132
- /* 터치 디바이스에서는 길게 눌러 텍스트 선택 허용 */
1216
+ /* 터치 기기에서는 길게 누르면 텍스트를 선택할 수 있게 둔다 */
1133
1217
  :host {
1134
1218
  user-select: auto;
1135
1219
  }
@@ -1173,6 +1257,9 @@ __decorate([
1173
1257
  __decorate([
1174
1258
  property({ type: Object })
1175
1259
  ], DataGridBody.prototype, "focused", void 0);
1260
+ __decorate([
1261
+ property({ type: Number })
1262
+ ], DataGridBody.prototype, "ariaRowIndexBase", void 0);
1176
1263
  __decorate([
1177
1264
  property({ type: Object })
1178
1265
  ], DataGridBody.prototype, "editTarget", void 0);