panelui-native 0.36.0 → 0.40.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/README.md +13 -1
- package/lib/module/components/combobox/index.js +718 -0
- package/lib/module/components/combobox/index.js.map +1 -0
- package/lib/module/components/form/use-field.js +18 -0
- package/lib/module/components/form/use-field.js.map +1 -1
- package/lib/module/components/form/use-form.js +91 -8
- package/lib/module/components/form/use-form.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +113 -6
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/input/index.js +10 -3
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/pagination/index.js +519 -0
- package/lib/module/components/pagination/index.js.map +1 -0
- package/lib/module/components/ring-chart/index.js +183 -45
- package/lib/module/components/ring-chart/index.js.map +1 -1
- package/lib/module/components/select/index.js +116 -15
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/steps/index.js +8 -6
- package/lib/module/components/steps/index.js.map +1 -1
- package/lib/module/components/table/index.js +100 -26
- package/lib/module/components/table/index.js.map +1 -1
- package/lib/module/components/timeline/index.js +8 -7
- package/lib/module/components/timeline/index.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/theme/use-theme.js +1 -1
- package/lib/typescript/src/components/combobox/index.d.ts +164 -0
- package/lib/typescript/src/components/combobox/index.d.ts.map +1 -0
- package/lib/typescript/src/components/form/use-field.d.ts.map +1 -1
- package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +51 -3
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input/index.d.ts +18 -4
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/pagination/index.d.ts +285 -0
- package/lib/typescript/src/components/pagination/index.d.ts.map +1 -0
- package/lib/typescript/src/components/ring-chart/index.d.ts +75 -8
- package/lib/typescript/src/components/ring-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/select/index.d.ts +32 -0
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/steps/index.d.ts +8 -6
- package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
- package/lib/typescript/src/components/table/index.d.ts +59 -17
- package/lib/typescript/src/components/table/index.d.ts.map +1 -1
- package/lib/typescript/src/components/timeline/index.d.ts +8 -7
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/theme/use-theme.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/combobox/index.tsx +927 -0
- package/src/components/form/use-field.ts +23 -0
- package/src/components/form/use-form.ts +106 -16
- package/src/components/heatmap-chart/index.tsx +147 -5
- package/src/components/input/index.tsx +21 -7
- package/src/components/pagination/index.tsx +591 -0
- package/src/components/ring-chart/index.tsx +269 -47
- package/src/components/select/index.tsx +114 -15
- package/src/components/steps/index.tsx +8 -6
- package/src/components/table/index.tsx +146 -30
- package/src/components/timeline/index.tsx +8 -7
- package/src/index.ts +25 -0
|
@@ -47,6 +47,29 @@ export function useField<T extends Record<string, any>, K extends keyof T>(
|
|
|
47
47
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
48
48
|
}, [registerValidator, name, !!validate]);
|
|
49
49
|
|
|
50
|
+
/*
|
|
51
|
+
* A name the form never declared is the one mistake this hook cannot
|
|
52
|
+
* recover from: there is no value to hand a validator, so a rule as ordinary
|
|
53
|
+
* as `value.length > 0` throws on a field that was only blurred, and nothing
|
|
54
|
+
* the user types is ever submitted. Both failures point away from the cause,
|
|
55
|
+
* so it is named here instead. Development only — the check costs a key
|
|
56
|
+
* lookup, but the message is for whoever is writing the form.
|
|
57
|
+
*/
|
|
58
|
+
const formValues = form.values;
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (process.env.NODE_ENV === 'production') return;
|
|
61
|
+
if (name in formValues) return;
|
|
62
|
+
console.warn(
|
|
63
|
+
`[PanelUI] Form field "${String(name)}" is not declared in the form's ` +
|
|
64
|
+
`defaultValues, so its value is undefined and it will not be submitted. ` +
|
|
65
|
+
`Add "${String(name)}" to defaultValues — an empty field is still a ` +
|
|
66
|
+
`declared one ("" for text, false for a toggle).`
|
|
67
|
+
);
|
|
68
|
+
// The declared names come from `defaultValues`, which is read once, so
|
|
69
|
+
// this is a question about the name and nothing else.
|
|
70
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
71
|
+
}, [name]);
|
|
72
|
+
|
|
50
73
|
const { value, error, touched } = form.getFieldState(name);
|
|
51
74
|
|
|
52
75
|
return {
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
* `defaultValues` is read once, on the first render. If default values
|
|
25
25
|
* arrive asynchronously, mount the form once you have them (e.g. behind a
|
|
26
26
|
* loading check) rather than expecting the hook to pick up a later change.
|
|
27
|
+
*
|
|
28
|
+
* It also declares the fields. A field's `name` has to be a key of it —
|
|
29
|
+
* including the empty ones, as `''`, `false`, `null`. A name it does not
|
|
30
|
+
* declare has no value to give a validator and nothing to submit, so
|
|
31
|
+
* `useField` says so in development rather than letting the field quietly do
|
|
32
|
+
* nothing.
|
|
27
33
|
*/
|
|
28
34
|
import { useCallback, useMemo, useReducer, useRef } from 'react';
|
|
29
35
|
|
|
@@ -123,6 +129,42 @@ function reducer<T extends Record<string, any>>(
|
|
|
123
129
|
}
|
|
124
130
|
}
|
|
125
131
|
|
|
132
|
+
/** Stands in for the message a rule that crashed never got to return. */
|
|
133
|
+
const VALIDATOR_THREW = 'This field could not be validated.';
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Run one field's rule, and survive a rule that throws.
|
|
137
|
+
*
|
|
138
|
+
* A validator is caller code reached from a path nothing awaits — `onBlur`
|
|
139
|
+
* fires and forgets, and `handleSubmit` is wired straight to a press. A throw
|
|
140
|
+
* in there becomes an unhandled rejection: a red box whose message ("cannot
|
|
141
|
+
* read property 'length' of undefined") names neither the field nor the rule,
|
|
142
|
+
* and which takes the screen for one line of one validator.
|
|
143
|
+
*
|
|
144
|
+
* So it is caught, reported against the field by name, and counted as a
|
|
145
|
+
* failure. Counted, rather than waved through: a rule that crashed reached no
|
|
146
|
+
* verdict, and a form that submits past a rule that never ran is the worse of
|
|
147
|
+
* the two outcomes — it puts unchecked values somewhere they cannot be taken
|
|
148
|
+
* back from. The placeholder message is what stands between the two; the
|
|
149
|
+
* console entry beside it is for whoever can fix the rule.
|
|
150
|
+
*/
|
|
151
|
+
async function runValidator<T extends Record<string, any>>(
|
|
152
|
+
name: keyof T,
|
|
153
|
+
validator: (value: any, values: T) => string | undefined | Promise<string | undefined>,
|
|
154
|
+
values: T
|
|
155
|
+
): Promise<string | undefined> {
|
|
156
|
+
try {
|
|
157
|
+
return await validator(values[name], values);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error(
|
|
160
|
+
`[PanelUI] The validator for form field "${String(name)}" threw, so the ` +
|
|
161
|
+
`field is being treated as invalid.`,
|
|
162
|
+
error
|
|
163
|
+
);
|
|
164
|
+
return VALIDATOR_THREW;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
126
168
|
export function useForm<T extends Record<string, any>>({
|
|
127
169
|
defaultValues,
|
|
128
170
|
validate,
|
|
@@ -140,7 +182,22 @@ export function useForm<T extends Record<string, any>>({
|
|
|
140
182
|
isSubmitting: false,
|
|
141
183
|
});
|
|
142
184
|
|
|
185
|
+
/*
|
|
186
|
+
* The values as of the last *change*, not as of the last render.
|
|
187
|
+
*
|
|
188
|
+
* Validation runs in the same tick as the edit that triggered it —
|
|
189
|
+
* `validateOn="change"` calls `validateField` immediately after
|
|
190
|
+
* `setFieldValue`, and submit runs from a press handler. React state is a
|
|
191
|
+
* render behind at that point, so a validator reading it would judge the
|
|
192
|
+
* character before the one just typed, and a submit fired straight after an
|
|
193
|
+
* edit would submit the value before it. The ref is written on the way into
|
|
194
|
+
* the dispatch so both see what the user actually entered.
|
|
195
|
+
*/
|
|
196
|
+
const valuesRef = useRef(state.values);
|
|
197
|
+
valuesRef.current = state.values;
|
|
198
|
+
|
|
143
199
|
const setFieldValue = useCallback(<K extends keyof T>(name: K, value: T[K]) => {
|
|
200
|
+
valuesRef.current = { ...valuesRef.current, [name]: value } as T;
|
|
144
201
|
dispatch({ type: 'SET_VALUE', name, value });
|
|
145
202
|
}, []);
|
|
146
203
|
|
|
@@ -175,32 +232,64 @@ export function useForm<T extends Record<string, any>>({
|
|
|
175
232
|
[state.values, state.errors, state.touched]
|
|
176
233
|
);
|
|
177
234
|
|
|
178
|
-
const validateField = useCallback(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
[state.values]
|
|
187
|
-
);
|
|
235
|
+
const validateField = useCallback(async <K extends keyof T>(name: K) => {
|
|
236
|
+
const validator = validatorsRef.current[name];
|
|
237
|
+
if (!validator) return true;
|
|
238
|
+
const values = valuesRef.current;
|
|
239
|
+
const error = await runValidator(name, validator, values);
|
|
240
|
+
dispatch({ type: 'SET_ERROR', name, error });
|
|
241
|
+
return !error;
|
|
242
|
+
}, []);
|
|
188
243
|
|
|
189
244
|
const handleSubmit = useCallback(async () => {
|
|
190
|
-
const
|
|
245
|
+
const values = valuesRef.current;
|
|
246
|
+
/*
|
|
247
|
+
* Every field the form knows of, which is not the same as every key of
|
|
248
|
+
* `defaultValues`: a field can register a validator under a name that was
|
|
249
|
+
* never declared there. Submitting only the declared ones meant such a
|
|
250
|
+
* field was never validated and never blocked anything, so a form of
|
|
251
|
+
* required-but-undeclared fields submitted itself while still empty.
|
|
252
|
+
*
|
|
253
|
+
* It is still a mistake to name a field that `defaultValues` does not
|
|
254
|
+
* declare — its value stays undefined and never reaches `onSubmit` —
|
|
255
|
+
* which is why `useField` says so. Validating it anyway is what turns
|
|
256
|
+
* that mistake into a visible error instead of a silent submit.
|
|
257
|
+
*/
|
|
258
|
+
const names = Array.from(
|
|
259
|
+
new Set([...Object.keys(values), ...Object.keys(validatorsRef.current)])
|
|
260
|
+
) as (keyof T)[];
|
|
191
261
|
dispatch({ type: 'TOUCH_ALL', names });
|
|
192
262
|
|
|
193
263
|
const fieldErrorEntries = await Promise.all(
|
|
194
264
|
names.map(async (name) => {
|
|
195
265
|
const validator = validatorsRef.current[name];
|
|
196
266
|
const error = validator
|
|
197
|
-
? await
|
|
267
|
+
? await runValidator(name, validator, values)
|
|
198
268
|
: undefined;
|
|
199
269
|
return [name, error] as const;
|
|
200
270
|
})
|
|
201
271
|
);
|
|
202
272
|
|
|
203
|
-
|
|
273
|
+
/*
|
|
274
|
+
* Same reasoning as a field's rule, one level up: a cross-field check that
|
|
275
|
+
* throws must not take the screen, and must not let the submit through.
|
|
276
|
+
* It belongs to no single field, so it blocks without marking one — there
|
|
277
|
+
* is no field whose error line would be the honest place to put it.
|
|
278
|
+
*/
|
|
279
|
+
let formErrors: FieldErrors<T> = {};
|
|
280
|
+
let formValidateThrew = false;
|
|
281
|
+
if (validate) {
|
|
282
|
+
try {
|
|
283
|
+
formErrors = await validate(values);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error(
|
|
286
|
+
'[PanelUI] The form-level validate() threw, so the form is being ' +
|
|
287
|
+
'treated as invalid and the submit was not run.',
|
|
288
|
+
error
|
|
289
|
+
);
|
|
290
|
+
formValidateThrew = true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
204
293
|
|
|
205
294
|
const cleared = Object.fromEntries(names.map((name) => [name, undefined]));
|
|
206
295
|
const nextErrors: FieldErrors<T> = {
|
|
@@ -210,17 +299,18 @@ export function useForm<T extends Record<string, any>>({
|
|
|
210
299
|
};
|
|
211
300
|
dispatch({ type: 'SET_ERRORS', errors: nextErrors });
|
|
212
301
|
|
|
213
|
-
if (Object.values(nextErrors).some(Boolean)) return;
|
|
302
|
+
if (formValidateThrew || Object.values(nextErrors).some(Boolean)) return;
|
|
214
303
|
|
|
215
304
|
dispatch({ type: 'SUBMIT_START' });
|
|
216
305
|
try {
|
|
217
|
-
await onSubmit(
|
|
306
|
+
await onSubmit(values);
|
|
218
307
|
} finally {
|
|
219
308
|
dispatch({ type: 'SUBMIT_END' });
|
|
220
309
|
}
|
|
221
|
-
}, [
|
|
310
|
+
}, [validate, onSubmit]);
|
|
222
311
|
|
|
223
312
|
const reset = useCallback((values: T = defaultsRef.current) => {
|
|
313
|
+
valuesRef.current = values;
|
|
224
314
|
dispatch({ type: 'RESET', values });
|
|
225
315
|
}, []);
|
|
226
316
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* ```tsx
|
|
13
13
|
* <HeatmapChart data={weeks}>
|
|
14
|
+
* <HeatmapChart.Header title="Contributions" value="1,204" legend />
|
|
14
15
|
* <HeatmapChart.YAxis />
|
|
15
16
|
* <HeatmapChart.XAxis />
|
|
16
17
|
* <HeatmapChart.Cells />
|
|
@@ -132,7 +133,7 @@ const MONTHS = [
|
|
|
132
133
|
* composition stays a flat list of children instead of four nested slots the
|
|
133
134
|
* caller has to remember the order of.
|
|
134
135
|
*/
|
|
135
|
-
type Slot = 'cells' | 'x-axis' | 'y-axis' | 'rules' | 'tooltip' | 'legend';
|
|
136
|
+
type Slot = 'cells' | 'x-axis' | 'y-axis' | 'rules' | 'tooltip' | 'legend' | 'header';
|
|
136
137
|
|
|
137
138
|
export type HeatmapLayout = 'fluid' | 'fill';
|
|
138
139
|
|
|
@@ -566,6 +567,10 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
|
|
|
566
567
|
onLayout={onLayout}
|
|
567
568
|
className={cn('w-full', className)}
|
|
568
569
|
>
|
|
570
|
+
{/* Above the axis gutter as well as the grid: the header is about the
|
|
571
|
+
whole chart, so it starts at the chart's edge, not the grid's. */}
|
|
572
|
+
{parts.header}
|
|
573
|
+
|
|
569
574
|
{parts.x ? (
|
|
570
575
|
<View style={{ paddingLeft: axisWidth }}>{parts.x}</View>
|
|
571
576
|
) : null}
|
|
@@ -605,6 +610,7 @@ interface Parts {
|
|
|
605
610
|
y: React.ReactElement<{ width?: number }> | null;
|
|
606
611
|
tooltip: ReactNode[];
|
|
607
612
|
legend: ReactNode[];
|
|
613
|
+
header: ReactNode[];
|
|
608
614
|
columns: number;
|
|
609
615
|
}
|
|
610
616
|
|
|
@@ -617,6 +623,7 @@ function splitParts(children: ReactNode): Parts {
|
|
|
617
623
|
y: null,
|
|
618
624
|
tooltip: [],
|
|
619
625
|
legend: [],
|
|
626
|
+
header: [],
|
|
620
627
|
columns: 0,
|
|
621
628
|
};
|
|
622
629
|
|
|
@@ -630,6 +637,7 @@ function splitParts(children: ReactNode): Parts {
|
|
|
630
637
|
else if (slot === 'rules') parts.rules.push(keyed);
|
|
631
638
|
else if (slot === 'tooltip') parts.tooltip.push(keyed);
|
|
632
639
|
else if (slot === 'legend') parts.legend.push(keyed);
|
|
640
|
+
else if (slot === 'header') parts.header.push(keyed);
|
|
633
641
|
else parts.cells.push(keyed);
|
|
634
642
|
});
|
|
635
643
|
|
|
@@ -753,6 +761,12 @@ export interface HeatmapXAxisProps {
|
|
|
753
761
|
* derived. Return an empty string to leave the column unlabelled.
|
|
754
762
|
*/
|
|
755
763
|
formatLabel?: (date: Date, column: number) => string;
|
|
764
|
+
/**
|
|
765
|
+
* Column labels, left to right. Overrides the month names — for a grid whose
|
|
766
|
+
* columns are not weeks, where there is no month to change and so nothing to
|
|
767
|
+
* emit a label on.
|
|
768
|
+
*/
|
|
769
|
+
labels?: string[];
|
|
756
770
|
}
|
|
757
771
|
|
|
758
772
|
/**
|
|
@@ -760,13 +774,21 @@ export interface HeatmapXAxisProps {
|
|
|
760
774
|
*
|
|
761
775
|
* A label is emitted where the month changes rather than at a fixed interval,
|
|
762
776
|
* because months are not the same length — spacing them evenly puts "Mar" over
|
|
763
|
-
* a week in February.
|
|
777
|
+
* a week in February. A grid whose columns are not weeks has no such signal, so
|
|
778
|
+
* it passes `labels` and gets one over every column.
|
|
764
779
|
*/
|
|
765
|
-
function HeatmapXAxis({ className, formatLabel }: HeatmapXAxisProps) {
|
|
780
|
+
function HeatmapXAxis({ className, formatLabel, labels: given }: HeatmapXAxisProps) {
|
|
766
781
|
const { data, grid } = useHeatmap('HeatmapChart.XAxis');
|
|
767
782
|
const step = grid.size + grid.gap;
|
|
768
783
|
|
|
769
784
|
const labels = useMemo(() => {
|
|
785
|
+
if (given) {
|
|
786
|
+
return given
|
|
787
|
+
.slice(0, data.length)
|
|
788
|
+
.map((label, column) => ({ column, label }))
|
|
789
|
+
.filter((entry) => entry.label);
|
|
790
|
+
}
|
|
791
|
+
|
|
770
792
|
const out: { column: number; label: string }[] = [];
|
|
771
793
|
let lastMonth = -1;
|
|
772
794
|
|
|
@@ -784,7 +806,7 @@ function HeatmapXAxis({ className, formatLabel }: HeatmapXAxisProps) {
|
|
|
784
806
|
// collides with the next one. Drop it when it has no room.
|
|
785
807
|
if (out.length > 1 && out[1]!.column - out[0]!.column < 3) out.shift();
|
|
786
808
|
return out;
|
|
787
|
-
}, [data, formatLabel]);
|
|
809
|
+
}, [data, formatLabel, given]);
|
|
788
810
|
|
|
789
811
|
return (
|
|
790
812
|
<View className={cn('h-4', className)} style={{ width: grid.width }}>
|
|
@@ -1002,9 +1024,16 @@ function HeatmapTooltip({
|
|
|
1002
1024
|
}
|
|
1003
1025
|
HeatmapTooltip.slot = 'tooltip' as Slot;
|
|
1004
1026
|
|
|
1027
|
+
/*
|
|
1028
|
+
* A cell with no date is not a cell with no data — it is a cell in a grid that
|
|
1029
|
+
* is not a calendar, and it has a count like any other. Saying "No data" over
|
|
1030
|
+
* it reports a hole in the data that is not there. Without a date there is
|
|
1031
|
+
* simply nothing to say after the number, so the number is the whole label,
|
|
1032
|
+
* and a grid that wants a noun for it passes `formatLabel`.
|
|
1033
|
+
*/
|
|
1005
1034
|
function defaultTooltipLabel(cell: HeatmapCell) {
|
|
1035
|
+
if (!cell.date) return `${cell.count}`;
|
|
1006
1036
|
const count = `${cell.count} ${cell.count === 1 ? 'contribution' : 'contributions'}`;
|
|
1007
|
-
if (!cell.date) return 'No data';
|
|
1008
1037
|
const date = `${MONTHS[cell.date.getMonth()]} ${cell.date.getDate()}`;
|
|
1009
1038
|
return `${count} on ${date}`;
|
|
1010
1039
|
}
|
|
@@ -1061,7 +1090,120 @@ function HeatmapLegend({
|
|
|
1061
1090
|
}
|
|
1062
1091
|
HeatmapLegend.slot = 'legend' as Slot;
|
|
1063
1092
|
|
|
1093
|
+
/* -------------------------------------------------------------------------- */
|
|
1094
|
+
/* Header */
|
|
1095
|
+
/* -------------------------------------------------------------------------- */
|
|
1096
|
+
|
|
1097
|
+
export interface HeatmapHeaderProps extends ViewProps {
|
|
1098
|
+
className?: string;
|
|
1099
|
+
/** Small line above the value — what the grid is of. */
|
|
1100
|
+
title?: string;
|
|
1101
|
+
/** The readout. The largest thing on the card, and the first thing read. */
|
|
1102
|
+
value?: string;
|
|
1103
|
+
/** One muted line under the value — a period, a total, the held cell. */
|
|
1104
|
+
caption?: string;
|
|
1105
|
+
/**
|
|
1106
|
+
* Draw the ramp along the trailing edge, `Less ▢▢▢▢▢ More`. The key for a
|
|
1107
|
+
* grid that scrolls sideways, where `HeatmapChart.Legend` under the cells
|
|
1108
|
+
* would scroll away with them.
|
|
1109
|
+
*/
|
|
1110
|
+
legend?: boolean;
|
|
1111
|
+
/** Text at the low end of the ramp, when `legend` is set. */
|
|
1112
|
+
lessLabel?: string;
|
|
1113
|
+
/** Text at the high end. */
|
|
1114
|
+
moreLabel?: string;
|
|
1115
|
+
/** Trailing slot — a control, a badge, a range picker. Wins over `legend`. */
|
|
1116
|
+
children?: ReactNode;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* The strip above the grid: what the chart is of, what it currently reads, and
|
|
1121
|
+
* what the shading means.
|
|
1122
|
+
*
|
|
1123
|
+
* It belongs to the chart rather than to the card around it because it is about
|
|
1124
|
+
* the *grid* — the number changes as a finger moves across the cells, and the
|
|
1125
|
+
* ramp is the scale the chart itself derived. The card's header is a caption on
|
|
1126
|
+
* the tray the chart sits in; this is the chart introducing itself.
|
|
1127
|
+
*
|
|
1128
|
+
* The value is not derived here. Take it from `onActiveCellChange` and pass the
|
|
1129
|
+
* formatted string down, so one header can show a total when nothing is held
|
|
1130
|
+
* and a day's own count when something is.
|
|
1131
|
+
*/
|
|
1132
|
+
function HeatmapHeader({
|
|
1133
|
+
className,
|
|
1134
|
+
title,
|
|
1135
|
+
value,
|
|
1136
|
+
caption,
|
|
1137
|
+
legend = false,
|
|
1138
|
+
lessLabel = 'Less',
|
|
1139
|
+
moreLabel = 'More',
|
|
1140
|
+
children,
|
|
1141
|
+
...props
|
|
1142
|
+
}: HeatmapHeaderProps) {
|
|
1143
|
+
const { ramp, opacities, grid, cornerRadius } = useHeatmap('HeatmapChart.Header');
|
|
1144
|
+
// Small enough to sit on one line beside the text, whatever the cells are.
|
|
1145
|
+
const size = Math.max(Math.min(grid.size, 12), 8);
|
|
1146
|
+
|
|
1147
|
+
const trailing =
|
|
1148
|
+
children ??
|
|
1149
|
+
(legend ? (
|
|
1150
|
+
<View className="flex-row items-center gap-1.5">
|
|
1151
|
+
<Text size="xs" muted>
|
|
1152
|
+
{lessLabel}
|
|
1153
|
+
</Text>
|
|
1154
|
+
{ramp.map((fill, level) => (
|
|
1155
|
+
<Svg key={level} width={size} height={size}>
|
|
1156
|
+
<Rect
|
|
1157
|
+
x={0}
|
|
1158
|
+
y={0}
|
|
1159
|
+
width={size}
|
|
1160
|
+
height={size}
|
|
1161
|
+
rx={cornerRadius}
|
|
1162
|
+
ry={cornerRadius}
|
|
1163
|
+
fill={fill}
|
|
1164
|
+
fillOpacity={opacities[level]}
|
|
1165
|
+
/>
|
|
1166
|
+
</Svg>
|
|
1167
|
+
))}
|
|
1168
|
+
<Text size="xs" muted>
|
|
1169
|
+
{moreLabel}
|
|
1170
|
+
</Text>
|
|
1171
|
+
</View>
|
|
1172
|
+
) : null);
|
|
1173
|
+
|
|
1174
|
+
return (
|
|
1175
|
+
<View
|
|
1176
|
+
{...props}
|
|
1177
|
+
className={cn('flex-row items-start justify-between gap-3 pb-3', className)}
|
|
1178
|
+
>
|
|
1179
|
+
<View className="flex-1 gap-0.5">
|
|
1180
|
+
{title ? (
|
|
1181
|
+
<Text size="xs" muted>
|
|
1182
|
+
{title}
|
|
1183
|
+
</Text>
|
|
1184
|
+
) : null}
|
|
1185
|
+
{value ? (
|
|
1186
|
+
<Text size="xl" weight="bold">
|
|
1187
|
+
{value}
|
|
1188
|
+
</Text>
|
|
1189
|
+
) : null}
|
|
1190
|
+
{caption ? (
|
|
1191
|
+
<Text size="xs" muted>
|
|
1192
|
+
{caption}
|
|
1193
|
+
</Text>
|
|
1194
|
+
) : null}
|
|
1195
|
+
</View>
|
|
1196
|
+
{/* Shrinkable, unlike a view's default in React Native — held rigid, the
|
|
1197
|
+
ramp takes the width it wants and the caption wraps around it. */}
|
|
1198
|
+
{trailing ? <View className="shrink pt-1">{trailing}</View> : null}
|
|
1199
|
+
</View>
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
HeatmapHeader.displayName = 'HeatmapChart.Header';
|
|
1203
|
+
HeatmapHeader.slot = 'header' as Slot;
|
|
1204
|
+
|
|
1064
1205
|
export const HeatmapChart = Object.assign(HeatmapChartRoot, {
|
|
1206
|
+
Header: HeatmapHeader,
|
|
1065
1207
|
Cells: HeatmapCells,
|
|
1066
1208
|
Separator: HeatmapSeparator,
|
|
1067
1209
|
XAxis: HeatmapXAxis,
|
|
@@ -27,6 +27,13 @@
|
|
|
27
27
|
* field, so anything centred on the component as a whole drifts upward the
|
|
28
28
|
* moment a label is added.
|
|
29
29
|
*
|
|
30
|
+
* Both are pressable. A button dropped in there — a clear ✕, a show-password
|
|
31
|
+
* eye — gets its own touches, while the padding around it stays transparent so
|
|
32
|
+
* a tap that misses still lands on the field and puts the caret in it. An icon
|
|
33
|
+
* that is only decoration should say so with `interactiveContent={false}`,
|
|
34
|
+
* which gives the whole field back to the caret and takes the icon out of the
|
|
35
|
+
* accessibility tree.
|
|
36
|
+
*
|
|
30
37
|
* `InputGroup` is still the right answer for a decorator that is not part of
|
|
31
38
|
* the field — a button attached to its end, a select bolted to its start, an
|
|
32
39
|
* addon with its own background. It measures the same way; it just spans a
|
|
@@ -155,11 +162,18 @@ export interface InputProps
|
|
|
155
162
|
/** Content inside the field, after the text — an icon, a unit, a count. */
|
|
156
163
|
endContent?: ReactNode;
|
|
157
164
|
/**
|
|
158
|
-
*
|
|
165
|
+
* Whether touches reach the content.
|
|
166
|
+
*
|
|
167
|
+
* On by default, so a button placed in the field — a clear ✕, a
|
|
168
|
+
* show-password eye, a unit picker — is pressable without anything else
|
|
169
|
+
* being passed. Only the content itself takes those touches: the padding
|
|
170
|
+
* around it is transparent, so a tap that misses the button still lands on
|
|
171
|
+
* the field and puts the caret in it.
|
|
159
172
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* content
|
|
173
|
+
* Turn it off for pure decoration, where the icon should not be a target at
|
|
174
|
+
* all and every pixel of the field should focus it. That also drops the
|
|
175
|
+
* content from the accessibility tree, which is right for an icon that only
|
|
176
|
+
* restates the label.
|
|
163
177
|
*/
|
|
164
178
|
interactiveContent?: boolean;
|
|
165
179
|
/**
|
|
@@ -205,7 +219,7 @@ export const Input = forwardRef<TextInput, InputProps>(
|
|
|
205
219
|
disabled,
|
|
206
220
|
startContent,
|
|
207
221
|
endContent,
|
|
208
|
-
interactiveContent =
|
|
222
|
+
interactiveContent = true,
|
|
209
223
|
variant,
|
|
210
224
|
size,
|
|
211
225
|
avoidKeyboard = false,
|
|
@@ -342,7 +356,7 @@ export const Input = forwardRef<TextInput, InputProps>(
|
|
|
342
356
|
{startContent ? (
|
|
343
357
|
<View
|
|
344
358
|
onLayout={handleStartLayout}
|
|
345
|
-
pointerEvents={interactiveContent ? '
|
|
359
|
+
pointerEvents={interactiveContent ? 'box-none' : 'none'}
|
|
346
360
|
accessibilityElementsHidden={!interactiveContent}
|
|
347
361
|
importantForAccessibility={
|
|
348
362
|
interactiveContent ? 'auto' : 'no-hide-descendants'
|
|
@@ -358,7 +372,7 @@ export const Input = forwardRef<TextInput, InputProps>(
|
|
|
358
372
|
{endContent ? (
|
|
359
373
|
<View
|
|
360
374
|
onLayout={handleEndLayout}
|
|
361
|
-
pointerEvents={interactiveContent ? '
|
|
375
|
+
pointerEvents={interactiveContent ? 'box-none' : 'none'}
|
|
362
376
|
accessibilityElementsHidden={!interactiveContent}
|
|
363
377
|
importantForAccessibility={
|
|
364
378
|
interactiveContent ? 'auto' : 'no-hide-descendants'
|