@dhasdk/simple-ui 1.0.7 → 1.0.8
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/.babelrc +12 -0
- package/.storybook/main.ts +35 -0
- package/.storybook/preview.ts +4 -0
- package/BAKpostcss.config.jsBAK +15 -0
- package/BAKtailwind.config.mjsBAK +99 -0
- package/README.md +464 -16
- package/coverage/storybook/coverage-storybook.json +32411 -0
- package/coverage/storybook/lcov-report/Accordion.tsx.html +805 -0
- package/coverage/storybook/lcov-report/Badge.tsx.html +346 -0
- package/coverage/storybook/lcov-report/Breadcrumbs.tsx.html +742 -0
- package/coverage/storybook/lcov-report/Button.tsx.html +448 -0
- package/coverage/storybook/lcov-report/ButtonGroup.tsx.html +403 -0
- package/coverage/storybook/lcov-report/Card.tsx.html +292 -0
- package/coverage/storybook/lcov-report/CharacterCounter.tsx.html +253 -0
- package/coverage/storybook/lcov-report/CheckBox.tsx.html +1555 -0
- package/coverage/storybook/lcov-report/DatePicker.tsx.html +826 -0
- package/coverage/storybook/lcov-report/Input.tsx.html +1012 -0
- package/coverage/storybook/lcov-report/List.tsx.html +364 -0
- package/coverage/storybook/lcov-report/Modal.tsx.html +745 -0
- package/coverage/storybook/lcov-report/Pill.tsx.html +358 -0
- package/coverage/storybook/lcov-report/Search.tsx.html +997 -0
- package/coverage/storybook/lcov-report/SearchContent.tsx.html +235 -0
- package/coverage/storybook/lcov-report/SectionHeader.tsx.html +358 -0
- package/coverage/storybook/lcov-report/Select.tsx.html +1012 -0
- package/coverage/storybook/lcov-report/Shield.tsx.html +802 -0
- package/coverage/storybook/lcov-report/SideBarNav.tsx.html +490 -0
- package/coverage/storybook/lcov-report/Skeleton.tsx.html +394 -0
- package/coverage/storybook/lcov-report/Slider.tsx.html +385 -0
- package/coverage/storybook/lcov-report/Status.tsx.html +322 -0
- package/coverage/storybook/lcov-report/Tabs.tsx.html +610 -0
- package/coverage/storybook/lcov-report/Toggle.tsx.html +373 -0
- package/coverage/storybook/lcov-report/Tooltip.tsx.html +496 -0
- package/coverage/storybook/lcov-report/base.css +224 -0
- package/coverage/storybook/lcov-report/block-navigation.js +87 -0
- package/coverage/storybook/lcov-report/favicon.png +0 -0
- package/coverage/storybook/lcov-report/index.html +476 -0
- package/coverage/storybook/lcov-report/prettify.css +1 -0
- package/coverage/storybook/lcov-report/prettify.js +2 -0
- package/coverage/storybook/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/storybook/lcov-report/sorter.js +196 -0
- package/coverage/storybook/lcov.info +2312 -0
- package/dist/README.md +1815 -0
- package/eslint.config.mjs +13 -0
- package/package.json +6 -7
- package/project.json +11 -0
- package/src/assets/img/Frame.svg +5 -0
- package/src/assets/img/backArrowRight.svg +10 -0
- package/src/assets/img/bc-separator.png +0 -0
- package/src/assets/img/calendar.png +0 -0
- package/src/assets/img/calendar.svg +4 -0
- package/src/assets/img/check.svg +5 -0
- package/src/assets/img/check_box.svg +10 -0
- package/src/assets/img/check_box_empty.svg +10 -0
- package/src/assets/img/check_box_fill.svg +10 -0
- package/src/assets/img/check_box_fill_empty.svg +10 -0
- package/src/assets/img/chevron-down-white.svg +2 -0
- package/src/assets/img/chevron-down.svg +2 -0
- package/src/assets/img/chevron-left.svg +1 -0
- package/src/assets/img/chevron-right-light.svg +4 -0
- package/src/assets/img/chevron-right.svg +3 -0
- package/src/assets/img/chevron-up-white.svg +1 -0
- package/src/assets/img/chevron-up.svg +1 -0
- package/src/assets/img/clock.svg +6 -0
- package/src/assets/img/close.svg +1 -0
- package/src/assets/img/close2.svg +6 -0
- package/src/assets/img/closeModal.svg +10 -0
- package/src/assets/img/close_icon_dark.svg +10 -0
- package/src/assets/img/close_small.svg +3 -0
- package/src/assets/img/emergency_home.svg +10 -0
- package/src/assets/img/first-aid-kit.svg +7 -0
- package/src/assets/img/heartbeat.svg +4 -0
- package/src/assets/img/home-gray.svg +3 -0
- package/src/assets/img/home.svg +3 -0
- package/src/assets/img/hospital.jpg +0 -0
- package/src/assets/img/indeterminate_check_box.svg +10 -0
- package/src/assets/img/indeterminate_check_box_fill.svg +10 -0
- package/src/assets/img/info_24_ 1d4ed8.svg +3 -0
- package/src/assets/img/info_24_ 2c6441.svg +3 -0
- package/src/assets/img/marker_check_by_default.svg +10 -0
- package/src/assets/img/marker_check_by_default_fill.svg +10 -0
- package/src/assets/img/minus-accordion.svg +5 -0
- package/src/assets/img/minus.svg +3 -0
- package/src/assets/img/open.svg +1 -0
- package/src/assets/img/pill-white.svg +7 -0
- package/src/assets/img/pill.svg +5 -0
- package/src/assets/img/plus-accordion.svg +5 -0
- package/src/assets/img/plus.svg +4 -0
- package/src/assets/img/prescription.svg +6 -0
- package/src/assets/img/search.svg +10 -0
- package/src/assets/img/search_icon_light.svg +10 -0
- package/src/assets/img/separator.svg +3 -0
- package/src/assets/img/stethoscope-white.svg +8 -0
- package/src/assets/img/stethoscope.svg +8 -0
- package/src/assets/img/thumb_up.svg +10 -0
- package/src/assets/img/vector.svg +3 -0
- package/src/assets/img/warning-badge-disabled.svg +11 -0
- package/src/assets/img/warning-badge-green.svg +11 -0
- package/src/assets/img/warning-badge-red.svg +11 -0
- package/src/assets/img/warning-badge-yellow.svg +11 -0
- package/src/assets/img/warning.svg +10 -0
- package/src/global.d.ts +13 -0
- package/{index.d.ts → src/index.ts} +13 -5
- package/src/lib/Accordian--Accordian.stories.tsx +312 -0
- package/src/lib/Accordion.spec.tsx +384 -0
- package/src/lib/Accordion.tsx +240 -0
- package/src/lib/AppointmentPicker.spec.tsx +138 -0
- package/src/lib/AppointmentPicker.tsx +97 -0
- package/src/lib/Badge--Badge.stories.tsx +60 -0
- package/src/lib/Badge.spec.tsx +70 -0
- package/src/lib/Badge.tsx +87 -0
- package/src/lib/Breadcrumbs-Breadcrumbs.stories.tsx +114 -0
- package/src/lib/Breadcrumbs.spec.tsx +218 -0
- package/src/lib/Breadcrumbs.tsx +219 -0
- package/src/lib/Button--Button.stories.tsx +220 -0
- package/src/lib/Button.spec.tsx +241 -0
- package/src/lib/Button.tsx +121 -0
- package/src/lib/ButtonGroup--ButtonGroup.stories.tsx +129 -0
- package/src/lib/ButtonGroup.spec.tsx +89 -0
- package/src/lib/ButtonGroup.tsx +107 -0
- package/src/lib/Card--Card.stories.tsx +113 -0
- package/src/lib/Card.spec.tsx +112 -0
- package/src/lib/Card.tsx +69 -0
- package/src/lib/CharacterCounter--CharacterCounter.stories.tsx +169 -0
- package/src/lib/CharacterCounter.spec.tsx +123 -0
- package/src/lib/CharacterCounter.tsx +56 -0
- package/src/lib/CheckBox--CheckBox.stories.tsx +107 -0
- package/src/lib/CheckBox.spec.tsx +412 -0
- package/src/lib/CheckBox.tsx +491 -0
- package/src/lib/DatePicker--DatePicker.stories.tsx +228 -0
- package/src/lib/DatePicker.spec.tsx +424 -0
- package/src/lib/DatePicker.tsx +247 -0
- package/src/lib/Input--Input.stories.tsx +449 -0
- package/src/lib/Input.spec.tsx +281 -0
- package/src/lib/Input.tsx +309 -0
- package/src/lib/List--List.stories.tsx +157 -0
- package/src/lib/List.spec.tsx +211 -0
- package/src/lib/List.tsx +93 -0
- package/src/lib/Modal--Modal.stories.tsx +454 -0
- package/src/lib/Modal.spec.tsx +202 -0
- package/src/lib/Modal.tsx +220 -0
- package/src/lib/Pill--Pill.stories.tsx +98 -0
- package/src/lib/Pill.spec.tsx +103 -0
- package/src/lib/Pill.tsx +91 -0
- package/src/lib/ProgressBar.spec.tsx +106 -0
- package/src/lib/ProgressBar.tsx +112 -0
- package/src/lib/RadioGroup.spec.tsx +84 -0
- package/src/lib/RadioGroup.tsx +74 -0
- package/src/lib/RadioIcon.tsx +13 -0
- package/src/lib/Search--Search.stories.tsx +67 -0
- package/src/lib/Search.spec.tsx +182 -0
- package/src/lib/Search.tsx +304 -0
- package/src/lib/SearchContent.tsx +51 -0
- package/src/lib/SectionHeader--SectionHeader.stories.tsx +98 -0
- package/src/lib/SectionHeader.spec.tsx +60 -0
- package/src/lib/SectionHeader.tsx +91 -0
- package/src/lib/Select--Select.stories.tsx +387 -0
- package/src/lib/Select.spec.tsx +493 -0
- package/src/lib/Select.tsx +311 -0
- package/src/lib/Shield--Shield.stories.tsx +196 -0
- package/src/lib/Shield.spec.tsx +275 -0
- package/src/lib/Shield.tsx +239 -0
- package/src/lib/SideBarNav--SideBarNav.stories.tsx +136 -0
- package/src/lib/SideBarNav.spec.tsx +178 -0
- package/src/lib/SideBarNav.tsx +135 -0
- package/src/lib/Skeleton--Skeleton.stories.tsx +77 -0
- package/src/lib/Skeleton.module.css +16 -0
- package/src/lib/Skeleton.spec.tsx +83 -0
- package/src/lib/Skeleton.tsx +103 -0
- package/src/lib/SkipLink.spec.tsx +76 -0
- package/src/lib/SkipLink.tsx +48 -0
- package/src/lib/Slider--Slider.stories.tsx +108 -0
- package/src/lib/Slider.module.css +109 -0
- package/src/lib/Slider.spec.tsx +67 -0
- package/src/lib/Slider.tsx +101 -0
- package/src/lib/Status--Status.stories.tsx +93 -0
- package/src/lib/Status.spec.tsx +118 -0
- package/src/lib/Status.tsx +79 -0
- package/src/lib/Tabs--Tabs.stories.tsx +294 -0
- package/src/lib/Tabs.spec.tsx +249 -0
- package/src/lib/Tabs.tsx +188 -0
- package/src/lib/Tester.spec.tsx +17 -0
- package/src/lib/Toggle--Toggle.stories.tsx +162 -0
- package/src/lib/Toggle.spec.tsx +122 -0
- package/src/lib/Toggle.tsx +96 -0
- package/src/lib/Tooltip--Tooltip.stories.tsx +315 -0
- package/src/lib/Tooltip.spec.tsx +307 -0
- package/src/lib/Tooltip.tsx +137 -0
- package/src/lib/bak-simple-ui.stories.tsx-bak +24 -0
- package/src/styles.css +190 -0
- package/tsconfig.json +25 -0
- package/tsconfig.lib.json +42 -0
- package/tsconfig.spec.json +29 -0
- package/tsconfig.storybook.json +36 -0
- package/vite.config.mts +87 -0
- package/vitest.setup.ts +12 -0
- package/index.css +0 -1
- package/index.js +0 -35
- package/index.mjs +0 -4981
- package/lib/Accordion.d.ts +0 -36
- package/lib/AppointmentPicker.d.ts +0 -21
- package/lib/Badge.d.ts +0 -11
- package/lib/Breadcrumbs.d.ts +0 -13
- package/lib/Button.d.ts +0 -15
- package/lib/ButtonGroup.d.ts +0 -8
- package/lib/Card.d.ts +0 -11
- package/lib/CharacterCounter.d.ts +0 -11
- package/lib/CheckBox.d.ts +0 -30
- package/lib/DatePicker.d.ts +0 -7
- package/lib/Input.d.ts +0 -16
- package/lib/List.d.ts +0 -22
- package/lib/Modal.d.ts +0 -18
- package/lib/Pill.d.ts +0 -13
- package/lib/ProgressBar.d.ts +0 -19
- package/lib/RadioGroup.d.ts +0 -15
- package/lib/Search.d.ts +0 -26
- package/lib/SearchContent.d.ts +0 -6
- package/lib/SectionHeader.d.ts +0 -18
- package/lib/Select.d.ts +0 -19
- package/lib/Shield.d.ts +0 -12
- package/lib/SideBarNav.d.ts +0 -21
- package/lib/Skeleton.d.ts +0 -15
- package/lib/SkipLink.d.ts +0 -22
- package/lib/Slider.d.ts +0 -14
- package/lib/Status.d.ts +0 -10
- package/lib/Tabs.d.ts +0 -23
- package/lib/Toggle.d.ts +0 -11
- package/lib/Tooltip.d.ts +0 -14
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Meta } from '@storybook/react';
|
|
2
|
+
import { Card, CardProps } from './Card';
|
|
3
|
+
|
|
4
|
+
// import imagePath from 'src/assets/img/pill.svg';
|
|
5
|
+
const imagePath = 'src/assets/img/hospital.jpg';
|
|
6
|
+
// imagePath: 'src/assets/img/heartbeat.svg',
|
|
7
|
+
|
|
8
|
+
// Meta object - defines basic storybook options for this story
|
|
9
|
+
export default {
|
|
10
|
+
title: 'Components/Card',
|
|
11
|
+
component: Card,
|
|
12
|
+
argTypes: {
|
|
13
|
+
variant: {
|
|
14
|
+
control: 'select',
|
|
15
|
+
options: ['','']
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
args: {
|
|
19
|
+
disabled: false, // set default argument values
|
|
20
|
+
imagePath: imagePath,
|
|
21
|
+
alt: "default image alt",
|
|
22
|
+
children:
|
|
23
|
+
<>
|
|
24
|
+
<h1 className="font-bold mb-2 text-lg" aria-label="Heading">Heading</h1>
|
|
25
|
+
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit.
|
|
26
|
+
Facilis earum tenetur quo cupiditate, eaque qui officia recusandae.</p>
|
|
27
|
+
</>
|
|
28
|
+
// label: 'Button', // set default argument values
|
|
29
|
+
},
|
|
30
|
+
parameters: {
|
|
31
|
+
layout: 'centered', // options are 'centered', 'fullscreen', and 'padded' (default value)
|
|
32
|
+
backgrounds: { default: 'dark' }, // options are light, medium, or dark
|
|
33
|
+
},
|
|
34
|
+
} as Meta<CardProps>;
|
|
35
|
+
|
|
36
|
+
// Define "Default" story
|
|
37
|
+
export const Default = {
|
|
38
|
+
args: {
|
|
39
|
+
ariaLabel: 'test label',
|
|
40
|
+
alt: 'image alt'
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const ImageInsetDefault = {
|
|
45
|
+
args: {
|
|
46
|
+
imageInset: true,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const ImageBotton = {
|
|
51
|
+
args: {
|
|
52
|
+
variant: 'imageBottom',
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const Outline = {
|
|
57
|
+
args: {
|
|
58
|
+
variantStyle: 'outline',
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
export const ImageLeft = {
|
|
64
|
+
args: {
|
|
65
|
+
variant: 'imageLeft',
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const ImageLeftInsetDefault = {
|
|
70
|
+
args: {
|
|
71
|
+
variant: 'imageLeft',
|
|
72
|
+
imageInset: true,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const ImageLeftOutline = {
|
|
77
|
+
args: {
|
|
78
|
+
variant: 'imageLeft',
|
|
79
|
+
variantStyle: 'outline',
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const ImageLeftInsetDefaultOutline = {
|
|
84
|
+
args: {
|
|
85
|
+
variant: 'imageLeft',
|
|
86
|
+
imageInset: true,
|
|
87
|
+
variantStyle: 'outline',
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const ImageRight = {
|
|
92
|
+
args: {
|
|
93
|
+
variant: 'imageRight',
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Define "DHA Med Card" story that extends "Default"
|
|
98
|
+
// export const DHAMedCard = {
|
|
99
|
+
// args: {
|
|
100
|
+
// children: 'DHA Med Card',
|
|
101
|
+
// variant: 'MedCard',
|
|
102
|
+
// }
|
|
103
|
+
// };
|
|
104
|
+
|
|
105
|
+
// Define "DHA Disabled" story
|
|
106
|
+
// export const DHADisabled = {
|
|
107
|
+
// args: {
|
|
108
|
+
// // children: 'Disabled Button',
|
|
109
|
+
// disabled: true,
|
|
110
|
+
// label: 'Disabled Button',
|
|
111
|
+
// }
|
|
112
|
+
// };
|
|
113
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { axe } from "vitest-axe";
|
|
3
|
+
import { describe, it, expect, vi } from "vitest"; // required for ".toHaveNoViolations"
|
|
4
|
+
import { Card, CardProps } from './Card';
|
|
5
|
+
|
|
6
|
+
describe('Card default variant', () => {
|
|
7
|
+
it('default variant with image should render successfully', () => {
|
|
8
|
+
render(<Card variant='default' imagePath='path/to/some/image.png' alt='test alt' ariaLabel='test card' />);
|
|
9
|
+
const parentDiv = screen.getByLabelText('test card');
|
|
10
|
+
expect(parentDiv).toBeTruthy();
|
|
11
|
+
expect(parentDiv).toHaveClass('flex-col');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('imageBottom variant with image should render successfully', () => {
|
|
15
|
+
render(<Card variant='imageBottom' imagePath='path/to/some/image.png' ariaLabel='test card' alt='test alt' />);
|
|
16
|
+
const parentDiv = screen.getByLabelText('test card');
|
|
17
|
+
expect(parentDiv).toBeTruthy();
|
|
18
|
+
expect(parentDiv).toHaveClass('flex-col-reverse');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('default variant + imageInset true should render successfully', () => {
|
|
22
|
+
render(<Card variant='default' alt='test alt' imageInset={true} ariaLabel='test card' />);
|
|
23
|
+
const parentDiv = screen.getByLabelText('test card');
|
|
24
|
+
expect(parentDiv).toBeTruthy();
|
|
25
|
+
expect(parentDiv).toHaveClass('flex-col');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('imageLeft variant should render successfully', () => {
|
|
29
|
+
render(<Card variant='imageLeft' alt='test alt' ariaLabel='test card'/>);
|
|
30
|
+
const parentDiv = screen.getByLabelText('test card');
|
|
31
|
+
expect(parentDiv).toBeTruthy();
|
|
32
|
+
expect(parentDiv).toHaveClass('flex-row');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('imageLeft variant + imageInset true should render successfully', () => {
|
|
36
|
+
render(<Card variant='imageLeft' alt='test alt' ariaLabel='test card' imageInset={true}/>);
|
|
37
|
+
const parentDiv = screen.getByLabelText('test card');
|
|
38
|
+
expect(parentDiv).toBeTruthy();
|
|
39
|
+
expect(parentDiv).toHaveClass('flex-row');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('imageRight variant + imageInset true should render successfully', () => {
|
|
43
|
+
render(<Card variant='imageRight' alt='test alt' ariaLabel='test card' imageInset={true}/>);
|
|
44
|
+
const parentDiv = screen.getByLabelText('test card');
|
|
45
|
+
expect(parentDiv).toBeTruthy();
|
|
46
|
+
expect(parentDiv).toHaveClass('flex-row-reverse');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
describe('Card Accessibility Tests', () => {
|
|
53
|
+
const renderComponent = (props: CardProps) =>
|
|
54
|
+
render(<Card {...props}>Card Content</Card>);
|
|
55
|
+
|
|
56
|
+
it('should have no accessibility violations with default variant', async () => {
|
|
57
|
+
const { container } = renderComponent({
|
|
58
|
+
imagePath: 'https://via.placeholder.com/150',
|
|
59
|
+
alt: 'Placeholder Image',
|
|
60
|
+
});
|
|
61
|
+
const results = await axe(container);
|
|
62
|
+
expect(results).toHaveNoViolations();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should have no accessibility violations with image and alt text', async () => {
|
|
66
|
+
const { container } = renderComponent({
|
|
67
|
+
imagePath: 'https://via.placeholder.com/150',
|
|
68
|
+
alt: 'Placeholder Image',
|
|
69
|
+
});
|
|
70
|
+
const results = await axe(container);
|
|
71
|
+
expect(results).toHaveNoViolations();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should have no accessibility violations with outline variant style', async () => {
|
|
75
|
+
const { container } = renderComponent({ variantStyle: 'outline', alt: 'test alt' });
|
|
76
|
+
const results = await axe(container);
|
|
77
|
+
expect(results).toHaveNoViolations();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should include alt text for images when provided', () => {
|
|
81
|
+
const { getByAltText } = renderComponent({
|
|
82
|
+
imagePath: 'https://via.placeholder.com/150',
|
|
83
|
+
alt: 'Accessible Image',
|
|
84
|
+
});
|
|
85
|
+
expect(getByAltText('Accessible Image')).toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should have an accessible name via aria-label', () => {
|
|
89
|
+
const { getByLabelText } = renderComponent({ ariaLabel: 'Custom Card Label', alt: 'test alt' });
|
|
90
|
+
expect(getByLabelText('Custom Card Label')).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should render different layout variants without accessibility violations', async () => {
|
|
94
|
+
const variants = ['default', 'imageBottom', 'imageLeft', 'imageRight'];
|
|
95
|
+
|
|
96
|
+
for (const variant of variants) {
|
|
97
|
+
const { container } = renderComponent({ variant, alt: 'test alt' });
|
|
98
|
+
const results = await axe(container);
|
|
99
|
+
expect(results).toHaveNoViolations();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should support custom classes without accessibility issues', async () => {
|
|
104
|
+
const { container } = renderComponent({
|
|
105
|
+
className: 'custom-class bg-blue-500',
|
|
106
|
+
alt: 'test alt',
|
|
107
|
+
});
|
|
108
|
+
const results = await axe(container);
|
|
109
|
+
expect(results).toHaveNoViolations();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
});
|
package/src/lib/Card.tsx
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
interface VariantType {
|
|
5
|
+
[key: string]: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// variant styling definition, both size and layout (portrait vs landscape)
|
|
9
|
+
const variants: VariantType = {
|
|
10
|
+
default: 'flex flex-col w-64 max-w-64', // image top
|
|
11
|
+
imageBottom: 'flex flex-col-reverse w-64 mx-w-64',
|
|
12
|
+
imageLeft: 'flex flex-row h-auto max-w-lg min-w-96',
|
|
13
|
+
imageRight: 'flex flex-row-reverse h-auto max-w-lg min-w-96',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// default styles, i.e. outline, etc.
|
|
17
|
+
const variantStyles: VariantType = {
|
|
18
|
+
default: 'bg-white overflow-hidden rounded-lg',
|
|
19
|
+
outline: 'bg-white overflow-hidden rounded-lg shadow-md border border-slate-300',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const imageDivInsetStyleDefault = 'px-4 my-2';
|
|
23
|
+
const imageDivInsetStyleFlag = 'px-2 my-2';
|
|
24
|
+
|
|
25
|
+
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
26
|
+
variant?: string;
|
|
27
|
+
subVariant?: string;
|
|
28
|
+
imagePath?: string;
|
|
29
|
+
classNameImage?: string;
|
|
30
|
+
imageInset?: boolean;
|
|
31
|
+
alt: string;
|
|
32
|
+
ariaLabel?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|
36
|
+
({ className, variant = 'default', subVariant: variantStyle = 'default',
|
|
37
|
+
imagePath, classNameImage = 'w-full h-full', imageInset = false, alt,
|
|
38
|
+
ariaLabel='card component', children, ...props }, ref) => {
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className={
|
|
42
|
+
twMerge(
|
|
43
|
+
'flex ',
|
|
44
|
+
variantStyles[variantStyle],
|
|
45
|
+
variants[variant],
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
aria-label={ariaLabel}
|
|
49
|
+
ref={ref}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
|
|
53
|
+
{/* Image */}
|
|
54
|
+
<div
|
|
55
|
+
className={twMerge('flex-initial items-center justify-center w-full', imageInset &&
|
|
56
|
+
(variant === 'default' || variant === 'imageBottom' ? imageDivInsetStyleDefault : imageDivInsetStyleFlag ))}
|
|
57
|
+
>
|
|
58
|
+
{imagePath && <img src={imagePath} alt={alt} className={classNameImage} />}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{/* children / content */}
|
|
62
|
+
<div className="flex-auto px-2 py-3">
|
|
63
|
+
{children}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
Card.displayName = 'Card';
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Meta, StoryContext, StoryFn, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Input, InputProps } from './Input';
|
|
3
|
+
import { CharacterCounter, CharacterCounterProps } from './CharacterCounter';
|
|
4
|
+
import { userEvent, within } from 'storybook/test';
|
|
5
|
+
import { expect } from 'storybook/test';
|
|
6
|
+
import { FC, useRef, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
title: 'Components/CharacterCounter',
|
|
11
|
+
component: CharacterCounter,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'centered',
|
|
14
|
+
backgrounds: {
|
|
15
|
+
default: 'white',
|
|
16
|
+
values: [
|
|
17
|
+
{ name: 'white', value: '#ffffff' },
|
|
18
|
+
{ name: 'light', value: '#f0f0f0' },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
args: {
|
|
23
|
+
maxCharacters: 25
|
|
24
|
+
},
|
|
25
|
+
} as Meta<CharacterCounterProps>;
|
|
26
|
+
|
|
27
|
+
export const DefaultWithInput: StoryObj<typeof CharacterCounter> = {
|
|
28
|
+
args: {
|
|
29
|
+
maxCharacters: 10,
|
|
30
|
+
children: <Input variant="default" />,
|
|
31
|
+
},
|
|
32
|
+
play: async ({ canvasElement, args }) => {
|
|
33
|
+
const canvas = within(canvasElement);
|
|
34
|
+
// 1) The initial text might be "10 characters allowed"
|
|
35
|
+
expect(await canvas.findByText('10 characters allowed')).toBeInTheDocument();
|
|
36
|
+
|
|
37
|
+
// 2) Type "Hello"
|
|
38
|
+
const input = canvas.getByRole('textbox');
|
|
39
|
+
await userEvent.type(input, 'Hello');
|
|
40
|
+
// After typing 5 chars, we expect "5 remaining" (or your actual text).
|
|
41
|
+
expect(await canvas.findByText('5 remaining')).toBeInTheDocument();
|
|
42
|
+
|
|
43
|
+
// 3) Clear the input to test the zero-length scenario
|
|
44
|
+
await userEvent.clear(input);
|
|
45
|
+
// Now it should go back to "10 characters allowed"
|
|
46
|
+
expect(await canvas.findByText('10 characters allowed')).toBeInTheDocument();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const WithInputHandler: StoryFn = () => {
|
|
51
|
+
|
|
52
|
+
const [inputValue, setInputValue] = useState<string>('');
|
|
53
|
+
const onChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
54
|
+
setInputValue(event.target.value);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<div className="mb-4 font-bold">Input Value: {inputValue}</div>
|
|
60
|
+
<CharacterCounter maxCharacters={10}>
|
|
61
|
+
{/* <div className="ms-2">10 Characters Max</div> */}
|
|
62
|
+
<Input
|
|
63
|
+
variant="default"
|
|
64
|
+
onChange={onChangeHandler}
|
|
65
|
+
/>
|
|
66
|
+
</CharacterCounter>
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// export const WithTextAreaHandler: StoryFn = () => {
|
|
73
|
+
|
|
74
|
+
// const [inputValue, setInputValue] = useState<string>('');
|
|
75
|
+
// const onChangeHandler = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
76
|
+
// setInputValue(event.target.value);
|
|
77
|
+
// };
|
|
78
|
+
|
|
79
|
+
// return (
|
|
80
|
+
// <>
|
|
81
|
+
// <div className="mb-4 font-bold">Input Value:
|
|
82
|
+
// <span className="font-normal">{inputValue}</span>
|
|
83
|
+
// </div>
|
|
84
|
+
// <CharacterCounter maxCharacters={20}>
|
|
85
|
+
// <textarea
|
|
86
|
+
// className='border-2'
|
|
87
|
+
// cols={20}
|
|
88
|
+
// onChange={onChangeHandler}
|
|
89
|
+
// />
|
|
90
|
+
// </CharacterCounter>
|
|
91
|
+
// </>
|
|
92
|
+
// );
|
|
93
|
+
// };
|
|
94
|
+
|
|
95
|
+
export const OverLimitTest: StoryObj<typeof CharacterCounter> = {
|
|
96
|
+
args: {
|
|
97
|
+
maxCharacters: 5,
|
|
98
|
+
children: <Input variant="default" />,
|
|
99
|
+
},
|
|
100
|
+
play: async ({ canvasElement }) => {
|
|
101
|
+
const canvas = within(canvasElement);
|
|
102
|
+
|
|
103
|
+
// Type 6 characters, e.g., "abcdef"
|
|
104
|
+
const input = canvas.getByRole('textbox');
|
|
105
|
+
await userEvent.type(input, 'abcdef');
|
|
106
|
+
|
|
107
|
+
// Expect the over-limit message: "1 characters too many"
|
|
108
|
+
expect(await canvas.findByText('1 characters too many')).toBeInTheDocument();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
export const WithTextAreaHandler: StoryObj<typeof CharacterCounter> = {
|
|
114
|
+
args: {
|
|
115
|
+
maxCharacters: 20,
|
|
116
|
+
// We'll inline a small component that keeps state outside CharacterCounter,
|
|
117
|
+
// but we still rely on CharacterCounter for the message display.
|
|
118
|
+
children: (
|
|
119
|
+
<textarea
|
|
120
|
+
className="border-2"
|
|
121
|
+
cols={20}
|
|
122
|
+
// onChange is optional if your story doesn't need an external state
|
|
123
|
+
/>
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
render: (args) => <CharacterCounter {...args} />,
|
|
127
|
+
play: async ({ canvasElement }) => {
|
|
128
|
+
const canvas = within(canvasElement);
|
|
129
|
+
|
|
130
|
+
// 1) Should see "20 characters allowed" initially
|
|
131
|
+
expect(await canvas.findByText('20 characters allowed')).toBeInTheDocument();
|
|
132
|
+
|
|
133
|
+
// 2) Type some text
|
|
134
|
+
const textarea = canvas.getByRole('textbox'); // <textarea> is still a "textbox" role
|
|
135
|
+
await userEvent.type(textarea, 'This is more than 10 chars');
|
|
136
|
+
// That’s 26 chars, so we are over limit by 6
|
|
137
|
+
// We expect "6 characters too many"
|
|
138
|
+
expect(await canvas.findByText('6 characters too many')).toBeInTheDocument();
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
export const UsingAltMessages: StoryObj<typeof CharacterCounter> = {
|
|
144
|
+
args: {
|
|
145
|
+
maxCharacters: 5,
|
|
146
|
+
altRemainingMessageText: ' left to go!',
|
|
147
|
+
altOverageMessageText: ' too many!',
|
|
148
|
+
children: <Input variant="default" />,
|
|
149
|
+
},
|
|
150
|
+
play: async ({ canvasElement }) => {
|
|
151
|
+
const canvas = within(canvasElement);
|
|
152
|
+
const input = canvas.getByRole('textbox');
|
|
153
|
+
|
|
154
|
+
// Initially, "5 characters allowed"
|
|
155
|
+
expect(await canvas.findByText('5 characters allowed')).toBeInTheDocument();
|
|
156
|
+
|
|
157
|
+
// Type 3 characters => "2 left to go!"
|
|
158
|
+
await userEvent.type(input, 'abc');
|
|
159
|
+
expect(await canvas.findByText('2 left to go!')).toBeInTheDocument();
|
|
160
|
+
|
|
161
|
+
// Clear and confirm it goes back to "5 characters allowed"
|
|
162
|
+
await userEvent.clear(input);
|
|
163
|
+
expect(await canvas.findByText('5 characters allowed')).toBeInTheDocument();
|
|
164
|
+
|
|
165
|
+
// Type 6 => "1 too many!"
|
|
166
|
+
await userEvent.type(input, 'abcdef');
|
|
167
|
+
expect(await canvas.findByText('1 too many!')).toBeInTheDocument();
|
|
168
|
+
},
|
|
169
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CharacterCounter.test.tsx
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
5
|
+
import { userEvent } from '@testing-library/user-event';
|
|
6
|
+
import { describe, it, expect } from "vitest";
|
|
7
|
+
import { axe } from "vitest-axe";
|
|
8
|
+
import { CharacterCounter } from "./CharacterCounter";
|
|
9
|
+
import { SourceTextModule } from "vm";
|
|
10
|
+
|
|
11
|
+
describe("CharacterCounter", () => {
|
|
12
|
+
it("renders without crashing", () => {
|
|
13
|
+
render(<CharacterCounter maxCharacters={10}>
|
|
14
|
+
<input size={20} />
|
|
15
|
+
</CharacterCounter>
|
|
16
|
+
);
|
|
17
|
+
// We can check for certain text, placeholder, or aria attributes
|
|
18
|
+
// that appear when the component is rendered.
|
|
19
|
+
// Example: verify it shows '0' characters by default:
|
|
20
|
+
expect(screen.getByText(/0/i)).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("displays the correct character count when user types", async () => {
|
|
24
|
+
render(
|
|
25
|
+
<CharacterCounter maxCharacters={10}>
|
|
26
|
+
<input size={20} type="text" aria-label="input box" />
|
|
27
|
+
</CharacterCounter>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Assuming CharacterCounter has a text input element:
|
|
31
|
+
const input = screen.getByLabelText("input box");
|
|
32
|
+
|
|
33
|
+
// Type a few characters
|
|
34
|
+
userEvent.type(input, "Hello");
|
|
35
|
+
|
|
36
|
+
// Wait for potential asynchronous updates (if applicable)
|
|
37
|
+
await screen.findByText("5 remaining");
|
|
38
|
+
|
|
39
|
+
// Check that the character counter updated
|
|
40
|
+
expect(screen.getByText("5 remaining")).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("is accessible according to axe", async () => {
|
|
44
|
+
// Render your component
|
|
45
|
+
const { container } = render(
|
|
46
|
+
<CharacterCounter maxCharacters={10}>
|
|
47
|
+
<input size={20} aria-label="a test input box" />
|
|
48
|
+
</CharacterCounter>
|
|
49
|
+
);
|
|
50
|
+
// Use axe to analyze the rendered DOM
|
|
51
|
+
const results = await axe(container);
|
|
52
|
+
// Expect no accessibility violations
|
|
53
|
+
expect(results).toHaveNoViolations();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("shows the overage message when user types too many characters", async () => {
|
|
57
|
+
render(
|
|
58
|
+
<CharacterCounter maxCharacters={5}>
|
|
59
|
+
<input type="text" />
|
|
60
|
+
</CharacterCounter>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const input = screen.getByRole("textbox");
|
|
64
|
+
|
|
65
|
+
// Type 6 characters (more than the max of 5)
|
|
66
|
+
await userEvent.type(input, "abcdef");
|
|
67
|
+
|
|
68
|
+
// For 6 chars typed and a max of 5, we expect: "1 characters too many"
|
|
69
|
+
// The difference is (6 - 5) = 1
|
|
70
|
+
expect(screen.getByText("1 characters too many")).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("uses the altOverageMessageText if provided", async () => {
|
|
74
|
+
render(
|
|
75
|
+
<CharacterCounter
|
|
76
|
+
maxCharacters={5}
|
|
77
|
+
altOverageMessageText="too many! Please shorten."
|
|
78
|
+
>
|
|
79
|
+
<input type="text" />
|
|
80
|
+
</CharacterCounter>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const input = screen.getByRole("textbox");
|
|
84
|
+
|
|
85
|
+
// Again, exceed maxCharacters
|
|
86
|
+
await userEvent.type(input, "abcdef");
|
|
87
|
+
|
|
88
|
+
// Now we expect it to say: "1 too many! Please shorten."
|
|
89
|
+
expect(screen.getByText("1 too many! Please shorten.")).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
it("displays correct starting message before user types", async () => {
|
|
94
|
+
render(
|
|
95
|
+
<CharacterCounter maxCharacters={10}>
|
|
96
|
+
<input size={20} type="text" aria-label="input box" />
|
|
97
|
+
</CharacterCounter>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Check that the character counter updated
|
|
101
|
+
expect(screen.getByText("10 characters allowed")).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("sets the correct message when the input is cleared (numCharacters === 0)", async () => {
|
|
105
|
+
render(
|
|
106
|
+
<CharacterCounter maxCharacters={5}>
|
|
107
|
+
<input type="text" />
|
|
108
|
+
</CharacterCounter>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const input = screen.getByRole("textbox");
|
|
112
|
+
|
|
113
|
+
// Type some text first
|
|
114
|
+
await userEvent.type(input, "abcde");
|
|
115
|
+
|
|
116
|
+
// Now clear it (this should reset numCharacters to 0)
|
|
117
|
+
await userEvent.clear(input);
|
|
118
|
+
|
|
119
|
+
// If numCharacters === 0, the default message is "5 allowed"
|
|
120
|
+
expect(screen.getByText("5 characters allowed")).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export interface CharacterCounterProps {
|
|
5
|
+
maxCharacters: number; // required prop
|
|
6
|
+
children: React.ReactNode; // required prop
|
|
7
|
+
className?: string; // optional class for parent container div
|
|
8
|
+
classNameMessage?: string; // optional class for message text (div)
|
|
9
|
+
classNameOverLimitMessage?: string; // optional class applied when over limit
|
|
10
|
+
altRemainingMessageText?: string;
|
|
11
|
+
altOverageMessageText?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const CharacterCounter = ({
|
|
15
|
+
className = '',
|
|
16
|
+
classNameMessage = '',
|
|
17
|
+
classNameOverLimitMessage = '',
|
|
18
|
+
maxCharacters,
|
|
19
|
+
children,
|
|
20
|
+
altRemainingMessageText,
|
|
21
|
+
altOverageMessageText,
|
|
22
|
+
}: CharacterCounterProps) => {
|
|
23
|
+
const [counterMessage, setCounterMessage] = useState<string>(`${maxCharacters} characters allowed`);
|
|
24
|
+
const [overLimit, setOverLimit] = useState<boolean>(false);
|
|
25
|
+
|
|
26
|
+
// is this valid w/ just an HTMLInputElement interface?
|
|
27
|
+
const handleInput = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
28
|
+
const editableText = event.target.value;
|
|
29
|
+
const numCharacters = editableText.length;
|
|
30
|
+
|
|
31
|
+
setOverLimit(numCharacters > maxCharacters);
|
|
32
|
+
|
|
33
|
+
if (numCharacters === 0) {
|
|
34
|
+
setCounterMessage(`${maxCharacters} characters allowed`);
|
|
35
|
+
} else if (numCharacters <= maxCharacters) {
|
|
36
|
+
const msgText = ' ' + (altRemainingMessageText ?? "remaining");
|
|
37
|
+
setCounterMessage(`${maxCharacters - numCharacters}${msgText}`);
|
|
38
|
+
} else {
|
|
39
|
+
const msgText = ' ' + (altOverageMessageText ?? "characters too many");
|
|
40
|
+
setCounterMessage(`${numCharacters - maxCharacters}${msgText}`);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className={className}>
|
|
46
|
+
<div
|
|
47
|
+
onInput={handleInput}
|
|
48
|
+
className="mb-2"
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</div>
|
|
52
|
+
<div className={twMerge('ml-2', classNameMessage, overLimit && 'text-red-500', overLimit && classNameOverLimitMessage)}
|
|
53
|
+
>{counterMessage}</div> {/* separate from the editable area */}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|