panelui-native 0.93.0 → 0.94.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/lib/module/components/bottom-sheet/index.js +19 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/bubble-chart/index.js +23 -9
- package/lib/module/components/bubble-chart/index.js.map +1 -1
- package/lib/module/components/questionnaire/index.js +242 -22
- package/lib/module/components/questionnaire/index.js.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bubble-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/questionnaire/index.d.ts +19 -5
- package/lib/typescript/src/components/questionnaire/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/bottom-sheet/index.tsx +29 -3
- package/src/components/bubble-chart/index.tsx +29 -9
- package/src/components/questionnaire/index.tsx +249 -19
|
@@ -39,6 +39,15 @@
|
|
|
39
39
|
* question needs, for free. Pass `frame={false}` to drop it — for a
|
|
40
40
|
* questionnaire inside a `BottomSheet` or a card that already draws a border.
|
|
41
41
|
*
|
|
42
|
+
* It is the `inset` variant, so the panel the question is written on floats in
|
|
43
|
+
* a recessed band rather than sitting flush in a tray, and the actions go in
|
|
44
|
+
* the band rather than in a section under the question. That separation is the
|
|
45
|
+
* point: the question changes and the row under it does not, and a row drawn
|
|
46
|
+
* on the band is visibly not part of the card that keeps being replaced.
|
|
47
|
+
*
|
|
48
|
+
* The band shapes its actions into equal pills, which is why
|
|
49
|
+
* `Questionnaire.Spacer` is dropped on the way in — see `bandActions`.
|
|
50
|
+
*
|
|
42
51
|
* ## Why the root reads its children instead of collecting registrations
|
|
43
52
|
*
|
|
44
53
|
* Only the active question is mounted, so an unmounted one cannot report that
|
|
@@ -50,8 +59,8 @@
|
|
|
50
59
|
* which makes the whole set knowable without mounting any of it.
|
|
51
60
|
*
|
|
52
61
|
* That same pass sorts the parts into the shell: the title and progress go to
|
|
53
|
-
* the header strip, the footer to
|
|
54
|
-
* everything else is a question.
|
|
62
|
+
* the header strip above the panel, the footer's actions to the band around
|
|
63
|
+
* it, and everything else is a question.
|
|
55
64
|
*
|
|
56
65
|
* ## Answers are one record, the way a form would submit them
|
|
57
66
|
*
|
|
@@ -168,17 +177,22 @@ export interface QuestionnaireProgressState {
|
|
|
168
177
|
last: boolean;
|
|
169
178
|
}
|
|
170
179
|
/** How the position is drawn. */
|
|
171
|
-
export type QuestionnaireProgressVariant = 'pips' | 'numbers' | 'count';
|
|
180
|
+
export type QuestionnaireProgressVariant = 'ring' | 'pips' | 'numbers' | 'count';
|
|
172
181
|
export interface QuestionnaireProgressProps {
|
|
173
182
|
className?: string;
|
|
174
183
|
/**
|
|
184
|
+
* `ring` is an arc that sweeps round as the reader advances — how far
|
|
185
|
+
* through the set they are, without saying how many questions there are.
|
|
186
|
+
* It is the only one that holds its size and its meaning at any length,
|
|
187
|
+
* which is why it is the default.
|
|
188
|
+
*
|
|
175
189
|
* `pips` is a bar per question, filled up to the one being asked and widened
|
|
176
190
|
* on it. `numbers` counts them out instead, which is what you want when the
|
|
177
191
|
* reader will be sent back to a particular question. `count` is the plain
|
|
178
192
|
* `Question 2 of 5`.
|
|
179
193
|
*
|
|
180
194
|
* `pips` and `numbers` fall back to `count` past eight questions, where
|
|
181
|
-
* neither is countable at a glance any more.
|
|
195
|
+
* neither is countable at a glance any more. `ring` never does.
|
|
182
196
|
*/
|
|
183
197
|
variant?: QuestionnaireProgressVariant;
|
|
184
198
|
/**
|
|
@@ -221,7 +235,7 @@ export interface QuestionnaireQuestionProps extends ViewProps {
|
|
|
221
235
|
children?: ReactNode;
|
|
222
236
|
}
|
|
223
237
|
/** The question being asked. */
|
|
224
|
-
declare function QuestionnaireQuestion({ className, children, ...props }: QuestionnaireQuestionProps): import("react").JSX.Element;
|
|
238
|
+
declare function QuestionnaireQuestion({ className, children, ...props }: QuestionnaireQuestionProps): import("react").JSX.Element | null;
|
|
225
239
|
declare namespace QuestionnaireQuestion {
|
|
226
240
|
var displayName: string;
|
|
227
241
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/questionnaire/index.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/questionnaire/index.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panelui-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.94.0",
|
|
4
4
|
"description": "High-performance React Native UI components for Expo. Semantic design tokens, powered by Uniwind (Tailwind v4) and Reanimated.",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"module": "./lib/module/index.js",
|
|
@@ -586,6 +586,17 @@ function BottomSheetContent({
|
|
|
586
586
|
const pan = useMemo(
|
|
587
587
|
() =>
|
|
588
588
|
Gesture.Pan()
|
|
589
|
+
/*
|
|
590
|
+
* A locked sheet does not take the drag at all.
|
|
591
|
+
*
|
|
592
|
+
* Refusing the close is not enough on its own: the drag still moves
|
|
593
|
+
* the sheet, and a controlled caller that declines `onOpenChange`
|
|
594
|
+
* leaves it parked off the bottom of the screen — open as far as the
|
|
595
|
+
* caller is concerned, invisible, and still taking every touch behind
|
|
596
|
+
* a fully transparent backdrop. The backdrop press and the back
|
|
597
|
+
* gesture were already gated; this is the third way out.
|
|
598
|
+
*/
|
|
599
|
+
.enabled(dismissible)
|
|
589
600
|
/*
|
|
590
601
|
* A drag has to travel before it takes the touch. This detector wraps
|
|
591
602
|
* the whole sheet, close button included, and an unqualified Pan
|
|
@@ -626,9 +637,16 @@ function BottomSheetContent({
|
|
|
626
637
|
* your hand from one that merely goes where you put it.
|
|
627
638
|
*/
|
|
628
639
|
const projected = translateY.value + project(event.velocityY);
|
|
640
|
+
/*
|
|
641
|
+
* `dismissible` again, behind `.enabled()` rather than instead of
|
|
642
|
+
* it. Disabling a gesture does not cancel a touch already in flight,
|
|
643
|
+
* so a sheet locked mid-drag still arrives here — and without this
|
|
644
|
+
* it would fling itself away on the strength of a decision the
|
|
645
|
+
* caller has since revoked. Falling through snaps it back instead.
|
|
646
|
+
*/
|
|
629
647
|
if (
|
|
630
|
-
|
|
631
|
-
event.velocityY > DISMISS_VELOCITY
|
|
648
|
+
dismissible &&
|
|
649
|
+
(projected > DISMISS_DISTANCE || event.velocityY > DISMISS_VELOCITY)
|
|
632
650
|
) {
|
|
633
651
|
translateY.value = withSpring(
|
|
634
652
|
screenHeight,
|
|
@@ -647,7 +665,15 @@ function BottomSheetContent({
|
|
|
647
665
|
// Rebuilt only when one of these changes. Built inline it would be a new
|
|
648
666
|
// gesture on every render — and the sheet re-renders while it is being
|
|
649
667
|
// used, each time re-attaching the handler and dropping the live touch.
|
|
650
|
-
[
|
|
668
|
+
[
|
|
669
|
+
close,
|
|
670
|
+
dismissible,
|
|
671
|
+
screenHeight,
|
|
672
|
+
translateY,
|
|
673
|
+
scrollGesture,
|
|
674
|
+
scrollOffset,
|
|
675
|
+
hasScrollable,
|
|
676
|
+
]
|
|
651
677
|
);
|
|
652
678
|
|
|
653
679
|
const sheetStyle = useAnimatedStyle(() => ({
|
|
@@ -165,6 +165,20 @@ const AXIS_TITLE_HEIGHT = 18;
|
|
|
165
165
|
/** Gap between a quadrant's caption and the corner it is written in. */
|
|
166
166
|
const QUADRANT_LABEL_INSET = 6;
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* How strongly the overlays draw: the quadrant crosshair, and the size key's
|
|
170
|
+
* rings.
|
|
171
|
+
*
|
|
172
|
+
* Both are furniture rather than data, and both were faint enough to be missed
|
|
173
|
+
* — the crosshair at 0.4 and the rings at 0.6, on a token that is already a
|
|
174
|
+
* mid-grey chosen to stay behind the circles. The restraint has to come from
|
|
175
|
+
* the token, not from a second reduction on top of it, or the mark is one
|
|
176
|
+
* nobody finds. `Trend` has drawn at 0.7 all along and reads correctly in both
|
|
177
|
+
* themes, so that is the number.
|
|
178
|
+
*/
|
|
179
|
+
const CROSSHAIR_OPACITY = 0.7;
|
|
180
|
+
const KEY_RING_OPACITY = 0.85;
|
|
181
|
+
|
|
168
182
|
/** Column the size key's values are written in, beside its circles. */
|
|
169
183
|
const SIZE_KEY_LABEL_WIDTH = 44;
|
|
170
184
|
|
|
@@ -1307,6 +1321,12 @@ function BubbleChartQuadrants({
|
|
|
1307
1321
|
/>
|
|
1308
1322
|
</>
|
|
1309
1323
|
) : null}
|
|
1324
|
+
{/*
|
|
1325
|
+
The same weight `Trend` draws at, because it is the same kind of mark:
|
|
1326
|
+
a reference laid over the field rather than a reading taken from it.
|
|
1327
|
+
At 0.4 of a mid-grey the split was a line you had to already know was
|
|
1328
|
+
there, which leaves four labelled corners and nothing dividing them.
|
|
1329
|
+
*/}
|
|
1310
1330
|
<Animated.View
|
|
1311
1331
|
style={[
|
|
1312
1332
|
{
|
|
@@ -1315,7 +1335,7 @@ function BubbleChartQuadrants({
|
|
|
1315
1335
|
height: plot.height,
|
|
1316
1336
|
width: 1,
|
|
1317
1337
|
backgroundColor: stroke,
|
|
1318
|
-
opacity:
|
|
1338
|
+
opacity: CROSSHAIR_OPACITY,
|
|
1319
1339
|
},
|
|
1320
1340
|
verticalStyle,
|
|
1321
1341
|
]}
|
|
@@ -1328,7 +1348,7 @@ function BubbleChartQuadrants({
|
|
|
1328
1348
|
width: plot.width,
|
|
1329
1349
|
height: 1,
|
|
1330
1350
|
backgroundColor: stroke,
|
|
1331
|
-
opacity:
|
|
1351
|
+
opacity: CROSSHAIR_OPACITY,
|
|
1332
1352
|
},
|
|
1333
1353
|
horizontalStyle,
|
|
1334
1354
|
]}
|
|
@@ -1341,7 +1361,7 @@ function BubbleChartQuadrants({
|
|
|
1341
1361
|
{labels?.topLeft ? (
|
|
1342
1362
|
<Text
|
|
1343
1363
|
size="xs"
|
|
1344
|
-
|
|
1364
|
+
weight="medium"
|
|
1345
1365
|
numberOfLines={1}
|
|
1346
1366
|
style={{
|
|
1347
1367
|
...corner,
|
|
@@ -1355,7 +1375,7 @@ function BubbleChartQuadrants({
|
|
|
1355
1375
|
{labels?.topRight ? (
|
|
1356
1376
|
<Text
|
|
1357
1377
|
size="xs"
|
|
1358
|
-
|
|
1378
|
+
weight="medium"
|
|
1359
1379
|
numberOfLines={1}
|
|
1360
1380
|
style={{
|
|
1361
1381
|
...corner,
|
|
@@ -1370,7 +1390,7 @@ function BubbleChartQuadrants({
|
|
|
1370
1390
|
{labels?.bottomLeft ? (
|
|
1371
1391
|
<Text
|
|
1372
1392
|
size="xs"
|
|
1373
|
-
|
|
1393
|
+
weight="medium"
|
|
1374
1394
|
numberOfLines={1}
|
|
1375
1395
|
style={{
|
|
1376
1396
|
...corner,
|
|
@@ -1384,7 +1404,7 @@ function BubbleChartQuadrants({
|
|
|
1384
1404
|
{labels?.bottomRight ? (
|
|
1385
1405
|
<Text
|
|
1386
1406
|
size="xs"
|
|
1387
|
-
|
|
1407
|
+
weight="medium"
|
|
1388
1408
|
numberOfLines={1}
|
|
1389
1409
|
style={{
|
|
1390
1410
|
...corner,
|
|
@@ -1467,7 +1487,7 @@ function BubbleChartSizeKey({
|
|
|
1467
1487
|
className={cn(className)}
|
|
1468
1488
|
>
|
|
1469
1489
|
{label ? (
|
|
1470
|
-
<Text size="xs"
|
|
1490
|
+
<Text size="xs" weight="medium" numberOfLines={1}>
|
|
1471
1491
|
{label}
|
|
1472
1492
|
</Text>
|
|
1473
1493
|
) : null}
|
|
@@ -1486,7 +1506,7 @@ function BubbleChartSizeKey({
|
|
|
1486
1506
|
borderRadius: step.r,
|
|
1487
1507
|
borderWidth: 1,
|
|
1488
1508
|
borderColor: stroke,
|
|
1489
|
-
opacity:
|
|
1509
|
+
opacity: KEY_RING_OPACITY,
|
|
1490
1510
|
}}
|
|
1491
1511
|
/>
|
|
1492
1512
|
))}
|
|
@@ -1494,7 +1514,7 @@ function BubbleChartSizeKey({
|
|
|
1494
1514
|
<Text
|
|
1495
1515
|
key={`v${step.value}`}
|
|
1496
1516
|
size="xs"
|
|
1497
|
-
|
|
1517
|
+
weight="medium"
|
|
1498
1518
|
numberOfLines={1}
|
|
1499
1519
|
style={{
|
|
1500
1520
|
position: 'absolute',
|
|
@@ -39,6 +39,15 @@
|
|
|
39
39
|
* question needs, for free. Pass `frame={false}` to drop it — for a
|
|
40
40
|
* questionnaire inside a `BottomSheet` or a card that already draws a border.
|
|
41
41
|
*
|
|
42
|
+
* It is the `inset` variant, so the panel the question is written on floats in
|
|
43
|
+
* a recessed band rather than sitting flush in a tray, and the actions go in
|
|
44
|
+
* the band rather than in a section under the question. That separation is the
|
|
45
|
+
* point: the question changes and the row under it does not, and a row drawn
|
|
46
|
+
* on the band is visibly not part of the card that keeps being replaced.
|
|
47
|
+
*
|
|
48
|
+
* The band shapes its actions into equal pills, which is why
|
|
49
|
+
* `Questionnaire.Spacer` is dropped on the way in — see `bandActions`.
|
|
50
|
+
*
|
|
42
51
|
* ## Why the root reads its children instead of collecting registrations
|
|
43
52
|
*
|
|
44
53
|
* Only the active question is mounted, so an unmounted one cannot report that
|
|
@@ -50,8 +59,8 @@
|
|
|
50
59
|
* which makes the whole set knowable without mounting any of it.
|
|
51
60
|
*
|
|
52
61
|
* That same pass sorts the parts into the shell: the title and progress go to
|
|
53
|
-
* the header strip, the footer to
|
|
54
|
-
* everything else is a question.
|
|
62
|
+
* the header strip above the panel, the footer's actions to the band around
|
|
63
|
+
* it, and everything else is a question.
|
|
55
64
|
*
|
|
56
65
|
* ## Answers are one record, the way a form would submit them
|
|
57
66
|
*
|
|
@@ -101,12 +110,15 @@ import Animated, {
|
|
|
101
110
|
Easing,
|
|
102
111
|
FadeOut,
|
|
103
112
|
runOnJS,
|
|
113
|
+
useAnimatedProps,
|
|
104
114
|
useAnimatedStyle,
|
|
115
|
+
useReducedMotion,
|
|
105
116
|
useSharedValue,
|
|
106
117
|
withSpring,
|
|
107
118
|
withTiming,
|
|
108
119
|
type EntryExitAnimationFunction,
|
|
109
120
|
} from 'react-native-reanimated';
|
|
121
|
+
import Svg, { Circle } from 'react-native-svg';
|
|
110
122
|
import { tv } from 'tailwind-variants';
|
|
111
123
|
import { useCSSVariable } from 'uniwind';
|
|
112
124
|
import { CheckIcon, ChevronLeftIcon, ChevronRightIcon } from '../../icons';
|
|
@@ -131,6 +143,16 @@ const EXIT_DURATION = 140;
|
|
|
131
143
|
/** How long a progress pip takes to fill once its question has been reached. */
|
|
132
144
|
const PIP_DURATION = 260;
|
|
133
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The progress ring: its diameter, and the weight of the arc drawn round it.
|
|
148
|
+
*
|
|
149
|
+
* Sized against the title beside it rather than against the strip it sits on.
|
|
150
|
+
* A ring the height of a line of text reads as the trailing half of a pair;
|
|
151
|
+
* one much larger reads as the header's subject, which it is not.
|
|
152
|
+
*/
|
|
153
|
+
const RING_SIZE = 22;
|
|
154
|
+
const RING_STROKE = 2.5;
|
|
155
|
+
|
|
134
156
|
/** How far across the body a swipe has to travel before it commits. */
|
|
135
157
|
const SWIPE_FRACTION = 0.25;
|
|
136
158
|
|
|
@@ -272,6 +294,11 @@ interface QuestionnaireContextValue {
|
|
|
272
294
|
shortcuts: QuestionnaireShortcutMode | null;
|
|
273
295
|
/** Whether the root drew the frame, which decides who owns the insets. */
|
|
274
296
|
framed: boolean;
|
|
297
|
+
/**
|
|
298
|
+
* The question is drawn on the header strip rather than in the panel, so
|
|
299
|
+
* `Questionnaire.Question` renders nothing where it stands.
|
|
300
|
+
*/
|
|
301
|
+
questionOnStrip: boolean;
|
|
275
302
|
/** The active question is required and has no answer, so the way on is shut. */
|
|
276
303
|
blocked: boolean;
|
|
277
304
|
}
|
|
@@ -314,6 +341,80 @@ function isAnswered(value: string | string[] | undefined): boolean {
|
|
|
314
341
|
return typeof value === 'string' && value.trim().length > 0;
|
|
315
342
|
}
|
|
316
343
|
|
|
344
|
+
/**
|
|
345
|
+
* The footer's actions, on their way into the band.
|
|
346
|
+
*
|
|
347
|
+
* `Frame.Footer` shapes its *direct* children into the band's pills, so the
|
|
348
|
+
* row itself has to be unwrapped — handed the `Questionnaire.Footer` element
|
|
349
|
+
* whole it would be the wrapper that came out a pill, with the buttons
|
|
350
|
+
* untouched inside it.
|
|
351
|
+
*
|
|
352
|
+
* The spacer goes at the same time. It exists to push the actions apart in a
|
|
353
|
+
* row that lays them out at their own widths; in a band where every action is
|
|
354
|
+
* already a share of the row it would take a share of its own, and the buttons
|
|
355
|
+
* would come out narrower the more carefully the caller had spaced them.
|
|
356
|
+
*
|
|
357
|
+
* The band's own rule is equal widths, and it is the wrong one here. A row of
|
|
358
|
+
* equal pills says these are equal decisions; Back and Skip are ways off the
|
|
359
|
+
* path and Next is the path, so they are sized to their labels and the primary
|
|
360
|
+
* action takes what is left. Equal thirds also truncate it — "Continue" does
|
|
361
|
+
* not fit in a third of the band, and the button that carries the flow is the
|
|
362
|
+
* worst one to lose the end of.
|
|
363
|
+
*
|
|
364
|
+
* `flex-none` and nothing else on those two: the button's own padding is what
|
|
365
|
+
* a label needs either side of it, and anything added here comes off the
|
|
366
|
+
* primary action, which has the longest label and the least room to lose.
|
|
367
|
+
*
|
|
368
|
+
* The primary action goes up one size instead — `lg`, not the default. It is
|
|
369
|
+
* the only thing on the band anybody is aiming for: the whole widget is one
|
|
370
|
+
* decision repeated, and this is the button that takes the reader to the next
|
|
371
|
+
* one, so it carries a larger label and a taller box than the way back beside
|
|
372
|
+
* it. One size and no more; `xl` is for a button that is the whole of a
|
|
373
|
+
* screen, and this one shares its row. The band's fixed height comes off with
|
|
374
|
+
* it, or the taller box would be clamped back to the row's.
|
|
375
|
+
*/
|
|
376
|
+
function bandActions(footer: ReactNode): ReactNode {
|
|
377
|
+
if (!isValidElement<QuestionnaireFooterProps>(footer)) return footer;
|
|
378
|
+
return Children.toArray(footer.props.children).flatMap((child) => {
|
|
379
|
+
if (!isValidElement<QuestionnaireActionProps>(child)) return [child];
|
|
380
|
+
if (child.type === QuestionnaireSpacer) return [];
|
|
381
|
+
|
|
382
|
+
// Merged after the band's own classes rather than before, so what is set
|
|
383
|
+
// here is what survives.
|
|
384
|
+
if (child.type === QuestionnaireBack || child.type === QuestionnaireSkip) {
|
|
385
|
+
return [cloneElement(child, { className: cn('flex-none', child.props.className) })];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return [
|
|
389
|
+
cloneElement(child, {
|
|
390
|
+
// A caller who asked for a size meant it.
|
|
391
|
+
size: child.props.size ?? 'lg',
|
|
392
|
+
className: cn('h-auto', child.props.className),
|
|
393
|
+
}),
|
|
394
|
+
];
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The text of a question's `Questionnaire.Question`, for the header strip.
|
|
400
|
+
*
|
|
401
|
+
* Read off the element rather than reported up by the mounted question: only
|
|
402
|
+
* the active one is mounted, and the strip has to be drawn in the same render
|
|
403
|
+
* as the pane it labels — a value arriving from below would land one frame
|
|
404
|
+
* late, which on a question change is a header still naming the question that
|
|
405
|
+
* has just left.
|
|
406
|
+
*/
|
|
407
|
+
function questionOf(item: ReactElement<QuestionnaireItemProps> | undefined): ReactNode {
|
|
408
|
+
if (!item) return null;
|
|
409
|
+
let found: ReactNode = null;
|
|
410
|
+
Children.forEach(item.props.children, (child) => {
|
|
411
|
+
if (found != null) return;
|
|
412
|
+
if (!isValidElement<QuestionnaireQuestionProps>(child)) return;
|
|
413
|
+
if (child.type === QuestionnaireQuestion) found = child.props.children;
|
|
414
|
+
});
|
|
415
|
+
return found;
|
|
416
|
+
}
|
|
417
|
+
|
|
317
418
|
/** The letter or number an answer at this position is badged with. */
|
|
318
419
|
function shortcutAt(mode: QuestionnaireShortcutMode, index: number): string | null {
|
|
319
420
|
if (mode === 'numbers') return index < 9 ? String(index + 1) : null;
|
|
@@ -621,6 +722,29 @@ function QuestionnaireRoot({
|
|
|
621
722
|
*/
|
|
622
723
|
const blocked = !!activeItem?.required && !isAnswered(answers[activeItem.name]);
|
|
623
724
|
|
|
725
|
+
/*
|
|
726
|
+
* With no title the header strip has nothing on it but the ring, and a ring
|
|
727
|
+
* centred on an empty strip is a widget that will not say what it is asking.
|
|
728
|
+
* The question stands in for the title instead — it is the only line that
|
|
729
|
+
* names the thing, and a questionnaire that has not been given a title is
|
|
730
|
+
* one where the question is the title.
|
|
731
|
+
*
|
|
732
|
+
* It takes the title's place exactly, at the leading edge with the ring
|
|
733
|
+
* still at the trailing one, so a titled questionnaire and an untitled one
|
|
734
|
+
* draw the same strip. Two arrangements of the same two things would make
|
|
735
|
+
* the header look like it meant something different in each.
|
|
736
|
+
*
|
|
737
|
+
* The trade is that the question no longer travels with the pane: the strip
|
|
738
|
+
* stays put while the answers slide under it. That is the right way round
|
|
739
|
+
* for a label, and the wrong way round for a question that is meant to
|
|
740
|
+
* arrive with its answers — which is why giving it a title turns this off.
|
|
741
|
+
*/
|
|
742
|
+
const questionOnStrip = frame && !titleNode;
|
|
743
|
+
const stripQuestion = useMemo(
|
|
744
|
+
() => (questionOnStrip ? questionOf(activeItem?.element) : null),
|
|
745
|
+
[questionOnStrip, activeItem]
|
|
746
|
+
);
|
|
747
|
+
|
|
624
748
|
const context = useMemo<QuestionnaireContextValue>(
|
|
625
749
|
() => ({
|
|
626
750
|
current,
|
|
@@ -640,6 +764,7 @@ function QuestionnaireRoot({
|
|
|
640
764
|
submit,
|
|
641
765
|
shortcuts: shortcuts ?? null,
|
|
642
766
|
framed: frame,
|
|
767
|
+
questionOnStrip,
|
|
643
768
|
blocked,
|
|
644
769
|
}),
|
|
645
770
|
[
|
|
@@ -660,6 +785,7 @@ function QuestionnaireRoot({
|
|
|
660
785
|
submit,
|
|
661
786
|
shortcuts,
|
|
662
787
|
frame,
|
|
788
|
+
questionOnStrip,
|
|
663
789
|
blocked,
|
|
664
790
|
]
|
|
665
791
|
);
|
|
@@ -679,10 +805,11 @@ function QuestionnaireRoot({
|
|
|
679
805
|
);
|
|
680
806
|
|
|
681
807
|
/*
|
|
682
|
-
*
|
|
683
|
-
* default is set for a line of text
|
|
684
|
-
*
|
|
685
|
-
*
|
|
808
|
+
* A little more room under the strip than the `inset` header takes by
|
|
809
|
+
* default. That default is set for a line of text; the ring is a shape whose
|
|
810
|
+
* stroke reaches the edge of its box, with none of the leading a line of
|
|
811
|
+
* text has, so against the same padding it sits closer to the panel than the
|
|
812
|
+
* title beside it does.
|
|
686
813
|
*
|
|
687
814
|
* With no title, the progress centres rather than staying hard right. Right
|
|
688
815
|
* is where it belongs when it is the trailing half of a pair; on its own at
|
|
@@ -690,8 +817,17 @@ function QuestionnaireRoot({
|
|
|
690
817
|
* rather than as the strip's subject.
|
|
691
818
|
*/
|
|
692
819
|
const header = (
|
|
693
|
-
<Frame.Header className=
|
|
694
|
-
{titleNode
|
|
820
|
+
<Frame.Header className="pb-3 pt-1.5">
|
|
821
|
+
{titleNode ??
|
|
822
|
+
textChildren(stripQuestion, (text) => (
|
|
823
|
+
// One line, and it truncates rather than wrapping: the strip is a
|
|
824
|
+
// label for the panel under it, and a label that grows to two lines
|
|
825
|
+
// pushes the answers down the screen every time a longer question
|
|
826
|
+
// comes round.
|
|
827
|
+
<Text size="sm" weight="medium" numberOfLines={1} className="min-w-0 shrink">
|
|
828
|
+
{text}
|
|
829
|
+
</Text>
|
|
830
|
+
))}
|
|
695
831
|
{progressNode ? <Frame.Action>{progressNode}</Frame.Action> : null}
|
|
696
832
|
</Frame.Header>
|
|
697
833
|
);
|
|
@@ -699,12 +835,23 @@ function QuestionnaireRoot({
|
|
|
699
835
|
return (
|
|
700
836
|
<QuestionnaireContext.Provider value={context}>
|
|
701
837
|
{frame ? (
|
|
702
|
-
|
|
838
|
+
/*
|
|
839
|
+
* The panel floats in a recessed band rather than sitting flush in a
|
|
840
|
+
* tray, and the band is where the actions go. A questionnaire is a
|
|
841
|
+
* screen's worth of one decision repeated — answer, then move — and
|
|
842
|
+
* the band draws that row as equal pills clear of the card the
|
|
843
|
+
* question is written on, which is the distinction the old footer
|
|
844
|
+
* section inside the panel did not make.
|
|
845
|
+
*/
|
|
846
|
+
<Frame variant="inset" className={className} {...props}>
|
|
703
847
|
{header}
|
|
704
|
-
<Frame.Panel dividers={false}>
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
848
|
+
<Frame.Panel dividers={false}>{body}</Frame.Panel>
|
|
849
|
+
{footerNode ? (
|
|
850
|
+
// Tighter than the band's own gap. That one is set for two or
|
|
851
|
+
// three equal pills; this row is three actions of three different
|
|
852
|
+
// widths, and every point between them comes off the longest label.
|
|
853
|
+
<Frame.Footer className="gap-2">{bandActions(footerNode)}</Frame.Footer>
|
|
854
|
+
) : null}
|
|
708
855
|
</Frame>
|
|
709
856
|
) : (
|
|
710
857
|
<View className={cn('w-full', className)} {...props}>
|
|
@@ -964,18 +1111,23 @@ export interface QuestionnaireProgressState {
|
|
|
964
1111
|
}
|
|
965
1112
|
|
|
966
1113
|
/** How the position is drawn. */
|
|
967
|
-
export type QuestionnaireProgressVariant = 'pips' | 'numbers' | 'count';
|
|
1114
|
+
export type QuestionnaireProgressVariant = 'ring' | 'pips' | 'numbers' | 'count';
|
|
968
1115
|
|
|
969
1116
|
export interface QuestionnaireProgressProps {
|
|
970
1117
|
className?: string;
|
|
971
1118
|
/**
|
|
1119
|
+
* `ring` is an arc that sweeps round as the reader advances — how far
|
|
1120
|
+
* through the set they are, without saying how many questions there are.
|
|
1121
|
+
* It is the only one that holds its size and its meaning at any length,
|
|
1122
|
+
* which is why it is the default.
|
|
1123
|
+
*
|
|
972
1124
|
* `pips` is a bar per question, filled up to the one being asked and widened
|
|
973
1125
|
* on it. `numbers` counts them out instead, which is what you want when the
|
|
974
1126
|
* reader will be sent back to a particular question. `count` is the plain
|
|
975
1127
|
* `Question 2 of 5`.
|
|
976
1128
|
*
|
|
977
1129
|
* `pips` and `numbers` fall back to `count` past eight questions, where
|
|
978
|
-
* neither is countable at a glance any more.
|
|
1130
|
+
* neither is countable at a glance any more. `ring` never does.
|
|
979
1131
|
*/
|
|
980
1132
|
variant?: QuestionnaireProgressVariant;
|
|
981
1133
|
/**
|
|
@@ -985,6 +1137,74 @@ export interface QuestionnaireProgressProps {
|
|
|
985
1137
|
children?: ReactNode | ((state: QuestionnaireProgressState) => ReactNode);
|
|
986
1138
|
}
|
|
987
1139
|
|
|
1140
|
+
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* How far through the set, as an arc.
|
|
1144
|
+
*
|
|
1145
|
+
* A fraction rather than a count, which is what lets one ring stand for three
|
|
1146
|
+
* questions or thirty: the marks that name each question individually stop
|
|
1147
|
+
* being countable somewhere around eight, and this never has that problem.
|
|
1148
|
+
* What it gives up is any answer to "how many left" — reach for `numbers`
|
|
1149
|
+
* where that matters.
|
|
1150
|
+
*
|
|
1151
|
+
* Drawn with `strokeDasharray` rather than `strokeDashoffset`: a dash the
|
|
1152
|
+
* length of the filled arc and a gap the length of the rest leaves exactly one
|
|
1153
|
+
* visible stroke, and its length is the only number that has to be animated.
|
|
1154
|
+
* The circle is turned back a quarter because a stroke starts at three
|
|
1155
|
+
* o'clock, and an arc that begins there reads as a gauge already part-way
|
|
1156
|
+
* along.
|
|
1157
|
+
*/
|
|
1158
|
+
function ProgressRing({ current, total }: { current: number; total: number }) {
|
|
1159
|
+
const reduceMotion = useReducedMotion();
|
|
1160
|
+
const tokens = useCSSVariable(['--color-primary', '--color-border']);
|
|
1161
|
+
// Narrowed on the way out because `useCSSVariable` resolves to a number for
|
|
1162
|
+
// any token that happens to be one.
|
|
1163
|
+
const fillColor = typeof tokens[0] === 'string' ? tokens[0] : 'rgb(120,120,255)';
|
|
1164
|
+
const trackColor = typeof tokens[1] === 'string' ? tokens[1] : 'rgba(128,128,128,0.2)';
|
|
1165
|
+
|
|
1166
|
+
const radius = (RING_SIZE - RING_STROKE) / 2;
|
|
1167
|
+
const circumference = 2 * Math.PI * radius;
|
|
1168
|
+
const centre = RING_SIZE / 2;
|
|
1169
|
+
|
|
1170
|
+
const fraction = total > 0 ? Math.min(Math.max(current / total, 0), 1) : 0;
|
|
1171
|
+
const filled = useSharedValue(fraction);
|
|
1172
|
+
|
|
1173
|
+
useEffect(() => {
|
|
1174
|
+
filled.value = reduceMotion
|
|
1175
|
+
? fraction
|
|
1176
|
+
: withTiming(fraction, { duration: PIP_DURATION, easing: EASE });
|
|
1177
|
+
}, [fraction, reduceMotion, filled]);
|
|
1178
|
+
|
|
1179
|
+
const arc = useAnimatedProps(() => ({
|
|
1180
|
+
strokeDasharray: [circumference * filled.value, circumference],
|
|
1181
|
+
}));
|
|
1182
|
+
|
|
1183
|
+
return (
|
|
1184
|
+
<Svg width={RING_SIZE} height={RING_SIZE}>
|
|
1185
|
+
<Circle
|
|
1186
|
+
cx={centre}
|
|
1187
|
+
cy={centre}
|
|
1188
|
+
r={radius}
|
|
1189
|
+
stroke={trackColor}
|
|
1190
|
+
strokeWidth={RING_STROKE}
|
|
1191
|
+
fill="none"
|
|
1192
|
+
/>
|
|
1193
|
+
<AnimatedCircle
|
|
1194
|
+
animatedProps={arc}
|
|
1195
|
+
cx={centre}
|
|
1196
|
+
cy={centre}
|
|
1197
|
+
r={radius}
|
|
1198
|
+
stroke={fillColor}
|
|
1199
|
+
strokeWidth={RING_STROKE}
|
|
1200
|
+
strokeLinecap="round"
|
|
1201
|
+
fill="none"
|
|
1202
|
+
transform={`rotate(-90 ${centre} ${centre})`}
|
|
1203
|
+
/>
|
|
1204
|
+
</Svg>
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
988
1208
|
/**
|
|
989
1209
|
* One question's worth of the track. Filled once it has been reached, and the
|
|
990
1210
|
* one being asked is drawn wider than the rest so the reader's place in the
|
|
@@ -1055,7 +1275,7 @@ function ProgressNumber({
|
|
|
1055
1275
|
/** Where the reader is in the set, announced as a progress bar. */
|
|
1056
1276
|
function QuestionnaireProgress({
|
|
1057
1277
|
className,
|
|
1058
|
-
variant = '
|
|
1278
|
+
variant = 'ring',
|
|
1059
1279
|
children,
|
|
1060
1280
|
}: QuestionnaireProgressProps) {
|
|
1061
1281
|
const { current, total, first, last } = useQuestionnaire('Questionnaire.Progress');
|
|
@@ -1064,11 +1284,15 @@ function QuestionnaireProgress({
|
|
|
1064
1284
|
/*
|
|
1065
1285
|
* Marks while they can still be counted, the count itself once they cannot.
|
|
1066
1286
|
* Twenty of either is a texture rather than a number, and the text says the
|
|
1067
|
-
* same thing in less room.
|
|
1287
|
+
* same thing in less room. The ring is exempt: it never claimed to be
|
|
1288
|
+
* countable, so there is nothing for a long set to take away from it.
|
|
1068
1289
|
*/
|
|
1069
|
-
const drawable =
|
|
1290
|
+
const drawable =
|
|
1291
|
+
variant !== 'count' && variant !== 'ring' && total > 0 && total <= MAX_PIPS;
|
|
1070
1292
|
|
|
1071
|
-
const fallback =
|
|
1293
|
+
const fallback = variant === 'ring' ? (
|
|
1294
|
+
<ProgressRing current={current} total={total} />
|
|
1295
|
+
) : drawable ? (
|
|
1072
1296
|
<View className={cn('flex-row items-center', variant === 'numbers' ? 'gap-1.5' : 'gap-1')}>
|
|
1073
1297
|
{Array.from({ length: total }, (_, index) =>
|
|
1074
1298
|
variant === 'numbers' ? (
|
|
@@ -1197,6 +1421,12 @@ export interface QuestionnaireQuestionProps extends ViewProps {
|
|
|
1197
1421
|
|
|
1198
1422
|
/** The question being asked. */
|
|
1199
1423
|
function QuestionnaireQuestion({ className, children, ...props }: QuestionnaireQuestionProps) {
|
|
1424
|
+
const { questionOnStrip } = useQuestionnaire('Questionnaire.Question');
|
|
1425
|
+
|
|
1426
|
+
// Drawn on the header strip instead, where an untitled questionnaire uses it
|
|
1427
|
+
// as its label. Rendering here as well would ask the same thing twice.
|
|
1428
|
+
if (questionOnStrip) return null;
|
|
1429
|
+
|
|
1200
1430
|
return (
|
|
1201
1431
|
<View className={className} {...props}>
|
|
1202
1432
|
{textChildren(children, (text) => (
|