@lovett/ui 0.0.11 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-RBYWGBQ2.js +2752 -0
- package/dist/chunk-RBYWGBQ2.js.map +1 -0
- package/dist/index.d.ts +5574 -57
- package/dist/index.js +21650 -11206
- package/dist/index.js.map +1 -1
- package/dist/rich-composer-impl-5NO443A6.js +1859 -0
- package/dist/rich-composer-impl-5NO443A6.js.map +1 -0
- package/dist/styles.css +1570 -0
- package/dist/tokens.css +112 -0
- package/package.json +8 -1
- package/src/__tests__/avatar.test.tsx +272 -0
- package/src/__tests__/bar-chart.test.tsx +809 -0
- package/src/__tests__/board.test.tsx +420 -0
- package/src/__tests__/chart-math.test.ts +922 -0
- package/src/__tests__/chart-series.test.ts +339 -0
- package/src/__tests__/code-block.test.tsx +134 -0
- package/src/__tests__/display-popover.test.tsx +195 -0
- package/src/__tests__/display-store.test.tsx +307 -0
- package/src/__tests__/donut-chart.test.tsx +397 -0
- package/src/__tests__/dropdown-menu.test.tsx +156 -2
- package/src/__tests__/filter-menu.test.tsx +175 -0
- package/src/__tests__/gauge-ring.test.tsx +233 -0
- package/src/__tests__/line-chart.test.tsx +612 -0
- package/src/__tests__/ranked-bars.test.tsx +343 -0
- package/src/__tests__/remark-underline.test.ts +194 -0
- package/src/__tests__/sortable.test.tsx +394 -0
- package/src/__tests__/sparkline.test.tsx +368 -0
- package/src/__tests__/stat-layer.test.tsx +271 -0
- package/src/__tests__/stat-strip.test.tsx +175 -0
- package/src/__tests__/status.test.tsx +293 -0
- package/src/__tests__/tabs.test.tsx +303 -0
- package/src/__tests__/token-shape.test.ts +132 -2
- package/src/avatar.tsx +352 -0
- package/src/bar-chart.tsx +1214 -0
- package/src/board.tsx +658 -0
- package/src/chart-frame.tsx +960 -0
- package/src/chart-legend.tsx +304 -0
- package/src/chart-tooltip.tsx +267 -0
- package/src/code-block.tsx +62 -8
- package/src/delta-chip.tsx +263 -0
- package/src/detail/__tests__/activity-pane.test.tsx +369 -0
- package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
- package/src/detail/__tests__/detail-surface.test.tsx +529 -0
- package/src/detail/__tests__/field-row.test.tsx +357 -0
- package/src/detail/activity-pane.tsx +611 -0
- package/src/detail/calendar.tsx +355 -0
- package/src/detail/detail-divider.tsx +261 -0
- package/src/detail/detail-header.tsx +287 -0
- package/src/detail/detail-menu.tsx +254 -0
- package/src/detail/detail-surface.tsx +1110 -0
- package/src/detail/field-list.tsx +196 -0
- package/src/detail/field-row.tsx +1131 -0
- package/src/detail/index.ts +58 -0
- package/src/detail/segmented-choice.tsx +94 -0
- package/src/detail/types.ts +129 -0
- package/src/display-popover.tsx +487 -0
- package/src/display-store.tsx +301 -0
- package/src/donut-chart.tsx +988 -0
- package/src/dropdown-menu.tsx +290 -19
- package/src/filter-core/EXPORTS.md +85 -0
- package/src/filter-core/__tests__/columns.test.ts +159 -0
- package/src/filter-core/__tests__/faceting.test.ts +193 -0
- package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
- package/src/filter-core/__tests__/operators.test.ts +235 -0
- package/src/filter-core/__tests__/state.test.ts +268 -0
- package/src/filter-core/__tests__/url.test.ts +350 -0
- package/src/filter-core/columns.ts +134 -0
- package/src/filter-core/date-utils.ts +38 -0
- package/src/filter-core/examples/task-filter-columns.ts +121 -0
- package/src/filter-core/faceting.ts +120 -0
- package/src/filter-core/filter-fns.ts +335 -0
- package/src/filter-core/index.ts +105 -0
- package/src/filter-core/operators.ts +433 -0
- package/src/filter-core/state.ts +280 -0
- package/src/filter-core/types.ts +247 -0
- package/src/filter-core/url.ts +261 -0
- package/src/filter-dropdown.tsx +12 -0
- package/src/filter-menu.tsx +649 -0
- package/src/floating-drawer.tsx +19 -1
- package/src/gauge-ring.tsx +435 -0
- package/src/hue.ts +52 -0
- package/src/index.ts +303 -0
- package/src/kbd.tsx +27 -4
- package/src/lib/chart.ts +866 -0
- package/src/lib/focus.ts +43 -1
- package/src/lib/layer-stack.ts +30 -3
- package/src/lib/remark-underline.ts +443 -0
- package/src/lib/series.ts +169 -0
- package/src/line-chart.tsx +1176 -0
- package/src/markdown.tsx +26 -7
- package/src/modal.tsx +42 -18
- package/src/progress-ledger.tsx +304 -0
- package/src/ranked-bars.tsx +386 -0
- package/src/segmented-pill.tsx +32 -9
- package/src/sortable.tsx +520 -1
- package/src/sparkline.tsx +416 -0
- package/src/stat-card.tsx +376 -0
- package/src/stat-strip.tsx +327 -0
- package/src/status.tsx +215 -0
- package/src/styles.css +1570 -0
- package/src/tabs.tsx +206 -25
- package/src/task-card.tsx +610 -0
- package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
- package/src/thread/__tests__/comment-tree.test.ts +151 -0
- package/src/thread/__tests__/emoji.test.ts +187 -0
- package/src/thread/__tests__/fixtures/thread-fixture.ts +252 -0
- package/src/thread/__tests__/link-preview-source.test.ts +120 -0
- package/src/thread/__tests__/link-preview.test.tsx +600 -0
- package/src/thread/__tests__/markdown-format.test.ts +82 -0
- package/src/thread/__tests__/markdown-spec.test.ts +469 -0
- package/src/thread/__tests__/relative-time.test.ts +71 -0
- package/src/thread/__tests__/rich-composer.test.tsx +799 -0
- package/src/thread/__tests__/scroll-caret.test.ts +58 -0
- package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
- package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
- package/src/thread/__tests__/thread.test.tsx +742 -0
- package/src/thread/__tests__/use-attachments.test.tsx +679 -0
- package/src/thread/actions.tsx +196 -0
- package/src/thread/attachments.tsx +1071 -0
- package/src/thread/comment-body.tsx +148 -0
- package/src/thread/comment-tree.ts +182 -0
- package/src/thread/comment.tsx +967 -0
- package/src/thread/composer-footer.tsx +125 -0
- package/src/thread/composer.tsx +319 -0
- package/src/thread/emoji.ts +283 -0
- package/src/thread/index.ts +153 -0
- package/src/thread/link-preview.tsx +341 -0
- package/src/thread/markdown-format.ts +155 -0
- package/src/thread/markdown-spec.ts +754 -0
- package/src/thread/rail.tsx +372 -0
- package/src/thread/reactions.tsx +415 -0
- package/src/thread/relative-time.tsx +94 -0
- package/src/thread/rich-composer-impl.tsx +1601 -0
- package/src/thread/rich-composer.tsx +195 -0
- package/src/thread/scroll-caret.ts +37 -0
- package/src/thread/suggestion-list.tsx +182 -0
- package/src/thread/thread.tsx +718 -0
- package/src/thread/types.ts +232 -0
- package/src/thread/use-attachments.ts +598 -0
- package/src/thread/use-now.ts +73 -0
- package/src/thread/use-thread.ts +316 -0
- package/src/tokens.css +112 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FieldRow — engage, revert, commit, and the resting appearance.
|
|
3
|
+
*
|
|
4
|
+
* The resting assertions are not decoration: §4's whole point is that a field
|
|
5
|
+
* does NOT look like a form control until you engage it, so "there is no
|
|
6
|
+
* textbox on screen until the row is clicked" is the requirement, stated the
|
|
7
|
+
* only way a test can state it.
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from 'react'
|
|
10
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
11
|
+
import { render, screen, within } from '@testing-library/react'
|
|
12
|
+
import userEvent from '@testing-library/user-event'
|
|
13
|
+
import { FieldList } from '../field-list'
|
|
14
|
+
import { FieldRow } from '../field-row'
|
|
15
|
+
|
|
16
|
+
const noop = () => {}
|
|
17
|
+
|
|
18
|
+
/** Local noon on a fixed day, so the calendar always opens on Sept 2026. */
|
|
19
|
+
const FROZEN = new Date(2026, 8, 15, 12, 0, 0).getTime()
|
|
20
|
+
|
|
21
|
+
describe('FieldRow', () => {
|
|
22
|
+
it('reads as plain text at rest, with a verb phrase when empty', () => {
|
|
23
|
+
render(<FieldRow.Text label="Next steps" value="" onCommit={noop} placeholder="Empty" />)
|
|
24
|
+
expect(screen.queryByRole('textbox')).toBeNull()
|
|
25
|
+
expect(screen.getByRole('button', { name: /Next steps/ })).toHaveTextContent('Empty')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('commits on Enter', async () => {
|
|
29
|
+
const onCommit = vi.fn()
|
|
30
|
+
render(<FieldRow.Text label="Next steps" value="" onCommit={onCommit} />)
|
|
31
|
+
await userEvent.click(screen.getByRole('button', { name: /Next steps/ }))
|
|
32
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Next steps' }), 'Ship it{Enter}')
|
|
33
|
+
expect(onCommit).toHaveBeenCalledWith('Ship it')
|
|
34
|
+
expect(screen.queryByRole('textbox')).toBeNull()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('reverts on Escape and writes nothing', async () => {
|
|
38
|
+
const onCommit = vi.fn()
|
|
39
|
+
render(<FieldRow.Text label="Next steps" value="Original" onCommit={onCommit} />)
|
|
40
|
+
await userEvent.click(screen.getByRole('button', { name: /Next steps/ }))
|
|
41
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Next steps' }), ' and more{Escape}')
|
|
42
|
+
expect(onCommit).not.toHaveBeenCalled()
|
|
43
|
+
expect(screen.getByRole('button', { name: /Next steps/ })).toHaveTextContent('Original')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('does not close the surface when Escape reverts a field', async () => {
|
|
47
|
+
// The layer stack listens on `document`. Without `stopPropagation` in the
|
|
48
|
+
// editor, reverting a field would throw the operator out of the item.
|
|
49
|
+
const onEscape = vi.fn()
|
|
50
|
+
document.addEventListener('keydown', onEscape)
|
|
51
|
+
try {
|
|
52
|
+
render(<FieldRow.Text label="Next steps" value="Original" onCommit={noop} />)
|
|
53
|
+
await userEvent.click(screen.getByRole('button', { name: /Next steps/ }))
|
|
54
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Next steps' }), '{Escape}')
|
|
55
|
+
expect(onEscape).not.toHaveBeenCalled()
|
|
56
|
+
} finally {
|
|
57
|
+
document.removeEventListener('keydown', onEscape)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('commits on blur', async () => {
|
|
62
|
+
const onCommit = vi.fn()
|
|
63
|
+
render(
|
|
64
|
+
<>
|
|
65
|
+
<FieldRow.Text label="Next steps" value="" onCommit={onCommit} />
|
|
66
|
+
<button type="button">Elsewhere</button>
|
|
67
|
+
</>,
|
|
68
|
+
)
|
|
69
|
+
await userEvent.click(screen.getByRole('button', { name: /Next steps/ }))
|
|
70
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Next steps' }), 'Typed')
|
|
71
|
+
await userEvent.click(screen.getByRole('button', { name: 'Elsewhere' }))
|
|
72
|
+
expect(onCommit).toHaveBeenCalledWith('Typed')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('does not fire a commit when nothing changed', async () => {
|
|
76
|
+
const onCommit = vi.fn()
|
|
77
|
+
render(<FieldRow.Text label="Next steps" value="Same" onCommit={onCommit} />)
|
|
78
|
+
await userEvent.click(screen.getByRole('button', { name: /Next steps/ }))
|
|
79
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Next steps' }), '{Enter}')
|
|
80
|
+
expect(onCommit).not.toHaveBeenCalled()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('picks a date through the calendar and confirms it', async () => {
|
|
84
|
+
const onCommit = vi.fn()
|
|
85
|
+
render(
|
|
86
|
+
<FieldRow.Date label="Due date" value={null} onCommit={onCommit} now={FROZEN} locale="en-US" />,
|
|
87
|
+
)
|
|
88
|
+
await userEvent.click(screen.getByRole('button', { name: /Due date/ }))
|
|
89
|
+
const grid = screen.getByRole('grid')
|
|
90
|
+
await userEvent.click(within(grid).getByText('10'))
|
|
91
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save date' }))
|
|
92
|
+
expect(onCommit).toHaveBeenCalledWith('2026-09-10')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('adds a time when Include time is on', async () => {
|
|
96
|
+
const onCommit = vi.fn()
|
|
97
|
+
render(
|
|
98
|
+
<FieldRow.Date
|
|
99
|
+
label="Due date"
|
|
100
|
+
value="2026-09-10"
|
|
101
|
+
onCommit={onCommit}
|
|
102
|
+
now={FROZEN}
|
|
103
|
+
locale="en-US"
|
|
104
|
+
/>,
|
|
105
|
+
)
|
|
106
|
+
await userEvent.click(screen.getByRole('button', { name: /Due date/ }))
|
|
107
|
+
await userEvent.click(screen.getByRole('checkbox', { name: 'Include time' }))
|
|
108
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save date' }))
|
|
109
|
+
expect(onCommit).toHaveBeenCalledWith('2026-09-10T09:00')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('clears a date', async () => {
|
|
113
|
+
const onCommit = vi.fn()
|
|
114
|
+
render(
|
|
115
|
+
<FieldRow.Date label="Due date" value="2026-09-10" onCommit={onCommit} now={FROZEN} />,
|
|
116
|
+
)
|
|
117
|
+
await userEvent.click(screen.getByRole('button', { name: /Due date/ }))
|
|
118
|
+
await userEvent.click(screen.getByRole('button', { name: 'Clear' }))
|
|
119
|
+
expect(onCommit).toHaveBeenCalledWith(null)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('picks one option through the select row', async () => {
|
|
123
|
+
const onCommit = vi.fn()
|
|
124
|
+
render(
|
|
125
|
+
<FieldRow.Select
|
|
126
|
+
label="Status"
|
|
127
|
+
value={null}
|
|
128
|
+
onCommit={onCommit}
|
|
129
|
+
options={[
|
|
130
|
+
{ value: 'building', label: 'Building' },
|
|
131
|
+
{ value: 'review', label: 'Review' },
|
|
132
|
+
]}
|
|
133
|
+
/>,
|
|
134
|
+
)
|
|
135
|
+
await userEvent.click(screen.getByRole('button', { name: /Status/ }))
|
|
136
|
+
await userEvent.click(screen.getByRole('option', { name: /Review/ }))
|
|
137
|
+
expect(onCommit).toHaveBeenCalledWith('review')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('assigns people and commits the pending set when the picker closes', async () => {
|
|
141
|
+
const onCommit = vi.fn()
|
|
142
|
+
function Host() {
|
|
143
|
+
const [value, setValue] = useState<readonly string[]>([])
|
|
144
|
+
return (
|
|
145
|
+
<>
|
|
146
|
+
<FieldRow.People
|
|
147
|
+
label="Assignees"
|
|
148
|
+
value={value}
|
|
149
|
+
people={[
|
|
150
|
+
{ id: 'u1', name: 'Ada Lovelace' },
|
|
151
|
+
{ id: 'u2', name: 'Grace Hopper' },
|
|
152
|
+
]}
|
|
153
|
+
onCommit={(next) => {
|
|
154
|
+
onCommit(next)
|
|
155
|
+
setValue(next)
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
158
|
+
<button type="button">Elsewhere</button>
|
|
159
|
+
</>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
render(<Host />)
|
|
163
|
+
await userEvent.click(screen.getByRole('button', { name: /Assignees/ }))
|
|
164
|
+
await userEvent.click(screen.getByRole('option', { name: /Ada Lovelace/ }))
|
|
165
|
+
expect(onCommit).not.toHaveBeenCalled()
|
|
166
|
+
await userEvent.click(screen.getByRole('button', { name: 'Elsewhere' }))
|
|
167
|
+
expect(onCommit).toHaveBeenCalledWith(['u1'])
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('edits tags in place, and Escape puts them back', async () => {
|
|
171
|
+
const onCommit = vi.fn()
|
|
172
|
+
render(
|
|
173
|
+
<>
|
|
174
|
+
<FieldRow.Tags label="Tags" value={['alpha']} onCommit={onCommit} />
|
|
175
|
+
<button type="button">Elsewhere</button>
|
|
176
|
+
</>,
|
|
177
|
+
)
|
|
178
|
+
await userEvent.click(screen.getByRole('button', { name: /Tags/ }))
|
|
179
|
+
const input = screen.getByRole('textbox')
|
|
180
|
+
await userEvent.type(input, 'beta{Enter}')
|
|
181
|
+
await userEvent.keyboard('{Escape}')
|
|
182
|
+
expect(onCommit).not.toHaveBeenCalled()
|
|
183
|
+
expect(screen.getByRole('button', { name: /Tags/ })).toHaveTextContent('alpha')
|
|
184
|
+
|
|
185
|
+
await userEvent.click(screen.getByRole('button', { name: /Tags/ }))
|
|
186
|
+
await userEvent.type(screen.getByRole('textbox'), 'gamma{Enter}')
|
|
187
|
+
await userEvent.click(screen.getByRole('button', { name: 'Elsewhere' }))
|
|
188
|
+
expect(onCommit).toHaveBeenCalledWith(['alpha', 'gamma'])
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// The file had ZERO `.focus()` calls before 2026-09-06: every editor here
|
|
193
|
+
// replaces the resting value, so Enter and Escape — the two commonest
|
|
194
|
+
// gestures in the detail surface — unmounted the focused element and
|
|
195
|
+
// dropped the caret to <body>.
|
|
196
|
+
describe('FieldRow focus restore', () => {
|
|
197
|
+
const resting = (name: RegExp) => screen.getByRole('button', { name })
|
|
198
|
+
|
|
199
|
+
it('Text: Enter commits and returns focus to the resting row', async () => {
|
|
200
|
+
render(<FieldRow.Text label="Next steps" value="" onCommit={noop} />)
|
|
201
|
+
await userEvent.click(resting(/Next steps/))
|
|
202
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Next steps' }), 'Ship it{Enter}')
|
|
203
|
+
expect(document.activeElement).toBe(resting(/Next steps/))
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('Text: Escape reverts and returns focus to the resting row', async () => {
|
|
207
|
+
render(<FieldRow.Text label="Next steps" value="Original" onCommit={noop} />)
|
|
208
|
+
await userEvent.click(resting(/Next steps/))
|
|
209
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Next steps' }), '{Escape}')
|
|
210
|
+
expect(document.activeElement).toBe(resting(/Next steps/))
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('Text (multiline): Cmd+Enter commits and returns focus', async () => {
|
|
214
|
+
render(<FieldRow.Text multiline label="Notes" value="" onCommit={noop} />)
|
|
215
|
+
await userEvent.click(resting(/Notes/))
|
|
216
|
+
const editor = screen.getByRole('textbox', { name: 'Notes' })
|
|
217
|
+
await userEvent.type(editor, 'Long form')
|
|
218
|
+
await userEvent.keyboard('{Meta>}{Enter}{/Meta}')
|
|
219
|
+
expect(document.activeElement).toBe(resting(/Notes/))
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('Tags: Escape returns focus to the resting row', async () => {
|
|
223
|
+
render(<FieldRow.Tags label="Tags" value={['alpha']} onCommit={noop} />)
|
|
224
|
+
await userEvent.click(resting(/Tags/))
|
|
225
|
+
await userEvent.keyboard('{Escape}')
|
|
226
|
+
expect(document.activeElement).toBe(resting(/Tags/))
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('Select: choosing an option returns focus to the resting row', async () => {
|
|
230
|
+
render(
|
|
231
|
+
<FieldRow.Select
|
|
232
|
+
label="Status"
|
|
233
|
+
value={null}
|
|
234
|
+
onCommit={noop}
|
|
235
|
+
options={[{ value: 'review', label: 'Review' }]}
|
|
236
|
+
/>,
|
|
237
|
+
)
|
|
238
|
+
await userEvent.click(resting(/Status/))
|
|
239
|
+
await userEvent.click(screen.getByRole('option', { name: /Review/ }))
|
|
240
|
+
expect(document.activeElement).toBe(resting(/Status/))
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('People: closing the picker returns focus to the resting row', async () => {
|
|
244
|
+
render(
|
|
245
|
+
<FieldRow.People
|
|
246
|
+
label="Assignees"
|
|
247
|
+
value={[]}
|
|
248
|
+
people={[{ id: 'u1', name: 'Ada Lovelace' }]}
|
|
249
|
+
onCommit={noop}
|
|
250
|
+
/>,
|
|
251
|
+
)
|
|
252
|
+
await userEvent.click(resting(/Assignees/))
|
|
253
|
+
await userEvent.keyboard('{Escape}')
|
|
254
|
+
expect(document.activeElement).toBe(resting(/Assignees/))
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('Date: saving returns focus to the resting row', async () => {
|
|
258
|
+
render(<FieldRow.Date label="Due date" value={null} onCommit={noop} now={FROZEN} />)
|
|
259
|
+
await userEvent.click(resting(/Due date/))
|
|
260
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save date' }))
|
|
261
|
+
expect(document.activeElement).toBe(resting(/Due date/))
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('leaves focus alone when a blur-commit hands it to another control', async () => {
|
|
265
|
+
render(
|
|
266
|
+
<>
|
|
267
|
+
<FieldRow.Text label="Next steps" value="" onCommit={noop} />
|
|
268
|
+
<button type="button">Elsewhere</button>
|
|
269
|
+
</>,
|
|
270
|
+
)
|
|
271
|
+
await userEvent.click(resting(/Next steps/))
|
|
272
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Next steps' }), 'Typed')
|
|
273
|
+
await userEvent.click(screen.getByRole('button', { name: 'Elsewhere' }))
|
|
274
|
+
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Elsewhere' }))
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
describe('FieldRow value semantics', () => {
|
|
279
|
+
it('associates a read-only value with its label', () => {
|
|
280
|
+
render(<FieldRow.Text readOnly label="Owner" value="Ada Lovelace" onCommit={noop} />)
|
|
281
|
+
// A bare <span> is role=generic, where a name is ignored — the value
|
|
282
|
+
// reached AT as a loose string with no tie to the label beside it.
|
|
283
|
+
expect(screen.getByRole('group', { name: 'Owner' })).toHaveTextContent('Ada Lovelace')
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('associates a static <FieldRow value> with its label', () => {
|
|
287
|
+
render(<FieldRow label="Created" filled value="Yesterday" />)
|
|
288
|
+
expect(screen.getByRole('group', { name: 'Created' })).toHaveTextContent('Yesterday')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('tells you what each row will actually do', () => {
|
|
292
|
+
render(
|
|
293
|
+
<>
|
|
294
|
+
<FieldRow.Text label="Next steps" value="" onCommit={noop} />
|
|
295
|
+
<FieldRow.Date label="Due date" value={null} onCommit={noop} now={FROZEN} />
|
|
296
|
+
<FieldRow.Select label="Status" value={null} onCommit={noop} options={[]} />
|
|
297
|
+
<FieldRow.People label="Assignees" value={[]} people={[]} onCommit={noop} />
|
|
298
|
+
</>,
|
|
299
|
+
)
|
|
300
|
+
// One affordance, five behaviours — so each row states its own.
|
|
301
|
+
expect(screen.getByRole('button', { name: /Due date/ })).toHaveAttribute(
|
|
302
|
+
'aria-haspopup',
|
|
303
|
+
'dialog',
|
|
304
|
+
)
|
|
305
|
+
expect(screen.getByRole('button', { name: /Status/ })).toHaveAttribute(
|
|
306
|
+
'aria-haspopup',
|
|
307
|
+
'listbox',
|
|
308
|
+
)
|
|
309
|
+
expect(screen.getByRole('button', { name: /Assignees/ })).toHaveAttribute(
|
|
310
|
+
'aria-haspopup',
|
|
311
|
+
'listbox',
|
|
312
|
+
)
|
|
313
|
+
const text = screen.getByRole('button', { name: /Next steps/ })
|
|
314
|
+
expect(text).not.toHaveAttribute('aria-haspopup')
|
|
315
|
+
expect(text).toHaveAccessibleDescription(/Edits in place/)
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
describe('FieldList', () => {
|
|
320
|
+
const rows = (
|
|
321
|
+
<>
|
|
322
|
+
<FieldRow label="Due date" filled value="10 Sept" />
|
|
323
|
+
<FieldRow label="Assignees" filled={false} placeholder="Unassigned" />
|
|
324
|
+
</>
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
it('hides rows that do not match the search, without unmounting them', async () => {
|
|
328
|
+
render(<FieldList>{rows}</FieldList>)
|
|
329
|
+
await userEvent.type(screen.getByRole('searchbox', { name: 'Search fields' }), 'due')
|
|
330
|
+
|
|
331
|
+
const due = document.querySelector('[data-field-row]:not([hidden])')
|
|
332
|
+
expect(due).toHaveTextContent('Due date')
|
|
333
|
+
// Still in the DOM — a half-finished edit survives a keystroke here.
|
|
334
|
+
expect(document.querySelectorAll('[data-field-row]')).toHaveLength(2)
|
|
335
|
+
expect(document.querySelectorAll('[data-field-row][hidden]')).toHaveLength(1)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('filters by filled and empty', async () => {
|
|
339
|
+
render(<FieldList>{rows}</FieldList>)
|
|
340
|
+
await userEvent.click(screen.getByRole('button', { name: 'Empty' }))
|
|
341
|
+
const visible = document.querySelectorAll('[data-field-row]:not([hidden])')
|
|
342
|
+
expect(visible).toHaveLength(1)
|
|
343
|
+
expect(visible[0]).toHaveTextContent('Unassigned')
|
|
344
|
+
|
|
345
|
+
await userEvent.click(screen.getByRole('button', { name: 'Filled' }))
|
|
346
|
+
const filled = document.querySelectorAll('[data-field-row]:not([hidden])')
|
|
347
|
+
expect(filled).toHaveLength(1)
|
|
348
|
+
expect(filled[0]).toHaveTextContent('Due date')
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('draws no Manage fields control unless the host supplies one', () => {
|
|
352
|
+
const view = render(<FieldList>{rows}</FieldList>)
|
|
353
|
+
expect(screen.queryByRole('button', { name: 'Manage fields' })).toBeNull()
|
|
354
|
+
view.rerender(<FieldList onManageFields={noop}>{rows}</FieldList>)
|
|
355
|
+
expect(screen.getByRole('button', { name: 'Manage fields' })).toBeInTheDocument()
|
|
356
|
+
})
|
|
357
|
+
})
|