@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.
- package/CHANGELOG.md +24 -0
- package/dist/src/configure/column-builder.js +16 -2
- package/dist/src/configure/column-builder.js.map +1 -1
- package/dist/src/configure/config-builder.js +1 -1
- package/dist/src/configure/config-builder.js.map +1 -1
- package/dist/src/data-grid/data-grid-body-style.js +12 -1
- package/dist/src/data-grid/data-grid-body-style.js.map +1 -1
- package/dist/src/data-grid/data-grid-body.d.ts +24 -0
- package/dist/src/data-grid/data-grid-body.js +125 -40
- package/dist/src/data-grid/data-grid-body.js.map +1 -1
- package/dist/src/data-grid/data-grid-field.d.ts +2 -2
- package/dist/src/data-grid/data-grid-field.js +29 -29
- package/dist/src/data-grid/data-grid-field.js.map +1 -1
- package/dist/src/data-grid/data-grid-footer.js +1 -1
- package/dist/src/data-grid/data-grid-footer.js.map +1 -1
- package/dist/src/data-grid/data-grid-header.d.ts +12 -1
- package/dist/src/data-grid/data-grid-header.js +82 -8
- package/dist/src/data-grid/data-grid-header.js.map +1 -1
- package/dist/src/data-grid/data-grid.d.ts +9 -1
- package/dist/src/data-grid/data-grid.js +79 -27
- package/dist/src/data-grid/data-grid.js.map +1 -1
- package/dist/src/data-grist.d.ts +7 -0
- package/dist/src/data-grist.js +12 -8
- package/dist/src/data-grist.js.map +1 -1
- package/dist/src/editors/ox-grist-editor.d.ts +16 -1
- package/dist/src/editors/ox-grist-editor.js +29 -3
- package/dist/src/editors/ox-grist-editor.js.map +1 -1
- package/dist/src/editors/registry.d.ts +2 -2
- package/dist/src/editors/registry.js +2 -2
- package/dist/src/editors/registry.js.map +1 -1
- package/dist/src/filters/filter-input.js +16 -0
- package/dist/src/filters/filter-input.js.map +1 -1
- package/dist/src/filters/filter-range-date.js +9 -0
- package/dist/src/filters/filter-range-date.js.map +1 -1
- package/dist/src/filters/filter-range-number.js +9 -0
- package/dist/src/filters/filter-range-number.js.map +1 -1
- package/dist/src/gutters/gutter-row-selector.js +16 -0
- package/dist/src/gutters/gutter-row-selector.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/renderers/ox-grist-renderer-boolean.js +8 -0
- package/dist/src/renderers/ox-grist-renderer-boolean.js.map +1 -1
- package/dist/src/types.d.ts +11 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/column-text.d.ts +18 -0
- package/dist/src/utils/column-text.js +37 -0
- package/dist/src/utils/column-text.js.map +1 -0
- package/dist/src/utils/index.d.ts +1 -0
- package/dist/src/utils/index.js +1 -0
- package/dist/src/utils/index.js.map +1 -1
- package/dist/test/accessible-name.test.d.ts +2 -0
- package/dist/test/accessible-name.test.js +322 -0
- package/dist/test/accessible-name.test.js.map +1 -0
- package/dist/test/row-hover-visible.test.js +2 -2
- package/dist/test/row-hover-visible.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/test/accessible-name.test.ts +406 -0
- package/test/row-hover-visible.test.ts +2 -2
- package/translations/en.json +8 -0
- package/translations/ja.json +8 -0
- package/translations/ko.json +7 -0
- package/translations/ms.json +8 -0
- 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.
|
|
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/
|
|
73
|
-
"@operato/
|
|
74
|
-
"@operato/
|
|
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.
|
|
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": "
|
|
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
|
-
* [
|
|
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
|
package/translations/en.json
CHANGED
|
@@ -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",
|
package/translations/ja.json
CHANGED
|
@@ -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": "機密情報を入力",
|
package/translations/ko.json
CHANGED
|
@@ -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": "보안 정보 입력",
|
package/translations/ms.json
CHANGED
|
@@ -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",
|
package/translations/zh.json
CHANGED
|
@@ -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": "输入机密信息",
|