@boostdev/design-system-react-native 0.4.0 → 0.5.1
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/CHANGELOG.md +65 -0
- package/README.md +10 -0
- package/dist/index.cjs +854 -764
- package/dist/index.d.cts +35 -4
- package/dist/index.d.ts +35 -4
- package/dist/index.js +645 -659
- package/package.json +1 -1
- package/src/components/interaction/Button/Button.mdx +5 -1
- package/src/components/interaction/Button/Button.spec.tsx +40 -0
- package/src/components/interaction/Button/Button.tsx +15 -8
- package/src/components/interaction/Dialog/Dialog.tsx +2 -9
- package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +2 -13
- package/src/components/interaction/Rating/Rating.tsx +2 -1
- package/src/components/interaction/SideSheet/SideSheet.tsx +2 -11
- package/src/components/interaction/Toast/Toast.tsx +2 -9
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +2 -1
- package/src/components/interaction/form/Combobox/Combobox.spec.tsx +9 -0
- package/src/components/interaction/form/Combobox/Combobox.tsx +4 -12
- package/src/components/interaction/form/FileInput/FileInput.spec.tsx +6 -0
- package/src/components/interaction/form/FileInput/FileInput.tsx +3 -2
- package/src/components/interaction/form/FormInput/FormInput.spec.tsx +18 -9
- package/src/components/interaction/form/FormInput/FormInput.tsx +5 -11
- package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +8 -0
- package/src/components/interaction/form/NumberInput/NumberInput.tsx +6 -13
- package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +2 -1
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +40 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +8 -2
- package/src/components/interaction/form/Select/Select.mdx +2 -1
- package/src/components/interaction/form/Select/Select.spec.tsx +22 -1
- package/src/components/interaction/form/Select/Select.tsx +9 -17
- package/src/components/interaction/form/Slider/Slider.tsx +2 -9
- package/src/components/interaction/form/Textarea/Textarea.spec.tsx +6 -0
- package/src/components/interaction/form/Textarea/Textarea.tsx +3 -8
- package/src/components/interaction/form/atoms/Label.tsx +2 -1
- package/src/components/interaction/form/atoms/Message.tsx +2 -1
- package/src/components/layout/SectionHeader/SectionHeader.tsx +2 -1
- package/src/components/ui/Accordion/Accordion.tsx +2 -11
- package/src/components/ui/Alert/Alert.tsx +2 -1
- package/src/components/ui/Avatar/Avatar.tsx +2 -1
- package/src/components/ui/Badge/Badge.spec.tsx +28 -0
- package/src/components/ui/Badge/Badge.tsx +9 -3
- package/src/components/ui/Breadcrumb/Breadcrumb.tsx +2 -1
- package/src/components/ui/Calendar/Calendar.tsx +2 -8
- package/src/components/ui/Carousel/Carousel.tsx +2 -12
- package/src/components/ui/Collapsible/Collapsible.spec.tsx +1 -1
- package/src/components/ui/Collapsible/Collapsible.tsx +7 -22
- package/src/components/ui/DescriptionList/DescriptionList.tsx +2 -1
- package/src/components/ui/Link/Link.tsx +2 -1
- package/src/components/ui/NotificationBanner/NotificationBanner.tsx +2 -1
- package/src/components/ui/Pagination/Pagination.tsx +2 -1
- package/src/components/ui/Progress/Progress.tsx +2 -1
- package/src/components/ui/ProgressCircle/ProgressCircle.tsx +2 -1
- package/src/components/ui/Table/Table.tsx +2 -9
- package/src/components/ui/Tabs/Tabs.tsx +2 -10
- package/src/components/ui/Tooltip/Tooltip.tsx +2 -1
- package/src/components/ui/Typography/Typography.mdx +2 -0
- package/src/components/ui/Typography/Typography.spec.tsx +59 -0
- package/src/components/ui/Typography/Typography.tsx +18 -2
- package/src/index.ts +2 -0
- package/src/native/Chevron.spec.tsx +34 -0
- package/src/native/Chevron.tsx +50 -0
- package/src/native/ScaledText.guard.spec.ts +25 -0
- package/src/native/ScaledText.spec.tsx +77 -0
- package/src/native/ScaledText.tsx +30 -0
- package/src/native/fontScale.spec.ts +35 -0
- package/src/native/fontScale.ts +15 -0
- package/src/test/cornerRadii.ts +15 -0
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
Text,
|
|
6
|
-
GestureResponderEvent,
|
|
7
|
-
LayoutChangeEvent,
|
|
8
|
-
StyleProp,
|
|
9
|
-
ViewStyle,
|
|
10
|
-
} from 'react-native';
|
|
2
|
+
import { StyleSheet, View, GestureResponderEvent, LayoutChangeEvent, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../../native/ScaledText';
|
|
11
4
|
import { spacing, font, lineHeight } from '../../../../native/tokens';
|
|
12
5
|
import { useTheme } from '../../../../native/ThemeContext';
|
|
13
6
|
import { Label } from '../atoms/Label';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
3
3
|
import { Textarea } from './Textarea';
|
|
4
|
+
import { cornerRadii } from '../../../../test/cornerRadii';
|
|
4
5
|
|
|
5
6
|
function renderWithTheme(ui: React.ReactElement) {
|
|
6
7
|
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
@@ -47,4 +48,9 @@ describe('Textarea (native)', () => {
|
|
|
47
48
|
const inputs = container.querySelectorAll('textarea, [style*="opacity"]');
|
|
48
49
|
expect(inputs.length).toBeGreaterThan(0);
|
|
49
50
|
});
|
|
51
|
+
|
|
52
|
+
it('gives the bordered field box the medium border radius', () => {
|
|
53
|
+
renderWithTheme(<Textarea label="Message" name="message" />);
|
|
54
|
+
expect(cornerRadii(screen.getByRole('textbox'))).toEqual(['16px', '16px', '16px', '16px']);
|
|
55
|
+
});
|
|
50
56
|
});
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { ReactNode, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
TextInput,
|
|
6
|
-
StyleProp,
|
|
7
|
-
ViewStyle,
|
|
8
|
-
} from 'react-native';
|
|
2
|
+
import { StyleSheet, View, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledTextInput as TextInput } from '../../../../native/ScaledText';
|
|
9
4
|
import { spacing, font, border, lineHeight } from '../../../../native/tokens';
|
|
10
5
|
import { useTheme } from '../../../../native/ThemeContext';
|
|
11
6
|
import { Label } from '../atoms/Label';
|
|
@@ -38,7 +33,7 @@ const styles = StyleSheet.create({
|
|
|
38
33
|
paddingHorizontal: spacing.s,
|
|
39
34
|
paddingVertical: spacing.xs,
|
|
40
35
|
borderWidth: 1,
|
|
41
|
-
borderRadius: border.radius.
|
|
36
|
+
borderRadius: border.radius.m,
|
|
42
37
|
minHeight: 100,
|
|
43
38
|
textAlignVertical: 'top',
|
|
44
39
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { StyleProp, TextStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../../native/ScaledText';
|
|
3
4
|
import { font, lineHeight } from '../../../../native/tokens';
|
|
4
5
|
import { useTheme } from '../../../../native/ThemeContext';
|
|
5
6
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { StyleSheet,
|
|
1
|
+
import { StyleSheet, StyleProp, TextStyle } from 'react-native';
|
|
2
|
+
import { ScaledText as Text } from '../../../../native/ScaledText';
|
|
2
3
|
import { font, lineHeight } from '../../../../native/tokens';
|
|
3
4
|
import { useTheme } from '../../../../native/ThemeContext';
|
|
4
5
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { StyleSheet, View,
|
|
1
|
+
import { StyleSheet, View, StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
2
3
|
import { spacing, font, lineHeight } from '../../../native/tokens';
|
|
3
4
|
import { useTheme } from '../../../native/ThemeContext';
|
|
4
5
|
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import { ReactNode, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
Text,
|
|
6
|
-
Pressable,
|
|
7
|
-
LayoutAnimation,
|
|
8
|
-
Platform,
|
|
9
|
-
UIManager,
|
|
10
|
-
StyleProp,
|
|
11
|
-
ViewStyle,
|
|
12
|
-
} from 'react-native';
|
|
2
|
+
import { StyleSheet, View, Pressable, LayoutAnimation, Platform, UIManager, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
13
4
|
import { spacing, font, border, lineHeight } from '../../../native/tokens';
|
|
14
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
15
6
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { StyleSheet, View,
|
|
2
|
+
import { StyleSheet, View, Pressable, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
3
4
|
import { spacing, font, border, lineHeight } from '../../../native/tokens';
|
|
4
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
5
6
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { StyleSheet, View,
|
|
1
|
+
import { StyleSheet, View, Image, StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
2
3
|
import { font, border } from '../../../native/tokens';
|
|
3
4
|
import { useTheme } from '../../../native/ThemeContext';
|
|
4
5
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import type React from 'react';
|
|
2
3
|
import { ThemeProvider } from '../../../native/ThemeContext';
|
|
3
4
|
import { Badge } from './Badge';
|
|
4
5
|
|
|
@@ -6,6 +7,23 @@ function renderWithTheme(ui: React.ReactElement) {
|
|
|
6
7
|
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
// Mocks react-native's Text to expose maxFontSizeMultiplier as a data attribute
|
|
11
|
+
// (react-native-web drops it from the DOM).
|
|
12
|
+
vi.mock('react-native', async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal<typeof import('react-native')>();
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
Text: ({
|
|
17
|
+
children,
|
|
18
|
+
maxFontSizeMultiplier,
|
|
19
|
+
}: Record<string, unknown> & { children?: React.ReactNode }) => (
|
|
20
|
+
<span data-testid="text" data-max-font-scale={String(maxFontSizeMultiplier)}>
|
|
21
|
+
{children}
|
|
22
|
+
</span>
|
|
23
|
+
),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
9
27
|
describe('Badge (native)', () => {
|
|
10
28
|
it('renders children text', () => {
|
|
11
29
|
renderWithTheme(<Badge>New</Badge>);
|
|
@@ -42,4 +60,14 @@ describe('Badge (native)', () => {
|
|
|
42
60
|
renderWithTheme(<Badge style={{ marginTop: 10 }}>Styled</Badge>);
|
|
43
61
|
expect(screen.getByText('Styled')).toBeInTheDocument();
|
|
44
62
|
});
|
|
63
|
+
|
|
64
|
+
it('caps its text at a multiplier of 2 by default', () => {
|
|
65
|
+
renderWithTheme(<Badge>3</Badge>);
|
|
66
|
+
expect(screen.getByText('3')).toHaveAttribute('data-max-font-scale', '2');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('uses the override multiplier when given', () => {
|
|
70
|
+
renderWithTheme(<Badge maxFontSizeMultiplier={1}>3</Badge>);
|
|
71
|
+
expect(screen.getByText('3')).toHaveAttribute('data-max-font-scale', '1');
|
|
72
|
+
});
|
|
45
73
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { StyleSheet, View,
|
|
2
|
+
import { StyleSheet, View, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
3
4
|
import { spacing, font, border } from '../../../native/tokens';
|
|
4
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
5
6
|
|
|
@@ -9,6 +10,11 @@ interface BadgeProps {
|
|
|
9
10
|
children: ReactNode;
|
|
10
11
|
variant?: BadgeVariant;
|
|
11
12
|
style?: StyleProp<ViewStyle>;
|
|
13
|
+
/**
|
|
14
|
+
* Overrides the ceiling on how far the text grows with the OS text size (default: from its
|
|
15
|
+
* font size, see `maxFontScaleFor`). Pass `1` for a count on a fixed shape.
|
|
16
|
+
*/
|
|
17
|
+
maxFontSizeMultiplier?: number;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
const styles = StyleSheet.create({
|
|
@@ -29,7 +35,7 @@ const styles = StyleSheet.create({
|
|
|
29
35
|
},
|
|
30
36
|
});
|
|
31
37
|
|
|
32
|
-
export function Badge({ children, variant = 'primary', style }: BadgeProps) {
|
|
38
|
+
export function Badge({ children, variant = 'primary', style, maxFontSizeMultiplier }: BadgeProps) {
|
|
33
39
|
const { colors } = useTheme();
|
|
34
40
|
|
|
35
41
|
const variantColors: Record<BadgeVariant, { bg: string; text: string }> = {
|
|
@@ -44,7 +50,7 @@ export function Badge({ children, variant = 'primary', style }: BadgeProps) {
|
|
|
44
50
|
|
|
45
51
|
return (
|
|
46
52
|
<View style={[styles.badge, { backgroundColor: bg }, style]}>
|
|
47
|
-
<Text style={[styles.text, { color: text }]}>{children}</Text>
|
|
53
|
+
<Text style={[styles.text, { color: text }]} maxFontSizeMultiplier={maxFontSizeMultiplier}>{children}</Text>
|
|
48
54
|
</View>
|
|
49
55
|
);
|
|
50
56
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { AccessibilityRole, StyleSheet, View,
|
|
1
|
+
import { AccessibilityRole, StyleSheet, View, Pressable, StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
2
3
|
import { spacing, font, lineHeight } from '../../../native/tokens';
|
|
3
4
|
import { useTheme } from '../../../native/ThemeContext';
|
|
4
5
|
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
Text,
|
|
6
|
-
Pressable,
|
|
7
|
-
StyleProp,
|
|
8
|
-
ViewStyle,
|
|
9
|
-
} from 'react-native';
|
|
2
|
+
import { StyleSheet, View, Pressable, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
10
4
|
import { spacing, font, border, lineHeight } from '../../../native/tokens';
|
|
11
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
12
6
|
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import { ReactNode, useCallback, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
Text,
|
|
6
|
-
Pressable,
|
|
7
|
-
ScrollView,
|
|
8
|
-
NativeSyntheticEvent,
|
|
9
|
-
NativeScrollEvent,
|
|
10
|
-
Dimensions,
|
|
11
|
-
StyleProp,
|
|
12
|
-
ViewStyle,
|
|
13
|
-
} from 'react-native';
|
|
2
|
+
import { StyleSheet, View, Pressable, ScrollView, NativeSyntheticEvent, NativeScrollEvent, Dimensions, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
14
4
|
import { spacing, border } from '../../../native/tokens';
|
|
15
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
16
6
|
|
|
@@ -23,7 +23,7 @@ describe('Collapsible (native)', () => {
|
|
|
23
23
|
</Collapsible>,
|
|
24
24
|
);
|
|
25
25
|
const chevron = screen.getByTestId('bds-collapsible-chevron');
|
|
26
|
-
expect(chevron).toHaveStyle({ borderRightWidth:
|
|
26
|
+
expect(chevron).toHaveStyle({ borderRightWidth: '1.5px' });
|
|
27
27
|
expect(chevron).not.toHaveTextContent('▸');
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import { ReactNode, useCallback, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
Text,
|
|
6
|
-
Pressable,
|
|
7
|
-
Animated,
|
|
8
|
-
LayoutAnimation,
|
|
9
|
-
Platform,
|
|
10
|
-
UIManager,
|
|
11
|
-
StyleProp,
|
|
12
|
-
ViewStyle,
|
|
13
|
-
} from 'react-native';
|
|
2
|
+
import { StyleSheet, View, Pressable, Animated, LayoutAnimation, Platform, UIManager, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
14
4
|
import { spacing, font, border, lineHeight } from '../../../native/tokens';
|
|
15
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
6
|
+
import { Chevron } from '../../../native/Chevron';
|
|
16
7
|
|
|
17
8
|
if (
|
|
18
9
|
Platform.OS === 'android' &&
|
|
@@ -58,12 +49,6 @@ const styles = StyleSheet.create({
|
|
|
58
49
|
lineHeight: lineHeight.body,
|
|
59
50
|
},
|
|
60
51
|
chevron: {
|
|
61
|
-
// Border-box chevron (no glyph font dependency): a small square with two
|
|
62
|
-
// adjacent borders, rotated to form a crisp "›" / "⌄" disclosure indicator.
|
|
63
|
-
width: 8,
|
|
64
|
-
height: 8,
|
|
65
|
-
borderRightWidth: 2,
|
|
66
|
-
borderBottomWidth: 2,
|
|
67
52
|
marginStart: spacing.xs,
|
|
68
53
|
},
|
|
69
54
|
content: {
|
|
@@ -127,11 +112,11 @@ export function Collapsible({
|
|
|
127
112
|
style={({ pressed }) => [styles.header, pressed && styles.pressed]}
|
|
128
113
|
>
|
|
129
114
|
<Text style={[styles.summaryText, { color: colors.colorOnBg }]}>{summary}</Text>
|
|
130
|
-
<
|
|
115
|
+
<Chevron
|
|
131
116
|
testID="bds-collapsible-chevron"
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
117
|
+
color={colors.colorOnBgSubtle}
|
|
118
|
+
rotate={rotate}
|
|
119
|
+
style={styles.chevron}
|
|
135
120
|
/>
|
|
136
121
|
</Pressable>
|
|
137
122
|
{isOpen && (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { AccessibilityRole, StyleSheet, View,
|
|
2
|
+
import { AccessibilityRole, StyleSheet, View, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
3
4
|
import { spacing, font, lineHeight } from '../../../native/tokens';
|
|
4
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
5
6
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { StyleSheet,
|
|
2
|
+
import { StyleSheet, Pressable, Linking, StyleProp, TextStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
3
4
|
import { font } from '../../../native/tokens';
|
|
4
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
5
6
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { StyleSheet, View,
|
|
2
|
+
import { StyleSheet, View, Pressable, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
3
4
|
import { spacing, font, lineHeight } from '../../../native/tokens';
|
|
4
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
5
6
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { AccessibilityRole, StyleSheet, View,
|
|
1
|
+
import { AccessibilityRole, StyleSheet, View, Pressable, StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
2
3
|
import { spacing, font, border, lineHeight } from '../../../native/tokens';
|
|
3
4
|
import { useTheme } from '../../../native/ThemeContext';
|
|
4
5
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { StyleSheet, View,
|
|
1
|
+
import { StyleSheet, View, StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
2
3
|
import { spacing, font, border, lineHeight } from '../../../native/tokens';
|
|
3
4
|
import { useTheme } from '../../../native/ThemeContext';
|
|
4
5
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { StyleSheet, View,
|
|
1
|
+
import { StyleSheet, View, StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
2
3
|
import { font } from '../../../native/tokens';
|
|
3
4
|
import { useTheme } from '../../../native/ThemeContext';
|
|
4
5
|
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
Text,
|
|
6
|
-
ScrollView,
|
|
7
|
-
Pressable,
|
|
8
|
-
StyleProp,
|
|
9
|
-
ViewStyle,
|
|
10
|
-
} from 'react-native';
|
|
2
|
+
import { StyleSheet, View, ScrollView, Pressable, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
11
4
|
import { spacing, font, border, lineHeight } from '../../../native/tokens';
|
|
12
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
13
6
|
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import { ReactNode, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
StyleSheet,
|
|
5
|
-
View,
|
|
6
|
-
Text,
|
|
7
|
-
Pressable,
|
|
8
|
-
ScrollView,
|
|
9
|
-
StyleProp,
|
|
10
|
-
ViewStyle,
|
|
11
|
-
} from 'react-native';
|
|
2
|
+
import { AccessibilityRole, StyleSheet, View, Pressable, ScrollView, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
12
4
|
import { spacing, font, lineHeight } from '../../../native/tokens';
|
|
13
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
14
6
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactNode, useState } from 'react';
|
|
2
|
-
import { StyleSheet, View,
|
|
2
|
+
import { StyleSheet, View, Pressable, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
3
4
|
import { spacing, font, border } from '../../../native/tokens';
|
|
4
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
5
6
|
|
|
@@ -49,6 +49,8 @@ import { Typography } from '@boostdev/design-system-components';
|
|
|
49
49
|
| `variant` | `"h1" \| "h2" \| "h3" \| "body" \| "body_s"` | `"body"` | Typographic scale |
|
|
50
50
|
| `children` | `ReactNode` | — | Text content |
|
|
51
51
|
| `style` | `StyleProp<TextStyle>` | — | Override text styles |
|
|
52
|
+
| `maxFontSizeMultiplier` | `number` | — | Overrides the ceiling on how far the text grows with the OS text size (default: from its font size, see `maxFontScaleFor`). `1` keeps the text at its own size. |
|
|
53
|
+
| `numberOfLines` | `number` | — | Truncates the text with an ellipsis after this many lines. |
|
|
52
54
|
|
|
53
55
|
## Accessibility
|
|
54
56
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import type React from 'react';
|
|
2
3
|
import { ThemeProvider } from '../../../native/ThemeContext';
|
|
3
4
|
import { Typography } from './Typography';
|
|
4
5
|
|
|
@@ -6,6 +7,37 @@ function renderWithTheme(ui: React.ReactElement) {
|
|
|
6
7
|
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
// Mocks react-native's Text to expose maxFontSizeMultiplier/numberOfLines as data attributes
|
|
11
|
+
// (react-native-web drops them from the DOM). `accessibilityRole` is mapped to `role` the same
|
|
12
|
+
// way react-native-web does ("header" -> "heading") so the heading-role tests above keep working
|
|
13
|
+
// once this mock is in effect for the whole file.
|
|
14
|
+
vi.mock('react-native', async (importOriginal) => {
|
|
15
|
+
const actual = await importOriginal<typeof import('react-native')>();
|
|
16
|
+
const accessibilityRoleToWebRole: Record<string, string> = { header: 'heading' };
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
Text: ({
|
|
20
|
+
children,
|
|
21
|
+
maxFontSizeMultiplier,
|
|
22
|
+
numberOfLines,
|
|
23
|
+
adjustsFontSizeToFit,
|
|
24
|
+
minimumFontScale,
|
|
25
|
+
accessibilityRole,
|
|
26
|
+
}: Record<string, unknown> & { children?: React.ReactNode; accessibilityRole?: string }) => (
|
|
27
|
+
<span
|
|
28
|
+
data-testid="text"
|
|
29
|
+
role={accessibilityRole ? accessibilityRoleToWebRole[accessibilityRole] : undefined}
|
|
30
|
+
data-max-font-scale={String(maxFontSizeMultiplier)}
|
|
31
|
+
data-lines={String(numberOfLines)}
|
|
32
|
+
data-fit={String(adjustsFontSizeToFit)}
|
|
33
|
+
data-min-scale={String(minimumFontScale)}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</span>
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
9
41
|
describe('Typography (native)', () => {
|
|
10
42
|
it('renders children text', () => {
|
|
11
43
|
renderWithTheme(<Typography>Hello world</Typography>);
|
|
@@ -47,3 +79,30 @@ describe('Typography (native)', () => {
|
|
|
47
79
|
expect(screen.getByText('Styled')).toBeInTheDocument();
|
|
48
80
|
});
|
|
49
81
|
});
|
|
82
|
+
|
|
83
|
+
describe('Typography text size', () => {
|
|
84
|
+
it('caps h1 at a multiplier of 1.3 (32 pt)', () => {
|
|
85
|
+
renderWithTheme(<Typography variant="h1">Heading</Typography>);
|
|
86
|
+
expect(screen.getByText('Heading')).toHaveAttribute('data-max-font-scale', '1.3');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('caps body at a multiplier of 2', () => {
|
|
90
|
+
renderWithTheme(<Typography variant="body">Body</Typography>);
|
|
91
|
+
expect(screen.getByText('Body')).toHaveAttribute('data-max-font-scale', '2');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('caps a large custom font size at a multiplier of 1', () => {
|
|
95
|
+
renderWithTheme(<Typography style={{ fontSize: 88 }}>Big</Typography>);
|
|
96
|
+
expect(screen.getByText('Big')).toHaveAttribute('data-max-font-scale', '1');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('uses the override multiplier when given', () => {
|
|
100
|
+
renderWithTheme(<Typography maxFontSizeMultiplier={1}>Body</Typography>);
|
|
101
|
+
expect(screen.getByText('Body')).toHaveAttribute('data-max-font-scale', '1');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('passes numberOfLines through to truncate the text', () => {
|
|
105
|
+
renderWithTheme(<Typography numberOfLines={1}>Body</Typography>);
|
|
106
|
+
expect(screen.getByText('Body')).toHaveAttribute('data-lines', '1');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { StyleSheet,
|
|
2
|
+
import { StyleSheet, StyleProp, TextStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
3
4
|
import { font, lineHeight } from '../../../native/tokens';
|
|
4
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
5
6
|
|
|
@@ -9,6 +10,13 @@ interface TypographyProps {
|
|
|
9
10
|
variant?: TypographyVariant;
|
|
10
11
|
children: ReactNode;
|
|
11
12
|
style?: StyleProp<TextStyle>;
|
|
13
|
+
/**
|
|
14
|
+
* Overrides the ceiling on how far the text grows with the OS text size (default: from its
|
|
15
|
+
* font size, see `maxFontScaleFor`). `1` keeps the text at its own size.
|
|
16
|
+
*/
|
|
17
|
+
maxFontSizeMultiplier?: number;
|
|
18
|
+
/** Truncates the text with an ellipsis after this many lines. */
|
|
19
|
+
numberOfLines?: number;
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
const styles = StyleSheet.create({
|
|
@@ -52,12 +60,20 @@ const accessibilityRoles: Record<TypographyVariant, 'header' | 'text'> = {
|
|
|
52
60
|
body_s: 'text',
|
|
53
61
|
};
|
|
54
62
|
|
|
55
|
-
export function Typography({
|
|
63
|
+
export function Typography({
|
|
64
|
+
variant = 'body',
|
|
65
|
+
children,
|
|
66
|
+
style,
|
|
67
|
+
maxFontSizeMultiplier,
|
|
68
|
+
numberOfLines,
|
|
69
|
+
}: TypographyProps) {
|
|
56
70
|
const { colors } = useTheme();
|
|
57
71
|
return (
|
|
58
72
|
<Text
|
|
59
73
|
accessibilityRole={accessibilityRoles[variant]}
|
|
60
74
|
style={[styles.base, styles[variant], { color: colors.colorOnBg }, style]}
|
|
75
|
+
maxFontSizeMultiplier={maxFontSizeMultiplier}
|
|
76
|
+
numberOfLines={numberOfLines}
|
|
61
77
|
>
|
|
62
78
|
{children}
|
|
63
79
|
</Text>
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type {
|
|
|
14
14
|
ReactNavigationFontStyle,
|
|
15
15
|
CreateReactNavigationThemeOptions,
|
|
16
16
|
} from './native/ReactNavigationTheme';
|
|
17
|
+
export { ScaledText, ScaledTextInput, maxFontScaleForStyle } from './native/ScaledText';
|
|
18
|
+
export { maxFontScaleFor } from './native/fontScale';
|
|
17
19
|
|
|
18
20
|
// UI
|
|
19
21
|
export { Typography } from './components/ui/Typography/Typography';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { Chevron } from './Chevron';
|
|
3
|
+
|
|
4
|
+
describe('Chevron', () => {
|
|
5
|
+
it('draws an open V from two adjacent borders, without a glyph', () => {
|
|
6
|
+
render(<Chevron color="#123456" testID="chevron" />);
|
|
7
|
+
const chevron = screen.getByTestId('chevron');
|
|
8
|
+
expect(chevron).toHaveStyle({ borderRightWidth: '1.5px', borderBottomWidth: '1.5px' });
|
|
9
|
+
expect(chevron).toBeEmptyDOMElement();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('strokes the V in the given colour', () => {
|
|
13
|
+
render(<Chevron color="#123456" testID="chevron" />);
|
|
14
|
+
expect(screen.getByTestId('chevron')).toHaveStyle({
|
|
15
|
+
borderRightColor: 'rgb(18, 52, 86)',
|
|
16
|
+
borderBottomColor: 'rgb(18, 52, 86)',
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('points down by default', () => {
|
|
21
|
+
render(<Chevron color="#123456" testID="chevron" />);
|
|
22
|
+
expect(getComputedStyle(screen.getByTestId('chevron')).transform).toMatch(/rotate\(45deg\)/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('takes another rotation', () => {
|
|
26
|
+
render(<Chevron color="#123456" rotate="-45deg" testID="chevron" />);
|
|
27
|
+
expect(getComputedStyle(screen.getByTestId('chevron')).transform).toMatch(/rotate\(-45deg\)/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('is hidden from assistive technology', () => {
|
|
31
|
+
render(<Chevron color="#123456" testID="chevron" />);
|
|
32
|
+
expect(screen.getByTestId('chevron')).toHaveAttribute('aria-hidden', 'true');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Animated, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/** Edge of the bordered square. Rotated 45deg, the V is about 11pt wide: body-text size. */
|
|
4
|
+
const SIZE = 8;
|
|
5
|
+
const STROKE = 1.5;
|
|
6
|
+
/**
|
|
7
|
+
* The V's ink sits in the bordered corner of the square, not on its centre. Shifting the
|
|
8
|
+
* square back along its own diagonal (before the rotation) centres the ink on the layout
|
|
9
|
+
* box whichever way the chevron points: (SIZE - STROKE) / 4 on each axis.
|
|
10
|
+
*/
|
|
11
|
+
const CENTRE_SHIFT = -(SIZE - STROKE) / 4;
|
|
12
|
+
|
|
13
|
+
const styles = StyleSheet.create({
|
|
14
|
+
chevron: {
|
|
15
|
+
// Border-box chevron (no glyph font dependency): a small square with two
|
|
16
|
+
// adjacent borders, rotated to form a crisp "›" / "⌄" indicator.
|
|
17
|
+
width: SIZE,
|
|
18
|
+
height: SIZE,
|
|
19
|
+
borderRightWidth: STROKE,
|
|
20
|
+
borderBottomWidth: STROKE,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export interface ChevronProps {
|
|
25
|
+
color: string;
|
|
26
|
+
/** '45deg' points down (the default), '-45deg' points right. Takes an Animated interpolation. */
|
|
27
|
+
rotate?: string | Animated.AnimatedInterpolation<string>;
|
|
28
|
+
testID?: string;
|
|
29
|
+
style?: StyleProp<ViewStyle>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Decorative open "V", hidden from assistive technology. Internal: not part of the public API. */
|
|
33
|
+
export function Chevron({ color, rotate = '45deg', testID, style }: Readonly<ChevronProps>) {
|
|
34
|
+
return (
|
|
35
|
+
<Animated.View
|
|
36
|
+
testID={testID}
|
|
37
|
+
style={[
|
|
38
|
+
styles.chevron,
|
|
39
|
+
{
|
|
40
|
+
borderColor: color,
|
|
41
|
+
transform: [{ rotate }, { translateX: CENTRE_SHIFT }, { translateY: CENTRE_SHIFT }],
|
|
42
|
+
},
|
|
43
|
+
style,
|
|
44
|
+
]}
|
|
45
|
+
aria-hidden
|
|
46
|
+
importantForAccessibility="no-hide-descendants"
|
|
47
|
+
accessibilityElementsHidden
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|