@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.
- package/CHANGELOG.md +38 -0
- package/dist/index.cjs +572 -534
- package/dist/index.js +464 -426
- package/package.json +1 -1
- package/src/components/interaction/Button/Button.mdx +5 -1
- package/src/components/interaction/Button/Button.spec.tsx +23 -0
- package/src/components/interaction/Button/Button.tsx +7 -6
- package/src/components/interaction/form/Combobox/Combobox.spec.tsx +9 -0
- package/src/components/interaction/form/Combobox/Combobox.tsx +2 -2
- package/src/components/interaction/form/FileInput/FileInput.spec.tsx +6 -0
- package/src/components/interaction/form/FileInput/FileInput.tsx +1 -1
- package/src/components/interaction/form/FormInput/FormInput.spec.tsx +7 -0
- package/src/components/interaction/form/FormInput/FormInput.tsx +2 -2
- package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +8 -0
- package/src/components/interaction/form/NumberInput/NumberInput.tsx +4 -4
- 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 +7 -6
- package/src/components/interaction/form/Textarea/Textarea.spec.tsx +6 -0
- package/src/components/interaction/form/Textarea/Textarea.tsx +1 -1
- package/src/components/ui/Collapsible/Collapsible.spec.tsx +1 -1
- package/src/components/ui/Collapsible/Collapsible.tsx +5 -10
- package/src/native/Chevron.spec.tsx +34 -0
- package/src/native/Chevron.tsx +50 -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.5.
|
|
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>);
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
});
|
|
@@ -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
|
|
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.
|
|
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.
|
|
44
|
-
borderBottomStartRadius: border.radius.
|
|
43
|
+
borderTopStartRadius: border.radius.m,
|
|
44
|
+
borderBottomStartRadius: border.radius.m,
|
|
45
45
|
borderEndWidth: 0,
|
|
46
46
|
},
|
|
47
47
|
stepperEnd: {
|
|
48
|
-
borderTopEndRadius: border.radius.
|
|
49
|
-
borderBottomEndRadius: border.radius.
|
|
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
|
|
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.
|
|
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
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
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
|
});
|
|
@@ -23,7 +23,7 @@ describe('Collapsible (native)', () => {
|
|
|
23
23
|
</Collapsible>,
|
|
24
24
|
);
|
|
25
25
|
const chevron = screen.getByTestId('bds-collapsible-chevron');
|
|
26
|
-
expect(chevron).toHaveStyle({ borderRightWidth:
|
|
26
|
+
expect(chevron).toHaveStyle({ borderRightWidth: '1.5px' });
|
|
27
27
|
expect(chevron).not.toHaveTextContent('▸');
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -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
|
-
<
|
|
115
|
+
<Chevron
|
|
121
116
|
testID="bds-collapsible-chevron"
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
}
|