@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,281 @@
|
|
|
1
|
+
import React, { createRef } from "react";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { userEvent } from "@testing-library/user-event"; // userEvent.type functionality
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { axe } from "vitest-axe";
|
|
6
|
+
import { Input } from "./Input";
|
|
7
|
+
|
|
8
|
+
describe("Input Component", () => {
|
|
9
|
+
it("renders the input with default styles", () => {
|
|
10
|
+
render(<Input label="Default Input" />);
|
|
11
|
+
const input = screen.getByLabelText("Default Input");
|
|
12
|
+
// console.log('input.className: ', input.className);
|
|
13
|
+
expect(input).toBeInTheDocument();
|
|
14
|
+
expect(input).toHaveClass("px-4 py-3 arial rounded-lg border border-[#B3B3B3] mx-2 bg-white");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("applies custom classes and merges with default classes", () => {
|
|
18
|
+
render(<Input label="Custom Input" className="custom-class" />);
|
|
19
|
+
const input = screen.getByLabelText("Custom Input");
|
|
20
|
+
|
|
21
|
+
expect(input).toHaveClass("custom-class");
|
|
22
|
+
expect(input).toHaveClass("px-4 py-3 arial rounded-lg border border-[#B3B3B3] mx-2 bg-white");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("applies variant-specific styles", () => {
|
|
26
|
+
render(<Input label="Outline Input" variant="outline" />);
|
|
27
|
+
const input = screen.getByLabelText("Outline Input");
|
|
28
|
+
|
|
29
|
+
expect(input).toHaveClass("border-dha-mc-true-blue bg-white border text-dha-mc-true-blue");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders a required indicator in the label", () => {
|
|
33
|
+
render(<Input label="Required Input" required />);
|
|
34
|
+
const label = screen.getByText("*");
|
|
35
|
+
|
|
36
|
+
expect(label).toBeInTheDocument();
|
|
37
|
+
expect(label).toHaveClass("text-red-500");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("applies label styles with gradient background", () => {
|
|
41
|
+
render(
|
|
42
|
+
<Input
|
|
43
|
+
label="Styled Label"
|
|
44
|
+
labelBaseColor="#ffffff"
|
|
45
|
+
textShadow={true}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
const label = screen.getByText("Styled Label");
|
|
49
|
+
|
|
50
|
+
expect(label).toHaveStyle("background: linear-gradient");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("sets the correct type for the input", () => {
|
|
54
|
+
render(<Input label="Password Input" type="password" />);
|
|
55
|
+
const input = screen.getByLabelText("Password Input");
|
|
56
|
+
|
|
57
|
+
expect(input).toHaveAttribute("type", "password");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should forward the ref to the input element", () => {
|
|
61
|
+
const ref = createRef<HTMLInputElement>();
|
|
62
|
+
render(<Input label="Input with Ref" ref={ref} />);
|
|
63
|
+
|
|
64
|
+
expect(ref.current).toBeInstanceOf(HTMLInputElement);
|
|
65
|
+
expect(ref.current?.getAttribute("aria-label")).toBe("Input with Ref");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("renders a disabled input with appropriate styles", () => {
|
|
69
|
+
render(
|
|
70
|
+
<Input label="Disabled Input" disabled variant="nonHover" />
|
|
71
|
+
);
|
|
72
|
+
const input = screen.getByLabelText("Disabled Input");
|
|
73
|
+
|
|
74
|
+
expect(input).toBeDisabled();
|
|
75
|
+
expect(input).toHaveClass("disabled:border-[#F2F2F2]");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
it("throws an error for an invalid variant", () => {
|
|
80
|
+
// Temporarily silence the console.error output
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
82
|
+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
83
|
+
|
|
84
|
+
expect(() => {
|
|
85
|
+
render(<Input label="Default Input" variant="incorrect" />);
|
|
86
|
+
}).toThrowError(
|
|
87
|
+
'Invalid variant: "incorrect". Valid variants are: "default", "outline", and "nonHover"'
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
consoleErrorSpy.mockRestore();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns early if inputRef is null', () => {
|
|
94
|
+
// Mock the useRef hook to return null
|
|
95
|
+
vitest.spyOn(React, 'useRef').mockReturnValueOnce({ current: null });
|
|
96
|
+
|
|
97
|
+
// Render the component
|
|
98
|
+
const { container } = render(<Input label="Test Label" />);
|
|
99
|
+
|
|
100
|
+
// Assert that the component renders without throwing any errors
|
|
101
|
+
expect(container).toBeDefined();
|
|
102
|
+
|
|
103
|
+
// Clean up the mock
|
|
104
|
+
vitest.restoreAllMocks();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
it('covers the early return scenario (null ref)', () => {
|
|
109
|
+
// 1) Spy on React's useRef
|
|
110
|
+
const useRefSpy = vitest.spyOn(React, 'useRef');
|
|
111
|
+
// 2) Mock once to return null
|
|
112
|
+
useRefSpy.mockReturnValueOnce({ current: null });
|
|
113
|
+
|
|
114
|
+
// 3) Render -> triggers early return in useEffect
|
|
115
|
+
render(<Input label="Some label" />);
|
|
116
|
+
|
|
117
|
+
// Cleanup
|
|
118
|
+
useRefSpy.mockRestore();
|
|
119
|
+
// No assertion needed, just ensure no crash
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('covers the scenario where inputRef is not null', () => {
|
|
123
|
+
// Normal render: React will attach an actual input element to ref
|
|
124
|
+
const { getByLabelText } = render(
|
|
125
|
+
<Input label="Another label" data-testid="inputEl" />
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Assert that the element is found (and hence not null)
|
|
129
|
+
const input = getByLabelText('Another label');
|
|
130
|
+
expect(input).toBeInTheDocument();
|
|
131
|
+
// This ensures code *after* the `if(!inputEl) return;` runs
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('calls the ref function if ref is a function', () => {
|
|
135
|
+
// Mock a ref function
|
|
136
|
+
const mockRefFunction = vitest.fn();
|
|
137
|
+
|
|
138
|
+
// Render the Input component with the mock ref
|
|
139
|
+
const { getByRole } = render(<Input label="Test Label" ref={mockRefFunction} />);
|
|
140
|
+
|
|
141
|
+
// Get the input element
|
|
142
|
+
const inputElement = getByRole('textbox');
|
|
143
|
+
|
|
144
|
+
// Assert that the ref function was called with the input element
|
|
145
|
+
expect(mockRefFunction).toHaveBeenCalledWith(inputElement);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("applies phone mask correctly", async () => {
|
|
149
|
+
render(<Input label="Phone Input" mask="(###) ###-####" />);
|
|
150
|
+
const input = screen.getByLabelText("Phone Input") as HTMLInputElement;
|
|
151
|
+
await userEvent.type(input, "a1b2c3d4e5f6g7h8i9j0k");
|
|
152
|
+
expect(input.value).toBe("(123) 456-7890");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
it("applies letter mask correctly", () => {
|
|
157
|
+
render(<Input label="Name Code" mask="@@@-@@" />);
|
|
158
|
+
const input = screen.getByLabelText("Name Code") as HTMLInputElement;
|
|
159
|
+
fireEvent.change(input, { target: { value: "abcDE" } });
|
|
160
|
+
// Expected output: "abc-de"
|
|
161
|
+
expect(input.value).toBe("abc-DE");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
it("ignores invalid characters for a numeric mask", () => {
|
|
166
|
+
render(<Input label="Number Mask" mask="###" />);
|
|
167
|
+
const input = screen.getByLabelText("Number Mask") as HTMLInputElement;
|
|
168
|
+
// Providing letters when numbers are expected should result in an empty string.
|
|
169
|
+
fireEvent.change(input, { target: { value: "abc" } });
|
|
170
|
+
expect(input.value).toBe("");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("ignores extra characters beyond the mask length", () => {
|
|
174
|
+
render(<Input label="Extra Characters" mask="###-###" />);
|
|
175
|
+
const input = screen.getByLabelText("Extra Characters") as HTMLInputElement;
|
|
176
|
+
// Input has more digits than required. Only the first 6 digits should be applied.
|
|
177
|
+
fireEvent.change(input, { target: { value: "1234567" } });
|
|
178
|
+
expect(input.value).toBe("123-456");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("takes input without mask", () => {
|
|
182
|
+
render(<Input label="Extra Characters" />);
|
|
183
|
+
const input = screen.getByLabelText("Extra Characters") as HTMLInputElement;
|
|
184
|
+
// Input has more digits than required. Only the first 6 digits should be applied.
|
|
185
|
+
fireEvent.change(input, { target: { value: "1234567abc" } });
|
|
186
|
+
expect(input.value).toBe("1234567abc");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
it("applies a complex mask with mixed literals correctly", () => {
|
|
191
|
+
render(<Input label="Complex Mask" mask="(@@) ###-####" />);
|
|
192
|
+
const input = screen.getByLabelText("Complex Mask") as HTMLInputElement;
|
|
193
|
+
// With input "ab1234567890", the mask should yield "(ab) 123-4567"
|
|
194
|
+
fireEvent.change(input, { target: { value: "ab1234567890" } });
|
|
195
|
+
expect(input.value).toBe("(ab) 123-4567");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("applies a complex mask with mixed literals and mask input correctly", () => {
|
|
199
|
+
render(<Input label="Complex Mask" mask="(@@) ###-####" />);
|
|
200
|
+
const input = screen.getByLabelText("Complex Mask") as HTMLInputElement;
|
|
201
|
+
// With input "(ab)1234567890", the mask should yield "(ab) 123-4567"
|
|
202
|
+
fireEvent.change(input, { target: { value: "(ab)1234567890" } });
|
|
203
|
+
expect(input.value).toBe("(ab) 123-4567");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
describe('Input Component Accessibility', () => {
|
|
209
|
+
it('should have no accessibility violations with default props', async () => {
|
|
210
|
+
const { container } = render(<Input label="Accessible Input" />);
|
|
211
|
+
const results = await axe(container);
|
|
212
|
+
expect(results).toHaveNoViolations();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should have no accessibility violations when required', async () => {
|
|
216
|
+
const { container } = render(<Input label="Required Input" required />);
|
|
217
|
+
const results = await axe(container);
|
|
218
|
+
expect(results).toHaveNoViolations();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should have no accessibility violations for each variant', async () => {
|
|
222
|
+
const variants = ['default', 'outline', 'nonHover'];
|
|
223
|
+
for (const variant of variants) {
|
|
224
|
+
const { container } = render(<Input label={`Input ${variant}`} variant={variant} />);
|
|
225
|
+
const results = await axe(container);
|
|
226
|
+
expect(results).toHaveNoViolations();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should associate the input with its label', () => {
|
|
231
|
+
render(<Input label="Accessible Label" id="input-id" />);
|
|
232
|
+
const input = screen.getByLabelText('Accessible Label');
|
|
233
|
+
expect(input).toBeInTheDocument();
|
|
234
|
+
expect(input).toHaveAttribute('id', 'input-id');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should render required indicator for required inputs', () => {
|
|
238
|
+
render(<Input label="Required Input" required />);
|
|
239
|
+
const label = screen.getByText('Required Input');
|
|
240
|
+
const requiredIndicator = screen.getByText('*');
|
|
241
|
+
expect(label).toBeInTheDocument();
|
|
242
|
+
expect(requiredIndicator).toBeInTheDocument();
|
|
243
|
+
expect(requiredIndicator).toHaveClass('text-red-500');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should apply custom classes without accessibility violations', async () => {
|
|
247
|
+
const { container } = render(
|
|
248
|
+
<Input label="Custom Class Input" className="custom-class" classNameLabel="custom-label-class" />
|
|
249
|
+
);
|
|
250
|
+
const input = screen.getByLabelText('Custom Class Input');
|
|
251
|
+
expect(input).toHaveClass('custom-class');
|
|
252
|
+
const label = screen.getByText('Custom Class Input');
|
|
253
|
+
expect(label).toHaveClass('custom-label-class');
|
|
254
|
+
const results = await axe(container);
|
|
255
|
+
expect(results).toHaveNoViolations();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should handle text shadow without accessibility violations', async () => {
|
|
259
|
+
const { container } = render(<Input label="Text Shadow Input" textShadow={true} />);
|
|
260
|
+
const label = screen.getByText('Text Shadow Input');
|
|
261
|
+
const computedStyle = window.getComputedStyle(label);
|
|
262
|
+
expect(computedStyle.textShadow).toContain('1px 1px 2px');
|
|
263
|
+
// expect(label).toHaveStyle({
|
|
264
|
+
// textShadow: expect.stringContaining('1px 1px 2px'),
|
|
265
|
+
// });
|
|
266
|
+
const results = await axe(container);
|
|
267
|
+
expect(results).toHaveNoViolations();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should handle text shadow + insetLabel without accessibility violations', async () => {
|
|
271
|
+
const { container } = render(<Input label="Text Shadow Input" insetLabel={true} textShadow={true} />);
|
|
272
|
+
const label = screen.getByText('Text Shadow Input');
|
|
273
|
+
const computedStyle = window.getComputedStyle(label);
|
|
274
|
+
expect(computedStyle.textShadow).toContain('1px 1px 2px');
|
|
275
|
+
// expect(label).toHaveStyle({
|
|
276
|
+
// textShadow: expect.stringContaining('1px 1px 2px'),
|
|
277
|
+
// });
|
|
278
|
+
const results = await axe(container);
|
|
279
|
+
expect(results).toHaveNoViolations();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { ChangeEvent, forwardRef, InputHTMLAttributes, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
// return true if a number, false otherwise
|
|
5
|
+
function isNumber(c: string): boolean {
|
|
6
|
+
if ('0123456789'.includes(c))
|
|
7
|
+
return true;
|
|
8
|
+
else
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// return true if a letter, i.e. a...z or A...Z, false otherwise
|
|
13
|
+
function isLetter(char: string): boolean {
|
|
14
|
+
// if (char.length !== 1) {
|
|
15
|
+
// return false; // Not a single character
|
|
16
|
+
// }
|
|
17
|
+
|
|
18
|
+
const charCode = char.charCodeAt(0);
|
|
19
|
+
|
|
20
|
+
// Check if it's an uppercase letter (A-Z)
|
|
21
|
+
if (charCode >= 65 && charCode <= 90) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check if it's a lowercase letter (a-z)
|
|
26
|
+
if (charCode >= 97 && charCode <= 122) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return false; // Not a letter
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// given an input string & number, return the next type that is a letter or number
|
|
34
|
+
function nextMaskCharType(input: string, startIndex: number): string {
|
|
35
|
+
for (let i = startIndex; i < input.length; i++) {
|
|
36
|
+
if (input[i] === '@')
|
|
37
|
+
return '@';
|
|
38
|
+
else if (input[i] === '#')
|
|
39
|
+
return '#';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// return default empty string if there is no following letter or number
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
// NOTE: If updated, please update error message below as well re invalid VariantType
|
|
48
|
+
type LegalVariantTypes = "default" | "outline" | "nonHover";
|
|
49
|
+
|
|
50
|
+
interface VariantType {
|
|
51
|
+
variant: LegalVariantTypes;
|
|
52
|
+
classes: string;
|
|
53
|
+
labelClasses: string;
|
|
54
|
+
insetLabelClasses: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const baseClasses = 'px-4 py-3 arial rounded-lg border border-[#B3B3B3] mx-2 bg-white ' +
|
|
58
|
+
'hover:outline-[#c6c6c6] active:outline-[#0256ab] focus:outline-4 focus:outline-[#fa89f1] text-base md:text-lg';
|
|
59
|
+
|
|
60
|
+
const variants: VariantType[] = [
|
|
61
|
+
{
|
|
62
|
+
variant: 'default',
|
|
63
|
+
classes: 'hover:bg-dha-mc-pale-blue text-black hover:dha-mc-primary-text ' +
|
|
64
|
+
'hover:border-dha-mc-secondary-border disabled:bg-dha-mc-bottom-nav-background ' +
|
|
65
|
+
'disabled:text-dha-mc-checkbox-inactive disabled:border-[#F2F2F2] ' +
|
|
66
|
+
'disabled:border w-[90vw] lg:max-w-[25em]',
|
|
67
|
+
labelClasses: 'ms-2.5 text-base',
|
|
68
|
+
insetLabelClasses: 'absolute ms-5 -translate-y-1/2 px-2 text-[0.8em] w-auto h-auto',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
variant: 'outline',
|
|
72
|
+
classes: 'border-dha-mc-true-blue bg-white border text-dha-mc-true-blue ' +
|
|
73
|
+
'disabled:border-[#F2F2F2] disabled:text-dha-mc-checkbox-inactive w-[35vw] ' +
|
|
74
|
+
'min-w-min sm:max-w-[15em]',
|
|
75
|
+
labelClasses: 'ms-2.5 text-base',
|
|
76
|
+
insetLabelClasses: 'absolute ms-5 -translate-y-1/2 px-2 text-[0.8em] w-auto h-auto',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
variant: 'nonHover',
|
|
80
|
+
classes: 'text-black disabled:bg-dha-mc-bottom-nav-background ' +
|
|
81
|
+
'disabled:text-dha-mc-checkbox-inactive disabled:border-[#F2F2F2] ' +
|
|
82
|
+
'disabled:border w-[90vw] lg:max-w-[25em]',
|
|
83
|
+
labelClasses: 'ms-2.5 text-base',
|
|
84
|
+
insetLabelClasses: 'absolute ms-5 -translate-y-1/2 px-2 text-[0.8em] w-auto h-auto',
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
export interface InputProps
|
|
89
|
+
extends InputHTMLAttributes<HTMLInputElement> {
|
|
90
|
+
className?: string;
|
|
91
|
+
classNameLabel?: string;
|
|
92
|
+
classNameRequired?: string
|
|
93
|
+
variant?: string;
|
|
94
|
+
label?: string;
|
|
95
|
+
insetLabel?: boolean;
|
|
96
|
+
labelBaseColor?: string;
|
|
97
|
+
// labelInputColor?: string;
|
|
98
|
+
textShadow?: boolean;
|
|
99
|
+
error?: boolean;
|
|
100
|
+
mask?: string;
|
|
101
|
+
disabled?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
105
|
+
(
|
|
106
|
+
{
|
|
107
|
+
className,
|
|
108
|
+
variant = 'default',
|
|
109
|
+
label,
|
|
110
|
+
insetLabel = false,
|
|
111
|
+
classNameLabel = '',
|
|
112
|
+
classNameRequired = '',
|
|
113
|
+
labelBaseColor = '#fff',
|
|
114
|
+
error = false,
|
|
115
|
+
// labelInputColor = '#fff',
|
|
116
|
+
textShadow = false,
|
|
117
|
+
mask,
|
|
118
|
+
disabled,
|
|
119
|
+
...props
|
|
120
|
+
},
|
|
121
|
+
ref
|
|
122
|
+
) => {
|
|
123
|
+
const [labelBackground, setLabelBackground] = useState<string | undefined>();
|
|
124
|
+
const [inputClasses, setInputClasses] = useState(baseClasses);
|
|
125
|
+
const [labelClasses, setLabelClasses] = useState('');
|
|
126
|
+
const [hover, setHover] = useState<boolean>(false);
|
|
127
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
128
|
+
const [value, setValue] = useState('');
|
|
129
|
+
|
|
130
|
+
const errorClasses = 'border-[#D54309] border';
|
|
131
|
+
|
|
132
|
+
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
133
|
+
const newValue = event.target.value;
|
|
134
|
+
if (mask) {
|
|
135
|
+
setValue(applyMask(newValue));
|
|
136
|
+
} else {
|
|
137
|
+
setValue(newValue);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Takes the input string and validates it against the mask prop. If validation
|
|
143
|
+
* is good, it returns a string built from the input formatted to match the mask.
|
|
144
|
+
*
|
|
145
|
+
* @param input
|
|
146
|
+
*/
|
|
147
|
+
function applyMask(input: string): string {
|
|
148
|
+
|
|
149
|
+
let output = "";
|
|
150
|
+
let maskPosition = 0;
|
|
151
|
+
|
|
152
|
+
if (mask) {
|
|
153
|
+
while (input.length > 0 && maskPosition < mask.length) {
|
|
154
|
+
|
|
155
|
+
// let logString = '';
|
|
156
|
+
|
|
157
|
+
if (mask[maskPosition] !== '#' && mask[maskPosition] !== '@') {
|
|
158
|
+
// logString += 'mask is not letter or number, ';
|
|
159
|
+
// if char at mask & input match
|
|
160
|
+
if (mask[maskPosition] === input[0]) {
|
|
161
|
+
// logString += 'mask === input, assigning input[0] to output';
|
|
162
|
+
output += input[0];
|
|
163
|
+
input = input.slice(1);
|
|
164
|
+
maskPosition++;
|
|
165
|
+
// add mask char to output if input[0] chartype matches mask
|
|
166
|
+
// char for maskPosition + 1
|
|
167
|
+
} else
|
|
168
|
+
if (
|
|
169
|
+
( (isLetter(input[0]) && nextMaskCharType(mask, maskPosition) === '@') ||
|
|
170
|
+
(isNumber(input[0]) && nextMaskCharType(mask, maskPosition) === '#') )
|
|
171
|
+
)
|
|
172
|
+
{
|
|
173
|
+
// logString += 'mask !== input && following charType is correct, ' +
|
|
174
|
+
// 'assigning mask char to output - ' + mask[maskPosition];
|
|
175
|
+
output += mask[maskPosition];
|
|
176
|
+
maskPosition++;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// logString += ' incrementing maskPosition'
|
|
180
|
+
maskPosition++;
|
|
181
|
+
}
|
|
182
|
+
// we have a num/alpha character
|
|
183
|
+
} else {
|
|
184
|
+
// logString += 'mask is letter/number, '
|
|
185
|
+
// if mask & input are both numbers
|
|
186
|
+
if (mask[maskPosition] === '#' && isNumber(input[0])) {
|
|
187
|
+
// logString += 'is number, assigning input[0] to output';
|
|
188
|
+
output += input[0];
|
|
189
|
+
input = input.slice(1);
|
|
190
|
+
maskPosition++;
|
|
191
|
+
// if mask & input are both letters
|
|
192
|
+
} else if (mask[maskPosition] === '@' && isLetter(input[0])) {
|
|
193
|
+
// logString += 'is letter, assigning input[0] to output';
|
|
194
|
+
output += input[0];
|
|
195
|
+
input = input.slice(1);
|
|
196
|
+
maskPosition++;
|
|
197
|
+
} else {
|
|
198
|
+
// throw away this character - incorrect char type per mask
|
|
199
|
+
// logString += 'does not match mask type, ignoring'
|
|
200
|
+
input = input.slice(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// console.log(logString);
|
|
205
|
+
// console.log('output: ' + output);
|
|
206
|
+
// console.log('maskPosition: ' + (maskPosition - 1));
|
|
207
|
+
// console.log('input.length: ' + input.length);
|
|
208
|
+
// console.log('input: ' + input);
|
|
209
|
+
// console.log('------------------------------');
|
|
210
|
+
// console.log('\n\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return output;
|
|
216
|
+
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (variant) {
|
|
222
|
+
setInputClasses(twMerge(baseClasses, variants.find((v) => v.variant === variant)?.classes,
|
|
223
|
+
error && errorClasses, className)); // display error state if error state boolean value true
|
|
224
|
+
if (insetLabel)
|
|
225
|
+
setLabelClasses(twMerge(variants.find((v) => v.variant === variant)?.insetLabelClasses, classNameLabel));
|
|
226
|
+
else
|
|
227
|
+
setLabelClasses(twMerge(variants.find((v) => v.variant === variant)?.labelClasses, classNameLabel));
|
|
228
|
+
}
|
|
229
|
+
}, [className, classNameLabel, variant, hover, insetLabel, error]);
|
|
230
|
+
|
|
231
|
+
// Update label background to match input's default and hover state
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (insetLabel) {
|
|
234
|
+
const inputEl = inputRef.current;
|
|
235
|
+
|
|
236
|
+
if (!inputEl) return;
|
|
237
|
+
|
|
238
|
+
// (boolean) --> setHover(bool-value)
|
|
239
|
+
const updateLabelBackground = (isHover: boolean) => {
|
|
240
|
+
const computedStyle = getComputedStyle(inputEl);
|
|
241
|
+
setLabelBackground(computedStyle.backgroundColor);
|
|
242
|
+
setHover(isHover);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Update label background initially and on hover
|
|
246
|
+
updateLabelBackground(true);
|
|
247
|
+
|
|
248
|
+
// event listeners
|
|
249
|
+
inputEl.addEventListener('mouseover', () => updateLabelBackground(true));
|
|
250
|
+
inputEl.addEventListener('mouseout', () => updateLabelBackground(false));
|
|
251
|
+
|
|
252
|
+
return () => {
|
|
253
|
+
inputEl.removeEventListener('mouseover', () => updateLabelBackground);
|
|
254
|
+
inputEl.removeEventListener('mouseout', () => updateLabelBackground);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
}
|
|
258
|
+
}, [labelBaseColor, insetLabel, hover, disabled]);
|
|
259
|
+
|
|
260
|
+
// Ensure the variant is valid
|
|
261
|
+
if (!["default", "outline", "nonHover"].includes(variant)) {
|
|
262
|
+
// return (<div>invalid variant on Input</div>)
|
|
263
|
+
throw new Error(
|
|
264
|
+
`Invalid variant: "${variant}". Valid variants are: "default", "outline", and "nonHover".`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<span className="group relative">
|
|
270
|
+
{/* Label */}
|
|
271
|
+
|
|
272
|
+
<div
|
|
273
|
+
className={twMerge(`${!insetLabel && 'pb-2'}`, labelClasses)}
|
|
274
|
+
style={insetLabel ? {
|
|
275
|
+
background: `linear-gradient(to bottom, ${labelBaseColor} 0%, ${labelBaseColor} 50%, ${labelBackground} 60%, ${labelBackground} 100%)`,
|
|
276
|
+
textShadow: textShadow ?
|
|
277
|
+
`1px 1px 2px ${labelBaseColor}, 0 0 1em ${labelBaseColor}, 0 0 0.2em ${labelBaseColor}`
|
|
278
|
+
: '',
|
|
279
|
+
} : {textShadow: textShadow ?
|
|
280
|
+
`1px 1px 2px ${labelBaseColor}, 0 0 1em ${labelBaseColor}, 0 0 0.2em ${labelBaseColor}`
|
|
281
|
+
: '' }}
|
|
282
|
+
>
|
|
283
|
+
{label}
|
|
284
|
+
{props.required && <span className={twMerge("absolute text-red-500 ms-0.5 -mt-1", classNameRequired)}>*</span>}
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{/* Input */}
|
|
288
|
+
<input
|
|
289
|
+
className={inputClasses}
|
|
290
|
+
aria-label={label}
|
|
291
|
+
value={value}
|
|
292
|
+
onChange={handleInputChange}
|
|
293
|
+
disabled={disabled}
|
|
294
|
+
// className={cn(inputVariants({variant, inputSize, className}))}
|
|
295
|
+
ref={(node) => {
|
|
296
|
+
if (typeof ref === 'function') ref(node);
|
|
297
|
+
else if (ref) ref.current = node;
|
|
298
|
+
inputRef.current = node; // Maintain local reference
|
|
299
|
+
}}
|
|
300
|
+
{...props}
|
|
301
|
+
/>
|
|
302
|
+
</span>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
Input.displayName = 'Input';
|
|
308
|
+
|
|
309
|
+
export { Input };
|