@lotics/ui 24.0.0 → 24.0.2
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/MIGRATION.md +3 -3
- package/docs/catalog.md +6 -2
- package/package.json +1 -1
- package/src/locale.tsx +7 -0
- package/src/reference_field.tsx +6 -2
- package/src/use_section_nav.ts +31 -1
package/MIGRATION.md
CHANGED
|
@@ -139,10 +139,10 @@ Porting, by what the slot held:
|
|
|
139
139
|
|
|
140
140
|
// AFTER — a value in a row, in whatever section the reference belongs to.
|
|
141
141
|
// It wears the field surface, carries its go-to verb INSIDE, and peeks its
|
|
142
|
-
// facts on press; detach lives in the peek. `
|
|
143
|
-
// is the worked composition.
|
|
142
|
+
// facts on press; detach lives in the peek. `ReferenceField` is the kit
|
|
143
|
+
// primitive; `tpl_record` § General is the worked composition.
|
|
144
144
|
<DetailRow label="Customer">
|
|
145
|
-
<
|
|
145
|
+
<ReferenceField name={c.name} code={c.code} facts={…} openLabel={`Open ${c.name}`} onOpen={open} onRemove={detach} />
|
|
146
146
|
</DetailRow>
|
|
147
147
|
```
|
|
148
148
|
|
package/docs/catalog.md
CHANGED
|
@@ -825,7 +825,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
825
825
|
like the editors above and below it), whose press opens a PEEK of that record's facts.
|
|
826
826
|
`name` + optional `code` + `facts` (label/value pairs) + `onOpen`/`openLabel` and an
|
|
827
827
|
optional `onRemove`. The verbs live INSIDE the peek, never on the field: `Open` goes to
|
|
828
|
-
the referenced record's own page, `Remove` detaches
|
|
828
|
+
the referenced record's own page, `Remove` detaches — both come from the locale pack
|
|
829
|
+
(`referenceField.open` / `.remove`), while `openLabel` stays a prop because it names the
|
|
830
|
+
DESTINATION for a screen reader, which only the caller knows. The value is a `TextLink` marker
|
|
829
831
|
(underlined, neutral) because the press opens a summary rather than navigating. A
|
|
830
832
|
reference is a FIELD, not a section — one pointer to one other row never earns a heading
|
|
831
833
|
and a rail entry. Worked example: [`tpl_record`](../examples/tpl_record.tsx) § General
|
|
@@ -1051,7 +1053,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1051
1053
|
rail item is navigation, never a selected option);
|
|
1052
1054
|
section wrappers must be DIRECT children of the ScrollView content. The highlight and the
|
|
1053
1055
|
jump both re-measure the live DOM on web, so content loading in above a section never
|
|
1054
|
-
strands them.
|
|
1056
|
+
strands them. Reaching the END of the scroll activates the LAST key regardless of offsets —
|
|
1057
|
+
a final section shorter than the viewport never scrolls its top past the trigger line, so
|
|
1058
|
+
without that it could never be the active one. On NARROW containers
|
|
1055
1059
|
the rail becomes a PINNED bar naming the CURRENT section that opens a full-page
|
|
1056
1060
|
section-picker `Modal` — never a horizontal tab strip. Worked example:
|
|
1057
1061
|
[`tpl_record`](../examples/tpl_record.tsx).
|
package/package.json
CHANGED
package/src/locale.tsx
CHANGED
|
@@ -37,6 +37,11 @@ export interface LoticsLocale {
|
|
|
37
37
|
pagination: Required<PaginationLabels>;
|
|
38
38
|
/** `SortHeader` a11y prefix + asc/desc suffixes. */
|
|
39
39
|
sortHeader: Required<SortHeaderLabels>;
|
|
40
|
+
/** `ReferenceField`'s peek footer — the go-to and detach verbs. `openLabel`
|
|
41
|
+
* stays a per-instance PROP because it names the DESTINATION ("Open
|
|
42
|
+
* customer"), which is a11y text only a caller knows; these two are the
|
|
43
|
+
* VISIBLE chrome, which the pack owns. */
|
|
44
|
+
referenceField: { open: string; remove: string };
|
|
40
45
|
/** `OptionList` (and everything built on it — `Select`, `Combobox`, the
|
|
41
46
|
* in-cell editors): the select-all/deselect-all links, the empty state, the
|
|
42
47
|
* internal search-field placeholder, and the `Combobox` recents header. */
|
|
@@ -215,6 +220,7 @@ export const en: LoticsLocale = {
|
|
|
215
220
|
ascending: ", ascending",
|
|
216
221
|
descending: ", descending",
|
|
217
222
|
},
|
|
223
|
+
referenceField: { open: "Open", remove: "Remove" },
|
|
218
224
|
optionList: { selectAll: "Select all", deselectAll: "Deselect all", clear: "Clear", noResults: "No results", recent: "Recent", searchPlaceholder: "Search…" },
|
|
219
225
|
datePicker: { today: "Today", now: "Now", clear: "Clear", done: "Done", openCalendar: "Open calendar", time: "Time", startTime: "Start time", endTime: "End time", startDate: "Start date", endDate: "End date", addTime: "Add time", removeTime: "Remove time", year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", dayPeriod: "AM/PM", invalidDate: "Enter a complete date", invalidTime: "Enter a complete time" },
|
|
220
226
|
calendar: { previousMonth: "Previous month", nextMonth: "Next month" },
|
|
@@ -373,6 +379,7 @@ export const vi: LoticsLocale = {
|
|
|
373
379
|
ascending: " (tăng dần)",
|
|
374
380
|
descending: " (giảm dần)",
|
|
375
381
|
},
|
|
382
|
+
referenceField: { open: "Mở", remove: "Gỡ" },
|
|
376
383
|
optionList: { selectAll: "Chọn tất cả", deselectAll: "Bỏ chọn tất cả", clear: "Xóa", noResults: "Không có kết quả", recent: "Gần đây", searchPlaceholder: "Tìm…" },
|
|
377
384
|
datePicker: { today: "Hôm nay", now: "Bây giờ", clear: "Xóa", done: "Xong", openCalendar: "Mở lịch", time: "Giờ", startTime: "Giờ bắt đầu", endTime: "Giờ kết thúc", startDate: "Ngày bắt đầu", endDate: "Ngày kết thúc", addTime: "Thêm giờ", removeTime: "Bỏ giờ", year: "Năm", month: "Tháng", day: "Ngày", hour: "Giờ", minute: "Phút", dayPeriod: "SA/CH", invalidDate: "Nhập ngày đầy đủ", invalidTime: "Nhập giờ đầy đủ" },
|
|
378
385
|
calendar: { previousMonth: "Tháng trước", nextMonth: "Tháng sau" },
|
package/src/reference_field.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { DialogSectionHeadingTitle } from "./section_heading";
|
|
|
9
9
|
import { Text } from "./text";
|
|
10
10
|
import { TextButton } from "./text_button";
|
|
11
11
|
import { TextLink } from "./text_link";
|
|
12
|
+
import { useLoticsLocale } from "./locale";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* A REFERENCE to another record, rendered as a FIELD VALUE.
|
|
@@ -63,6 +64,9 @@ export interface ReferenceFieldProps {
|
|
|
63
64
|
|
|
64
65
|
export function ReferenceField(props: ReferenceFieldProps) {
|
|
65
66
|
const { name, code, facts, accessibilityLabel, onOpen, openLabel, onRemove } = props;
|
|
67
|
+
// The peek's two verbs are the component's OWN chrome, so they come from the
|
|
68
|
+
// pack — hardcoding them shipped "Open"/"Remove" into every localized app.
|
|
69
|
+
const t = useLoticsLocale().referenceField;
|
|
66
70
|
const anchor = useRef<View>(null);
|
|
67
71
|
const [peekOpen, setPeekOpen] = useState(false);
|
|
68
72
|
return (
|
|
@@ -159,13 +163,13 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
159
163
|
// the quiet/destructive verb at text weight beside a filled primary.
|
|
160
164
|
// No icon: an ✕ beside "Remove" says it twice, and the glyph alone
|
|
161
165
|
// made this read as the popover's dismiss rather than a decision.
|
|
162
|
-
<TextButton color="danger" onPress={() => { setPeekOpen(false); onRemove(); }}>
|
|
166
|
+
<TextButton color="danger" onPress={() => { setPeekOpen(false); onRemove(); }}>{t.remove}</TextButton>
|
|
163
167
|
) : null}
|
|
164
168
|
<View style={{ flex: 1 }} />
|
|
165
169
|
{/* `openLabel` names the DESTINATION ("Open customer"): a page carries
|
|
166
170
|
four of these peeks, and four buttons announcing a bare "Open"
|
|
167
171
|
are four controls a screen reader cannot tell apart. */}
|
|
168
|
-
<Button title=
|
|
172
|
+
<Button title={t.open} color="secondary" accessibilityLabel={openLabel} onPress={() => { setPeekOpen(false); onOpen(); }} />
|
|
169
173
|
</View>
|
|
170
174
|
</View>
|
|
171
175
|
</PopoverContent>
|
package/src/use_section_nav.ts
CHANGED
|
@@ -14,6 +14,14 @@ export interface SectionNavHandle<K extends string> {
|
|
|
14
14
|
onScroll: (e: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* How close to the content's end still counts as "at the end". Fractional
|
|
19
|
+
* scroll offsets (zoom, hi-dpi, momentum) land a pixel or two short of the
|
|
20
|
+
* exact bottom, and a strict equality would drop the rule on the frame that
|
|
21
|
+
* matters most — the one the reader stops on.
|
|
22
|
+
*/
|
|
23
|
+
const END_SLACK = 2;
|
|
24
|
+
|
|
17
25
|
/**
|
|
18
26
|
* Scroll-spy for a LONG record surface with a left outline rail: one scrolling
|
|
19
27
|
* page whose sections register their offsets, a rail of `MenuButton`s that
|
|
@@ -39,6 +47,10 @@ export interface SectionNavHandle<K extends string> {
|
|
|
39
47
|
* grid, a late-loading table) can never stale either. On native there is no
|
|
40
48
|
* re-measure — content above should settle before precision matters
|
|
41
49
|
* (`onLayout` won't refire on a pure position shift).
|
|
50
|
+
*
|
|
51
|
+
* The LAST key is active whenever the scroll reaches the end, regardless of
|
|
52
|
+
* offsets: a final section shorter than the viewport never scrolls its top past
|
|
53
|
+
* the trigger line, so without that rule the rail can never point at it.
|
|
42
54
|
*/
|
|
43
55
|
export function useSectionNav<K extends string>(keys: readonly [K, ...K[]]): SectionNavHandle<K> {
|
|
44
56
|
const scrollRef = useRef<ScrollView>(null);
|
|
@@ -93,13 +105,31 @@ export function useSectionNav<K extends string>(keys: readonly [K, ...K[]]): Sec
|
|
|
93
105
|
scrollRef.current?.scrollTo({ y: Math.max(0, y - 12), animated: true });
|
|
94
106
|
};
|
|
95
107
|
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
96
|
-
const
|
|
108
|
+
const { contentOffset, layoutMeasurement, contentSize } = e.nativeEvent;
|
|
109
|
+
const y = contentOffset.y + 80;
|
|
97
110
|
const offsets = measure();
|
|
98
111
|
let cur: K = keys[0];
|
|
99
112
|
for (const k of keys) {
|
|
100
113
|
const sy = offsets[k];
|
|
101
114
|
if (sy != null && sy <= y) cur = k;
|
|
102
115
|
}
|
|
116
|
+
// THE LAST SECTION IS OTHERWISE UNREACHABLE. A section opening within one
|
|
117
|
+
// viewport of the content's end can never scroll its top past the trigger
|
|
118
|
+
// line: at maximum scroll `y` is `contentHeight - viewportHeight + 80`,
|
|
119
|
+
// which is still ABOVE it, so the rule above pins the rail to the section
|
|
120
|
+
// before it and the reader watches the wrong item stay lit while they read.
|
|
121
|
+
// A trailing spacer would "fix" it by making every record end in blank
|
|
122
|
+
// space, which is paying in layout for an arithmetic problem.
|
|
123
|
+
//
|
|
124
|
+
// At the end of the scroll there is nothing left to disambiguate — the
|
|
125
|
+
// reader is as deep into the last section as the page permits — so the end
|
|
126
|
+
// of the scroll IS the last section. Guarded on both measurements being
|
|
127
|
+
// present: RN gives them on every scroll event, but a caller synthesising
|
|
128
|
+
// one cannot be assumed to, and without them "at the end" is unknowable.
|
|
129
|
+
if (layoutMeasurement != null && contentSize != null) {
|
|
130
|
+
const remaining = contentSize.height - (contentOffset.y + layoutMeasurement.height);
|
|
131
|
+
if (remaining <= END_SLACK) cur = keys[keys.length - 1];
|
|
132
|
+
}
|
|
103
133
|
if (cur !== activeKey) setActiveKey(cur);
|
|
104
134
|
};
|
|
105
135
|
return { scrollRef, activeKey, register, jumpTo, onScroll };
|