@operato/data-grist 10.9.0 → 10.10.1

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 +24 -0
  2. package/dist/src/configure/column-builder.js +16 -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 +125 -40
  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 +82 -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 +16 -1
  26. package/dist/src/editors/ox-grist-editor.js +29 -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 +16 -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 +322 -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 +406 -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,30 @@
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.1](https://github.com/hatiolab/operato/compare/v10.10.0...v10.10.1) (2026-09-06)
7
+
8
+
9
+ ### :memo: Documentation
10
+
11
+ * **data-grist, input, attachment:** rewrite today's comments and test names in English ([09983c0](https://github.com/hatiolab/operato/commit/09983c01bd7d47962180e7d5557a20bc5901b174))
12
+
13
+
14
+
15
+ ## [10.10.0](https://github.com/hatiolab/operato/compare/v10.9.0...v10.10.0) (2026-09-06)
16
+
17
+
18
+ ### :memo: Documentation
19
+
20
+ * **webcomponents:** 주석의 지어낸 한국어를 고친다 ([2553211](https://github.com/hatiolab/operato/commit/255321196bcfc6dc2fce0a8b9806d1341a6a7ce3))
21
+ * 지어낸 말 「재보니·재봤음」을 걷는다 ([0b2a04d](https://github.com/hatiolab/operato/commit/0b2a04d874f2e6f3bc44be4d04f4b3eafde998ef))
22
+
23
+
24
+ ### :rocket: New Features
25
+
26
+ * **data-grist:** 표를 표라고 선언하고, 입력칸에 이름을 붙인다 ([e45922b](https://github.com/hatiolab/operato/commit/e45922b51c0bd38f220a618ddc387459a92bef20))
27
+
28
+
29
+
6
30
  ## [10.9.0](https://github.com/hatiolab/operato/compare/v10.8.1...v10.9.0) (2026-09-05)
7
31
 
8
32
 
@@ -13,15 +13,26 @@ 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
+ * Keep the name as text too — with only a renderer, the name disappears here.
25
+ *
26
+ * `header: 'Status'` leaves this block as `() => 'Status'` and nothing else. That function returns
27
+ * a lit template, which cannot be read back, so everything that needs the name outside of
28
+ * rendering — accessible names, header filter inputs — had nowhere to get it and ended up
29
+ * spelling it out again. Two sources for one fact means editing `header` leaves the rest behind.
30
+ *
31
+ * Store what to draw and what it is called. `columnText()` reads this.
32
+ */
23
33
  if (typeof header == 'string') {
24
34
  compiled.header = {
35
+ text: header,
25
36
  renderer: () => header
26
37
  };
27
38
  }
@@ -29,6 +40,7 @@ export const buildColumn = (column) => {
29
40
  const text = compiled.header.renderer;
30
41
  compiled.header = {
31
42
  ...compiled.header,
43
+ text,
32
44
  renderer: () => text
33
45
  };
34
46
  }
@@ -38,11 +50,13 @@ export const buildColumn = (column) => {
38
50
  switch (type) {
39
51
  case 'boolean':
40
52
  compiled.label = {
41
- renderer: ((_b = compiled.header) === null || _b === void 0 ? void 0 : _b.renderer) || (() => name)
53
+ text: ((_b = compiled.header) === null || _b === void 0 ? void 0 : _b.text) || name,
54
+ renderer: ((_c = compiled.header) === null || _c === void 0 ? void 0 : _c.renderer) || (() => name)
42
55
  };
43
56
  break;
44
57
  case 'string':
45
58
  compiled.label = {
59
+ text: label,
46
60
  renderer: () => label
47
61
  };
48
62
  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 * Keep the name as text too — with only a renderer, the name disappears here.\n *\n * `header: 'Status'` leaves this block as `() => 'Status'` and nothing else. That function returns\n * a lit template, which cannot be read back, so everything that needs the name outside of\n * rendering — accessible names, header filter inputs — had nowhere to get it and ended up\n * spelling it out again. Two sources for one fact means editing `header` leaves the rest behind.\n *\n * Store what to draw and what it is called. `columnText()` reads this.\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
+ * Row wrapper — generates no box.
28
+ *
29
+ * It exists only to carry role=row, so it must stay out of layout. With display: contents the
30
+ * element produces no box and its children remain items of the outer grid — column widths, fixed
31
+ * columns and the grid-column placement of group headers are untouched.
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 * Row wrapper — generates no box.\n *\n * It exists only to carry role=row, so it must stay out of layout. With display: contents the\n * element produces no box and its children remain items of the outer grid — column widths, fixed\n * columns and the grid-column placement of group headers are untouched.\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
+ * Where this body's first row sits in the whole table, counting from 1.
18
+ *
19
+ * It combines header row count and page offset, so ox-grid computes it and passes it down —
20
+ * working it out here would mean the body has to know whether the header is one row or two.
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
+ * One row — the element holding that row's cells.
59
+ *
60
+ * This used to index `renderRoot.children` at `rowIndex * (columns + 1)`, where the +1 was the
61
+ * width filler; the comment above it existed to explain why that arithmetic was necessary. The
62
+ * code only worked while the count stayed correct.
63
+ *
64
+ * Rows are real elements now (role=row), so there is nothing to count.
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
+ * Which cell the IME buffer currently stands in for — the name a screen reader reads.
94
+ *
95
+ * The name comes from the column definition alone (columnText). When focus is on no cell, or the
96
+ * column yields no name, it falls back to "cell input" — no invented name, and no empty one.
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
+ * Where this body's first row sits in the whole table, counting from 1.
93
+ *
94
+ * It combines header row count and page offset, so ox-grid computes it and passes it down —
95
+ * working it out here would mean the body has to know whether the header is one row or two.
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
+ * Only emit aria-selected where rows can actually be selected.
137
+ *
138
+ * The attribute means "selectable, currently not selected", so putting false on every row of a
139
+ * grid without selection announces a control that is not there. Whether selection exists is
140
+ * something the grid already knows: it has a row-selector gutter or it does not.
141
+ */
142
+ const selectable = columns.some(column => column.gutterName === 'row-selector');
126
143
  /*
127
144
  * 레코드를 추가할 수 있는 경우에는 항상 추가 레코드를 보여준다.
128
145
  * 만약, 이전 방식처럼, 커서를 옮겨야만 새로운 레코드가 보이게 하고 싶다면, 조건부를 다음의 코드로 대체한다.
@@ -134,6 +151,15 @@ let DataGridBody = class DataGridBody extends LitElement {
134
151
  if (accumulator) {
135
152
  var accumRecord = this.buildAccumulatorRecord();
136
153
  }
154
+ /*
155
+ * aria-rowindex is the position in the whole table, not within the page on screen. The first
156
+ * row of page 2 is row 22, not row 1 again — otherwise every page turn resets the count and
157
+ * there is no way to tell how far through the data you are.
158
+ *
159
+ * Header rows count too, from 1. With a grouped header that is two rows, so the body starts
160
+ * at 3.
161
+ */
162
+ const rowIndexBase = this.ariaRowIndexBase;
137
163
  return html `
138
164
  ${records.map((record, idxRow) => {
139
165
  var attrFocusedRow = idxRow === focusedRow;
@@ -142,8 +168,17 @@ let DataGridBody = class DataGridBody extends LitElement {
142
168
  var dirtyFields = record['__dirtyfields__'] || {};
143
169
  var { emphasized } = classifier.call(null, record, idxRow) || {};
144
170
  return html `
171
+ <div
172
+ grid-row
173
+ role="row"
174
+ aria-rowindex=${rowIndexBase + idxRow}
175
+ aria-selected=${ifDefined(selectable ? !!attrSelected : undefined)}
176
+ >
145
177
  ${columns.map((column, idxColumn) => html `
146
178
  <ox-grid-field
179
+ id=${'cell-' + idxRow + '-' + idxColumn}
180
+ role="gridcell"
181
+ aria-colindex=${idxColumn + 1}
147
182
  .data=${data}
148
183
  .type=${column.type}
149
184
  .rowIndex=${idxRow}
@@ -165,7 +200,12 @@ let DataGridBody = class DataGridBody extends LitElement {
165
200
  fixed=${ifDefined(this.fixedLefts[idxColumn])}
166
201
  ></ox-grid-field>
167
202
  `)}
203
+ <!--
204
+ Width filler — nothing to read. It stays inside the row so hover and selection colour it
205
+ with the rest, but it is not counted as a cell.
206
+ -->
168
207
  <ox-grid-field
208
+ role="presentation"
169
209
  .data=${data}
170
210
  .rowIndex=${idxRow}
171
211
  .columnIndex=${-1}
@@ -175,31 +215,54 @@ let DataGridBody = class DataGridBody extends LitElement {
175
215
  ?focused-row=${attrFocusedRow}
176
216
  ?selected-row=${attrSelected}
177
217
  ></ox-grid-field>
218
+ </div>
178
219
  `;
179
220
  })}
180
221
  ${accumulator
181
222
  ? 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>
223
+ <div grid-row role="row" aria-rowindex=${rowIndexBase + records.length}>
224
+ ${columns.map((column, idxColumn) => html `
225
+ <ox-grid-accum-field
226
+ role="gridcell"
227
+ aria-colindex=${idxColumn + 1}
228
+ .data=${data}
229
+ .columnIndex=${idxColumn}
230
+ .rowIndex=${records.length}
231
+ .column=${column}
232
+ .record=${accumRecord}
233
+ .value=${accumRecord[column.name]}
234
+ fixed=${ifDefined(this.fixedLefts[idxColumn])}
235
+ ></ox-grid-accum-field>
236
+ `)}
237
+ <ox-grid-accum-field
238
+ role="presentation"
239
+ .data=${data}
240
+ .columnIndex=${-1}
241
+ .rowIndex=${records.length}
242
+ .record=${accumRecord}
243
+ ></ox-grid-accum-field>
244
+ </div>
199
245
  `
200
246
  : nothing}
201
247
  ${start && end && start !== end ? html ` <div select-block></div> ` : html ``}
202
- <input id="ime-buffer" type="text" />
248
+ <!--
249
+ This input takes the grid's keystrokes. Focus rests here until a cell opens for editing, and
250
+ IME composition builds up in it — this is where focus actually lives.
251
+
252
+ It had no name. opacity: 0 makes that look harmless, but invisible is not the same as absent:
253
+ it receives focus and receives text, so a screen reader reads it. Unnamed, it reads as one
254
+ "edit text" and never changes, no matter how far the arrow keys travel — there is no way to
255
+ tell which cell you are on.
256
+
257
+ aria-hidden is not the fix. Hiding the thing that holds focus makes focus vanish into
258
+ nothing. Instead of hiding it, name it after the cell it currently stands in for.
259
+ -->
260
+ <input
261
+ id="ime-buffer"
262
+ type="text"
263
+ aria-label=${this._imeBufferLabel(columns[focusedColumn])}
264
+ aria-activedescendant=${ifDefined(focusedRow >= 0 && focusedColumn >= 0 ? `cell-${focusedRow}-${focusedColumn}` : undefined)}
265
+ />
203
266
  <slot></slot>
204
267
  `;
205
268
  }
@@ -362,7 +425,7 @@ let DataGridBody = class DataGridBody extends LitElement {
362
425
  * 표를 벗어나면 지운다 — 남겨 두면 마지막 줄이 골라진 것처럼 보인다.
363
426
  *
364
427
  * **요소에 붙인다.** renderRoot 는 ShadowRoot 이고 pointerleave 는 요소에 가는 사건이라 거기서는
365
- * 안 먹었다. [재봤음] 표를 벗어난 뒤에도 표식 8개가 남아 있었다. 2026-09-02
428
+ * 안 먹었다. [확인함] 표를 벗어난 뒤에도 표식 8개가 남아 있었다. 2026-09-02
366
429
  */
367
430
  this.addEventListener('pointerleave', () => this.setHoveredRow(-1));
368
431
  this.renderRoot.addEventListener('click', dataGridBodyClickHandler.bind(this));
@@ -407,37 +470,44 @@ let DataGridBody = class DataGridBody extends LitElement {
407
470
  this.markRow(rowIndex, true);
408
471
  this._hoveredRow = rowIndex;
409
472
  }
410
- markRow(rowIndex, on) {
473
+ /**
474
+ * One row — the element holding that row's cells.
475
+ *
476
+ * This used to index `renderRoot.children` at `rowIndex * (columns + 1)`, where the +1 was the
477
+ * width filler; the comment above it existed to explain why that arithmetic was necessary. The
478
+ * code only worked while the count stayed correct.
479
+ *
480
+ * Rows are real elements now (role=row), so there is nothing to count.
481
+ */
482
+ rowElementAt(rowIndex) {
411
483
  var _a;
412
484
  if (rowIndex < 0) {
485
+ return null;
486
+ }
487
+ return (_a = this.renderRoot) === null || _a === void 0 ? void 0 : _a.querySelectorAll('div[grid-row]').item(rowIndex);
488
+ }
489
+ markRow(rowIndex, on) {
490
+ const row = this.rowElementAt(rowIndex);
491
+ if (!row) {
413
492
  return;
414
493
  }
415
- const columns = this.columns.filter(column => !column.hidden).length;
416
494
  /*
417
- * 한 줄은 칸 수보다 **하나 많은** 자식을 갖는다. 마지막 하나는 남는 폭을 메우는 빈 칸이다
418
- * (`columnIndex` 가 -1 인 `ox-grid-field` — 위 render 참조).
419
- *
420
- * 전에는 칸 수만큼만 돌아서 **그 빈 칸만 색이 안 바뀌었다.** 표가 화면보다 좁으면 줄 오른쪽이
421
- * 잘려 보인다.
422
- *
423
- * `getFieldByIndex` 로는 그 칸에 닿을 수 없다 — 열 번호를 `% columns` 로 감기 때문에 칸 수를
424
- * 넘기면 0번으로 돌아온다. 그래서 자식을 직접 센다.
495
+ * Mark every cell in the row, width filler included. It used to be left out, so on a grid
496
+ * narrower than the viewport the right end of the row stayed uncoloured.
425
497
  */
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
- }
498
+ Array.from(row.children).forEach(cell => cell.toggleAttribute('hovered-row', on));
431
499
  }
432
500
  getFieldByIndex(rowIndex, columnIndex, residue = true) {
433
- if (rowIndex < 0) {
501
+ const row = this.rowElementAt(rowIndex);
502
+ if (!row) {
434
503
  return;
435
504
  }
436
505
  var columns = this.columns.filter(column => !column.hidden).length;
437
506
  if (!residue && columnIndex >= columns) {
438
507
  return;
439
508
  }
440
- return this.renderRoot.children.item(rowIndex * (columns + 1) /* 1 means last dummy column */ + ((columnIndex + columns) % columns));
509
+ /* Column index wraps: moving right from the last column returns to the first (residue). */
510
+ return row.children.item((columnIndex + columns) % columns);
441
511
  }
442
512
  /**
443
513
  * 칸의 **팝업 표식**을 눌렀다 — 그 칸의 편집을 연다.
@@ -759,7 +829,7 @@ let DataGridBody = class DataGridBody extends LitElement {
759
829
  this.focusedField.isWorking = false;
760
830
  }
761
831
  }
762
- /* 엑셀식 타일링: 타겟 선택 영역이 소스 크기의 정수배이면 그 차원으로 반복, 아니면 1회 (차원별 독립 판정) */
832
+ /* 엑셀식 타일링: 타겟 선택 영역이 소스 크기의 정수배이면 그 차원으로 반복하고, 아니면 한 번만 붙여 넣는다 (차원마다 따로 판단) */
763
833
  const trs = Array.from(table.querySelectorAll('tr')).filter((r) => r instanceof HTMLTableRowElement);
764
834
  if (!trs.length) {
765
835
  return;
@@ -879,7 +949,7 @@ let DataGridBody = class DataGridBody extends LitElement {
879
949
  }
880
950
  else if (type === 'text/plain') {
881
951
  /* TSV 포맷 파싱: 줄바꿈으로 행 분리, 탭으로 열 분리 (엑셀 등 외부 소스 붙여넣기 지원) */
882
- /* CRLF 정규화 + 마지막 빈 줄 제거 (엑셀 TSV는 개행으로 끝남 — 빈 행이 소스 크기 판정을 깨뜨림) */
952
+ /* CRLF 정규화 + 마지막 빈 줄 제거 (엑셀 TSV 는 개행으로 끝난다. 빈 행이 소스 크기 판단을 깨뜨린다) */
883
953
  const tsvRows = content
884
954
  .replace(/\r\n/g, '\n')
885
955
  .replace(/\n$/, '')
@@ -888,7 +958,7 @@ let DataGridBody = class DataGridBody extends LitElement {
888
958
  if (!tsvRows.length) {
889
959
  return;
890
960
  }
891
- /* 엑셀식 타일링: 타겟 선택 영역이 소스 크기의 정수배이면 그 차원으로 반복, 아니면 1회 (차원별 독립 판정) */
961
+ /* 엑셀식 타일링: 타겟 선택 영역이 소스 크기의 정수배이면 그 차원으로 반복하고, 아니면 한 번만 붙여 넣는다 (차원마다 따로 판단) */
892
962
  const srcRows = tsvRows.length;
893
963
  const srcCols = Math.max(...tsvRows.map(r => r.length));
894
964
  const repeatR = srcRows > 0 && tgtRows % srcRows === 0 ? tgtRows / srcRows : 1;
@@ -977,6 +1047,18 @@ let DataGridBody = class DataGridBody extends LitElement {
977
1047
  return record;
978
1048
  }, {});
979
1049
  }
1050
+ /**
1051
+ * Which cell the IME buffer currently stands in for — the name a screen reader reads.
1052
+ *
1053
+ * The name comes from the column definition alone (columnText). When focus is on no cell, or the
1054
+ * column yields no name, it falls back to "cell input" — no invented name, and no empty one.
1055
+ */
1056
+ _imeBufferLabel(column) {
1057
+ const name = columnText(column);
1058
+ return name
1059
+ ? i18next.t('text.grid-cell-input', { name, defaultValue: name })
1060
+ : i18next.t('text.grid-cell-input-plain', { defaultValue: 'cell input' });
1061
+ }
980
1062
  /** IME buffer를 포커스된 셀 위에 위치시킨다 */
981
1063
  _positionIMEBuffer() {
982
1064
  var _a;
@@ -1129,7 +1211,7 @@ DataGridBody.styles = [
1129
1211
  }
1130
1212
 
1131
1213
  @media (pointer: coarse) {
1132
- /* 터치 디바이스에서는 길게 눌러 텍스트 선택 허용 */
1214
+ /* 터치 기기에서는 길게 누르면 텍스트를 선택할 수 있게 둔다 */
1133
1215
  :host {
1134
1216
  user-select: auto;
1135
1217
  }
@@ -1173,6 +1255,9 @@ __decorate([
1173
1255
  __decorate([
1174
1256
  property({ type: Object })
1175
1257
  ], DataGridBody.prototype, "focused", void 0);
1258
+ __decorate([
1259
+ property({ type: Number })
1260
+ ], DataGridBody.prototype, "ariaRowIndexBase", void 0);
1176
1261
  __decorate([
1177
1262
  property({ type: Object })
1178
1263
  ], DataGridBody.prototype, "editTarget", void 0);