@lotics/ui 23.1.1 → 23.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/AGENTS.md +5 -0
- package/docs/catalog.md +12 -2
- package/package.json +1 -1
- package/src/breakdown.tsx +3 -1
- package/src/choice_list.tsx +3 -1
- package/src/date_calendar.tsx +3 -1
- package/src/funnel.tsx +3 -1
- package/src/heatmap.tsx +3 -1
- package/src/matrix.tsx +3 -1
- package/src/press_door.tsx +12 -1
- package/src/status_grid.tsx +3 -1
- package/src/table.tsx +47 -4
package/AGENTS.md
CHANGED
|
@@ -54,6 +54,11 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
54
54
|
(a CTA, ⋯, a checkbox, a `Link`) is a role-less `PressableRow` + a **`PressDoor`** sibling
|
|
55
55
|
(the tab stop, name, focus ring), never `PressableHighlight`, which wraps its children in the
|
|
56
56
|
button. `Table`/`TableRow` already does this. → [composition.md](./docs/composition.md).
|
|
57
|
+
- **A fact every row needs is a COLUMN; a fact one row needs expands in place.** Sending the
|
|
58
|
+
reader to a drawer for either loses the surrounding rows and charges a navigation to come
|
|
59
|
+
back — which is what makes scanning twenty records unaffordable. `TableRow`'s `detail` +
|
|
60
|
+
`expanded` reveal the detail beneath the row; keep the drawer for a heavy form.
|
|
61
|
+
→ [catalog.md](./docs/catalog.md).
|
|
57
62
|
- **A reference to another record is a FIELD** — it wears the inline editor's own resting
|
|
58
63
|
surface, carries an **`InlineButton`** Open inside it, and PEEKS its facts on press (detach
|
|
59
64
|
lives in the peek). Never its own Section, never a card, never the other record's fields as
|
package/docs/catalog.md
CHANGED
|
@@ -129,8 +129,8 @@ See [`tpl_item_list`](../examples/tpl_item_list.tsx) for the checklist grammar,
|
|
|
129
129
|
Two columnar shapes, and the choice is about data size:
|
|
130
130
|
|
|
131
131
|
- **High-volume register** (thousands+ you BROWSE) — `Table` (columns defined once; sortable
|
|
132
|
-
headers via `SortHeader`; paired with `Pagination`) +
|
|
133
|
-
|
|
132
|
+
headers via `SortHeader`; paired with `Pagination`) + rows that either open a `Drawer` to
|
|
133
|
+
edit or expand in place (`detail` + `expanded`) when the detail is read or light-edit. It scales by PAGING — renders one page, never the whole set — so you FILTER +
|
|
134
134
|
search, you don't group. Worked example: [`tpl_item_list`](../examples/tpl_item_list.tsx).
|
|
135
135
|
Never an HTML `<table>` or a `.map` of rows. The register adapts to its container on its
|
|
136
136
|
own — hides columns by `priority`, stacks rows below the two-column floor (see the `table`
|
|
@@ -807,6 +807,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
807
807
|
On pressable rows dropped values stay one tap away — the row opens the record; a READ-ONLY
|
|
808
808
|
register (rows without `onPress`) has no door, so give the columns it can't afford to lose
|
|
809
809
|
a low `priority` (e.g. `priority: 1` — outlives its right-side neighbours).
|
|
810
|
+
**`detail` + `expanded` reveal a row's detail BENEATH it instead of navigating away.** A
|
|
811
|
+
register answers "which one"; the moment it cannot answer "and what about it" the reader is
|
|
812
|
+
sent to a drawer, loses the surrounding rows, and pays a navigation to come back — which is
|
|
813
|
+
what makes scanning twenty records unaffordable. Reach for it when the detail is read or
|
|
814
|
+
light-edit; a heavy form still deserves its own surface. `expanded` is CONTROLLED — how many
|
|
815
|
+
may be open at once is the caller's rule, so the component does not pick one. The detail
|
|
816
|
+
renders as a SIBLING of the press surface (a control inside it would otherwise be swallowed
|
|
817
|
+
by the row's own toggle), the row shows the open wash while it is out, and the door announces
|
|
818
|
+
`aria-expanded`. Prefer the columns first: a fact every row needs is a column, not a reason
|
|
819
|
+
to expand.
|
|
810
820
|
- **`sort_header`** — `SortHeader` + `SortState`/`SortDir` + `cycleSort` + `sortBy` +
|
|
811
821
|
`SortHeaderLabels`: the sortable column header and the sort-state helpers `Table`/
|
|
812
822
|
`DataGrid` consumers drive.
|
package/package.json
CHANGED
package/src/breakdown.tsx
CHANGED
|
@@ -97,7 +97,9 @@ export function Breakdown(props: BreakdownProps) {
|
|
|
97
97
|
<PressableHighlight
|
|
98
98
|
focusRing key={item.key}
|
|
99
99
|
accessibilityRole="button"
|
|
100
|
-
accessibilityState
|
|
100
|
+
// The W3C prop — `accessibilityState` is dropped by this react-native-web
|
|
101
|
+
// build, and `selected` has no meaning on a `button` role regardless.
|
|
102
|
+
aria-pressed={selected}
|
|
101
103
|
accessibilityLabel={`${item.label}: ${formatValue(item.value)}`}
|
|
102
104
|
onPress={() => onSelect(selected ? null : item.key)}
|
|
103
105
|
style={[styles.row, styles.pressable, selected ? styles.selected : null]}
|
package/src/choice_list.tsx
CHANGED
|
@@ -46,7 +46,9 @@ function ChoiceRow({ option, selected, onSelect }: { option: ChoiceOption; selec
|
|
|
46
46
|
<Pressable
|
|
47
47
|
accessibilityRole="button"
|
|
48
48
|
accessibilityLabel={option.label}
|
|
49
|
-
accessibilityState
|
|
49
|
+
// The W3C prop — `accessibilityState` is dropped by this react-native-web
|
|
50
|
+
// build, and `selected` has no meaning on a `button` role regardless.
|
|
51
|
+
aria-pressed={selected}
|
|
50
52
|
onPress={onSelect}
|
|
51
53
|
{...focusProps}
|
|
52
54
|
style={({ hovered, pressed }) => [
|
package/src/date_calendar.tsx
CHANGED
|
@@ -31,7 +31,9 @@ function DayCell(props: {
|
|
|
31
31
|
<Pressable
|
|
32
32
|
accessibilityRole="button"
|
|
33
33
|
accessibilityLabel={props.label}
|
|
34
|
-
accessibilityState
|
|
34
|
+
// The W3C prop — `accessibilityState` is dropped by this react-native-web
|
|
35
|
+
// build, and `selected` has no meaning on a `button` role regardless.
|
|
36
|
+
aria-pressed={props.isActive}
|
|
35
37
|
{...focusProps}
|
|
36
38
|
style={({ hovered }) => [
|
|
37
39
|
styles.dayCell,
|
package/src/funnel.tsx
CHANGED
|
@@ -91,7 +91,9 @@ export function Funnel(props: FunnelProps) {
|
|
|
91
91
|
return (
|
|
92
92
|
<PressableHighlight
|
|
93
93
|
accessibilityRole="button"
|
|
94
|
-
accessibilityState
|
|
94
|
+
// The W3C prop — `accessibilityState` is dropped by this react-native-web
|
|
95
|
+
// build, and `selected` has no meaning on a `button` role regardless.
|
|
96
|
+
aria-pressed={selectedKey === key}
|
|
95
97
|
accessibilityLabel={label}
|
|
96
98
|
onPress={() => onSelect(selectedKey === key ? null : key)}
|
|
97
99
|
style={fill ? styles.pressFill : undefined}
|
package/src/heatmap.tsx
CHANGED
|
@@ -108,7 +108,9 @@ function Cell(props: CellProps) {
|
|
|
108
108
|
return (
|
|
109
109
|
<Pressable
|
|
110
110
|
accessibilityRole="button"
|
|
111
|
-
accessibilityState
|
|
111
|
+
// The W3C prop — `accessibilityState` is dropped by this react-native-web
|
|
112
|
+
// build, and `selected` has no meaning on a `button` role regardless.
|
|
113
|
+
aria-pressed={selected}
|
|
112
114
|
accessibilityLabel={label}
|
|
113
115
|
onPress={onPress}
|
|
114
116
|
{...focusProps}
|
package/src/matrix.tsx
CHANGED
|
@@ -222,7 +222,9 @@ function MatrixCell({ ctx, display, row, col, value }: MatrixCellProps) {
|
|
|
222
222
|
return (
|
|
223
223
|
<FocusRingPressable
|
|
224
224
|
accessibilityRole="button"
|
|
225
|
-
accessibilityState
|
|
225
|
+
// The W3C prop — `accessibilityState` is dropped by this react-native-web
|
|
226
|
+
// build, and `selected` has no meaning on a `button` role regardless.
|
|
227
|
+
aria-pressed={isSelected}
|
|
226
228
|
accessibilityLabel={label}
|
|
227
229
|
onPress={() => onSelectCell?.(isSelected ? null : { row: row.key, col: col.key })}
|
|
228
230
|
style={(state) => [
|
package/src/press_door.tsx
CHANGED
|
@@ -10,6 +10,13 @@ export interface PressDoorProps {
|
|
|
10
10
|
* without a label it is a nameless button. Name the destination ("Open ORD-1042").
|
|
11
11
|
*/
|
|
12
12
|
accessibilityLabel: string;
|
|
13
|
+
/**
|
|
14
|
+
* Set ONLY when the press toggles a disclosure the door owns (`TableRow`'s
|
|
15
|
+
* `detail`), never when it navigates: it emits `aria-expanded`, which promises
|
|
16
|
+
* a screen-reader user that this control shows and hides something in place.
|
|
17
|
+
* Omit and no state is announced.
|
|
18
|
+
*/
|
|
19
|
+
expanded?: boolean;
|
|
13
20
|
/** Focus-ring corner radius — match the surface the door spans. Default 10 (the register wash). */
|
|
14
21
|
radius?: number;
|
|
15
22
|
}
|
|
@@ -44,12 +51,16 @@ export interface PressDoorProps {
|
|
|
44
51
|
*
|
|
45
52
|
* `TableRow` is the in-kit consumer.
|
|
46
53
|
*/
|
|
47
|
-
export function PressDoor({ onPress, accessibilityLabel, radius = 10 }: PressDoorProps) {
|
|
54
|
+
export function PressDoor({ onPress, accessibilityLabel, expanded, radius = 10 }: PressDoorProps) {
|
|
48
55
|
const { focusVisible, focusProps } = useFocusRing();
|
|
49
56
|
return (
|
|
50
57
|
<Pressable
|
|
51
58
|
accessibilityRole="button"
|
|
52
59
|
accessibilityLabel={accessibilityLabel}
|
|
60
|
+
// The W3C prop, NOT `accessibilityState` — this react-native-web build drops
|
|
61
|
+
// the latter silently. Set only when the door TOGGLES something: on a door
|
|
62
|
+
// that navigates it would promise a disclosure the press does not perform.
|
|
63
|
+
aria-expanded={expanded}
|
|
53
64
|
onPress={onPress}
|
|
54
65
|
{...focusProps}
|
|
55
66
|
style={[styles.door, { borderRadius: radius }, focusVisible && { boxShadow: FOCUS_RING }]}
|
package/src/status_grid.tsx
CHANGED
|
@@ -133,7 +133,9 @@ export function StatusLegend(props: StatusLegendProps) {
|
|
|
133
133
|
<PressableHighlight
|
|
134
134
|
focusRing key={state.key}
|
|
135
135
|
accessibilityRole="button"
|
|
136
|
-
accessibilityState
|
|
136
|
+
// The W3C prop — `accessibilityState` is dropped by this react-native-web
|
|
137
|
+
// build, and `selected` has no meaning on a `button` role regardless.
|
|
138
|
+
aria-pressed={selected}
|
|
137
139
|
accessibilityLabel={`${state.label}: ${count}`}
|
|
138
140
|
onPress={() => onSelect(selected ? null : state.key)}
|
|
139
141
|
style={[styles.legendItem, styles.legendPressable, selected ? styles.legendSelected : null]}
|
package/src/table.tsx
CHANGED
|
@@ -164,6 +164,27 @@ export type TableRowProps = {
|
|
|
164
164
|
action?: ReactNode;
|
|
165
165
|
/** Min row height (register mode; stacked rows size to their content). Default 52. */
|
|
166
166
|
minHeight?: number;
|
|
167
|
+
/**
|
|
168
|
+
* The row's detail, revealed BENEATH it when `expanded` — the alternative to
|
|
169
|
+
* sending the reader somewhere else to see it.
|
|
170
|
+
*
|
|
171
|
+
* A register answers "which one", and the moment it cannot answer "and what
|
|
172
|
+
* about it" the reader is pushed into a drawer, loses the surrounding rows,
|
|
173
|
+
* and pays a navigation to come back — which is what makes scanning twenty
|
|
174
|
+
* records unaffordable. Expanding in place keeps the answer next to the
|
|
175
|
+
* question. Reach for it when the detail is READ or light-edit; a heavy form
|
|
176
|
+
* still deserves its own surface.
|
|
177
|
+
*
|
|
178
|
+
* Outside the pressable, so a control inside the detail is not swallowed by
|
|
179
|
+
* the row's own press.
|
|
180
|
+
*/
|
|
181
|
+
detail?: ReactNode;
|
|
182
|
+
/**
|
|
183
|
+
* Whether `detail` is showing. CONTROLLED — the caller owns it, because who
|
|
184
|
+
* may be open at once is the caller's rule (one at a time, several, or one
|
|
185
|
+
* per group) and a component-local default would silently pick one.
|
|
186
|
+
*/
|
|
187
|
+
expanded?: boolean;
|
|
167
188
|
/** The `TableCell`s, one per column, in column order. */
|
|
168
189
|
children: ReactNode;
|
|
169
190
|
} & (
|
|
@@ -198,9 +219,10 @@ export type TableRowProps = {
|
|
|
198
219
|
* slots, different geometry.
|
|
199
220
|
*/
|
|
200
221
|
export function TableRow(props: TableRowProps) {
|
|
201
|
-
const { onPress, selected, marked, accessibilityLabel, leading, trailing, action, minHeight = 52, children } = props;
|
|
222
|
+
const { onPress, selected, marked, accessibilityLabel, leading, trailing, action, minHeight = 52, detail, expanded, children } = props;
|
|
202
223
|
const ctx = useContext(TableContext);
|
|
203
224
|
if (!ctx) throw new Error("TableRow must be used within a Table");
|
|
225
|
+
const showDetail = detail != null && expanded === true;
|
|
204
226
|
|
|
205
227
|
const cells = (Children.toArray(children).filter(isValidElement) as ReactElement<TableCellProps>[])
|
|
206
228
|
.map((cell, i) => ({ cell, column: ctx.columns[i] as TableColumn | undefined }))
|
|
@@ -249,13 +271,13 @@ export function TableRow(props: TableRowProps) {
|
|
|
249
271
|
);
|
|
250
272
|
}
|
|
251
273
|
|
|
252
|
-
|
|
253
|
-
<PressableRow onPress={onPress} selected={selected} marked={marked} style={styles.row}>
|
|
274
|
+
const rowSurface = (
|
|
275
|
+
<PressableRow onPress={onPress} selected={selected || showDetail} marked={marked} style={styles.row}>
|
|
254
276
|
{!ctx.stacked && ctx.leading > 0 ? <View style={[styles.slot, { width: ctx.leading }]}>{leading}</View> : null}
|
|
255
277
|
{/* The door hit-tests above in-flow content; the cells/slots lift back above it
|
|
256
278
|
via zIndex 1 (see `PressDoor`), so the door gets only the keyboard. Radius
|
|
257
279
|
matches the register wash. */}
|
|
258
|
-
<PressDoor onPress={onPress} accessibilityLabel={accessibilityLabel} />
|
|
280
|
+
<PressDoor onPress={onPress} accessibilityLabel={accessibilityLabel} expanded={detail != null ? showDetail : undefined} />
|
|
259
281
|
{ctx.stacked ? (
|
|
260
282
|
body
|
|
261
283
|
) : (
|
|
@@ -271,6 +293,19 @@ export function TableRow(props: TableRowProps) {
|
|
|
271
293
|
)}
|
|
272
294
|
</PressableRow>
|
|
273
295
|
);
|
|
296
|
+
|
|
297
|
+
if (!showDetail) return rowSurface;
|
|
298
|
+
|
|
299
|
+
// The detail is a SIBLING of the pressable, never inside it: the row's press
|
|
300
|
+
// surface spans its own children, so a control nested in the detail would be
|
|
301
|
+
// swallowed by the toggle — and a `<button>` inside the door is invalid HTML
|
|
302
|
+
// for the same reason the cells sit outside it.
|
|
303
|
+
return (
|
|
304
|
+
<View>
|
|
305
|
+
{rowSurface}
|
|
306
|
+
<View style={styles.detail}>{detail}</View>
|
|
307
|
+
</View>
|
|
308
|
+
);
|
|
274
309
|
}
|
|
275
310
|
|
|
276
311
|
export interface TableCellProps {
|
|
@@ -301,6 +336,14 @@ export function TableCell(props: TableCellProps) {
|
|
|
301
336
|
}
|
|
302
337
|
|
|
303
338
|
const styles = StyleSheet.create({
|
|
339
|
+
// The expanded detail. Indented to the identity column's text edge so it reads
|
|
340
|
+
// as belonging to the row above rather than as a new row, and given the row's
|
|
341
|
+
// own gutter so its content lines up with the cells it explains.
|
|
342
|
+
detail: {
|
|
343
|
+
paddingLeft: ROW_GUTTER,
|
|
344
|
+
paddingRight: ROW_GUTTER,
|
|
345
|
+
paddingBottom: 16,
|
|
346
|
+
},
|
|
304
347
|
// A hairline under the column header anchors the columns; the rows below it are
|
|
305
348
|
// Divider-separated.
|
|
306
349
|
headerBand: {
|