@boostdev/design-system-react-native 0.3.0 → 0.5.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/CHANGELOG.md +44 -0
- package/README.md +10 -0
- package/dist/index.cjs +589 -536
- package/dist/index.d.cts +36 -5
- package/dist/index.d.ts +36 -5
- package/dist/index.js +442 -493
- package/package.json +1 -1
- package/src/components/interaction/Button/Button.spec.tsx +17 -0
- package/src/components/interaction/Button/Button.tsx +8 -2
- 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.tsx +2 -10
- package/src/components/interaction/form/FileInput/FileInput.tsx +2 -1
- package/src/components/interaction/form/FormInput/FormInput.spec.tsx +11 -9
- package/src/components/interaction/form/FormInput/FormInput.tsx +4 -10
- package/src/components/interaction/form/NumberInput/NumberInput.tsx +2 -9
- package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +2 -1
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +17 -3
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +55 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.stories.tsx +9 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +10 -3
- package/src/components/interaction/form/Select/Select.tsx +2 -11
- package/src/components/interaction/form/Slider/Slider.tsx +2 -9
- package/src/components/interaction/form/Textarea/Textarea.tsx +2 -7
- 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.tsx +2 -12
- 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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boostdev/design-system-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "BoostDev React Native component library: accessible, token-driven components built on @boostdev/design-system-foundation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -52,4 +52,21 @@ describe('Button (native)', () => {
|
|
|
52
52
|
expect(screen.getByRole('button', { name: 'Confirm' })).toBeInTheDocument();
|
|
53
53
|
expect(screen.getByTestId('icon-only-outline')).toBeInTheDocument();
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
it('has min-height instead of a fixed height so a long label can wrap', () => {
|
|
57
|
+
renderWithTheme(<Button size="large">Save</Button>);
|
|
58
|
+
const style = screen.getByRole('button').getAttribute('style') ?? '';
|
|
59
|
+
expect(style).toMatch(/min-height:\s*56px/);
|
|
60
|
+
expect(style).not.toMatch(/(^|[;\s])height:\s*56px/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('keeps a fixed height for an icon-only button', () => {
|
|
64
|
+
renderWithTheme(
|
|
65
|
+
<Button size="large" isIconOnly accessibilityLabel="Confirm">
|
|
66
|
+
<span data-testid="icon-only">✓</span>
|
|
67
|
+
</Button>,
|
|
68
|
+
);
|
|
69
|
+
const style = screen.getByRole('button').getAttribute('style') ?? '';
|
|
70
|
+
expect(style).toMatch(/(^|[;\s])height:\s*56px/);
|
|
71
|
+
});
|
|
55
72
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { StyleSheet, Pressable,
|
|
2
|
+
import { StyleSheet, Pressable, 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
|
|
|
@@ -38,6 +39,8 @@ const styles = StyleSheet.create({
|
|
|
38
39
|
fontFamily: font.family.body,
|
|
39
40
|
fontWeight: font.weight.semibold,
|
|
40
41
|
includeFontPadding: false,
|
|
42
|
+
flexShrink: 1,
|
|
43
|
+
textAlign: 'center',
|
|
41
44
|
},
|
|
42
45
|
});
|
|
43
46
|
|
|
@@ -64,12 +67,15 @@ export function Button({
|
|
|
64
67
|
|
|
65
68
|
const isOutline = variant === 'outline' || variant === 'ghost';
|
|
66
69
|
const containerStyle: ViewStyle = {
|
|
67
|
-
height,
|
|
70
|
+
minHeight: height,
|
|
68
71
|
paddingHorizontal: isIconOnly ? spacing.xs : px,
|
|
69
72
|
borderRadius: radius,
|
|
70
73
|
opacity: disabled ? 0.4 : 1,
|
|
71
74
|
// Icon-only: square with equal xs padding on both axes.
|
|
72
75
|
...(isIconOnly && { aspectRatio: 1, paddingVertical: spacing.xs }),
|
|
76
|
+
// Icon-only keeps a fixed height (stays square); a text button gets vertical padding
|
|
77
|
+
// instead, so a wrapped label grows the button from minHeight rather than clipping.
|
|
78
|
+
...(isIconOnly ? { height } : { paddingVertical: spacing.xxs }),
|
|
73
79
|
...(variant === 'default' && { backgroundColor: primaryColor }),
|
|
74
80
|
...(isOutline && {
|
|
75
81
|
backgroundColor: 'transparent',
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
Pressable,
|
|
6
|
-
Text,
|
|
7
|
-
StyleSheet,
|
|
8
|
-
StyleProp,
|
|
9
|
-
ViewStyle,
|
|
10
|
-
} from 'react-native';
|
|
2
|
+
import { Modal, View, Pressable, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
11
4
|
import { spacing, font, border } from '../../../native/tokens';
|
|
12
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
13
6
|
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import React, { ReactElement, ReactNode, useState, useEffect, useRef } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
Pressable,
|
|
6
|
-
Text,
|
|
7
|
-
FlatList,
|
|
8
|
-
Animated,
|
|
9
|
-
StyleSheet,
|
|
10
|
-
Dimensions,
|
|
11
|
-
SafeAreaView,
|
|
12
|
-
StyleProp,
|
|
13
|
-
ViewStyle,
|
|
14
|
-
} from 'react-native';
|
|
2
|
+
import { Modal, View, Pressable, FlatList, Animated, StyleSheet, Dimensions, SafeAreaView, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
15
4
|
import { spacing, font, border, lineHeight } from '../../../native/tokens';
|
|
16
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
17
6
|
|
|
@@ -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,15 +1,6 @@
|
|
|
1
1
|
import { ReactNode, useEffect, useRef } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
Pressable,
|
|
6
|
-
Text,
|
|
7
|
-
Animated,
|
|
8
|
-
StyleSheet,
|
|
9
|
-
Dimensions,
|
|
10
|
-
StyleProp,
|
|
11
|
-
ViewStyle,
|
|
12
|
-
} from 'react-native';
|
|
2
|
+
import { Modal, View, Pressable, Animated, StyleSheet, Dimensions, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
13
4
|
import { spacing, font, border } from '../../../native/tokens';
|
|
14
5
|
import { useTheme } from '../../../native/ThemeContext';
|
|
15
6
|
|
|
@@ -8,15 +8,8 @@ import {
|
|
|
8
8
|
useContext,
|
|
9
9
|
ReactNode,
|
|
10
10
|
} from 'react';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
Text,
|
|
14
|
-
Pressable,
|
|
15
|
-
Animated,
|
|
16
|
-
StyleSheet,
|
|
17
|
-
StyleProp,
|
|
18
|
-
ViewStyle,
|
|
19
|
-
} from 'react-native';
|
|
11
|
+
import { View, Pressable, Animated, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
|
12
|
+
import { ScaledText as Text } from '../../../native/ScaledText';
|
|
20
13
|
import { spacing, font, border } from '../../../native/tokens';
|
|
21
14
|
import { useTheme } from '../../../native/ThemeContext';
|
|
22
15
|
|
|
@@ -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, lineHeight } from '../../../../native/tokens';
|
|
4
5
|
import { useTheme } from '../../../../native/ThemeContext';
|
|
5
6
|
import { Message } from '../atoms/Message';
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import { useState, useMemo } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
TextInput,
|
|
6
|
-
Pressable,
|
|
7
|
-
Text,
|
|
8
|
-
ScrollView,
|
|
9
|
-
StyleProp,
|
|
10
|
-
ViewStyle,
|
|
11
|
-
} from 'react-native';
|
|
2
|
+
import { StyleSheet, View, Pressable, ScrollView, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text, ScaledTextInput as TextInput } from '../../../../native/ScaledText';
|
|
12
4
|
import { spacing, font, border, lineHeight } from '../../../../native/tokens'; // lineHeight used for optionText (Text, not TextInput)
|
|
13
5
|
import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
|
|
14
6
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { StyleSheet, View, Pressable,
|
|
1
|
+
import { 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
|
import { Label } from '../atoms/Label';
|
|
@@ -82,19 +82,21 @@ describe('FormInput (native)', () => {
|
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
* Touch-target invariant: the wrapper View that draws the pill border must provide
|
|
86
|
-
* the 44pt touch-target height (replacing the
|
|
87
|
-
*
|
|
85
|
+
* Touch-target invariant: the wrapper View that draws the pill border must provide at
|
|
86
|
+
* least the 44pt touch-target height (replacing the fixed height that was previously on
|
|
87
|
+
* the wrapper, so a large OS text size can grow the wrapper past 44pt instead of clipping).
|
|
88
|
+
*
|
|
89
|
+
* inputWrapper comes from StyleSheet.create, so react-native-web applies it as an atomic
|
|
90
|
+
* CSS class rather than an inline style attribute — jsdom has no layout engine, so a plain
|
|
91
|
+
* `height` computed-style read only reflects an explicit `height` declaration, not a
|
|
92
|
+
* `min-height` one; checking `minHeight` directly is the same invariant.
|
|
88
93
|
*/
|
|
89
|
-
it('has height of 44pt on the pill wrapper (touch target)', () => {
|
|
90
|
-
|
|
94
|
+
it('has a min-height of 44pt on the pill wrapper (touch target)', () => {
|
|
95
|
+
renderWithTheme(<FormInput label="Email" name="email" />);
|
|
91
96
|
// The wrapper is the first div that contains the input in react-native-web's output.
|
|
92
97
|
const input = screen.getByRole('textbox');
|
|
93
98
|
const wrapper = input.parentElement as HTMLElement;
|
|
94
|
-
|
|
95
|
-
expect(heightPx).toBeGreaterThanOrEqual(44);
|
|
96
|
-
// Suppress unused-variable warning from destructuring
|
|
97
|
-
void container;
|
|
99
|
+
expect(getComputedStyle(wrapper).minHeight).toBe('44px');
|
|
98
100
|
});
|
|
99
101
|
|
|
100
102
|
/**
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { ReactNode, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
TextInput,
|
|
6
|
-
KeyboardTypeOptions,
|
|
7
|
-
StyleProp,
|
|
8
|
-
ViewStyle,
|
|
9
|
-
} from 'react-native';
|
|
2
|
+
import { StyleSheet, View, KeyboardTypeOptions, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledTextInput as TextInput } from '../../../../native/ScaledText';
|
|
10
4
|
import { spacing, font, border } from '../../../../native/tokens';
|
|
11
5
|
import { useTheme } from '../../../../native/ThemeContext';
|
|
12
6
|
import { Label } from '../atoms/Label';
|
|
@@ -49,11 +43,11 @@ const styles = StyleSheet.create({
|
|
|
49
43
|
gap: spacing.xxs,
|
|
50
44
|
},
|
|
51
45
|
/**
|
|
52
|
-
* The wrapper draws the pill border and provides
|
|
46
|
+
* The wrapper draws the pill border and provides at least 44pt touch-target height.
|
|
53
47
|
* justifyContent: 'center' vertically centers the inner TextInput on both iOS and Android.
|
|
54
48
|
*/
|
|
55
49
|
inputWrapper: {
|
|
56
|
-
|
|
50
|
+
minHeight: INPUT_HEIGHT,
|
|
57
51
|
justifyContent: 'center',
|
|
58
52
|
borderWidth: 1,
|
|
59
53
|
borderRadius: border.radius.full,
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
TextInput,
|
|
6
|
-
Text,
|
|
7
|
-
Pressable,
|
|
8
|
-
StyleProp,
|
|
9
|
-
ViewStyle,
|
|
10
|
-
} from 'react-native';
|
|
2
|
+
import { StyleSheet, View, Pressable, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { ScaledText as Text, ScaledTextInput as TextInput } from '../../../../native/ScaledText';
|
|
11
4
|
import { spacing, font, border } from '../../../../native/tokens';
|
|
12
5
|
import { useTheme } from '../../../../native/ThemeContext';
|
|
13
6
|
import { Label } from '../atoms/Label';
|
|
@@ -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, lineHeight } from '../../../../native/tokens';
|
|
4
5
|
import { useTheme } from '../../../../native/ThemeContext';
|
|
5
6
|
import { Message } from '../atoms/Message';
|
|
@@ -5,7 +5,7 @@ import * as Stories from './SegmentedControl.stories';
|
|
|
5
5
|
|
|
6
6
|
# SegmentedControl (React Native)
|
|
7
7
|
|
|
8
|
-
A horizontal control with multiple segments, where each segment acts as a tab. Supports `outline` and `filled` variants with
|
|
8
|
+
A horizontal control with multiple segments, where each segment acts as a tab. Supports `outline` and `filled` variants with four size options.
|
|
9
9
|
|
|
10
10
|
> **Status: alpha** -- API may change before stable release.
|
|
11
11
|
|
|
@@ -46,6 +46,9 @@ function Example() {
|
|
|
46
46
|
### Large
|
|
47
47
|
<Canvas of={Stories.Large} />
|
|
48
48
|
|
|
49
|
+
### XLarge
|
|
50
|
+
<Canvas of={Stories.XLarge} />
|
|
51
|
+
|
|
49
52
|
### Disabled
|
|
50
53
|
<Canvas of={Stories.Disabled} />
|
|
51
54
|
|
|
@@ -65,16 +68,27 @@ function Example() {
|
|
|
65
68
|
|------|------|---------|-------------|
|
|
66
69
|
| `selectedIndex` | number | `0` | Zero-based index of the active segment |
|
|
67
70
|
| `disabled` | boolean | `false` | Disables all segments |
|
|
68
|
-
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Controls height and font size |
|
|
71
|
+
| `size` | `'small' \| 'medium' \| 'large' \| 'xlarge'` | `'medium'` | Controls height and font size |
|
|
69
72
|
| `variant` | `'outline' \| 'filled'` | `'outline'` | Visual style of the control |
|
|
70
73
|
| `children` | ReactNode | -- | Segment labels (strings or elements) |
|
|
71
74
|
| `accessibilityLabel` | string | -- | Label for the tab group |
|
|
72
75
|
| `onValueChange` | `(index: number) => void` | -- | Called with the tapped segment index |
|
|
73
76
|
|
|
77
|
+
## Sizes
|
|
78
|
+
|
|
79
|
+
| Size | minHeight | fontSize | Horizontal padding | Vertical padding |
|
|
80
|
+
|------|-----------|----------|---------------------|-------------------|
|
|
81
|
+
| `small` | 32 | `font.size.bodyS` | `spacing.s` | `spacing.xxs` |
|
|
82
|
+
| `medium` | 44 | `font.size.body` | `spacing.m` | `spacing.xs` |
|
|
83
|
+
| `large` | 48 | `font.size.body` | `spacing.m` | `spacing.s` |
|
|
84
|
+
| `xlarge` | 56 | `font.size.body` | `spacing.m` | `spacing.s` |
|
|
85
|
+
|
|
86
|
+
`xlarge` matches the DS `Button`'s own `large` height (56 pt) — use it wherever a 56 pt tap target is required (e.g. CoachHelpr's app-wide minimum).
|
|
87
|
+
|
|
74
88
|
## Visual structure
|
|
75
89
|
|
|
76
90
|
- **Track** — a single rounded container (`flexDirection: 'row'`, `borderRadius: full`, `colorBgSubtle` background, 2 px inner padding). No per-segment borders.
|
|
77
|
-
- **Segments** — equal-width (`flex: 1`), min touch target 44 dp for medium/large.
|
|
91
|
+
- **Segments** — equal-width (`flex: 1`), min touch target 44 dp for medium/large, 56 dp for xlarge.
|
|
78
92
|
- **Selected thumb** — contrasting background rendered on the active segment only:
|
|
79
93
|
- `outline` variant → `colorBg` (elevated white / dark-elevated) + subtle shadow, label uses `colorInteractiveOnBg`.
|
|
80
94
|
- `filled` variant → `colorInteractive` background, label uses `colorOnInteractive`.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import type React from 'react';
|
|
2
3
|
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
3
4
|
import { SegmentedControl } from './SegmentedControl';
|
|
4
5
|
|
|
@@ -6,6 +7,32 @@ function renderWithTheme(ui: React.ReactElement) {
|
|
|
6
7
|
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
// Mocks react-native's Text to expose numberOfLines/adjustsFontSizeToFit/minimumFontScale as data
|
|
11
|
+
// attributes (react-native-web drops adjustsFontSizeToFit and minimumFontScale 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
|
+
numberOfLines,
|
|
20
|
+
adjustsFontSizeToFit,
|
|
21
|
+
minimumFontScale,
|
|
22
|
+
}: Record<string, unknown> & { children?: React.ReactNode }) => (
|
|
23
|
+
<span
|
|
24
|
+
data-testid="text"
|
|
25
|
+
data-max-font-scale={String(maxFontSizeMultiplier)}
|
|
26
|
+
data-lines={String(numberOfLines)}
|
|
27
|
+
data-fit={String(adjustsFontSizeToFit)}
|
|
28
|
+
data-min-scale={String(minimumFontScale)}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</span>
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
9
36
|
describe('SegmentedControl (native)', () => {
|
|
10
37
|
it('renders all segments', () => {
|
|
11
38
|
renderWithTheme(
|
|
@@ -121,4 +148,32 @@ describe('SegmentedControl (native)', () => {
|
|
|
121
148
|
expect(screen.getByText('Day')).toBeInTheDocument();
|
|
122
149
|
expect(screen.getByText('Week')).toBeInTheDocument();
|
|
123
150
|
});
|
|
151
|
+
|
|
152
|
+
it('renders an xlarge segment with a 56pt minHeight', () => {
|
|
153
|
+
renderWithTheme(
|
|
154
|
+
<SegmentedControl accessibilityLabel="View" size="xlarge">
|
|
155
|
+
{'Day'}
|
|
156
|
+
{'Week'}
|
|
157
|
+
</SegmentedControl>,
|
|
158
|
+
);
|
|
159
|
+
const tabs = screen.getAllByRole('tab');
|
|
160
|
+
expect(tabs).toHaveLength(2);
|
|
161
|
+
tabs.forEach((tab) => {
|
|
162
|
+
const style = tab.getAttribute('style') ?? '';
|
|
163
|
+
expect(style).toMatch(/min-height:\s*56px/);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('shrinks a long label to fit its segment instead of wrapping', () => {
|
|
168
|
+
renderWithTheme(
|
|
169
|
+
<SegmentedControl accessibilityLabel="View">
|
|
170
|
+
{'A rather long segment label'}
|
|
171
|
+
{'Week'}
|
|
172
|
+
</SegmentedControl>,
|
|
173
|
+
);
|
|
174
|
+
const label = screen.getByText('A rather long segment label');
|
|
175
|
+
expect(label).toHaveAttribute('data-lines', '1');
|
|
176
|
+
expect(label).toHaveAttribute('data-fit', 'true');
|
|
177
|
+
expect(label).toHaveAttribute('data-min-scale', '0.5');
|
|
178
|
+
});
|
|
124
179
|
});
|
|
@@ -50,6 +50,15 @@ export const Large: Story = {
|
|
|
50
50
|
},
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
export const XLarge: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
size: 'xlarge',
|
|
56
|
+
selectedIndex: 0,
|
|
57
|
+
children: ['First', 'Second', 'Third'],
|
|
58
|
+
accessibilityLabel: 'Selection',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
53
62
|
export const Disabled: Story = {
|
|
54
63
|
args: {
|
|
55
64
|
selectedIndex: 0,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Children, ReactNode } from 'react';
|
|
2
|
-
import { StyleSheet, View, Pressable,
|
|
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
|
|
|
6
|
-
type SegmentedControlSize = 'small' | 'medium' | 'large';
|
|
7
|
+
type SegmentedControlSize = 'small' | 'medium' | 'large' | 'xlarge';
|
|
7
8
|
type SegmentedControlVariant = 'outline' | 'filled';
|
|
8
9
|
|
|
9
10
|
interface SegmentedControlProps {
|
|
@@ -24,6 +25,7 @@ const sizeTokens: Record<SegmentedControlSize, { minHeight: number; fontSize: nu
|
|
|
24
25
|
small: { minHeight: 32, fontSize: font.size.bodyS, px: spacing.s, py: spacing.xxs },
|
|
25
26
|
medium: { minHeight: 44, fontSize: font.size.body, px: spacing.m, py: spacing.xs },
|
|
26
27
|
large: { minHeight: 48, fontSize: font.size.body, px: spacing.m, py: spacing.s },
|
|
28
|
+
xlarge: { minHeight: 56, fontSize: font.size.body, px: spacing.m, py: spacing.s },
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
const thumbShadow = {
|
|
@@ -122,7 +124,12 @@ export function SegmentedControl({
|
|
|
122
124
|
]}
|
|
123
125
|
>
|
|
124
126
|
{label !== null ? (
|
|
125
|
-
<Text
|
|
127
|
+
<Text
|
|
128
|
+
style={[styles.label, { fontSize, color: labelColor }]}
|
|
129
|
+
numberOfLines={1}
|
|
130
|
+
adjustsFontSizeToFit
|
|
131
|
+
minimumFontScale={0.5}
|
|
132
|
+
>
|
|
126
133
|
{label}
|
|
127
134
|
</Text>
|
|
128
135
|
) : (
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import { ReactNode, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
View,
|
|
5
|
-
Text,
|
|
6
|
-
Pressable,
|
|
7
|
-
Modal,
|
|
8
|
-
FlatList,
|
|
9
|
-
SafeAreaView,
|
|
10
|
-
StyleProp,
|
|
11
|
-
ViewStyle,
|
|
12
|
-
} from 'react-native';
|
|
2
|
+
import { StyleSheet, View, Pressable, Modal, FlatList, SafeAreaView, 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
|
import { Label } from '../atoms/Label';
|
|
@@ -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,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';
|
|
@@ -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
|
|