@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +10 -0
  3. package/dist/index.cjs +854 -764
  4. package/dist/index.d.cts +35 -4
  5. package/dist/index.d.ts +35 -4
  6. package/dist/index.js +645 -659
  7. package/package.json +1 -1
  8. package/src/components/interaction/Button/Button.mdx +5 -1
  9. package/src/components/interaction/Button/Button.spec.tsx +40 -0
  10. package/src/components/interaction/Button/Button.tsx +15 -8
  11. package/src/components/interaction/Dialog/Dialog.tsx +2 -9
  12. package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +2 -13
  13. package/src/components/interaction/Rating/Rating.tsx +2 -1
  14. package/src/components/interaction/SideSheet/SideSheet.tsx +2 -11
  15. package/src/components/interaction/Toast/Toast.tsx +2 -9
  16. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +2 -1
  17. package/src/components/interaction/form/Combobox/Combobox.spec.tsx +9 -0
  18. package/src/components/interaction/form/Combobox/Combobox.tsx +4 -12
  19. package/src/components/interaction/form/FileInput/FileInput.spec.tsx +6 -0
  20. package/src/components/interaction/form/FileInput/FileInput.tsx +3 -2
  21. package/src/components/interaction/form/FormInput/FormInput.spec.tsx +18 -9
  22. package/src/components/interaction/form/FormInput/FormInput.tsx +5 -11
  23. package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +8 -0
  24. package/src/components/interaction/form/NumberInput/NumberInput.tsx +6 -13
  25. package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +2 -1
  26. package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +40 -0
  27. package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +8 -2
  28. package/src/components/interaction/form/Select/Select.mdx +2 -1
  29. package/src/components/interaction/form/Select/Select.spec.tsx +22 -1
  30. package/src/components/interaction/form/Select/Select.tsx +9 -17
  31. package/src/components/interaction/form/Slider/Slider.tsx +2 -9
  32. package/src/components/interaction/form/Textarea/Textarea.spec.tsx +6 -0
  33. package/src/components/interaction/form/Textarea/Textarea.tsx +3 -8
  34. package/src/components/interaction/form/atoms/Label.tsx +2 -1
  35. package/src/components/interaction/form/atoms/Message.tsx +2 -1
  36. package/src/components/layout/SectionHeader/SectionHeader.tsx +2 -1
  37. package/src/components/ui/Accordion/Accordion.tsx +2 -11
  38. package/src/components/ui/Alert/Alert.tsx +2 -1
  39. package/src/components/ui/Avatar/Avatar.tsx +2 -1
  40. package/src/components/ui/Badge/Badge.spec.tsx +28 -0
  41. package/src/components/ui/Badge/Badge.tsx +9 -3
  42. package/src/components/ui/Breadcrumb/Breadcrumb.tsx +2 -1
  43. package/src/components/ui/Calendar/Calendar.tsx +2 -8
  44. package/src/components/ui/Carousel/Carousel.tsx +2 -12
  45. package/src/components/ui/Collapsible/Collapsible.spec.tsx +1 -1
  46. package/src/components/ui/Collapsible/Collapsible.tsx +7 -22
  47. package/src/components/ui/DescriptionList/DescriptionList.tsx +2 -1
  48. package/src/components/ui/Link/Link.tsx +2 -1
  49. package/src/components/ui/NotificationBanner/NotificationBanner.tsx +2 -1
  50. package/src/components/ui/Pagination/Pagination.tsx +2 -1
  51. package/src/components/ui/Progress/Progress.tsx +2 -1
  52. package/src/components/ui/ProgressCircle/ProgressCircle.tsx +2 -1
  53. package/src/components/ui/Table/Table.tsx +2 -9
  54. package/src/components/ui/Tabs/Tabs.tsx +2 -10
  55. package/src/components/ui/Tooltip/Tooltip.tsx +2 -1
  56. package/src/components/ui/Typography/Typography.mdx +2 -0
  57. package/src/components/ui/Typography/Typography.spec.tsx +59 -0
  58. package/src/components/ui/Typography/Typography.tsx +18 -2
  59. package/src/index.ts +2 -0
  60. package/src/native/Chevron.spec.tsx +34 -0
  61. package/src/native/Chevron.tsx +50 -0
  62. package/src/native/ScaledText.guard.spec.ts +25 -0
  63. package/src/native/ScaledText.spec.tsx +77 -0
  64. package/src/native/ScaledText.tsx +30 -0
  65. package/src/native/fontScale.spec.ts +35 -0
  66. package/src/native/fontScale.ts +15 -0
  67. package/src/test/cornerRadii.ts +15 -0
@@ -1,13 +1,6 @@
1
1
  import { useState } from 'react';
2
- import {
3
- StyleSheet,
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
- StyleSheet,
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.xs,
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 { Text, StyleProp, TextStyle } from 'react-native';
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, Text, StyleProp, TextStyle } from 'react-native';
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, Text, StyleProp, ViewStyle } from 'react-native';
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
- StyleSheet,
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, Text, Pressable, StyleProp, ViewStyle } from 'react-native';
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, Text, Image, StyleProp, ViewStyle } from 'react-native';
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, Text, StyleProp, ViewStyle } from 'react-native';
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, Text, Pressable, StyleProp, ViewStyle } from 'react-native';
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
- StyleSheet,
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
- StyleSheet,
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: 2 });
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
- StyleSheet,
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
- <Animated.View
115
+ <Chevron
131
116
  testID="bds-collapsible-chevron"
132
- style={[styles.chevron, { borderColor: colors.colorOnBgSubtle, transform: [{ rotate }] }]}
133
- importantForAccessibility="no"
134
- accessibilityElementsHidden={true}
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, Text, StyleProp, ViewStyle } from 'react-native';
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, Text, Pressable, Linking, StyleProp, TextStyle } from 'react-native';
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, Text, Pressable, StyleProp, ViewStyle } from 'react-native';
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, Text, Pressable, StyleProp, ViewStyle } from 'react-native';
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, Text, StyleProp, ViewStyle } from 'react-native';
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, Text, StyleProp, ViewStyle } from 'react-native';
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
- StyleSheet,
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
- AccessibilityRole,
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, Text, Pressable, StyleProp, ViewStyle } from 'react-native';
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, Text, StyleProp, TextStyle } from 'react-native';
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({ variant = 'body', children, style }: TypographyProps) {
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
+ }