@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
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.1",
|
|
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",
|
|
@@ -45,6 +45,10 @@ export default function App() {
|
|
|
45
45
|
<Canvas of={Stories.Medium} />
|
|
46
46
|
<Canvas of={Stories.Large} />
|
|
47
47
|
|
|
48
|
+
## Shape
|
|
49
|
+
|
|
50
|
+
A button with a text label has `border.radius.m` corners on every size, the same as the form fields. An icon-only button (`isIconOnly`) stays round (`border.radius.full`). A `borderRadius` in `style` overrides both.
|
|
51
|
+
|
|
48
52
|
## Icons
|
|
49
53
|
|
|
50
54
|
<Canvas of={Stories.WithIconStart} />
|
|
@@ -52,7 +56,7 @@ export default function App() {
|
|
|
52
56
|
|
|
53
57
|
### Icon-only (1:1 aspect ratio)
|
|
54
58
|
|
|
55
|
-
Set `isIconOnly` to render a
|
|
59
|
+
Set `isIconOnly` to render a round 1:1 button with equal `spacing.xs` padding on both axes. Composable with any `variant`. Always pair with `accessibilityLabel` for assistive tech.
|
|
56
60
|
|
|
57
61
|
<Canvas of={Stories.IconOnly} />
|
|
58
62
|
<Canvas of={Stories.IconOnlyOutline} />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import { ThemeProvider } from '../../../native/ThemeContext';
|
|
3
3
|
import { Button } from './Button';
|
|
4
|
+
import { cornerRadii } from '../../../test/cornerRadii';
|
|
4
5
|
|
|
5
6
|
function renderWithTheme(ui: React.ReactElement) {
|
|
6
7
|
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
@@ -52,4 +53,43 @@ describe('Button (native)', () => {
|
|
|
52
53
|
expect(screen.getByRole('button', { name: 'Confirm' })).toBeInTheDocument();
|
|
53
54
|
expect(screen.getByTestId('icon-only-outline')).toBeInTheDocument();
|
|
54
55
|
});
|
|
56
|
+
|
|
57
|
+
it('has min-height instead of a fixed height so a long label can wrap', () => {
|
|
58
|
+
renderWithTheme(<Button size="large">Save</Button>);
|
|
59
|
+
const style = screen.getByRole('button').getAttribute('style') ?? '';
|
|
60
|
+
expect(style).toMatch(/min-height:\s*56px/);
|
|
61
|
+
expect(style).not.toMatch(/(^|[;\s])height:\s*56px/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('keeps a fixed height for an icon-only button', () => {
|
|
65
|
+
renderWithTheme(
|
|
66
|
+
<Button size="large" isIconOnly accessibilityLabel="Confirm">
|
|
67
|
+
<span data-testid="icon-only">✓</span>
|
|
68
|
+
</Button>,
|
|
69
|
+
);
|
|
70
|
+
const style = screen.getByRole('button').getAttribute('style') ?? '';
|
|
71
|
+
expect(style).toMatch(/(^|[;\s])height:\s*56px/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it.each(['small', 'medium', 'large'] as const)(
|
|
75
|
+
'gives a %s text button the medium border radius',
|
|
76
|
+
(size) => {
|
|
77
|
+
renderWithTheme(<Button size={size}>Save</Button>);
|
|
78
|
+
expect(cornerRadii(screen.getByRole('button'))).toEqual(['16px', '16px', '16px', '16px']);
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
it('keeps an icon-only button round', () => {
|
|
83
|
+
renderWithTheme(
|
|
84
|
+
<Button isIconOnly accessibilityLabel="Confirm">
|
|
85
|
+
<span>✓</span>
|
|
86
|
+
</Button>,
|
|
87
|
+
);
|
|
88
|
+
expect(cornerRadii(screen.getByRole('button'))).toEqual(['9999px', '9999px', '9999px', '9999px']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("lets the caller's style override the border radius", () => {
|
|
92
|
+
renderWithTheme(<Button style={{ borderRadius: 4 }}>Save</Button>);
|
|
93
|
+
expect(cornerRadii(screen.getByRole('button'))).toEqual(['4px', '4px', '4px', '4px']);
|
|
94
|
+
});
|
|
55
95
|
});
|
|
@@ -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
|
|
|
@@ -20,10 +21,10 @@ export interface ButtonProps {
|
|
|
20
21
|
style?: StyleProp<ViewStyle>;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
const sizeTokens: Record<ButtonSize, { height: number; px: number; fontSize: number
|
|
24
|
-
small: { height: 36, px: spacing.m, fontSize: font.size.bodyS
|
|
25
|
-
medium: { height: 48, px: spacing.m, fontSize: font.size.body
|
|
26
|
-
large: { height: 56, px: spacing.xxl, fontSize: font.size.heading3
|
|
24
|
+
const sizeTokens: Record<ButtonSize, { height: number; px: number; fontSize: number }> = {
|
|
25
|
+
small: { height: 36, px: spacing.m, fontSize: font.size.bodyS },
|
|
26
|
+
medium: { height: 48, px: spacing.m, fontSize: font.size.body },
|
|
27
|
+
large: { height: 56, px: spacing.xxl, fontSize: font.size.heading3 },
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
const styles = StyleSheet.create({
|
|
@@ -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
|
|
|
@@ -54,7 +57,7 @@ export function Button({
|
|
|
54
57
|
style,
|
|
55
58
|
}: Readonly<ButtonProps>) {
|
|
56
59
|
const { colors } = useTheme();
|
|
57
|
-
const { height, px, fontSize
|
|
60
|
+
const { height, px, fontSize } = sizeTokens[size];
|
|
58
61
|
|
|
59
62
|
// Color model: filled = interactive bg, outline = transparent bg + interactive_on-bg border/text
|
|
60
63
|
const primaryColor = colors.colorInteractive;
|
|
@@ -64,12 +67,16 @@ 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
|
+
// A text button has medium corners on every size; an icon-only button stays round.
|
|
73
|
+
borderRadius: isIconOnly ? border.radius.full : border.radius.m,
|
|
70
74
|
opacity: disabled ? 0.4 : 1,
|
|
71
75
|
// Icon-only: square with equal xs padding on both axes.
|
|
72
76
|
...(isIconOnly && { aspectRatio: 1, paddingVertical: spacing.xs }),
|
|
77
|
+
// Icon-only keeps a fixed height (stays square); a text button gets vertical padding
|
|
78
|
+
// instead, so a wrapped label grows the button from minHeight rather than clipping.
|
|
79
|
+
...(isIconOnly ? { height } : { paddingVertical: spacing.xxs }),
|
|
73
80
|
...(variant === 'default' && { backgroundColor: primaryColor }),
|
|
74
81
|
...(isOutline && {
|
|
75
82
|
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';
|
|
@@ -3,6 +3,7 @@ import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
|
3
3
|
import { Combobox } from './Combobox';
|
|
4
4
|
import type { ComboboxOption } from './Combobox';
|
|
5
5
|
import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
|
|
6
|
+
import { cornerRadii } from '../../../../test/cornerRadii';
|
|
6
7
|
|
|
7
8
|
function renderWithTheme(ui: React.ReactElement) {
|
|
8
9
|
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
@@ -100,4 +101,12 @@ describe('Combobox (native)', () => {
|
|
|
100
101
|
expect(OPTICAL_TEXT_SHIFT).toBe(-1.5);
|
|
101
102
|
expect(transform).toMatch(/(-1\.5|-1\.50)/);
|
|
102
103
|
});
|
|
104
|
+
|
|
105
|
+
it('gives the bordered field box the medium border radius', () => {
|
|
106
|
+
renderWithTheme(
|
|
107
|
+
<Combobox label="Fruit" name="fruit" options={fruits} />,
|
|
108
|
+
);
|
|
109
|
+
const wrapper = screen.getByRole('combobox').parentElement as HTMLElement;
|
|
110
|
+
expect(cornerRadii(wrapper)).toEqual(['16px', '16px', '16px', '16px']);
|
|
111
|
+
});
|
|
103
112
|
});
|
|
@@ -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
|
|
|
@@ -44,7 +36,7 @@ const styles = StyleSheet.create({
|
|
|
44
36
|
gap: spacing.xxs,
|
|
45
37
|
},
|
|
46
38
|
/**
|
|
47
|
-
* Draws the
|
|
39
|
+
* Draws the rounded border and provides the fixed 44pt touch-target height.
|
|
48
40
|
* justifyContent: 'center' vertically centers the inner TextInput.
|
|
49
41
|
* position: 'relative' is required for the dropdown's absolute positioning.
|
|
50
42
|
*/
|
|
@@ -53,7 +45,7 @@ const styles = StyleSheet.create({
|
|
|
53
45
|
height: INPUT_HEIGHT,
|
|
54
46
|
justifyContent: 'center',
|
|
55
47
|
borderWidth: 1,
|
|
56
|
-
borderRadius: border.radius.
|
|
48
|
+
borderRadius: border.radius.m,
|
|
57
49
|
},
|
|
58
50
|
/**
|
|
59
51
|
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
2
|
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
3
3
|
import { FileInput } from './FileInput';
|
|
4
|
+
import { cornerRadii } from '../../../../test/cornerRadii';
|
|
4
5
|
|
|
5
6
|
function renderWithTheme(ui: React.ReactElement) {
|
|
6
7
|
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
@@ -70,4 +71,9 @@ describe('FileInput (native)', () => {
|
|
|
70
71
|
);
|
|
71
72
|
expect(screen.getByText('Resume *')).toBeInTheDocument();
|
|
72
73
|
});
|
|
74
|
+
|
|
75
|
+
it('gives the drop zone the medium border radius', () => {
|
|
76
|
+
renderWithTheme(<FileInput label="Upload" name="upload" />);
|
|
77
|
+
expect(cornerRadii(screen.getByRole('button'))).toEqual(['16px', '16px', '16px', '16px']);
|
|
78
|
+
});
|
|
73
79
|
});
|
|
@@ -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';
|
|
@@ -32,7 +33,7 @@ const styles = StyleSheet.create({
|
|
|
32
33
|
paddingHorizontal: spacing.m,
|
|
33
34
|
borderWidth: 2,
|
|
34
35
|
borderStyle: 'dashed',
|
|
35
|
-
borderRadius: border.radius.
|
|
36
|
+
borderRadius: border.radius.m,
|
|
36
37
|
minHeight: 120,
|
|
37
38
|
},
|
|
38
39
|
icon: {
|
|
@@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react';
|
|
|
2
2
|
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
3
3
|
import { FormInput } from './FormInput';
|
|
4
4
|
import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
|
|
5
|
+
import { cornerRadii } from '../../../../test/cornerRadii';
|
|
5
6
|
|
|
6
7
|
function renderWithTheme(ui: React.ReactElement) {
|
|
7
8
|
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
@@ -82,19 +83,27 @@ describe('FormInput (native)', () => {
|
|
|
82
83
|
});
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
|
-
* Touch-target invariant: the wrapper View that draws the pill border must provide
|
|
86
|
-
* the 44pt touch-target height (replacing the
|
|
87
|
-
*
|
|
86
|
+
* Touch-target invariant: the wrapper View that draws the pill border must provide at
|
|
87
|
+
* least the 44pt touch-target height (replacing the fixed height that was previously on
|
|
88
|
+
* the wrapper, so a large OS text size can grow the wrapper past 44pt instead of clipping).
|
|
89
|
+
*
|
|
90
|
+
* inputWrapper comes from StyleSheet.create, so react-native-web applies it as an atomic
|
|
91
|
+
* CSS class rather than an inline style attribute — jsdom has no layout engine, so a plain
|
|
92
|
+
* `height` computed-style read only reflects an explicit `height` declaration, not a
|
|
93
|
+
* `min-height` one; checking `minHeight` directly is the same invariant.
|
|
88
94
|
*/
|
|
89
|
-
it('has height of 44pt on the pill wrapper (touch target)', () => {
|
|
90
|
-
|
|
95
|
+
it('has a min-height of 44pt on the pill wrapper (touch target)', () => {
|
|
96
|
+
renderWithTheme(<FormInput label="Email" name="email" />);
|
|
91
97
|
// The wrapper is the first div that contains the input in react-native-web's output.
|
|
92
98
|
const input = screen.getByRole('textbox');
|
|
93
99
|
const wrapper = input.parentElement as HTMLElement;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
expect(getComputedStyle(wrapper).minHeight).toBe('44px');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('gives the bordered field box the medium border radius', () => {
|
|
104
|
+
renderWithTheme(<FormInput label="Email" name="email" />);
|
|
105
|
+
const wrapper = screen.getByRole('textbox').parentElement as HTMLElement;
|
|
106
|
+
expect(cornerRadii(wrapper)).toEqual(['16px', '16px', '16px', '16px']);
|
|
98
107
|
});
|
|
99
108
|
|
|
100
109
|
/**
|
|
@@ -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,14 +43,14 @@ const styles = StyleSheet.create({
|
|
|
49
43
|
gap: spacing.xxs,
|
|
50
44
|
},
|
|
51
45
|
/**
|
|
52
|
-
* The wrapper draws the
|
|
46
|
+
* The wrapper draws the rounded 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
|
-
borderRadius: border.radius.
|
|
53
|
+
borderRadius: border.radius.m,
|
|
60
54
|
},
|
|
61
55
|
/**
|
|
62
56
|
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
@@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react';
|
|
|
2
2
|
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
3
3
|
import { NumberInput } from './NumberInput';
|
|
4
4
|
import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
|
|
5
|
+
import { cornerRadii } from '../../../../test/cornerRadii';
|
|
5
6
|
|
|
6
7
|
function renderWithTheme(ui: React.ReactElement) {
|
|
7
8
|
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
@@ -97,4 +98,11 @@ describe('NumberInput (native)', () => {
|
|
|
97
98
|
expect(OPTICAL_TEXT_SHIFT).toBe(-1.5);
|
|
98
99
|
expect(transform).toMatch(/(-1\.5|-1\.50)/);
|
|
99
100
|
});
|
|
101
|
+
|
|
102
|
+
it('rounds the outer corners of the field with the medium border radius', () => {
|
|
103
|
+
renderWithTheme(<NumberInput label="Quantity" name="qty" value={3} />);
|
|
104
|
+
// Clockwise from top-left: the steppers round only the field's outer corners.
|
|
105
|
+
expect(cornerRadii(screen.getByRole('button', { name: 'Decrease' }))).toEqual(['16px', '', '', '16px']);
|
|
106
|
+
expect(cornerRadii(screen.getByRole('button', { name: 'Increase' }))).toEqual(['', '16px', '16px', '']);
|
|
107
|
+
});
|
|
100
108
|
});
|
|
@@ -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';
|
|
@@ -47,13 +40,13 @@ const styles = StyleSheet.create({
|
|
|
47
40
|
borderWidth: 1,
|
|
48
41
|
},
|
|
49
42
|
stepperStart: {
|
|
50
|
-
borderTopStartRadius: border.radius.
|
|
51
|
-
borderBottomStartRadius: border.radius.
|
|
43
|
+
borderTopStartRadius: border.radius.m,
|
|
44
|
+
borderBottomStartRadius: border.radius.m,
|
|
52
45
|
borderEndWidth: 0,
|
|
53
46
|
},
|
|
54
47
|
stepperEnd: {
|
|
55
|
-
borderTopEndRadius: border.radius.
|
|
56
|
-
borderBottomEndRadius: border.radius.
|
|
48
|
+
borderTopEndRadius: border.radius.m,
|
|
49
|
+
borderBottomEndRadius: border.radius.m,
|
|
57
50
|
borderStartWidth: 0,
|
|
58
51
|
},
|
|
59
52
|
stepperText: {
|
|
@@ -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,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(
|
|
@@ -136,4 +163,17 @@ describe('SegmentedControl (native)', () => {
|
|
|
136
163
|
expect(style).toMatch(/min-height:\s*56px/);
|
|
137
164
|
});
|
|
138
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
|
+
});
|
|
139
179
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
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
|
|
|
@@ -123,7 +124,12 @@ export function SegmentedControl({
|
|
|
123
124
|
]}
|
|
124
125
|
>
|
|
125
126
|
{label !== null ? (
|
|
126
|
-
<Text
|
|
127
|
+
<Text
|
|
128
|
+
style={[styles.label, { fontSize, color: labelColor }]}
|
|
129
|
+
numberOfLines={1}
|
|
130
|
+
adjustsFontSizeToFit
|
|
131
|
+
minimumFontScale={0.5}
|
|
132
|
+
>
|
|
127
133
|
{label}
|
|
128
134
|
</Text>
|
|
129
135
|
) : (
|
|
@@ -5,7 +5,7 @@ import * as Stories from './Select.stories';
|
|
|
5
5
|
|
|
6
6
|
# Select (React Native)
|
|
7
7
|
|
|
8
|
-
Dropdown-style select component. Renders a trigger
|
|
8
|
+
Dropdown-style select component. Renders a field-shaped trigger (`border.radius.m`, like the other form fields) with a downward chevron. Pressing it opens a bottom sheet modal with a scrollable list of options.
|
|
9
9
|
|
|
10
10
|
> **Status: alpha** — API may change before stable release.
|
|
11
11
|
|
|
@@ -80,6 +80,7 @@ function Example() {
|
|
|
80
80
|
## Accessibility
|
|
81
81
|
|
|
82
82
|
- Trigger announces the label and current value via `accessibilityLabel`
|
|
83
|
+
- The chevron is decorative: it is drawn with borders (no glyph) and hidden from the accessibility tree
|
|
83
84
|
- `accessibilityState.expanded` reflects whether the modal is open
|
|
84
85
|
- Each option announces its label and selected state
|
|
85
86
|
- Disabled options are communicated via `accessibilityState.disabled`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
1
|
+
import { render, screen, within } from '@testing-library/react';
|
|
2
2
|
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
3
3
|
import { Select } from './Select';
|
|
4
|
+
import { cornerRadii } from '../../../../test/cornerRadii';
|
|
4
5
|
|
|
5
6
|
const options = [
|
|
6
7
|
{ value: 'apple', label: 'Apple' },
|
|
@@ -61,4 +62,24 @@ describe('Select (native)', () => {
|
|
|
61
62
|
const trigger = screen.getByRole('button', { name: /Fruit/ });
|
|
62
63
|
expect(trigger).toBeDisabled();
|
|
63
64
|
});
|
|
65
|
+
|
|
66
|
+
it('gives the trigger the medium border radius', () => {
|
|
67
|
+
renderWithTheme(<Select label="Fruit" name="fruit" options={options} />);
|
|
68
|
+
const trigger = screen.getByRole('button', { name: /Fruit/ });
|
|
69
|
+
expect(cornerRadii(trigger)).toEqual(['16px', '16px', '16px', '16px']);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('draws the dropdown indicator as a chevron, not a ▾ glyph', () => {
|
|
73
|
+
renderWithTheme(<Select label="Fruit" name="fruit" options={options} />);
|
|
74
|
+
const trigger = screen.getByRole('button', { name: /Fruit/ });
|
|
75
|
+
expect(trigger).not.toHaveTextContent('\u25BE');
|
|
76
|
+
expect(within(trigger).getByTestId('bds-select-chevron')).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('keeps the chevron out of the trigger accessibility label', () => {
|
|
80
|
+
renderWithTheme(
|
|
81
|
+
<Select label="Fruit" name="fruit" options={options} placeholder="Choose..." />,
|
|
82
|
+
);
|
|
83
|
+
expect(screen.getByRole('button', { name: 'Fruit, Choose...' })).toBeInTheDocument();
|
|
84
|
+
});
|
|
64
85
|
});
|
|
@@ -1,17 +1,9 @@
|
|
|
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';
|
|
6
|
+
import { Chevron } from '../../../../native/Chevron';
|
|
15
7
|
import { Label } from '../atoms/Label';
|
|
16
8
|
import { Message } from '../atoms/Message';
|
|
17
9
|
|
|
@@ -45,7 +37,7 @@ const styles = StyleSheet.create({
|
|
|
45
37
|
paddingHorizontal: spacing.s,
|
|
46
38
|
paddingVertical: spacing.xs,
|
|
47
39
|
borderWidth: 1,
|
|
48
|
-
borderRadius: border.radius.
|
|
40
|
+
borderRadius: border.radius.m,
|
|
49
41
|
minHeight: 44,
|
|
50
42
|
},
|
|
51
43
|
triggerText: {
|
|
@@ -55,8 +47,6 @@ const styles = StyleSheet.create({
|
|
|
55
47
|
flex: 1,
|
|
56
48
|
},
|
|
57
49
|
chevron: {
|
|
58
|
-
fontFamily: font.family.body,
|
|
59
|
-
fontSize: font.size.body,
|
|
60
50
|
marginStart: spacing.xs,
|
|
61
51
|
},
|
|
62
52
|
overlay: {
|
|
@@ -160,9 +150,11 @@ export function Select({
|
|
|
160
150
|
>
|
|
161
151
|
{displayText}
|
|
162
152
|
</Text>
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
153
|
+
<Chevron
|
|
154
|
+
testID="bds-select-chevron"
|
|
155
|
+
color={colors.colorOnBgSubtle}
|
|
156
|
+
style={styles.chevron}
|
|
157
|
+
/>
|
|
166
158
|
</Pressable>
|
|
167
159
|
|
|
168
160
|
<Modal
|