@boostdev/design-system-components 1.2.2 → 1.2.4
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/AGENTS.md +5 -0
- package/README.md +50 -5
- package/dist/client.cjs +50 -50
- package/dist/client.css +506 -503
- package/dist/client.js +50 -50
- package/dist/index.cjs +50 -50
- package/dist/index.css +506 -503
- package/dist/index.js +50 -50
- package/dist/native/index.cjs +3692 -352
- package/dist/native/index.d.cts +359 -3
- package/dist/native/index.d.ts +359 -3
- package/dist/native/index.js +3811 -364
- package/package.json +3 -3
- package/src/components/interaction/Button/Button.module.css +1 -4
- package/src/components/interaction/Button/Button.native.stories.tsx +10 -12
- package/src/components/interaction/Button/Button.native.tsx +3 -3
- package/src/components/interaction/Dialog/Dialog.module.css +1 -1
- package/src/components/interaction/Dialog/Dialog.native.mdx +61 -0
- package/src/components/interaction/Dialog/Dialog.native.spec.tsx +73 -0
- package/src/components/interaction/Dialog/Dialog.native.stories.tsx +53 -0
- package/src/components/interaction/Dialog/Dialog.native.tsx +128 -0
- package/src/components/interaction/Drawer/Drawer.module.css +1 -1
- package/src/components/interaction/Drawer/Drawer.native.mdx +58 -0
- package/src/components/interaction/Drawer/Drawer.native.spec.tsx +81 -0
- package/src/components/interaction/Drawer/Drawer.native.stories.tsx +33 -0
- package/src/components/interaction/Drawer/Drawer.native.tsx +175 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +4 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +74 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.spec.tsx +78 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.stories.tsx +51 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.tsx +254 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +4 -4
- package/src/components/interaction/Popover/Popover.native.mdx +61 -0
- package/src/components/interaction/Popover/Popover.native.spec.tsx +73 -0
- package/src/components/interaction/Popover/Popover.native.stories.tsx +44 -0
- package/src/components/interaction/Popover/Popover.native.tsx +87 -0
- package/src/components/interaction/Rating/Rating.native.mdx +55 -0
- package/src/components/interaction/Rating/Rating.native.spec.tsx +38 -0
- package/src/components/interaction/Rating/Rating.native.stories.tsx +37 -0
- package/src/components/interaction/Rating/Rating.native.tsx +50 -0
- package/src/components/interaction/Toast/Toast.native.mdx +81 -0
- package/src/components/interaction/Toast/Toast.native.spec.tsx +80 -0
- package/src/components/interaction/Toast/Toast.native.stories.tsx +47 -0
- package/src/components/interaction/Toast/Toast.native.tsx +202 -0
- package/src/components/interaction/form/Checkbox/Checkbox.native.stories.tsx +2 -4
- package/src/components/interaction/form/Checkbox/Checkbox.native.tsx +1 -1
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.mdx +78 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.spec.tsx +57 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.stories.tsx +96 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.tsx +61 -0
- package/src/components/interaction/form/Combobox/Combobox.module.css +1 -1
- package/src/components/interaction/form/Combobox/Combobox.native.mdx +86 -0
- package/src/components/interaction/form/Combobox/Combobox.native.spec.tsx +60 -0
- package/src/components/interaction/form/Combobox/Combobox.native.stories.tsx +99 -0
- package/src/components/interaction/form/Combobox/Combobox.native.tsx +211 -0
- package/src/components/interaction/form/FileInput/FileInput.native.mdx +86 -0
- package/src/components/interaction/form/FileInput/FileInput.native.spec.tsx +73 -0
- package/src/components/interaction/form/FileInput/FileInput.native.stories.tsx +69 -0
- package/src/components/interaction/form/FileInput/FileInput.native.tsx +129 -0
- package/src/components/interaction/form/FormInput/FormInput.module.css +1 -1
- package/src/components/interaction/form/FormInput/FormInput.native.mdx +81 -0
- package/src/components/interaction/form/FormInput/FormInput.native.spec.tsx +51 -0
- package/src/components/interaction/form/FormInput/FormInput.native.stories.tsx +54 -0
- package/src/components/interaction/form/FormInput/FormInput.native.tsx +122 -0
- package/src/components/interaction/form/NumberInput/NumberInput.module.css +1 -1
- package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +87 -0
- package/src/components/interaction/form/NumberInput/NumberInput.native.spec.tsx +63 -0
- package/src/components/interaction/form/NumberInput/NumberInput.native.stories.tsx +63 -0
- package/src/components/interaction/form/NumberInput/NumberInput.native.tsx +198 -0
- package/src/components/interaction/form/Radio/Radio.native.spec.tsx +79 -0
- package/src/components/interaction/form/Radio/Radio.native.stories.tsx +12 -14
- package/src/components/interaction/form/Radio/Radio.native.tsx +1 -1
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +79 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.spec.tsx +66 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.stories.tsx +96 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.tsx +61 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +5 -5
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +78 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.spec.tsx +82 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.stories.tsx +99 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.tsx +121 -0
- package/src/components/interaction/form/Select/Select.module.css +1 -1
- package/src/components/interaction/form/Select/Select.native.mdx +85 -0
- package/src/components/interaction/form/Select/Select.native.spec.tsx +64 -0
- package/src/components/interaction/form/Select/Select.native.stories.tsx +75 -0
- package/src/components/interaction/form/Select/Select.native.tsx +252 -0
- package/src/components/interaction/form/Slider/Slider.module.css +4 -4
- package/src/components/interaction/form/Slider/Slider.native.mdx +83 -0
- package/src/components/interaction/form/Slider/Slider.native.spec.tsx +51 -0
- package/src/components/interaction/form/Slider/Slider.native.stories.tsx +58 -0
- package/src/components/interaction/form/Slider/Slider.native.tsx +188 -0
- package/src/components/interaction/form/Switch/Switch.module.css +3 -3
- package/src/components/interaction/form/Switch/Switch.native.stories.tsx +2 -4
- package/src/components/interaction/form/Textarea/Textarea.native.mdx +76 -0
- package/src/components/interaction/form/Textarea/Textarea.native.spec.tsx +50 -0
- package/src/components/interaction/form/Textarea/Textarea.native.stories.tsx +50 -0
- package/src/components/interaction/form/Textarea/Textarea.native.tsx +108 -0
- package/src/components/interaction/form/atoms/Label.native.spec.tsx +39 -0
- package/src/components/interaction/form/atoms/Label.native.stories.tsx +1 -1
- package/src/components/interaction/form/atoms/Message.native.stories.tsx +1 -1
- package/src/components/layout/ButtonGroup/ButtonGroup.native.spec.tsx +83 -0
- package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +13 -19
- package/src/components/layout/Card/Card.native.spec.tsx +102 -0
- package/src/components/layout/Card/Card.native.stories.tsx +9 -11
- package/src/components/layout/Card/Card.native.tsx +1 -1
- package/src/components/layout/IconWrapper/IconWrapper.module.css +1 -1
- package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +7 -11
- package/src/components/layout/SectionHeader/SectionHeader.native.spec.tsx +51 -0
- package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +6 -8
- package/src/components/ui/Accordion/Accordion.native.mdx +65 -0
- package/src/components/ui/Accordion/Accordion.native.spec.tsx +69 -0
- package/src/components/ui/Accordion/Accordion.native.stories.tsx +52 -0
- package/src/components/ui/Accordion/Accordion.native.tsx +141 -0
- package/src/components/ui/Alert/Alert.module.css +1 -1
- package/src/components/ui/Alert/Alert.native.spec.tsx +76 -0
- package/src/components/ui/Alert/Alert.native.stories.tsx +8 -10
- package/src/components/ui/Alert/Alert.native.tsx +7 -7
- package/src/components/ui/Avatar/Avatar.module.css +1 -1
- package/src/components/ui/Avatar/Avatar.native.stories.tsx +6 -8
- package/src/components/ui/Avatar/Avatar.native.tsx +2 -2
- package/src/components/ui/Badge/Badge.module.css +1 -1
- package/src/components/ui/Badge/Badge.native.spec.tsx +45 -0
- package/src/components/ui/Badge/Badge.native.stories.tsx +6 -8
- package/src/components/ui/Badge/Badge.native.tsx +2 -2
- package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +52 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.native.spec.tsx +49 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.native.stories.tsx +46 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.native.tsx +83 -0
- package/src/components/ui/Calendar/Calendar.module.css +2 -2
- package/src/components/ui/Calendar/Calendar.native.mdx +58 -0
- package/src/components/ui/Calendar/Calendar.native.spec.tsx +92 -0
- package/src/components/ui/Calendar/Calendar.native.stories.tsx +45 -0
- package/src/components/ui/Calendar/Calendar.native.tsx +294 -0
- package/src/components/ui/Carousel/Carousel.module.css +1 -1
- package/src/components/ui/Carousel/Carousel.native.mdx +55 -0
- package/src/components/ui/Carousel/Carousel.native.spec.tsx +55 -0
- package/src/components/ui/Carousel/Carousel.native.stories.tsx +59 -0
- package/src/components/ui/Carousel/Carousel.native.tsx +184 -0
- package/src/components/ui/Collapsible/Collapsible.native.mdx +53 -0
- package/src/components/ui/Collapsible/Collapsible.native.spec.tsx +68 -0
- package/src/components/ui/Collapsible/Collapsible.native.stories.tsx +48 -0
- package/src/components/ui/Collapsible/Collapsible.native.tsx +139 -0
- package/src/components/ui/DescriptionList/DescriptionList.native.mdx +50 -0
- package/src/components/ui/DescriptionList/DescriptionList.native.spec.tsx +37 -0
- package/src/components/ui/DescriptionList/DescriptionList.native.stories.tsx +48 -0
- package/src/components/ui/DescriptionList/DescriptionList.native.tsx +54 -0
- package/src/components/ui/Link/Link.native.mdx +53 -0
- package/src/components/ui/Link/Link.native.spec.tsx +35 -0
- package/src/components/ui/Link/Link.native.stories.tsx +46 -0
- package/src/components/ui/Link/Link.native.tsx +48 -0
- package/src/components/ui/Loading/Loading.module.css +1 -1
- package/src/components/ui/Loading/Loading.native.stories.tsx +6 -8
- package/src/components/ui/NotificationBanner/NotificationBanner.module.css +1 -1
- package/src/components/ui/NotificationBanner/NotificationBanner.native.spec.tsx +85 -0
- package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +8 -10
- package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +7 -7
- package/src/components/ui/Pagination/Pagination.module.css +5 -1
- package/src/components/ui/Pagination/Pagination.native.mdx +62 -0
- package/src/components/ui/Pagination/Pagination.native.spec.tsx +86 -0
- package/src/components/ui/Pagination/Pagination.native.stories.tsx +47 -0
- package/src/components/ui/Pagination/Pagination.native.tsx +157 -0
- package/src/components/ui/Progress/Progress.module.css +2 -2
- package/src/components/ui/Progress/Progress.native.spec.tsx +56 -0
- package/src/components/ui/Progress/Progress.native.stories.tsx +1 -1
- package/src/components/ui/Progress/Progress.native.tsx +2 -2
- package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +63 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.native.spec.tsx +39 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.native.stories.tsx +36 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.native.tsx +176 -0
- package/src/components/ui/Separator/Separator.native.spec.tsx +39 -0
- package/src/components/ui/Separator/Separator.native.stories.tsx +10 -14
- package/src/components/ui/Skeleton/Skeleton.native.spec.tsx +39 -0
- package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +6 -8
- package/src/components/ui/Skeleton/Skeleton.native.tsx +2 -1
- package/src/components/ui/Table/Table.native.mdx +72 -0
- package/src/components/ui/Table/Table.native.spec.tsx +83 -0
- package/src/components/ui/Table/Table.native.stories.tsx +55 -0
- package/src/components/ui/Table/Table.native.tsx +196 -0
- package/src/components/ui/Tabs/Tabs.native.mdx +65 -0
- package/src/components/ui/Tabs/Tabs.native.spec.tsx +65 -0
- package/src/components/ui/Tabs/Tabs.native.stories.tsx +57 -0
- package/src/components/ui/Tabs/Tabs.native.tsx +133 -0
- package/src/components/ui/Tooltip/Tooltip.module.css +1 -1
- package/src/components/ui/Tooltip/Tooltip.native.mdx +47 -0
- package/src/components/ui/Tooltip/Tooltip.native.spec.tsx +37 -0
- package/src/components/ui/Tooltip/Tooltip.native.stories.tsx +38 -0
- package/src/components/ui/Tooltip/Tooltip.native.tsx +57 -0
- package/src/components/ui/Typography/Typography.native.spec.tsx +49 -0
- package/src/components/ui/Typography/Typography.native.stories.tsx +8 -10
- package/src/native/ThemeContext.tsx +3 -3
- package/src/native.ts +37 -0
- package/src/stories/ReactNative.mdx +48 -13
- package/src/web-components/interaction/BdsAccordion.stories.tsx +1 -1
- package/src/web-components/interaction/BdsCollapsible.stories.tsx +1 -1
- package/src/web-components/interaction/BdsSkipLink.stories.tsx +1 -1
- package/src/web-components/interaction/BdsTabs.stories.tsx +1 -1
- package/src/web-components/interaction/BdsTooltip.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsCombobox.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsFileInput.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsFormInput.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsRadio.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsSelect.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsSlider.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsSwitch.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsTextarea.stories.tsx +1 -1
- package/src/web-components/ui/BdsButtonGroup.stories.tsx +1 -1
- package/src/web-components/ui/BdsCard.stories.tsx +1 -1
- package/src/web-components/ui/BdsIconWrapper.stories.tsx +1 -1
- package/src/web-components/ui/BdsRating.stories.tsx +1 -1
- package/src/web-components/ui/BdsSectionHeader.stories.tsx +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
2
|
+
import * as Stories from './Collapsible.native.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={Stories} />
|
|
5
|
+
|
|
6
|
+
# Collapsible (React Native)
|
|
7
|
+
|
|
8
|
+
React Native collapsible section with an animated chevron and smooth expand/collapse transitions. Supports both controlled and uncontrolled modes.
|
|
9
|
+
|
|
10
|
+
> **Status: alpha** -- API may change before stable release.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { Collapsible } from '@boostdev/components';
|
|
16
|
+
|
|
17
|
+
<Collapsible summary="More details">
|
|
18
|
+
<Text>Hidden content revealed on press.</Text>
|
|
19
|
+
</Collapsible>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Examples
|
|
23
|
+
|
|
24
|
+
### Default (collapsed)
|
|
25
|
+
<Canvas of={Stories.Default} />
|
|
26
|
+
|
|
27
|
+
### Default open
|
|
28
|
+
<Canvas of={Stories.DefaultOpen} />
|
|
29
|
+
|
|
30
|
+
### Multiple sections
|
|
31
|
+
<Canvas of={Stories.Multiple} />
|
|
32
|
+
|
|
33
|
+
## Props
|
|
34
|
+
|
|
35
|
+
<ArgTypes of={Stories} />
|
|
36
|
+
|
|
37
|
+
## Props reference
|
|
38
|
+
|
|
39
|
+
| Prop | Type | Default | Description |
|
|
40
|
+
|------|------|---------|-------------|
|
|
41
|
+
| `summary` | `string` | -- | The trigger label, always visible |
|
|
42
|
+
| `children` | `ReactNode` | -- | Content revealed when expanded |
|
|
43
|
+
| `open` | `boolean` | -- | Controlled open state |
|
|
44
|
+
| `defaultOpen` | `boolean` | `false` | Uncontrolled initial open state |
|
|
45
|
+
| `onToggle` | `(open: boolean) => void` | -- | Called after each toggle with the new open state |
|
|
46
|
+
| `name` | `string` | -- | Grouping name (informational in native) |
|
|
47
|
+
| `style` | `StyleProp<ViewStyle>` | -- | Custom style for the container |
|
|
48
|
+
|
|
49
|
+
## Accessibility
|
|
50
|
+
|
|
51
|
+
- Header has `button` role with `expanded` state
|
|
52
|
+
- Chevron icon is hidden from the accessibility tree
|
|
53
|
+
- Content appears/disappears with LayoutAnimation for smooth transitions
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { ThemeProvider } from '../../../native/ThemeContext';
|
|
3
|
+
import { Collapsible } from './Collapsible.native';
|
|
4
|
+
|
|
5
|
+
function renderWithTheme(ui: React.ReactElement) {
|
|
6
|
+
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('Collapsible (native)', () => {
|
|
10
|
+
it('renders the summary text', () => {
|
|
11
|
+
renderWithTheme(
|
|
12
|
+
<Collapsible summary="Details">
|
|
13
|
+
<span>Content</span>
|
|
14
|
+
</Collapsible>,
|
|
15
|
+
);
|
|
16
|
+
expect(screen.getByText('Details')).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('hides content by default', () => {
|
|
20
|
+
renderWithTheme(
|
|
21
|
+
<Collapsible summary="Details">
|
|
22
|
+
<span>Hidden content</span>
|
|
23
|
+
</Collapsible>,
|
|
24
|
+
);
|
|
25
|
+
expect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('shows content when defaultOpen is true', () => {
|
|
29
|
+
renderWithTheme(
|
|
30
|
+
<Collapsible summary="Details" defaultOpen>
|
|
31
|
+
<span>Visible content</span>
|
|
32
|
+
</Collapsible>,
|
|
33
|
+
);
|
|
34
|
+
expect(screen.getByText('Visible content')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('toggles content on press', () => {
|
|
38
|
+
renderWithTheme(
|
|
39
|
+
<Collapsible summary="Toggle me">
|
|
40
|
+
<span>Toggled content</span>
|
|
41
|
+
</Collapsible>,
|
|
42
|
+
);
|
|
43
|
+
expect(screen.queryByText('Toggled content')).not.toBeInTheDocument();
|
|
44
|
+
fireEvent.click(screen.getByRole('button', { name: 'Toggle me' }));
|
|
45
|
+
expect(screen.getByText('Toggled content')).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('shows content when defaultOpen is set and has button role', () => {
|
|
49
|
+
renderWithTheme(
|
|
50
|
+
<Collapsible summary="Expandable" defaultOpen>
|
|
51
|
+
<span>Content</span>
|
|
52
|
+
</Collapsible>,
|
|
53
|
+
);
|
|
54
|
+
expect(screen.getByRole('button', { name: 'Expandable' })).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByText('Content')).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('calls onToggle with the new open state', () => {
|
|
59
|
+
const onToggle = vi.fn();
|
|
60
|
+
renderWithTheme(
|
|
61
|
+
<Collapsible summary="Toggle" onToggle={onToggle}>
|
|
62
|
+
<span>Content</span>
|
|
63
|
+
</Collapsible>,
|
|
64
|
+
);
|
|
65
|
+
fireEvent.click(screen.getByRole('button', { name: 'Toggle' }));
|
|
66
|
+
expect(onToggle).toHaveBeenCalledWith(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'react-native';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import { ThemeProvider } from '../../../native/ThemeContext';
|
|
5
|
+
import { Collapsible } from './Collapsible.native';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
tags: ['!stable', 'alpha'],
|
|
9
|
+
title: 'React Native/UI/Collapsible',
|
|
10
|
+
component: Collapsible,
|
|
11
|
+
decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
|
|
12
|
+
parameters: { layout: 'padded' },
|
|
13
|
+
} satisfies Meta<typeof Collapsible>;
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
summary: 'Click to expand',
|
|
21
|
+
children: <Text>This is the collapsible content that is revealed when expanded.</Text>,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const DefaultOpen: Story = {
|
|
26
|
+
args: {
|
|
27
|
+
summary: 'Already open',
|
|
28
|
+
defaultOpen: true,
|
|
29
|
+
children: <Text>This content is visible by default.</Text>,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Multiple: Story = {
|
|
34
|
+
args: { summary: '', children: null },
|
|
35
|
+
render: () => (
|
|
36
|
+
<>
|
|
37
|
+
<Collapsible summary="Section 1">
|
|
38
|
+
<Text>Content for section 1.</Text>
|
|
39
|
+
</Collapsible>
|
|
40
|
+
<Collapsible summary="Section 2">
|
|
41
|
+
<Text>Content for section 2.</Text>
|
|
42
|
+
</Collapsible>
|
|
43
|
+
<Collapsible summary="Section 3" defaultOpen>
|
|
44
|
+
<Text>Content for section 3 (open by default).</Text>
|
|
45
|
+
</Collapsible>
|
|
46
|
+
</>
|
|
47
|
+
),
|
|
48
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ReactNode, useCallback, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
StyleSheet,
|
|
4
|
+
View,
|
|
5
|
+
Text,
|
|
6
|
+
Pressable,
|
|
7
|
+
Animated,
|
|
8
|
+
LayoutAnimation,
|
|
9
|
+
Platform,
|
|
10
|
+
UIManager,
|
|
11
|
+
StyleProp,
|
|
12
|
+
ViewStyle,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import { spacing, font, border, lineHeight } from '../../../native/tokens';
|
|
15
|
+
import { useTheme } from '../../../native/ThemeContext';
|
|
16
|
+
|
|
17
|
+
if (
|
|
18
|
+
Platform.OS === 'android' &&
|
|
19
|
+
UIManager.setLayoutAnimationEnabledExperimental
|
|
20
|
+
) {
|
|
21
|
+
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CollapsibleProps {
|
|
25
|
+
/** The trigger label -- always visible */
|
|
26
|
+
summary: string;
|
|
27
|
+
/** The content revealed when expanded */
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
/** Controlled open state */
|
|
30
|
+
open?: boolean;
|
|
31
|
+
/** Uncontrolled initial open state */
|
|
32
|
+
defaultOpen?: boolean;
|
|
33
|
+
/** Called after each toggle with the new open state */
|
|
34
|
+
onToggle?: (open: boolean) => void;
|
|
35
|
+
/** Grouping name (informational in native) */
|
|
36
|
+
name?: string;
|
|
37
|
+
style?: StyleProp<ViewStyle>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const styles = StyleSheet.create({
|
|
41
|
+
container: {
|
|
42
|
+
borderRadius: border.radius.m,
|
|
43
|
+
overflow: 'hidden',
|
|
44
|
+
},
|
|
45
|
+
header: {
|
|
46
|
+
flexDirection: 'row',
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
justifyContent: 'space-between',
|
|
49
|
+
minHeight: 44,
|
|
50
|
+
paddingVertical: spacing.s,
|
|
51
|
+
paddingHorizontal: spacing.m,
|
|
52
|
+
},
|
|
53
|
+
summaryText: {
|
|
54
|
+
flex: 1,
|
|
55
|
+
fontSize: font.size.body,
|
|
56
|
+
fontFamily: font.family.body,
|
|
57
|
+
fontWeight: font.weight.semibold,
|
|
58
|
+
lineHeight: lineHeight.body,
|
|
59
|
+
},
|
|
60
|
+
chevron: {
|
|
61
|
+
fontSize: font.size.body,
|
|
62
|
+
marginStart: spacing.xs,
|
|
63
|
+
},
|
|
64
|
+
content: {
|
|
65
|
+
paddingHorizontal: spacing.m,
|
|
66
|
+
paddingBottom: spacing.m,
|
|
67
|
+
},
|
|
68
|
+
pressed: {
|
|
69
|
+
opacity: 0.75,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export function Collapsible({
|
|
74
|
+
summary,
|
|
75
|
+
children,
|
|
76
|
+
open: controlledOpen,
|
|
77
|
+
defaultOpen = false,
|
|
78
|
+
onToggle,
|
|
79
|
+
name: _name, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
80
|
+
style,
|
|
81
|
+
}: Readonly<CollapsibleProps>) {
|
|
82
|
+
const { colors } = useTheme();
|
|
83
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
84
|
+
const isControlled = controlledOpen !== undefined;
|
|
85
|
+
const isOpen = isControlled ? controlledOpen : internalOpen;
|
|
86
|
+
|
|
87
|
+
const rotation = useRef(new Animated.Value(isOpen ? 1 : 0)).current;
|
|
88
|
+
|
|
89
|
+
const toggle = useCallback(() => {
|
|
90
|
+
const next = !isOpen;
|
|
91
|
+
|
|
92
|
+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
93
|
+
|
|
94
|
+
Animated.timing(rotation, {
|
|
95
|
+
toValue: next ? 1 : 0,
|
|
96
|
+
duration: 200,
|
|
97
|
+
useNativeDriver: true,
|
|
98
|
+
}).start();
|
|
99
|
+
|
|
100
|
+
if (!isControlled) {
|
|
101
|
+
setInternalOpen(next);
|
|
102
|
+
}
|
|
103
|
+
onToggle?.(next);
|
|
104
|
+
}, [isOpen, isControlled, onToggle, rotation]);
|
|
105
|
+
|
|
106
|
+
const rotateZ = rotation.interpolate({
|
|
107
|
+
inputRange: [0, 1],
|
|
108
|
+
outputRange: ['0deg', '90deg'],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<View
|
|
113
|
+
style={[styles.container, { borderBottomWidth: 1, borderBottomColor: colors.colorBgSubtle }, style]}
|
|
114
|
+
accessibilityRole="summary"
|
|
115
|
+
>
|
|
116
|
+
<Pressable
|
|
117
|
+
onPress={toggle}
|
|
118
|
+
accessibilityRole="button"
|
|
119
|
+
accessibilityLabel={summary}
|
|
120
|
+
accessibilityState={{ expanded: isOpen }}
|
|
121
|
+
style={({ pressed }) => [styles.header, pressed && styles.pressed]}
|
|
122
|
+
>
|
|
123
|
+
<Text style={[styles.summaryText, { color: colors.colorOnBg }]}>{summary}</Text>
|
|
124
|
+
<Animated.Text
|
|
125
|
+
style={[styles.chevron, { color: colors.colorOnBgSubtle, transform: [{ rotateZ }] }]}
|
|
126
|
+
importantForAccessibility="no"
|
|
127
|
+
accessibilityElementsHidden={true}
|
|
128
|
+
>
|
|
129
|
+
▸
|
|
130
|
+
</Animated.Text>
|
|
131
|
+
</Pressable>
|
|
132
|
+
{isOpen && (
|
|
133
|
+
<View style={styles.content}>
|
|
134
|
+
{children}
|
|
135
|
+
</View>
|
|
136
|
+
)}
|
|
137
|
+
</View>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
2
|
+
import * as Stories from './DescriptionList.native.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={Stories} />
|
|
5
|
+
|
|
6
|
+
# DescriptionList (React Native)
|
|
7
|
+
|
|
8
|
+
React Native description list for displaying key-value pairs. Each item has a bold term and a description.
|
|
9
|
+
|
|
10
|
+
> **Status: alpha** — API may change before stable release.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { DescriptionList } from '@boostdev/components';
|
|
16
|
+
|
|
17
|
+
<DescriptionList
|
|
18
|
+
items={[
|
|
19
|
+
{ term: 'Name', description: 'Jane Doe' },
|
|
20
|
+
{ term: 'Email', description: 'jane@example.com' },
|
|
21
|
+
]}
|
|
22
|
+
/>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Examples
|
|
26
|
+
|
|
27
|
+
### Default
|
|
28
|
+
<Canvas of={Stories.Default} />
|
|
29
|
+
|
|
30
|
+
### Two items
|
|
31
|
+
<Canvas of={Stories.TwoItems} />
|
|
32
|
+
|
|
33
|
+
### Single item
|
|
34
|
+
<Canvas of={Stories.SingleItem} />
|
|
35
|
+
|
|
36
|
+
## Props
|
|
37
|
+
|
|
38
|
+
<ArgTypes of={Stories} />
|
|
39
|
+
|
|
40
|
+
## Props reference
|
|
41
|
+
|
|
42
|
+
| Prop | Type | Default | Description |
|
|
43
|
+
|------|------|---------|-------------|
|
|
44
|
+
| `items` | `Array<{ term: string, description: ReactNode }>` | — | List of term-description pairs |
|
|
45
|
+
| `style` | `StyleProp<ViewStyle>` | — | Override container styles |
|
|
46
|
+
|
|
47
|
+
## Accessibility
|
|
48
|
+
|
|
49
|
+
- Container uses `accessibilityRole="list"`, each group uses `accessibilityRole="listitem"`
|
|
50
|
+
- Terms render as bold text for visual distinction
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { ThemeProvider } from '../../../native/ThemeContext';
|
|
3
|
+
import { DescriptionList } from './DescriptionList.native';
|
|
4
|
+
|
|
5
|
+
function renderWithTheme(ui: React.ReactElement) {
|
|
6
|
+
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('DescriptionList (native)', () => {
|
|
10
|
+
const items = [
|
|
11
|
+
{ term: 'Name', description: 'Jane Doe' },
|
|
12
|
+
{ term: 'Email', description: 'jane@example.com' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
it('renders all term texts', () => {
|
|
16
|
+
renderWithTheme(<DescriptionList items={items} />);
|
|
17
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
18
|
+
expect(screen.getByText('Email')).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders all description texts', () => {
|
|
22
|
+
renderWithTheme(<DescriptionList items={items} />);
|
|
23
|
+
expect(screen.getByText('Jane Doe')).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByText('jane@example.com')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('has list role on the container', () => {
|
|
28
|
+
renderWithTheme(<DescriptionList items={items} />);
|
|
29
|
+
expect(screen.getByRole('list')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders a single item', () => {
|
|
33
|
+
renderWithTheme(<DescriptionList items={[{ term: 'Version', description: '1.0' }]} />);
|
|
34
|
+
expect(screen.getByText('Version')).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText('1.0')).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { ThemeProvider } from '../../../native/ThemeContext';
|
|
4
|
+
import { DescriptionList } from './DescriptionList.native';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
tags: ['!stable', 'alpha'],
|
|
8
|
+
title: 'React Native/UI/DescriptionList',
|
|
9
|
+
component: DescriptionList,
|
|
10
|
+
decorators: [
|
|
11
|
+
(Story) => (
|
|
12
|
+
<ThemeProvider colorScheme="light">
|
|
13
|
+
<div style={{ width: 300 }}>
|
|
14
|
+
<Story />
|
|
15
|
+
</div>
|
|
16
|
+
</ThemeProvider>
|
|
17
|
+
),
|
|
18
|
+
],
|
|
19
|
+
parameters: { layout: 'padded' },
|
|
20
|
+
} satisfies Meta<typeof DescriptionList>;
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof meta>;
|
|
24
|
+
|
|
25
|
+
export const Default: Story = {
|
|
26
|
+
args: {
|
|
27
|
+
items: [
|
|
28
|
+
{ term: 'Name', description: 'Jane Doe' },
|
|
29
|
+
{ term: 'Email', description: 'jane@example.com' },
|
|
30
|
+
{ term: 'Role', description: 'Administrator' },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const TwoItems: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
items: [
|
|
38
|
+
{ term: 'Status', description: 'Active' },
|
|
39
|
+
{ term: 'Plan', description: 'Enterprise' },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const SingleItem: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
items: [{ term: 'Version', description: '1.0.0' }],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { AccessibilityRole, StyleSheet, View, Text, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { spacing, font, lineHeight } from '../../../native/tokens';
|
|
4
|
+
import { useTheme } from '../../../native/ThemeContext';
|
|
5
|
+
|
|
6
|
+
export interface DescriptionItem {
|
|
7
|
+
term: string;
|
|
8
|
+
description: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface DescriptionListProps {
|
|
12
|
+
items: DescriptionItem[];
|
|
13
|
+
style?: StyleProp<ViewStyle>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const styles = StyleSheet.create({
|
|
17
|
+
list: {
|
|
18
|
+
gap: spacing.m,
|
|
19
|
+
},
|
|
20
|
+
group: {
|
|
21
|
+
gap: spacing.xxs,
|
|
22
|
+
},
|
|
23
|
+
term: {
|
|
24
|
+
fontSize: font.size.bodyS,
|
|
25
|
+
fontFamily: font.family.body,
|
|
26
|
+
fontWeight: font.weight.semibold,
|
|
27
|
+
lineHeight: lineHeight.bodyS,
|
|
28
|
+
},
|
|
29
|
+
description: {
|
|
30
|
+
fontSize: font.size.body,
|
|
31
|
+
fontFamily: font.family.body,
|
|
32
|
+
fontWeight: font.weight.body,
|
|
33
|
+
lineHeight: lineHeight.body,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export function DescriptionList({ items, style }: Readonly<DescriptionListProps>) {
|
|
38
|
+
const { colors } = useTheme();
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View style={[styles.list, style]} accessibilityRole="list">
|
|
42
|
+
{items.map((item, i) => (
|
|
43
|
+
<View key={i} style={styles.group} accessibilityRole={'listitem' as AccessibilityRole}>
|
|
44
|
+
<Text style={[styles.term, { color: colors.colorOnBgSubtle }]}>{item.term}</Text>
|
|
45
|
+
{typeof item.description === 'string' ? (
|
|
46
|
+
<Text style={[styles.description, { color: colors.colorOnBg }]}>{item.description}</Text>
|
|
47
|
+
) : (
|
|
48
|
+
<View>{item.description}</View>
|
|
49
|
+
)}
|
|
50
|
+
</View>
|
|
51
|
+
))}
|
|
52
|
+
</View>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
2
|
+
import * as Stories from './Link.native.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={Stories} />
|
|
5
|
+
|
|
6
|
+
# Link (React Native)
|
|
7
|
+
|
|
8
|
+
React Native link component using `Pressable` + `Text`. Opens URLs via `Linking.openURL` or calls a custom `onPress` handler.
|
|
9
|
+
|
|
10
|
+
> **Status: alpha** — API may change before stable release.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { Link } from '@boostdev/components';
|
|
16
|
+
|
|
17
|
+
<Link href="https://example.com">Visit website</Link>
|
|
18
|
+
<Link onPress={() => navigation.navigate('Settings')}>Settings</Link>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Examples
|
|
22
|
+
|
|
23
|
+
### Default
|
|
24
|
+
<Canvas of={Stories.Default} />
|
|
25
|
+
|
|
26
|
+
### With onPress callback
|
|
27
|
+
<Canvas of={Stories.WithOnPress} />
|
|
28
|
+
|
|
29
|
+
### With accessibility label
|
|
30
|
+
<Canvas of={Stories.WithAccessibilityLabel} />
|
|
31
|
+
|
|
32
|
+
### All variants
|
|
33
|
+
<Canvas of={Stories.AllVariants} />
|
|
34
|
+
|
|
35
|
+
## Props
|
|
36
|
+
|
|
37
|
+
<ArgTypes of={Stories} />
|
|
38
|
+
|
|
39
|
+
## Props reference
|
|
40
|
+
|
|
41
|
+
| Prop | Type | Default | Description |
|
|
42
|
+
|------|------|---------|-------------|
|
|
43
|
+
| `href` | `string` | — | URL to open via `Linking.openURL` |
|
|
44
|
+
| `children` | `ReactNode` | — | Link label |
|
|
45
|
+
| `onPress` | `() => void` | — | Custom press handler (used when `href` is not provided) |
|
|
46
|
+
| `accessibilityLabel` | `string` | — | Screen reader label |
|
|
47
|
+
| `style` | `StyleProp<TextStyle>` | — | Override text styles |
|
|
48
|
+
|
|
49
|
+
## Accessibility
|
|
50
|
+
|
|
51
|
+
- Uses `accessibilityRole="link"` for assistive technologies
|
|
52
|
+
- Provide `accessibilityLabel` when the visible text is not self-descriptive
|
|
53
|
+
- Pressed state reduces opacity for visual feedback
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { ThemeProvider } from '../../../native/ThemeContext';
|
|
3
|
+
import { Link } from './Link.native';
|
|
4
|
+
|
|
5
|
+
function renderWithTheme(ui: React.ReactElement) {
|
|
6
|
+
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('Link (native)', () => {
|
|
10
|
+
it('renders children text', () => {
|
|
11
|
+
renderWithTheme(<Link href="https://example.com">Visit</Link>);
|
|
12
|
+
expect(screen.getByText('Visit')).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('has link role', () => {
|
|
16
|
+
renderWithTheme(<Link href="https://example.com">Visit</Link>);
|
|
17
|
+
expect(screen.getByRole('link')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('calls onPress when provided without href', () => {
|
|
21
|
+
const onPress = vi.fn();
|
|
22
|
+
renderWithTheme(<Link onPress={onPress}>Navigate</Link>);
|
|
23
|
+
fireEvent.click(screen.getByRole('link'));
|
|
24
|
+
expect(onPress).toHaveBeenCalledTimes(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('uses accessibilityLabel when provided', () => {
|
|
28
|
+
renderWithTheme(
|
|
29
|
+
<Link href="https://example.com" accessibilityLabel="Read more about products">
|
|
30
|
+
Read more
|
|
31
|
+
</Link>,
|
|
32
|
+
);
|
|
33
|
+
expect(screen.getByLabelText('Read more about products')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { ThemeProvider } from '../../../native/ThemeContext';
|
|
4
|
+
import { Link } from './Link.native';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
tags: ['!stable', 'alpha'],
|
|
8
|
+
title: 'React Native/UI/Link',
|
|
9
|
+
component: Link,
|
|
10
|
+
decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
|
|
11
|
+
parameters: { layout: 'centered' },
|
|
12
|
+
argTypes: {
|
|
13
|
+
onPress: { action: 'pressed' },
|
|
14
|
+
},
|
|
15
|
+
} satisfies Meta<typeof Link>;
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
args: { children: 'Visit website', href: 'https://example.com' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const WithOnPress: Story = {
|
|
25
|
+
args: { children: 'Navigate', onPress: () => {} },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const WithAccessibilityLabel: Story = {
|
|
29
|
+
args: {
|
|
30
|
+
children: 'Read more',
|
|
31
|
+
href: 'https://example.com',
|
|
32
|
+
accessibilityLabel: 'Read more about our products',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const AllVariants: Story = {
|
|
37
|
+
render: () => (
|
|
38
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
39
|
+
<Link href="https://example.com">Default link</Link>
|
|
40
|
+
<Link onPress={() => {}}>Callback link</Link>
|
|
41
|
+
<Link href="https://example.com" accessibilityLabel="Accessible link">
|
|
42
|
+
With accessibility label
|
|
43
|
+
</Link>
|
|
44
|
+
</div>
|
|
45
|
+
),
|
|
46
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { StyleSheet, Text, Pressable, Linking, StyleProp, TextStyle } from 'react-native';
|
|
3
|
+
import { font } from '../../../native/tokens';
|
|
4
|
+
import { useTheme } from '../../../native/ThemeContext';
|
|
5
|
+
|
|
6
|
+
interface LinkProps {
|
|
7
|
+
href?: string;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
accessibilityLabel?: string;
|
|
10
|
+
style?: StyleProp<TextStyle>;
|
|
11
|
+
onPress?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const styles = StyleSheet.create({
|
|
15
|
+
text: {
|
|
16
|
+
fontSize: font.size.body,
|
|
17
|
+
fontFamily: font.family.body,
|
|
18
|
+
textDecorationLine: 'underline',
|
|
19
|
+
},
|
|
20
|
+
pressed: {
|
|
21
|
+
opacity: 0.75,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export function Link({ href, children, accessibilityLabel, style, onPress }: Readonly<LinkProps>) {
|
|
26
|
+
const { colors } = useTheme();
|
|
27
|
+
|
|
28
|
+
const handlePress = () => {
|
|
29
|
+
if (onPress) {
|
|
30
|
+
onPress();
|
|
31
|
+
} else if (href) {
|
|
32
|
+
Linking.openURL(href);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Pressable
|
|
38
|
+
onPress={handlePress}
|
|
39
|
+
accessibilityRole="link"
|
|
40
|
+
accessibilityLabel={accessibilityLabel}
|
|
41
|
+
style={({ pressed }) => pressed && styles.pressed}
|
|
42
|
+
>
|
|
43
|
+
<Text style={[styles.text, { color: colors.colorInteractiveOnBg }, style]}>
|
|
44
|
+
{children}
|
|
45
|
+
</Text>
|
|
46
|
+
</Pressable>
|
|
47
|
+
);
|
|
48
|
+
}
|