Update CBS Mobile App codebase
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import React, { memo } from 'react';
|
||||
import { StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
interface StepperProps {
|
||||
/** Two to four short labels. Longer than four and they stop fitting a 360pt phone. */
|
||||
steps: readonly string[];
|
||||
/** Zero-based. Steps before it are done, steps after it are still to come. */
|
||||
current: number;
|
||||
/**
|
||||
* Set when this sits on the navy brand wash rather than a light surface.
|
||||
*/
|
||||
onBrand?: boolean;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the customer is in a flow, and what is still ahead of them.
|
||||
*
|
||||
* Registration is three screens — choose a SIM, enter the code, set an MPIN —
|
||||
* and until this existed the first of those gave no hint that two more were
|
||||
* coming. Someone deciding whether to hand over their number is entitled to
|
||||
* know how long the process is before they start it.
|
||||
*
|
||||
* A completed step is marked with a tick as well as a fill, so progress is not
|
||||
* carried by colour alone. The whole thing is inert: it reports position, it is
|
||||
* not a way to navigate, and steps are not tappable — you cannot jump to an OTP
|
||||
* screen before an OTP has been sent.
|
||||
*/
|
||||
const StepperComponent = ({ steps, current, onBrand = false, style }: StepperProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
/**
|
||||
* The connector, and the outline of a step not yet reached.
|
||||
*
|
||||
* On brand this is a white at 0.55 rather than the shared `onBrandBorder`
|
||||
* (0.24). A border token is measured for a panel edge, which sits next to a
|
||||
* fill; a hairline on the open wash has nothing beside it and 0.24 came to
|
||||
* 1.75:1 — under the 3:1 that non-text contrast asks for. At 0.55 it is
|
||||
* 3.34:1 against the lightest point of the gradient and, at one point tall,
|
||||
* still reads as a rule rather than a bar.
|
||||
*
|
||||
* On a light surface it stays a border tone. The rail is reinforcement in
|
||||
* either case: sequence is carried by the numbers, and completion by the fill
|
||||
* and the tick, so a reader who cannot make out the line loses nothing.
|
||||
*/
|
||||
const railColour = onBrand ? 'rgba(255, 255, 255, 0.55)' : theme.colors.cardBorderStrong;
|
||||
const doneColour = onBrand ? theme.colors.onBrandPrimary : theme.colors.primary;
|
||||
|
||||
return (
|
||||
<View
|
||||
accessibilityRole="progressbar"
|
||||
accessibilityLabel={`Step ${current + 1} of ${steps.length}: ${steps[current] ?? ''}`}
|
||||
style={[styles.container, style]}
|
||||
>
|
||||
{steps.map((label, index) => {
|
||||
const done = index < current;
|
||||
const active = index === current;
|
||||
|
||||
/**
|
||||
* A done or active marker is a solid fill, so the number and tick on it
|
||||
* are the solid tone — the translucent on-brand tiers are measured
|
||||
* against the bare wash and lose their contrast over a fill.
|
||||
*/
|
||||
const markerFill = done || active ? doneColour : 'transparent';
|
||||
const markerBorder = done || active ? doneColour : railColour;
|
||||
const markerInk = done || active
|
||||
? onBrand
|
||||
? theme.colors.primary
|
||||
: theme.colors.textOnBrand
|
||||
: onBrand
|
||||
? theme.colors.onBrandSecondary
|
||||
: theme.colors.textMuted;
|
||||
|
||||
const labelColour = active
|
||||
? onBrand
|
||||
? theme.colors.onBrandPrimary
|
||||
: theme.colors.textPrimary
|
||||
: onBrand
|
||||
? theme.colors.onBrandSecondary
|
||||
: theme.colors.textMuted;
|
||||
|
||||
return (
|
||||
<View key={label} style={styles.step}>
|
||||
{/* The rails are halves either side of the marker, each flexing to
|
||||
fill its own gap. Drawing one line behind the whole row instead
|
||||
would need an absolute overlay measured against the markers,
|
||||
which drifts as soon as a label wraps. */}
|
||||
<View style={styles.railRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.rail,
|
||||
{ backgroundColor: index === 0 ? 'transparent' : railColour },
|
||||
]}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.marker,
|
||||
{ backgroundColor: markerFill, borderColor: markerBorder },
|
||||
]}
|
||||
>
|
||||
{done ? (
|
||||
<Ionicons name="checkmark" size={13} color={markerInk} />
|
||||
) : (
|
||||
<Text style={[theme.text.eyebrow, styles.markerText, { color: markerInk }]}>
|
||||
{index + 1}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.rail,
|
||||
{
|
||||
backgroundColor: index === steps.length - 1 ? 'transparent' : railColour,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[
|
||||
active ? theme.text.bodyStrong : theme.text.bodySmall,
|
||||
styles.label,
|
||||
{ color: labelColour },
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
step: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
gap: 7,
|
||||
},
|
||||
railRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'stretch',
|
||||
},
|
||||
rail: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
},
|
||||
marker: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1.5,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
markerText: {
|
||||
fontSize: 11,
|
||||
lineHeight: 14,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
label: {
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 2,
|
||||
},
|
||||
});
|
||||
|
||||
export const Stepper = memo(StepperComponent);
|
||||
Reference in New Issue
Block a user