@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/data-grist",
3
- "version": "10.9.0",
3
+ "version": "10.10.1",
4
4
  "description": "User interface for grid (desktop) and list (mobile)",
5
5
  "author": "heartyoh",
6
6
  "type": "module",
@@ -69,11 +69,12 @@
69
69
  "dependencies": {
70
70
  "@material/web": "^2.0.0",
71
71
  "@operato/headroom": "^10.0.0",
72
- "@operato/input": "^10.9.0",
73
- "@operato/p13n": "^10.9.0",
74
- "@operato/popup": "^10.9.0",
72
+ "@operato/i18n": "^10.0.0",
73
+ "@operato/input": "^10.10.1",
74
+ "@operato/p13n": "^10.10.0",
75
+ "@operato/popup": "^10.10.0",
75
76
  "@operato/pull-to-refresh": "^10.0.0",
76
- "@operato/styles": "^10.9.0",
77
+ "@operato/styles": "^10.10.0",
77
78
  "@operato/time-calculator": "^10.0.0",
78
79
  "@operato/utils": "^10.7.0",
79
80
  "i18next": "^24.2.1",
@@ -114,5 +115,5 @@
114
115
  "prettier --write"
115
116
  ]
116
117
  },
117
- "gitHead": "5282356a0fbf7601a1bd023cf65ba5dc60986337"
118
+ "gitHead": "b40e170b9e6a24e28935300dd2c77d8b8e5ce8a2"
118
119
  }
@@ -0,0 +1,406 @@
1
+ import { expect, fixture } from '@open-wc/testing'
2
+ import { html } from 'lit'
3
+
4
+ import '../src/data-grid/data-grid.js'
5
+ import '../src/data-grid/data-grid-body.js'
6
+ import { buildColumn } from '../src/configure/column-builder.js'
7
+ import { columnText } from '../src/utils/column-text.js'
8
+ import { ZERO_CONFIG, ZERO_DATA } from '../src/configure/zero-config.js'
9
+
10
+ /**
11
+ * Accessible names on the inputs inside the grid.
12
+ *
13
+ * ## Why this is tested
14
+ *
15
+ * Several inputs here had no name: the row-select checkboxes, the header filter boxes, and the IME
16
+ * buffer that holds focus. Position carries the meaning for a sighted reader — a narrow leftmost
17
+ * column means row select, a box under a header name filters that column — but position is a
18
+ * picture, not a relationship, so nothing of it reaches a screen reader.
19
+ *
20
+ * ## What has to hold
21
+ *
22
+ * The name has one source. Editing `header` must change what is announced, which means callers
23
+ * cannot spell the name out again. `columnText()` is that source.
24
+ *
25
+ * `buildColumn` turns a string header into `() => header`, and that function returns a lit template
26
+ * the name cannot be read back out of. The name was disappearing right there.
27
+ */
28
+
29
+ describe('column name has one source', () => {
30
+ it('keeps the plain text after a string header becomes a function', () => {
31
+ const column = buildColumn({ type: 'string', name: 'palletId', header: '파렛트 번호' })
32
+
33
+ expect(columnText(column)).to.equal('파렛트 번호')
34
+ })
35
+
36
+ it('keeps it when the string arrives as header.renderer', () => {
37
+ const column = buildColumn({ type: 'string', name: 'palletId', header: { renderer: '파렛트 번호' } })
38
+
39
+ expect(columnText(column)).to.equal('파렛트 번호')
40
+ })
41
+
42
+ it('lets label: true inherit the header name', () => {
43
+ const column = buildColumn({ type: 'string', name: 'palletId', header: '파렛트 번호', label: true })
44
+
45
+ expect(columnText(column)).to.equal('파렛트 번호')
46
+ })
47
+
48
+ it('falls back to column.name when the header is drawn by a function', () => {
49
+ const column = buildColumn({ type: 'string', name: 'palletId', header: { renderer: () => html`<i>x</i>` } })
50
+
51
+ expect(columnText(column)).to.equal('palletId')
52
+ })
53
+
54
+ it('invents no name when there is no column', () => {
55
+ expect(columnText(undefined)).to.equal('')
56
+ expect(columnText(null)).to.equal('')
57
+ })
58
+ })
59
+
60
+ describe('row-select boxes are named', () => {
61
+ const columns = [
62
+ { type: 'gutter', gutterName: 'row-selector', name: '__selected__', multiple: true },
63
+ { type: 'string', name: 'palletId', header: '파렛트 번호' }
64
+ ].map(buildColumn)
65
+
66
+ it('says which row each checkbox selects', async () => {
67
+ const body: any = await fixture(html`
68
+ <ox-grid-body
69
+ .config=${{ ...ZERO_CONFIG, columns }}
70
+ .columns=${columns}
71
+ .data=${{ ...ZERO_DATA, records: [{ palletId: 'P-0001' }, { palletId: 'P-0002' }] }}
72
+ ></ox-grid-body>
73
+ `)
74
+
75
+ await body.updateComplete
76
+
77
+ /* The checkboxes live one shadow root deeper, inside each ox-grid-field. */
78
+ const fields = Array.from(body.renderRoot.querySelectorAll('ox-grid-field')) as any[]
79
+
80
+ await Promise.all(fields.map(field => field.updateComplete))
81
+
82
+ const boxes = fields.flatMap(field =>
83
+ Array.from(field.renderRoot?.querySelectorAll('input[type=checkbox], input[type=radio]') || [])
84
+ ) as HTMLInputElement[]
85
+
86
+ expect(boxes.length, 'found no row-select box').to.be.greaterThan(0)
87
+ boxes.forEach(box =>
88
+ expect(box.getAttribute('aria-label'), 'an unnamed checkbox is left').to.be.a('string').and.not.equal('')
89
+ )
90
+ })
91
+ })
92
+
93
+ describe('IME buffer names the cell it stands in for', () => {
94
+ const columns = [
95
+ { type: 'string', name: 'palletId', header: '파렛트 번호' },
96
+ { type: 'string', name: 'location', header: '보관 위치' }
97
+ ].map(buildColumn)
98
+
99
+ async function bodyFocusedAt(column: number) {
100
+ const body: any = await fixture(html`
101
+ <ox-grid-body
102
+ .config=${{ ...ZERO_CONFIG, columns }}
103
+ .columns=${columns}
104
+ .data=${{ ...ZERO_DATA, records: [{ palletId: 'P-0001', location: 'A-01' }] }}
105
+ .focused=${{ row: 0, column }}
106
+ ></ox-grid-body>
107
+ `)
108
+
109
+ await body.updateComplete
110
+
111
+ return body.renderRoot.querySelector('#ime-buffer') as HTMLInputElement
112
+ }
113
+
114
+ it('reads the first column while focus sits there', async () => {
115
+ expect((await bodyFocusedAt(0)).getAttribute('aria-label')).to.contain('파렛트 번호')
116
+ })
117
+
118
+ it('changes what it reads when focus moves a column over', async () => {
119
+ expect((await bodyFocusedAt(1)).getAttribute('aria-label')).to.contain('보관 위치')
120
+ })
121
+
122
+ it('stays visible to assistive tech — it is where focus lands', async () => {
123
+ expect((await bodyFocusedAt(0)).getAttribute('aria-hidden')).to.be.null
124
+ })
125
+ })
126
+
127
+ /**
128
+ * Table semantics.
129
+ *
130
+ * ## Why this is tested
131
+ *
132
+ * The grid is drawn with CSS grid: cells sit flat and there is no row element. Rows are a picture
133
+ * made of equal offsetTop, so a screen reader saw a pile of inputs rather than a table — no "this
134
+ * is a table", no "row 3 column 3", no "40 rows in all".
135
+ *
136
+ * Adding row wrappers can break two things at once:
137
+ *
138
+ * ```
139
+ * semantics grid / row / columnheader / gridcell have to nest correctly
140
+ * layout display: contents must generate no box; if it does, the columns collapse
141
+ * ```
142
+ *
143
+ * The second one is why geometry is asserted: cells in a row must step right, and rows must step
144
+ * down.
145
+ */
146
+ describe('grid declares itself a grid', () => {
147
+ /* ox-grid takes already-compiled columns; compiling them is ox-grist's job. */
148
+ const columns = [
149
+ { type: 'gutter', gutterName: 'row-selector', name: '__selected__', multiple: true },
150
+ { type: 'string', name: 'palletId', header: '파렛트 번호', sortable: true },
151
+ { type: 'string', name: 'location', header: '보관 위치', sortable: true }
152
+ ].map(buildColumn)
153
+
154
+ async function grid(overrides: any = {}) {
155
+ const el: any = await fixture(html`
156
+ <ox-grid
157
+ aria-label="입고 대기 목록"
158
+ .config=${{ ...ZERO_CONFIG, columns, rows: { ...ZERO_CONFIG.rows } }}
159
+ .data=${{
160
+ page: 1,
161
+ limit: 20,
162
+ total: 40,
163
+ records: [
164
+ { palletId: 'P-0001', location: 'A-01' },
165
+ { palletId: 'P-0002', location: 'A-02' }
166
+ ],
167
+ ...overrides
168
+ }}
169
+ ></ox-grid>
170
+ `)
171
+
172
+ await el.updateComplete
173
+
174
+ const header = el.renderRoot.querySelector('ox-grid-header')
175
+ const body = el.renderRoot.querySelector('ox-grid-body')
176
+
177
+ await header?.updateComplete
178
+ await body?.updateComplete
179
+
180
+ return { el, header, body }
181
+ }
182
+
183
+ it('nests grid, row and cell', async () => {
184
+ const { el, header, body } = await grid()
185
+
186
+ const grid_ = el.renderRoot.querySelector('[role=grid]')
187
+
188
+ expect(grid_, 'no role=grid').to.exist
189
+ expect(header.getAttribute('role')).to.equal('rowgroup')
190
+ expect(body.getAttribute('role')).to.equal('rowgroup')
191
+
192
+ expect(header.renderRoot.querySelectorAll('[role=row]').length).to.equal(1)
193
+ expect(body.renderRoot.querySelectorAll('[role=row]').length).to.equal(2)
194
+ expect(header.renderRoot.querySelectorAll('[role=columnheader]').length).to.equal(3)
195
+ })
196
+
197
+ it('counts the whole table, not the page on screen', async () => {
198
+ const { el } = await grid()
199
+
200
+ const grid_ = el.renderRoot.querySelector('[role=grid]')
201
+
202
+ /* One header row plus 40 records. Two rows drawn is still a 40-row table. */
203
+ expect(grid_.getAttribute('aria-rowcount')).to.equal('41')
204
+ expect(grid_.getAttribute('aria-colcount')).to.equal('3')
205
+ expect(grid_.getAttribute('aria-label')).to.equal('입고 대기 목록')
206
+ })
207
+
208
+ it('numbers page 2 from row 22, not back at 1', async () => {
209
+ const { body } = await grid({ page: 2 })
210
+
211
+ const rows = Array.from(body.renderRoot.querySelectorAll('[role=row]')) as HTMLElement[]
212
+
213
+ /* One header row plus 20 records on page 1. */
214
+ expect(rows[0].getAttribute('aria-rowindex')).to.equal('22')
215
+ expect(rows[1].getAttribute('aria-rowindex')).to.equal('23')
216
+ })
217
+
218
+ it('numbers columns and leaves the width filler uncounted', async () => {
219
+ const { body } = await grid()
220
+
221
+ const row = body.renderRoot.querySelector('[role=row]') as HTMLElement
222
+ const cells = Array.from(row.querySelectorAll('[role=gridcell]')) as HTMLElement[]
223
+
224
+ expect(cells.map(cell => cell.getAttribute('aria-colindex'))).to.deep.equal(['1', '2', '3'])
225
+ expect(row.querySelectorAll('[role=presentation]').length, 'the filler was counted as a cell').to.equal(1)
226
+ })
227
+
228
+ it('states sort direction in text, not only as an arrow', async () => {
229
+ const el: any = await fixture(html`
230
+ <ox-grid
231
+ .config=${{ ...ZERO_CONFIG, columns, rows: { ...ZERO_CONFIG.rows } }}
232
+ .sorters=${[{ name: 'palletId', desc: true }]}
233
+ .data=${{ records: [{ palletId: 'P-0001', location: 'A-01' }] }}
234
+ ></ox-grid>
235
+ `)
236
+
237
+ await el.updateComplete
238
+
239
+ const header = el.renderRoot.querySelector('ox-grid-header')
240
+ await header.updateComplete
241
+
242
+ const headers = Array.from(header.renderRoot.querySelectorAll('[role=columnheader]')) as HTMLElement[]
243
+ const sorts = headers.map(cell => cell.getAttribute('aria-sort'))
244
+
245
+ /* A gutter cannot be sorted; claiming otherwise announces a control that is not there. */
246
+ expect(sorts[0]).to.be.null
247
+ expect(sorts[1], 'sorted descending').to.equal('descending')
248
+ expect(sorts[2], 'sortable but not currently sorted').to.equal('none')
249
+ })
250
+
251
+ it('keeps the layout — a row wrapper that generates a box collapses the grid', async () => {
252
+ const { body } = await grid()
253
+
254
+ const rows = Array.from(body.renderRoot.querySelectorAll('[role=row]')) as HTMLElement[]
255
+ const first = Array.from(rows[0].children) as HTMLElement[]
256
+
257
+ expect(getComputedStyle(rows[0]).display).to.equal('contents')
258
+
259
+ /* Cells step right; stacked at one x means the columns are gone. */
260
+ const lefts = first.map(cell => cell.offsetLeft)
261
+ expect(new Set(lefts).size, 'cells stacked at the same x').to.be.greaterThan(1)
262
+ lefts.slice(1).forEach((left, index) => expect(left).to.be.greaterThan(lefts[index]))
263
+
264
+ /* Rows step down. */
265
+ const firstTop = (rows[0].children[0] as HTMLElement).offsetTop
266
+ const secondTop = (rows[1].children[0] as HTMLElement).offsetTop
267
+ expect(secondTop, 'two rows at the same y').to.be.greaterThan(firstTop)
268
+ })
269
+ })
270
+
271
+ /**
272
+ * Grouped headers.
273
+ *
274
+ * The header becomes two rows and ungrouped columns span both (`.span-both`). Placement runs
275
+ * through CSS `grid-row` / `grid-column`, which makes this the riskiest spot for a row wrapper.
276
+ */
277
+ describe('grouped header spans two rows', () => {
278
+ const columns = [
279
+ { type: 'string', name: 'palletId', header: '파렛트 번호' },
280
+ { type: 'string', name: 'width', header: { renderer: '폭', group: '치수' } },
281
+ { type: 'string', name: 'height', header: { renderer: '높이', group: '치수' } }
282
+ ].map(buildColumn)
283
+
284
+ async function grouped() {
285
+ const el: any = await fixture(html`
286
+ <ox-grid
287
+ .config=${{ ...ZERO_CONFIG, columns, rows: { ...ZERO_CONFIG.rows } }}
288
+ .data=${{ total: 1, records: [{ palletId: 'P-0001', width: '1200', height: '900' }] }}
289
+ ></ox-grid>
290
+ `)
291
+
292
+ await el.updateComplete
293
+
294
+ const header = el.renderRoot.querySelector('ox-grid-header')
295
+ const body = el.renderRoot.querySelector('ox-grid-body')
296
+
297
+ await header.updateComplete
298
+ await body.updateComplete
299
+
300
+ return { el, header, body }
301
+ }
302
+
303
+ it('counts two header rows so the body starts at row 3', async () => {
304
+ const { el, header, body } = await grouped()
305
+
306
+ expect(header.renderRoot.querySelectorAll('[role=row]').length).to.equal(2)
307
+
308
+ /* Two header rows plus one record. */
309
+ expect(el.renderRoot.querySelector('[role=grid]').getAttribute('aria-rowcount')).to.equal('3')
310
+
311
+ const rows = Array.from(body.renderRoot.querySelectorAll('[role=row]')) as HTMLElement[]
312
+ expect(rows[0].getAttribute('aria-rowindex')).to.equal('3')
313
+ })
314
+
315
+ it('says how many columns a group covers', async () => {
316
+ const { header } = await grouped()
317
+
318
+ const cells = Array.from(header.renderRoot.querySelectorAll('[role=columnheader]')) as HTMLElement[]
319
+ const first = cells[0]
320
+ const group = cells.find(cell => cell.getAttribute('aria-colspan'))
321
+
322
+ /* An ungrouped column spans both header rows. */
323
+ expect(first.getAttribute('aria-rowspan')).to.equal('2')
324
+ expect(first.getAttribute('aria-colindex')).to.equal('1')
325
+
326
+ /* The group header covers columns 2 and 3. */
327
+ expect(group, 'found no group header').to.exist
328
+ expect(group!.getAttribute('aria-colindex')).to.equal('2')
329
+ expect(group!.getAttribute('aria-colspan')).to.equal('2')
330
+ })
331
+
332
+ it('keeps two-row header placement — group sits above its leaf columns', async () => {
333
+ const { header } = await grouped()
334
+
335
+ const cells = Array.from(header.renderRoot.querySelectorAll('[role=columnheader]')) as HTMLElement[]
336
+ const group = cells.find(cell => cell.getAttribute('aria-colspan'))!
337
+ const leaf = cells.find(cell => !cell.getAttribute('aria-colspan') && cell !== cells[0])!
338
+
339
+ expect(leaf.offsetTop, 'group header and its leaf column at the same height').to.be.greaterThan(group.offsetTop)
340
+ expect(group.offsetWidth, 'group header does not cover two columns').to.be.greaterThan(leaf.offsetWidth)
341
+ })
342
+ })
343
+
344
+ /**
345
+ * The grid wrapper must not disturb the surrounding layout.
346
+ *
347
+ * `role=grid` admits only rows, so the settings-button slot and the pager moved outside the
348
+ * wrapper. Two things then depend on this structure:
349
+ *
350
+ * ```
351
+ * column layout the host is a flex column. A wrapper that generates a box swallows header
352
+ * and body into one flex item, and the body's flex: 1 stops working
353
+ * sibling selector "show the settings button on header hover" is an adjacent-sibling rule, so
354
+ * the button has to sit directly after the wrapper
355
+ * ```
356
+ */
357
+ describe('grid wrapper stays out of the layout', () => {
358
+ const columns = [
359
+ { type: 'string', name: 'palletId', header: '파렛트 번호' },
360
+ { type: 'string', name: 'location', header: '보관 위치' }
361
+ ].map(buildColumn)
362
+
363
+ it('places the settings button after the grid, not inside it', async () => {
364
+ const el: any = await fixture(html`
365
+ <ox-grid
366
+ .config=${{ ...ZERO_CONFIG, columns, rows: { ...ZERO_CONFIG.rows } }}
367
+ .data=${{ total: 1, records: [{ palletId: 'P-0001', location: 'A-01' }] }}
368
+ ></ox-grid>
369
+ `)
370
+
371
+ await el.updateComplete
372
+
373
+ const grid = el.renderRoot.querySelector('[role=grid]') as HTMLElement
374
+ const setting = el.renderRoot.querySelector('div[setting]') as HTMLElement
375
+
376
+ expect(setting, 'no settings slot').to.exist
377
+ expect(grid.contains(setting), 'settings button inside the grid breaks table reading').to.be.false
378
+ expect(grid.nextElementSibling, 'adjacent-sibling rule no longer holds').to.equal(setting)
379
+ })
380
+
381
+ it('leaves header and body as flex items of the host', async () => {
382
+ const el: any = await fixture(html`
383
+ <div style="height: 300px; display: flex">
384
+ <ox-grid
385
+ style="flex: 1"
386
+ .config=${{ ...ZERO_CONFIG, columns, rows: { ...ZERO_CONFIG.rows } }}
387
+ .data=${{ total: 1, records: [{ palletId: 'P-0001', location: 'A-01' }] }}
388
+ ></ox-grid>
389
+ </div>
390
+ `)
391
+
392
+ const grid = el.querySelector('ox-grid') as any
393
+ await grid.updateComplete
394
+
395
+ const wrapper = grid.renderRoot.querySelector('[role=grid]') as HTMLElement
396
+ const header = grid.renderRoot.querySelector('ox-grid-header') as HTMLElement
397
+ const body = grid.renderRoot.querySelector('ox-grid-body') as HTMLElement
398
+
399
+ expect(getComputedStyle(wrapper).display).to.equal('contents')
400
+
401
+ /* The body takes the remaining height; a wrapper with a box shrinks it to header height. */
402
+ expect(body.offsetHeight, 'body does not take the remaining height').to.be.greaterThan(100)
403
+ expect(body.offsetTop, 'body is not below the header').to.be.greaterThan(0)
404
+ expect(header.offsetTop).to.equal(0)
405
+ })
406
+ })
@@ -12,7 +12,7 @@ import { DataGrid } from '../src/data-grid/data-grid.js'
12
12
  * 줄과 띠가 나란히 안 보여도 그 시험은 전부 초록이다. 실제로 그랬다.
13
13
  *
14
14
  * ```
15
- * [재봤음] 2026-09-04 · operato-plant 인원 화면(어두운 모드)
15
+ * [확인함] 2026-09-04 · operato-plant 인원 화면(어두운 모드)
16
16
  * 줄 rgb(30 34 40) · 띠 rgb(26 30 35) → 대비 1.048 보이지 않는다
17
17
  * 그때 고정 열 시험 열여섯 건은 전부 초록이었다
18
18
  * ```
@@ -123,7 +123,7 @@ const 팔레트들 = {
123
123
  * 얼마나 벌어져야 보이나.
124
124
  *
125
125
  * 지나는 줄 표시는 **훨씬 옅어야 한다** — 고른 줄과 같은 무게로 그리면 무엇이 골라졌는지 흐려진다.
126
- * 그래서 큰 값을 요구하지 않는다. 다만 1.05 는 눈에 안 들어온다(위 [재봤음]). 눈으로 확인한 값이
126
+ * 그래서 큰 값을 요구하지 않는다. 다만 1.05 는 눈에 안 들어온다(위 [확인함]). 눈으로 확인한 값이
127
127
  * 1.11 · 1.14 이므로 그 아래에 문턱을 둔다.
128
128
  */
129
129
  const 최소분리 = 1.08
@@ -6,10 +6,18 @@
6
6
  "label.accumulator_max": "min",
7
7
  "label.accumulator_min": "max",
8
8
  "label.accumulator_sum": "sum",
9
+ "label.filter": "filter",
10
+ "text.grid-cell-input": "{name} input",
11
+ "text.grid-cell-input-plain": "cell input",
9
12
  "text.grid-clear-search": "clear the search or filters to see everything",
13
+ "text.grid-filter-from": "{name} filter, from",
14
+ "text.grid-filter-of": "{name} filter",
15
+ "text.grid-filter-to": "{name} filter, to",
10
16
  "text.grid-no-records": "nothing to show",
11
17
  "text.grid-nothing-found": "nothing found",
12
18
  "text.grid-open-editor": "press to edit",
19
+ "text.grid-select-all-records": "select all rows",
20
+ "text.grid-select-record": "select row {row}",
13
21
  "text.grid-total-records": "{total} in total",
14
22
  "text.input-secret-hide": "hide",
15
23
  "text.input-secret-placeholder": "enter secret information",
@@ -6,10 +6,18 @@
6
6
  "label.accumulator_max": "最小",
7
7
  "label.accumulator_min": "最大",
8
8
  "label.accumulator_sum": "合計",
9
+ "label.filter": "フィルター",
10
+ "text.grid-cell-input": "{name} 入力",
11
+ "text.grid-cell-input-plain": "セル入力",
9
12
  "text.grid-clear-search": "検索語やフィルターを消すと全体が表示されます",
13
+ "text.grid-filter-from": "{name} フィルター 開始値",
14
+ "text.grid-filter-of": "{name} フィルター",
15
+ "text.grid-filter-to": "{name} フィルター 終了値",
10
16
  "text.grid-no-records": "まだ何もありません",
11
17
  "text.grid-nothing-found": "見つかりませんでした",
12
18
  "text.grid-open-editor": "押して編集します",
19
+ "text.grid-select-all-records": "すべての行を選択",
20
+ "text.grid-select-record": "{row} 行目を選択",
13
21
  "text.grid-total-records": "全 {total} 件",
14
22
  "text.input-secret-hide": "非表示",
15
23
  "text.input-secret-placeholder": "機密情報を入力",
@@ -7,10 +7,17 @@
7
7
  "label.accumulator_min": "최대",
8
8
  "label.accumulator_sum": "합계",
9
9
  "label.filter": "필터",
10
+ "text.grid-cell-input": "{name} 입력",
11
+ "text.grid-cell-input-plain": "칸 입력",
10
12
  "text.grid-clear-search": "검색어나 필터를 지우면 전체가 보입니다",
13
+ "text.grid-filter-from": "{name} 필터 시작값",
14
+ "text.grid-filter-of": "{name} 필터",
15
+ "text.grid-filter-to": "{name} 필터 끝값",
11
16
  "text.grid-no-records": "아직 아무것도 없습니다",
12
17
  "text.grid-nothing-found": "찾는 것이 없습니다",
13
18
  "text.grid-open-editor": "눌러서 고칩니다",
19
+ "text.grid-select-all-records": "모든 행 선택",
20
+ "text.grid-select-record": "{row}번째 행 선택",
14
21
  "text.grid-total-records": "모두 {total}건",
15
22
  "text.input-secret-hide": "숨기기",
16
23
  "text.input-secret-placeholder": "보안 정보 입력",
@@ -6,10 +6,18 @@
6
6
  "label.accumulator_max": "Minimum",
7
7
  "label.accumulator_min": "Maksimum",
8
8
  "label.accumulator_sum": "Jumlah",
9
+ "label.filter": "penapis",
10
+ "text.grid-cell-input": "input {name}",
11
+ "text.grid-cell-input-plain": "input sel",
9
12
  "text.grid-clear-search": "kosongkan carian atau penapis untuk melihat semua",
13
+ "text.grid-filter-from": "penapis {name}, dari",
14
+ "text.grid-filter-of": "penapis {name}",
15
+ "text.grid-filter-to": "penapis {name}, hingga",
10
16
  "text.grid-no-records": "tiada apa-apa untuk dipaparkan",
11
17
  "text.grid-nothing-found": "tiada apa-apa dijumpai",
12
18
  "text.grid-open-editor": "tekan untuk mengedit",
19
+ "text.grid-select-all-records": "pilih semua baris",
20
+ "text.grid-select-record": "pilih baris {row}",
13
21
  "text.grid-total-records": "{total} kesemuanya",
14
22
  "text.input-secret-hide": "Sembunyi",
15
23
  "text.input-secret-placeholder": "Masukkan maklumat rahsia",
@@ -6,10 +6,18 @@
6
6
  "label.accumulator_max": "最小",
7
7
  "label.accumulator_min": "最大",
8
8
  "label.accumulator_sum": "总和",
9
+ "label.filter": "筛选",
10
+ "text.grid-cell-input": "{name} 输入",
11
+ "text.grid-cell-input-plain": "单元格输入",
9
12
  "text.grid-clear-search": "清除搜索或筛选即可查看全部",
13
+ "text.grid-filter-from": "{name} 筛选起始值",
14
+ "text.grid-filter-of": "{name} 筛选",
15
+ "text.grid-filter-to": "{name} 筛选结束值",
10
16
  "text.grid-no-records": "还没有任何记录",
11
17
  "text.grid-nothing-found": "没有找到",
12
18
  "text.grid-open-editor": "点击进行编辑",
19
+ "text.grid-select-all-records": "选择所有行",
20
+ "text.grid-select-record": "选择第 {row} 行",
13
21
  "text.grid-total-records": "共 {total} 条",
14
22
  "text.input-secret-hide": "隐藏",
15
23
  "text.input-secret-placeholder": "输入机密信息",