@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,138 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
4
|
+
import { axe } from 'vitest-axe';
|
|
5
|
+
import { AppointmentPicker, generateOptions } from './AppointmentPicker';
|
|
6
|
+
import { Select } from './Select';
|
|
7
|
+
|
|
8
|
+
type Option = {
|
|
9
|
+
name: string;
|
|
10
|
+
value?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Mock the Select component to inspect props and simulate option selection
|
|
14
|
+
vi.mock('./Select', () => ({
|
|
15
|
+
Select: vi.fn(({
|
|
16
|
+
options,
|
|
17
|
+
optionsLabel,
|
|
18
|
+
setSelectedOption,
|
|
19
|
+
className,
|
|
20
|
+
classNameContainer,
|
|
21
|
+
label,
|
|
22
|
+
variant,
|
|
23
|
+
width,
|
|
24
|
+
...rest
|
|
25
|
+
}) => (
|
|
26
|
+
<div data-testid="mock-select" className={classNameContainer} {...rest}>
|
|
27
|
+
{label && <span data-testid="picker-label">{label}</span>}
|
|
28
|
+
<span data-testid="options-label">{optionsLabel}</span>
|
|
29
|
+
<div data-testid="options-container">
|
|
30
|
+
{options.map((opt: Option) => (
|
|
31
|
+
<button
|
|
32
|
+
key={opt.value}
|
|
33
|
+
data-value={opt.value}
|
|
34
|
+
onClick={() => setSelectedOption && setSelectedOption(opt.value || '')}
|
|
35
|
+
>
|
|
36
|
+
{opt.name}
|
|
37
|
+
</button>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
)),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
describe('generateOptions utility', () => {
|
|
45
|
+
it('generates 24 hourly options for default range in 24hr format', () => {
|
|
46
|
+
const opts = generateOptions('hour', '24hr');
|
|
47
|
+
expect(opts).toHaveLength(24);
|
|
48
|
+
expect(opts[0]).toEqual({ name: '00:00', value: '00:00' });
|
|
49
|
+
expect(opts[23]).toEqual({ name: '23:00', value: '23:00' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('generates half-hourly options in 12hr format with AM/PM', () => {
|
|
53
|
+
const opts = generateOptions('half-hour', '12hr');
|
|
54
|
+
expect(opts.find(o => o.value === '00:30')?.name).toBe('12:30 AM');
|
|
55
|
+
expect(opts.find(o => o.value === '12:00')?.name).toBe('12:00 PM');
|
|
56
|
+
expect(opts).toHaveLength((24 * 60) / 30);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('respects custom hourRange filter', () => {
|
|
60
|
+
const opts = generateOptions('quarter-hour', '24hr', [9, 10]);
|
|
61
|
+
// From 09:00 to 10:59 in 15-min steps: (2 hours * 4) = 8 options
|
|
62
|
+
expect(opts).toHaveLength(8);
|
|
63
|
+
expect(opts[0].value).toBe('09:00');
|
|
64
|
+
expect(opts[opts.length - 1].value).toBe('10:45');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('AppointmentPicker Component', () => {
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
vi.clearAllMocks();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('renders the Select component with provided className and container class', () => {
|
|
74
|
+
render(
|
|
75
|
+
<AppointmentPicker
|
|
76
|
+
className="picker-class"
|
|
77
|
+
classNameContainer="container-class"
|
|
78
|
+
label="Choose time"
|
|
79
|
+
optionsLabel="Pick"
|
|
80
|
+
variant="outline"
|
|
81
|
+
width="200px"
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const select = screen.getByTestId('mock-select');
|
|
86
|
+
expect(select).toHaveClass('container-class');
|
|
87
|
+
|
|
88
|
+
expect(Select).toHaveBeenCalledWith(
|
|
89
|
+
expect.objectContaining({
|
|
90
|
+
className: 'picker-class',
|
|
91
|
+
classNameContainer: 'container-class',
|
|
92
|
+
label: 'Choose time',
|
|
93
|
+
optionsLabel: 'Pick',
|
|
94
|
+
variant: 'outline',
|
|
95
|
+
width: '200px',
|
|
96
|
+
}),
|
|
97
|
+
{}
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('calls console.log with interval and options', () => {
|
|
102
|
+
const logSpy = vi.spyOn(console, 'log');
|
|
103
|
+
render(<AppointmentPicker interval="half-hour" />);
|
|
104
|
+
expect(logSpy).toHaveBeenCalledWith('half-hour');
|
|
105
|
+
expect(logSpy).toHaveBeenCalledWith(expect.arrayContaining([
|
|
106
|
+
expect.objectContaining({ name: '00:00', value: '00:00' })
|
|
107
|
+
]));
|
|
108
|
+
logSpy.mockRestore();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// this test contributes to satisfy all lines being covered (no specific lines specified as uncovered)
|
|
112
|
+
// however, % Funcs still scores a 66.66 -- something to look into
|
|
113
|
+
it('defaults to half-hour interval when interval is nullish', () => {
|
|
114
|
+
// Pass undefined to trigger the fallback
|
|
115
|
+
const opts = generateOptions(undefined as any, '24hr');
|
|
116
|
+
// Should produce 48 options (24h / 0.5h)
|
|
117
|
+
expect(opts).toHaveLength((24 * 60) / 30);
|
|
118
|
+
// First two entries: 00:00 and 00:30
|
|
119
|
+
expect(opts[0]).toEqual({ name: '00:00', value: '00:00' });
|
|
120
|
+
expect(opts[1]).toEqual({ name: '00:30', value: '00:30' });
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('AppointmentPicker Accessibility Tests', () => {
|
|
125
|
+
it('has no accessibility violations by default', async () => {
|
|
126
|
+
const { container } = render(<AppointmentPicker />);
|
|
127
|
+
const results = await axe(container);
|
|
128
|
+
expect(results).toHaveNoViolations();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('has no accessibility violations with custom props', async () => {
|
|
132
|
+
const { container } = render(
|
|
133
|
+
<AppointmentPicker interval="quarter-hour" timeFormat="12hr" hourRange={[8, 9]} />
|
|
134
|
+
);
|
|
135
|
+
const results = await axe(container);
|
|
136
|
+
expect(results).toHaveNoViolations();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { HTMLAttributes } from 'react';
|
|
2
|
+
import { Select } from './Select';
|
|
3
|
+
|
|
4
|
+
type Interval = 'hour' | 'half-hour' | 'quarter-hour';
|
|
5
|
+
|
|
6
|
+
const INTERVAL_MINUTES: Record<Interval, number> = {
|
|
7
|
+
'hour': 60,
|
|
8
|
+
'half-hour': 30,
|
|
9
|
+
'quarter-hour': 15
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type Option = {
|
|
13
|
+
name: string;
|
|
14
|
+
value?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface AppointmentPickerProps extends HTMLAttributes<HTMLDivElement> {
|
|
18
|
+
className?: string;
|
|
19
|
+
classNameContainer?: string;
|
|
20
|
+
interval?: 'hour' | 'half-hour' | 'quarter-hour';
|
|
21
|
+
timeFormat?: '12hr' | '24hr';
|
|
22
|
+
hourRange?: [number, number];
|
|
23
|
+
label?: string;
|
|
24
|
+
variant?: string;
|
|
25
|
+
optionsLabel?: string;
|
|
26
|
+
width?: string;
|
|
27
|
+
setSelectedOption?: (param: string) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const generateOptions = (
|
|
31
|
+
interval: Interval,
|
|
32
|
+
timeFormat: AppointmentPickerProps['timeFormat'],
|
|
33
|
+
hourRange?: [number, number]
|
|
34
|
+
): Option[] => {
|
|
35
|
+
const step = INTERVAL_MINUTES[interval ?? 'half-hour'];
|
|
36
|
+
const options: Option[] = [];
|
|
37
|
+
|
|
38
|
+
for (let minutes = 0; minutes < 24 * 60; minutes += step) {
|
|
39
|
+
const hrs = Math.floor(minutes / 60);
|
|
40
|
+
const mins = minutes % 60;
|
|
41
|
+
|
|
42
|
+
// Filter by custom hourRange
|
|
43
|
+
if (hourRange && (hrs < hourRange[0] || hrs > hourRange[1])) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Format label
|
|
48
|
+
let label: string;
|
|
49
|
+
if (timeFormat === '12hr') {
|
|
50
|
+
const period = hrs < 12 ? 'AM' : 'PM';
|
|
51
|
+
const displayHour = hrs % 12 === 0 ? 12 : hrs % 12;
|
|
52
|
+
label = `${displayHour}:${mins.toString().padStart(2, '0')} ${period}`;
|
|
53
|
+
} else {
|
|
54
|
+
label = `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const value = `${hrs.toString().padStart(2, '0')}:${mins
|
|
58
|
+
.toString()
|
|
59
|
+
.padStart(2, '0')}`;
|
|
60
|
+
|
|
61
|
+
options.push({ name: label, value });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return options;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const AppointmentPicker = ({
|
|
68
|
+
className,
|
|
69
|
+
classNameContainer = '',
|
|
70
|
+
interval = 'hour',
|
|
71
|
+
timeFormat = '24hr',
|
|
72
|
+
hourRange,
|
|
73
|
+
label,
|
|
74
|
+
optionsLabel = 'Select a time',
|
|
75
|
+
variant = 'default',
|
|
76
|
+
setSelectedOption,
|
|
77
|
+
width = '',
|
|
78
|
+
...props
|
|
79
|
+
}: AppointmentPickerProps) => {
|
|
80
|
+
|
|
81
|
+
const timeOptions: Option[] = generateOptions(interval, timeFormat, hourRange);
|
|
82
|
+
console.log(interval);
|
|
83
|
+
console.log(timeOptions);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Select
|
|
87
|
+
className={className}
|
|
88
|
+
classNameContainer={classNameContainer}
|
|
89
|
+
label={label}
|
|
90
|
+
optionsLabel={optionsLabel}
|
|
91
|
+
options={timeOptions}
|
|
92
|
+
variant={variant}
|
|
93
|
+
width={width}
|
|
94
|
+
setSelectedOption={() => setSelectedOption}>
|
|
95
|
+
</Select>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Badge.stories.tsx
|
|
2
|
+
|
|
3
|
+
// import { Meta, StoryFn} from '@storybook/react';
|
|
4
|
+
import { Badge } from './Badge';
|
|
5
|
+
import { Meta, StoryContext, StoryFn, StoryObj } from '@storybook/react';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// Import your images
|
|
9
|
+
import firstAidKit from '../assets/img/first-aid-kit.svg';
|
|
10
|
+
import heartbeat from '../assets/img/heartbeat.svg';
|
|
11
|
+
import pill from '../assets/img/pill.svg';
|
|
12
|
+
import prescription from '../assets/img/prescription.svg';
|
|
13
|
+
import stethoscope from '../assets/img/stethoscope.svg';
|
|
14
|
+
// import { useState } from 'react';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
title: 'Components/Badge',
|
|
19
|
+
component: Badge,
|
|
20
|
+
argTypes: {
|
|
21
|
+
variant: {
|
|
22
|
+
control: 'select',
|
|
23
|
+
options: ['success', 'caution', 'danger', 'disabled']
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
parameters: {
|
|
27
|
+
layout: 'centered',
|
|
28
|
+
backgrounds: { default: 'light' },
|
|
29
|
+
},
|
|
30
|
+
} as Meta<typeof Badge>;
|
|
31
|
+
|
|
32
|
+
// DefaultBadge story
|
|
33
|
+
export const DefaultBadge = {
|
|
34
|
+
args: {
|
|
35
|
+
variant: 'success',
|
|
36
|
+
children: 'Badge!',
|
|
37
|
+
className: 'bg-gray-200',
|
|
38
|
+
classNameImage: 'mr-2',
|
|
39
|
+
icon: firstAidKit,
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const CautionBadge = {
|
|
44
|
+
args: {
|
|
45
|
+
variant: 'caution',
|
|
46
|
+
children: 'Badge!',
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
export const DangerBadge = {
|
|
50
|
+
args: {
|
|
51
|
+
variant: 'danger',
|
|
52
|
+
children: 'Badge!',
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
export const DisabledBadge = {
|
|
56
|
+
args: {
|
|
57
|
+
variant: 'disabled',
|
|
58
|
+
children: 'Badge!',
|
|
59
|
+
}
|
|
60
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { Badge, BadgeProps } from './Badge';
|
|
4
|
+
import React, { createRef } from 'react';
|
|
5
|
+
import { axe } from "vitest-axe";
|
|
6
|
+
import closeIcon from '../assets/img/close_small.svg';
|
|
7
|
+
import warning from '../assets/img/warning.svg';
|
|
8
|
+
import emergency from '../assets/img/emergency_home.svg';
|
|
9
|
+
import thumbUp from '../assets/img/thumb_up.svg';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const imagePath = new URL('/src/assets/pill.svg', import.meta.url).href;
|
|
13
|
+
|
|
14
|
+
describe('Badge', () => {
|
|
15
|
+
|
|
16
|
+
it('renders with default warning icon if no icon is provided', () => {
|
|
17
|
+
render(<Badge>Test Badge</Badge>);
|
|
18
|
+
// The default fallback is an image with alt text "warning"
|
|
19
|
+
const warningImage = screen.getByAltText('warning');
|
|
20
|
+
expect(warningImage).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('renders with provided icon instead of the default warning icon', () => {
|
|
24
|
+
// Create a simple custom icon component for testing.
|
|
25
|
+
const CustomIcon = () => <span data-testid="custom-icon">Icon</span>;
|
|
26
|
+
render(<Badge icon={<CustomIcon />}>Test Badge</Badge>);
|
|
27
|
+
// Ensure the custom icon is rendered.
|
|
28
|
+
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
|
|
29
|
+
// And that the default warning icon is not rendered.
|
|
30
|
+
const warningImage = screen.queryByAltText('warning');
|
|
31
|
+
expect(warningImage).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders children text correctly', () => {
|
|
35
|
+
render(<Badge>Badge Content</Badge>);
|
|
36
|
+
expect(screen.getByText('Badge Content')).toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('calls onClick when clicked', () => {
|
|
40
|
+
const handleClick = vi.fn();
|
|
41
|
+
render(<Badge onClick={handleClick}>Clickable Badge</Badge>);
|
|
42
|
+
fireEvent.click(screen.getByText('Clickable Badge'));
|
|
43
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('forwards ref to the underlying div element', () => {
|
|
47
|
+
const ref = createRef<HTMLDivElement>();
|
|
48
|
+
render(<Badge ref={ref}>Ref Badge</Badge>);
|
|
49
|
+
// Check that the ref is set and points to a div element.
|
|
50
|
+
expect(ref.current).not.toBeNull();
|
|
51
|
+
expect(ref.current?.tagName).toBe('DIV');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('applies the correct classes for the danger variant', () => {
|
|
55
|
+
const { container } = render(<Badge variant="danger">Danger Badge</Badge>);
|
|
56
|
+
// Find the outer div by getting the closest ancestor of the children text.
|
|
57
|
+
const badgeElement = container.firstChild as HTMLElement;
|
|
58
|
+
// Check that the rendered className contains the danger variant background.
|
|
59
|
+
expect(badgeElement?.className).toContain('bg-[#f4c2c2]');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('Badge Accessibility Tests', () => {
|
|
65
|
+
it('has no accessibility violations', async () => {
|
|
66
|
+
const { container } = render(<Badge>Accessible Badge</Badge>);
|
|
67
|
+
const results = await axe(container);
|
|
68
|
+
expect(results).toHaveNoViolations();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { forwardRef, HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
import warningSuccess from '../assets/img/warning-badge-green.svg';
|
|
4
|
+
import warningDanger from '../assets/img/warning-badge-red.svg';
|
|
5
|
+
import warningCaution from '../assets/img/warning-badge-yellow.svg';
|
|
6
|
+
import warningDisabled from '../assets/img/warning-badge-disabled.svg';
|
|
7
|
+
|
|
8
|
+
type VariantsType = {
|
|
9
|
+
[key: string]: {
|
|
10
|
+
className: string;
|
|
11
|
+
classNameImage: string;
|
|
12
|
+
classNameChildren: string;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const badgeVariants: VariantsType = {
|
|
17
|
+
success: {
|
|
18
|
+
className: 'w-[79px] h-[26px] px-3 py-1 bg-[#d6f4d5] rounded-[40px] outline outline-1 -outline-offset-1 outline-[#387740] inline-flex justify-center items-center gap-0 md:w-[113px] md:h-[36px] md:px-5 md:py-1.5 md:gap-0.5 lg:w-[133px] lg:h-[43px] lg:px-6 lg:py-2 lg:gap-0',
|
|
19
|
+
classNameImage: 'relative size-2.5 mb-0.5 me-1.5 md:size-3.5 md:me-2 lg:size-4 lg:mb-0.5 lg:me-2.5 lg:ms-1',
|
|
20
|
+
classNameChildren: "text-[#387740] text-xs font-normal font-['Arial'] leading-[18px] md:text-md md:leading-normal lg:text-lg lg:leading-[27px]",
|
|
21
|
+
},
|
|
22
|
+
caution: {
|
|
23
|
+
className: 'w-[79px] h-[26px] px-3 py-1 bg-[#fff1be] rounded-[40px] outline outline-1 -outline-offset-1 outline-[#966121] inline-flex justify-center items-center gap-0 md:w-[113px] md:h-[36px] md:px-5 md:py-1.5 md:gap-0.5 lg:w-[133px] lg:h-[43px] lg:px-6 lg:py-2 lg:gap-0',
|
|
24
|
+
classNameImage: 'relative size-2.5 mb-0.5 me-1.5 md:size-3.5 md:me-2 lg:size-4 lg:mb-0.5 lg:me-2.5 lg:ms-1',
|
|
25
|
+
classNameChildren: "text-[#966222] text-xs font-normal font-['Arial'] leading-[18px] md:text-base md:leading-normal lg:text-lg lg:leading-[27px]",
|
|
26
|
+
},
|
|
27
|
+
danger: {
|
|
28
|
+
className: 'w-[79px] h-[26px] px-3 py-1 bg-[#f4c2c2] rounded-[40px] outline outline-1 -outline-offset-1 outline-[#a22b23] inline-flex justify-center items-center gap-0 md:w-[113px] md:h-[36px] md:px-5 md:py-1.5 md:gap-0.5 lg:w-[133px] lg:h-[43px] lg:px-6 lg:py-2 lg:gap-0',
|
|
29
|
+
classNameImage: 'relative size-2.5 mb-0.5 me-1.5 md:size-3.5 md:me-2 lg:size-4 lg:mb-0.5 lg:me-2.5 lg:ms-1',
|
|
30
|
+
classNameChildren: "text-[#A32C24] text-xs font-normal font-['Arial'] leading-[18px] md:text-base md:leading-normal lg:text-lg lg:leading-[27px]",
|
|
31
|
+
},
|
|
32
|
+
disabled: {
|
|
33
|
+
className: 'w-[79px] h-[26px] px-3 py-1 rounded-[40px] outline outline-1 -outline-offset-1 outline-[#394150] inline-flex justify-center items-center gap-0 md:w-[113px] md:h-[36px] md:px-5 md:py-1.5 md:gap-0.5 lg:w-[133px] lg:h-[43px] lg:px-6 lg:py-2 lg:gap-0',
|
|
34
|
+
classNameImage: 'relative size-2.5 mb-0.5 me-1.5 md:size-3.5 md:me-2 lg:size-4 lg:mb-0.5 lg:me-2.5 lg:ms-1',
|
|
35
|
+
classNameChildren: "text-[#394150] text-xs font-normal font-['Arial'] leading-[18px] md:text-md md:leading-normal lg:text-lg lg:leading-[27px]",
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface BadgeProps extends HTMLAttributes<HTMLDivElement> {
|
|
40
|
+
// variant?: keyof typeof badgeVariants;
|
|
41
|
+
variant?: 'success' | 'caution' | 'danger' | 'disabled';
|
|
42
|
+
className?: string;
|
|
43
|
+
classNameImage?: string;
|
|
44
|
+
classNameChildren?: string;
|
|
45
|
+
icon?: ReactNode;
|
|
46
|
+
iconAlt?: string;
|
|
47
|
+
children: ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Map each variant to its default SVG asset
|
|
51
|
+
const defaultIcons = {
|
|
52
|
+
success: warningSuccess,
|
|
53
|
+
caution: warningCaution,
|
|
54
|
+
danger: warningDanger,
|
|
55
|
+
disabled: warningDisabled,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
|
|
59
|
+
({ variant = 'success', className = '', classNameImage = '',
|
|
60
|
+
classNameChildren = '', icon, children, ...props }, ref) => {
|
|
61
|
+
// Use the provided icon or default to the one for the variant.
|
|
62
|
+
// Note: The imported SVGs are URLs so they can be passed as src.
|
|
63
|
+
const iconToDisplay = defaultIcons[variant];
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
className={twMerge(badgeVariants[variant].className, className)}
|
|
68
|
+
ref={ref}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{/* Conditionally render based on icon type */}
|
|
72
|
+
{icon ? (
|
|
73
|
+
<span className={twMerge(badgeVariants[variant].classNameImage, classNameImage)}>{icon}</span>
|
|
74
|
+
) : (
|
|
75
|
+
<img
|
|
76
|
+
className={twMerge(badgeVariants[variant].classNameImage, classNameImage)}
|
|
77
|
+
src={iconToDisplay as string}
|
|
78
|
+
alt={'warning'}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
<div className={twMerge(badgeVariants[variant].classNameChildren, classNameChildren)}>
|
|
82
|
+
{children}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
|
3
|
+
import { Breadcrumbs, BreadcrumbsProps } from './Breadcrumbs';
|
|
4
|
+
import { Meta, StoryFn } from '@storybook/react';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'Components/Breadcrumbs',
|
|
8
|
+
component: Breadcrumbs,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'centered',
|
|
11
|
+
backgrounds: {
|
|
12
|
+
default: 'white',
|
|
13
|
+
values: [
|
|
14
|
+
{ name: 'white', value: '#ffffff' },
|
|
15
|
+
{ name: 'medium', value: '#b5bbb7' },
|
|
16
|
+
{ name: 'dark', value: '#000' },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
decorators: [
|
|
21
|
+
(Story, context) => (
|
|
22
|
+
<MemoryRouter initialEntries={context.parameters.initialEntries || ['/home/products/details']}>
|
|
23
|
+
<Story />
|
|
24
|
+
</MemoryRouter>
|
|
25
|
+
),
|
|
26
|
+
],
|
|
27
|
+
} as Meta;
|
|
28
|
+
|
|
29
|
+
const Template: StoryFn<BreadcrumbsProps> = (args) => (
|
|
30
|
+
<Routes>
|
|
31
|
+
<Route path="*" element={<Breadcrumbs {...args} />} />
|
|
32
|
+
</Routes>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export const Default = Template.bind({});
|
|
36
|
+
Default.args = {
|
|
37
|
+
variant: 'default',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const BoldVariant = Template.bind({});
|
|
41
|
+
BoldVariant.args = {
|
|
42
|
+
variant: 'bold',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const CustomRoutes = Template.bind({});
|
|
46
|
+
CustomRoutes.args = {
|
|
47
|
+
routes: [
|
|
48
|
+
{ name: 'Section One', route: '/section-one' },
|
|
49
|
+
{ name: 'Section Two', route: '/section-one/section-two' },
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
CustomRoutes.parameters = {
|
|
53
|
+
initialEntries: ['/section-one/section-two'],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const HomeOnly = Template.bind({});
|
|
57
|
+
HomeOnly.args = {};
|
|
58
|
+
HomeOnly.parameters = {
|
|
59
|
+
initialEntries: ['/'],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const ContainerClass = Template.bind({});
|
|
63
|
+
ContainerClass.args = {
|
|
64
|
+
classNameContainer: 'border-2 border-gray-500 bg-gray-200 px-2 rounded-md',
|
|
65
|
+
className: 'capitalize font-bold',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const SeparatorChars = Template.bind({});
|
|
69
|
+
SeparatorChars.args = {
|
|
70
|
+
separator: '/',
|
|
71
|
+
className: 'capitalize font-bold',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const SeparatorEmoji = Template.bind({});
|
|
75
|
+
SeparatorEmoji.args = {
|
|
76
|
+
separator: '➜',
|
|
77
|
+
className: 'capitalize font-bold',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const ManySegments = Template.bind({});
|
|
81
|
+
ManySegments.args = {};
|
|
82
|
+
ManySegments.parameters = {
|
|
83
|
+
initialEntries: ['/a/b/c/d/e/f'],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// New stories to cover prettify (hyphens), home icon alt, and overflow behavior
|
|
87
|
+
export const HyphenatedSegment = Template.bind({});
|
|
88
|
+
HyphenatedSegment.args = {};
|
|
89
|
+
HyphenatedSegment.storyName = 'Hyphenated Segment';
|
|
90
|
+
HyphenatedSegment.parameters = {
|
|
91
|
+
initialEntries: ['/section-one/section-two-with-hyphen'],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const HomeIconOnly = Template.bind({});
|
|
95
|
+
HomeIconOnly.args = {};
|
|
96
|
+
HomeIconOnly.storyName = 'Home Icon Only';
|
|
97
|
+
HomeIconOnly.decorators = [
|
|
98
|
+
(Story) => <div style={{ width: 200 }}>{Story()}</div>,
|
|
99
|
+
];
|
|
100
|
+
HomeIconOnly.parameters = {
|
|
101
|
+
initialEntries: ['/only'],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const OverflowScenario = Template.bind({});
|
|
105
|
+
OverflowScenario.args = {
|
|
106
|
+
classNameContainer: 'w-24 overflow-hidden',
|
|
107
|
+
};
|
|
108
|
+
OverflowScenario.storyName = 'Overflow Scenario';
|
|
109
|
+
OverflowScenario.decorators = [
|
|
110
|
+
(Story) => <div style={{ width: 100 }}>{Story()}</div>,
|
|
111
|
+
];
|
|
112
|
+
OverflowScenario.parameters = {
|
|
113
|
+
initialEntries: ['/one/two/three/four'],
|
|
114
|
+
};
|