panelui-native 0.93.0 → 0.95.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 +1 -0
- 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/module/components/scroll-header/index.js +650 -0
- package/lib/module/components/scroll-header/index.js.map +1 -0
- package/lib/module/components/scroll-header/scroll-header-math.js +144 -0
- package/lib/module/components/scroll-header/scroll-header-math.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/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/lib/typescript/src/components/scroll-header/index.d.ts +237 -0
- package/lib/typescript/src/components/scroll-header/index.d.ts.map +1 -0
- package/lib/typescript/src/components/scroll-header/scroll-header-math.d.ts +99 -0
- package/lib/typescript/src/components/scroll-header/scroll-header-math.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/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
- package/src/components/scroll-header/index.tsx +814 -0
- package/src/components/scroll-header/scroll-header-math.ts +147 -0
- package/src/index.ts +9 -0
|
@@ -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) => (
|