@boostdev/design-system-react-native 0.5.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 (25) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/index.cjs +572 -534
  3. package/dist/index.js +464 -426
  4. package/package.json +1 -1
  5. package/src/components/interaction/Button/Button.mdx +5 -1
  6. package/src/components/interaction/Button/Button.spec.tsx +23 -0
  7. package/src/components/interaction/Button/Button.tsx +7 -6
  8. package/src/components/interaction/form/Combobox/Combobox.spec.tsx +9 -0
  9. package/src/components/interaction/form/Combobox/Combobox.tsx +2 -2
  10. package/src/components/interaction/form/FileInput/FileInput.spec.tsx +6 -0
  11. package/src/components/interaction/form/FileInput/FileInput.tsx +1 -1
  12. package/src/components/interaction/form/FormInput/FormInput.spec.tsx +7 -0
  13. package/src/components/interaction/form/FormInput/FormInput.tsx +2 -2
  14. package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +8 -0
  15. package/src/components/interaction/form/NumberInput/NumberInput.tsx +4 -4
  16. package/src/components/interaction/form/Select/Select.mdx +2 -1
  17. package/src/components/interaction/form/Select/Select.spec.tsx +22 -1
  18. package/src/components/interaction/form/Select/Select.tsx +7 -6
  19. package/src/components/interaction/form/Textarea/Textarea.spec.tsx +6 -0
  20. package/src/components/interaction/form/Textarea/Textarea.tsx +1 -1
  21. package/src/components/ui/Collapsible/Collapsible.spec.tsx +1 -1
  22. package/src/components/ui/Collapsible/Collapsible.tsx +5 -10
  23. package/src/native/Chevron.spec.tsx +34 -0
  24. package/src/native/Chevron.tsx +50 -0
  25. 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.5.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 square button with equal `spacing.xs` padding on both axes. Composable with any `variant`. Always pair with `accessibilityLabel` for assistive tech.
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>);
@@ -69,4 +70,26 @@ describe('Button (native)', () => {
69
70
  const style = screen.getByRole('button').getAttribute('style') ?? '';
70
71
  expect(style).toMatch(/(^|[;\s])height:\s*56px/);
71
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
+ });
72
95
  });
@@ -21,10 +21,10 @@ export interface ButtonProps {
21
21
  style?: StyleProp<ViewStyle>;
22
22
  }
23
23
 
24
- const sizeTokens: Record<ButtonSize, { height: number; px: number; fontSize: number; radius: number }> = {
25
- small: { height: 36, px: spacing.m, fontSize: font.size.bodyS, radius: border.radius.full },
26
- medium: { height: 48, px: spacing.m, fontSize: font.size.body, radius: border.radius.full },
27
- large: { height: 56, px: spacing.xxl, fontSize: font.size.heading3, radius: border.radius.full },
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 },
28
28
  };
29
29
 
30
30
  const styles = StyleSheet.create({
@@ -57,7 +57,7 @@ export function Button({
57
57
  style,
58
58
  }: Readonly<ButtonProps>) {
59
59
  const { colors } = useTheme();
60
- const { height, px, fontSize, radius } = sizeTokens[size];
60
+ const { height, px, fontSize } = sizeTokens[size];
61
61
 
62
62
  // Color model: filled = interactive bg, outline = transparent bg + interactive_on-bg border/text
63
63
  const primaryColor = colors.colorInteractive;
@@ -69,7 +69,8 @@ export function Button({
69
69
  const containerStyle: ViewStyle = {
70
70
  minHeight: height,
71
71
  paddingHorizontal: isIconOnly ? spacing.xs : px,
72
- borderRadius: radius,
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,
73
74
  opacity: disabled ? 0.4 : 1,
74
75
  // Icon-only: square with equal xs padding on both axes.
75
76
  ...(isIconOnly && { aspectRatio: 1, paddingVertical: spacing.xs }),
@@ -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
  });
@@ -36,7 +36,7 @@ const styles = StyleSheet.create({
36
36
  gap: spacing.xxs,
37
37
  },
38
38
  /**
39
- * Draws the pill border and provides the fixed 44pt touch-target height.
39
+ * Draws the rounded border and provides the fixed 44pt touch-target height.
40
40
  * justifyContent: 'center' vertically centers the inner TextInput.
41
41
  * position: 'relative' is required for the dropdown's absolute positioning.
42
42
  */
@@ -45,7 +45,7 @@ const styles = StyleSheet.create({
45
45
  height: INPUT_HEIGHT,
46
46
  justifyContent: 'center',
47
47
  borderWidth: 1,
48
- borderRadius: border.radius.full,
48
+ borderRadius: border.radius.m,
49
49
  },
50
50
  /**
51
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
  });
@@ -33,7 +33,7 @@ const styles = StyleSheet.create({
33
33
  paddingHorizontal: spacing.m,
34
34
  borderWidth: 2,
35
35
  borderStyle: 'dashed',
36
- borderRadius: border.radius.s,
36
+ borderRadius: border.radius.m,
37
37
  minHeight: 120,
38
38
  },
39
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>);
@@ -99,6 +100,12 @@ describe('FormInput (native)', () => {
99
100
  expect(getComputedStyle(wrapper).minHeight).toBe('44px');
100
101
  });
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']);
107
+ });
108
+
102
109
  /**
103
110
  * Optical-centering invariant: the TextInput must have a translateY transform equal
104
111
  * to OPTICAL_TEXT_SHIFT to compensate for Arial's asymmetric vertical metrics.
@@ -43,14 +43,14 @@ const styles = StyleSheet.create({
43
43
  gap: spacing.xxs,
44
44
  },
45
45
  /**
46
- * The wrapper draws the pill border and provides at least 44pt touch-target height.
46
+ * The wrapper draws the rounded border and provides at least 44pt touch-target height.
47
47
  * justifyContent: 'center' vertically centers the inner TextInput on both iOS and Android.
48
48
  */
49
49
  inputWrapper: {
50
50
  minHeight: INPUT_HEIGHT,
51
51
  justifyContent: 'center',
52
52
  borderWidth: 1,
53
- borderRadius: border.radius.full,
53
+ borderRadius: border.radius.m,
54
54
  },
55
55
  /**
56
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
  });
@@ -40,13 +40,13 @@ const styles = StyleSheet.create({
40
40
  borderWidth: 1,
41
41
  },
42
42
  stepperStart: {
43
- borderTopStartRadius: border.radius.full,
44
- borderBottomStartRadius: border.radius.full,
43
+ borderTopStartRadius: border.radius.m,
44
+ borderBottomStartRadius: border.radius.m,
45
45
  borderEndWidth: 0,
46
46
  },
47
47
  stepperEnd: {
48
- borderTopEndRadius: border.radius.full,
49
- borderBottomEndRadius: border.radius.full,
48
+ borderTopEndRadius: border.radius.m,
49
+ borderBottomEndRadius: border.radius.m,
50
50
  borderStartWidth: 0,
51
51
  },
52
52
  stepperText: {
@@ -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 button that opens a bottom sheet modal with a scrollable list of options.
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
  });
@@ -3,6 +3,7 @@ import { StyleSheet, View, Pressable, Modal, FlatList, SafeAreaView, StyleProp,
3
3
  import { ScaledText as Text } from '../../../../native/ScaledText';
4
4
  import { spacing, font, border, lineHeight } from '../../../../native/tokens';
5
5
  import { useTheme } from '../../../../native/ThemeContext';
6
+ import { Chevron } from '../../../../native/Chevron';
6
7
  import { Label } from '../atoms/Label';
7
8
  import { Message } from '../atoms/Message';
8
9
 
@@ -36,7 +37,7 @@ const styles = StyleSheet.create({
36
37
  paddingHorizontal: spacing.s,
37
38
  paddingVertical: spacing.xs,
38
39
  borderWidth: 1,
39
- borderRadius: border.radius.full,
40
+ borderRadius: border.radius.m,
40
41
  minHeight: 44,
41
42
  },
42
43
  triggerText: {
@@ -46,8 +47,6 @@ const styles = StyleSheet.create({
46
47
  flex: 1,
47
48
  },
48
49
  chevron: {
49
- fontFamily: font.family.body,
50
- fontSize: font.size.body,
51
50
  marginStart: spacing.xs,
52
51
  },
53
52
  overlay: {
@@ -151,9 +150,11 @@ export function Select({
151
150
  >
152
151
  {displayText}
153
152
  </Text>
154
- <Text style={[styles.chevron, { color: colors.colorOnBgSubtle }]} aria-hidden>
155
- {'\u25BE'}
156
- </Text>
153
+ <Chevron
154
+ testID="bds-select-chevron"
155
+ color={colors.colorOnBgSubtle}
156
+ style={styles.chevron}
157
+ />
157
158
  </Pressable>
158
159
 
159
160
  <Modal
@@ -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
  });
@@ -33,7 +33,7 @@ const styles = StyleSheet.create({
33
33
  paddingHorizontal: spacing.s,
34
34
  paddingVertical: spacing.xs,
35
35
  borderWidth: 1,
36
- borderRadius: border.radius.xs,
36
+ borderRadius: border.radius.m,
37
37
  minHeight: 100,
38
38
  textAlignVertical: 'top',
39
39
  },
@@ -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
 
@@ -3,6 +3,7 @@ import { StyleSheet, View, Pressable, Animated, LayoutAnimation, Platform, UIMan
3
3
  import { ScaledText as Text } from '../../../native/ScaledText';
4
4
  import { spacing, font, border, lineHeight } from '../../../native/tokens';
5
5
  import { useTheme } from '../../../native/ThemeContext';
6
+ import { Chevron } from '../../../native/Chevron';
6
7
 
7
8
  if (
8
9
  Platform.OS === 'android' &&
@@ -48,12 +49,6 @@ const styles = StyleSheet.create({
48
49
  lineHeight: lineHeight.body,
49
50
  },
50
51
  chevron: {
51
- // Border-box chevron (no glyph font dependency): a small square with two
52
- // adjacent borders, rotated to form a crisp "›" / "⌄" disclosure indicator.
53
- width: 8,
54
- height: 8,
55
- borderRightWidth: 2,
56
- borderBottomWidth: 2,
57
52
  marginStart: spacing.xs,
58
53
  },
59
54
  content: {
@@ -117,11 +112,11 @@ export function Collapsible({
117
112
  style={({ pressed }) => [styles.header, pressed && styles.pressed]}
118
113
  >
119
114
  <Text style={[styles.summaryText, { color: colors.colorOnBg }]}>{summary}</Text>
120
- <Animated.View
115
+ <Chevron
121
116
  testID="bds-collapsible-chevron"
122
- style={[styles.chevron, { borderColor: colors.colorOnBgSubtle, transform: [{ rotate }] }]}
123
- importantForAccessibility="no"
124
- accessibilityElementsHidden={true}
117
+ color={colors.colorOnBgSubtle}
118
+ rotate={rotate}
119
+ style={styles.chevron}
125
120
  />
126
121
  </Pressable>
127
122
  {isOpen && (
@@ -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
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The four corner radii of an element, clockwise from top-left.
3
+ *
4
+ * react-native-web expands `borderRadius` into the four corner longhands, and jsdom
5
+ * does not rebuild the `border-radius` shorthand from them, so a spec reads the corners.
6
+ */
7
+ export function cornerRadii(element: Element): string[] {
8
+ const style = getComputedStyle(element);
9
+ return [
10
+ style.borderTopLeftRadius,
11
+ style.borderTopRightRadius,
12
+ style.borderBottomRightRadius,
13
+ style.borderBottomLeftRadius,
14
+ ];
15
+ }