@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,79 @@
|
|
|
1
|
+
import { forwardRef, HTMLAttributes, ReactNode, useEffect, useState } from 'react';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
import clock from '../assets/img/clock.svg';
|
|
5
|
+
import check from '../assets/img/check.svg';
|
|
6
|
+
import close from '../assets/img/close2.svg';
|
|
7
|
+
|
|
8
|
+
interface VariantType {
|
|
9
|
+
variant?: string;
|
|
10
|
+
classes?: string;
|
|
11
|
+
imgDivClasses?: string;
|
|
12
|
+
childClasses?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const variants: VariantType[] = [
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
variant: 'available',
|
|
19
|
+
classes: 'h-[45px] px-4 py-3 bg-green-50 rounded-[100px] border border-green-700 justify-center items-center gap-2 inline-flex',
|
|
20
|
+
imgDivClasses: 'w-5 h-5 relative overflow-hidden',
|
|
21
|
+
childClasses: 'text-green-900 text-lg font-normal font-[Arial]',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
variant: 'inProgress',
|
|
25
|
+
classes: 'h-[45px] px-4 py-3 bg-yellow-50 rounded-[100px] border border-yellow-600 justify-center items-center gap-2 inline-flex',
|
|
26
|
+
imgDivClasses: 'w-5 h-5 relative overflow-hidden',
|
|
27
|
+
childClasses: 'text-yellow-900 text-lg font-normal font-[Arial]',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
variant: 'notAvailable',
|
|
31
|
+
classes: 'h-[45px] px-4 py-3 bg-red-50 rounded-[100px] border border-red-600 justify-center items-center gap-2 inline-flex',
|
|
32
|
+
imgDivClasses: 'w-5 h-5 relative overflow-hidden',
|
|
33
|
+
childClasses: 'text-red-900 text-lg font-normal font-[Arial]',
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
export interface StatusProps extends HTMLAttributes<HTMLDivElement> {
|
|
38
|
+
variant?: string;
|
|
39
|
+
className?: string;
|
|
40
|
+
classNameChild?: string;
|
|
41
|
+
image?: ReactNode;
|
|
42
|
+
classNameImage?: string;
|
|
43
|
+
children?: ReactNode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Status = forwardRef<HTMLDivElement, StatusProps>(
|
|
47
|
+
({ variant = 'available', className, image, classNameChild,
|
|
48
|
+
classNameImage, children, ...props }, ref) => {
|
|
49
|
+
|
|
50
|
+
const [classValue, setClassValue] = useState<string>();
|
|
51
|
+
const [imgDivClasses, setImgDivClasses] = useState<string>();
|
|
52
|
+
const [childDivClasses, setChildDivClasses] = useState<string>();
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
|
|
56
|
+
setClassValue(twMerge(
|
|
57
|
+
variants.find((v) => v.variant === variant)?.classes, className));
|
|
58
|
+
|
|
59
|
+
setChildDivClasses(twMerge(
|
|
60
|
+
variants.find((v) => v.variant === variant)?.childClasses, classNameChild));
|
|
61
|
+
|
|
62
|
+
setImgDivClasses(twMerge(
|
|
63
|
+
variants.find((v) => v.variant === variant)?.imgDivClasses, classNameImage));
|
|
64
|
+
|
|
65
|
+
}, [variant, className, classNameChild, classNameImage])
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className={classValue} ref={ref}>
|
|
69
|
+
<div className={imgDivClasses}>
|
|
70
|
+
{ image ? image : // if we have an image prop - display
|
|
71
|
+
variant === 'available' ? // else if we are variant 'available' ...
|
|
72
|
+
<img src={check} alt="check mark" /> :
|
|
73
|
+
variant === 'inProgress' ? <img src={clock} alt="clock" /> : // else we are variant 'inProgress' ...
|
|
74
|
+
<img src={close} alt="close" /> // else we are non of the above, i.e. 'notAvailable'
|
|
75
|
+
}</div>
|
|
76
|
+
<div className={childDivClasses}>{children}</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
});
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { Meta, StoryContext, StoryObj } from '@storybook/react';
|
|
2
|
+
import { expect, userEvent, within, waitFor } from 'storybook/test';
|
|
3
|
+
import { Tabs, IconPosition } from './Tabs';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Tabs> = {
|
|
6
|
+
component: Tabs,
|
|
7
|
+
title: 'Components/Tabs',
|
|
8
|
+
argTypes: {
|
|
9
|
+
variant: {
|
|
10
|
+
control: 'select',
|
|
11
|
+
options: ['default', 'outline', 'filled'],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
export default meta;
|
|
16
|
+
|
|
17
|
+
type Story = StoryObj<typeof Tabs>;
|
|
18
|
+
|
|
19
|
+
// 🧪 Primary Story
|
|
20
|
+
export const Primary = {
|
|
21
|
+
args: {
|
|
22
|
+
variant: 'default',
|
|
23
|
+
tabs: [
|
|
24
|
+
{ id: 'tab1', label: 'Tab 1', content: <p>Content for Tab 1</p> },
|
|
25
|
+
{ id: 'tab2', label: 'Tab 2', content: <p>Content for Tab 2</p> },
|
|
26
|
+
{ id: 'tab3', label: 'Tab 3', content: <p>Content for Tab 3</p> },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
play: async ({ canvasElement }: StoryContext) => {
|
|
30
|
+
const canvas = within(canvasElement);
|
|
31
|
+
|
|
32
|
+
// Check if the first tab is active by default
|
|
33
|
+
const tab1 = canvas.getByRole('tab', { name: 'Tab 1' });
|
|
34
|
+
expect(tab1).toHaveAttribute('aria-selected', 'true');
|
|
35
|
+
|
|
36
|
+
// Click on the second tab and verify its content is displayed
|
|
37
|
+
const tab2 = canvas.getByRole('tab', { name: 'Tab 2' });
|
|
38
|
+
await userEvent.click(tab2);
|
|
39
|
+
expect(tab2).toHaveAttribute('aria-selected', 'true');
|
|
40
|
+
expect(canvas.getByText('Content for Tab 2')).toBeVisible();
|
|
41
|
+
|
|
42
|
+
// Check that the first tab content is now hidden
|
|
43
|
+
expect(canvas.queryByText('Content for Tab 1')).not.toBeVisible();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const tabsData = [
|
|
48
|
+
{ id: 'tab1', label: 'Tab 1', content: <div>Content 1</div> },
|
|
49
|
+
{ id: 'tab2', label: 'Tab 2', content: <div>Content 2</div> },
|
|
50
|
+
{ id: 'tab3', label: 'Tab 3', content: <div>Content 3</div> },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export const KeyboardNavigation = {
|
|
54
|
+
args: {
|
|
55
|
+
variant: 'default',
|
|
56
|
+
tabs: tabsData,
|
|
57
|
+
},
|
|
58
|
+
play:async ({ canvasElement }: StoryContext) => {
|
|
59
|
+
const canvas = within(canvasElement);
|
|
60
|
+
const tabButtons = canvas.getAllByRole('tab');
|
|
61
|
+
|
|
62
|
+
// Focus the first tab and verify it has focus.
|
|
63
|
+
tabButtons[0].focus();
|
|
64
|
+
expect(tabButtons[0]).toHaveFocus();
|
|
65
|
+
|
|
66
|
+
// Press the "End" key to move focus to the last tab.
|
|
67
|
+
// This triggers the End key block setting the last tab as active.
|
|
68
|
+
await userEvent.keyboard('{End}');
|
|
69
|
+
const lastTab = tabButtons[tabButtons.length - 1];
|
|
70
|
+
expect(lastTab).toHaveFocus();
|
|
71
|
+
expect(lastTab.getAttribute('aria-selected')).toBe('true');
|
|
72
|
+
|
|
73
|
+
// Press the "Home" key to move focus back to the first tab.
|
|
74
|
+
// This triggers the Home key block setting the first tab as active.
|
|
75
|
+
await userEvent.keyboard('{Home}');
|
|
76
|
+
expect(tabButtons[0]).toHaveFocus();
|
|
77
|
+
expect(tabButtons[0].getAttribute('aria-selected')).toBe('true');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 🧪 Blank Variant Story
|
|
82
|
+
export const BlankVariant = {
|
|
83
|
+
args: {
|
|
84
|
+
variant: '',
|
|
85
|
+
tabs: [
|
|
86
|
+
{ id: 'tab1', label: 'Tab 1', content: <p>Content for Tab 1</p> },
|
|
87
|
+
{ id: 'tab2', label: 'Tab 2', content: <p>Content for Tab 2</p> },
|
|
88
|
+
{ id: 'tab3', label: 'Tab 3', content: <p>Content for Tab 3</p> },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
play: async ({ canvasElement }: StoryContext) => {
|
|
92
|
+
const canvas = within(canvasElement);
|
|
93
|
+
|
|
94
|
+
// Verify all tabs are rendered
|
|
95
|
+
expect(canvas.getByRole('tab', { name: 'Tab 1' })).toBeInTheDocument();
|
|
96
|
+
expect(canvas.getByRole('tab', { name: 'Tab 2' })).toBeInTheDocument();
|
|
97
|
+
expect(canvas.getByRole('tab', { name: 'Tab 3' })).toBeInTheDocument();
|
|
98
|
+
|
|
99
|
+
// Navigate through tabs using keyboard (ArrowRight and ArrowLeft)
|
|
100
|
+
const tab1 = canvas.getByRole('tab', { name: 'Tab 1' });
|
|
101
|
+
await userEvent.type(tab1, '{arrowright}');
|
|
102
|
+
expect(canvas.getByRole('tab', { name: 'Tab 2' })).toHaveFocus();
|
|
103
|
+
|
|
104
|
+
const tab2 = canvas.getByRole('tab', { name: 'Tab 2' });
|
|
105
|
+
await userEvent.type(tab2, '{arrowleft}');
|
|
106
|
+
expect(canvas.getByRole('tab', { name: 'Tab 1' })).toHaveFocus();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// 🧪 No Variant Story
|
|
111
|
+
export const NoVariant = {
|
|
112
|
+
args: {
|
|
113
|
+
tabs: [
|
|
114
|
+
{ id: 'tab1', label: 'Tab 1', content: <p>Content for Tab 1</p> },
|
|
115
|
+
{ id: 'tab2', label: 'Tab 2', content: <p>Content for Tab 2</p> },
|
|
116
|
+
{ id: 'tab3', label: 'Tab 3', content: <p>Content for Tab 3</p> },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
play: async ({ canvasElement }: StoryContext) => {
|
|
120
|
+
const canvas = within(canvasElement);
|
|
121
|
+
|
|
122
|
+
// Ensure the first tab content is visible by default
|
|
123
|
+
expect(canvas.getByText('Content for Tab 1')).toBeVisible();
|
|
124
|
+
|
|
125
|
+
// Click the third tab and verify its content is displayed
|
|
126
|
+
const tab3 = canvas.getByRole('tab', { name: 'Tab 3' });
|
|
127
|
+
await userEvent.click(tab3);
|
|
128
|
+
expect(tab3).toHaveAttribute('aria-selected', 'true');
|
|
129
|
+
expect(canvas.getByText('Content for Tab 3')).toBeVisible();
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// 🧪 Mismatched Content Story
|
|
134
|
+
export const MismatchedContent = {
|
|
135
|
+
args: {
|
|
136
|
+
tabs: [
|
|
137
|
+
{ id: 'tab1', label: 'Tab 1', content: <p>Content for Tab 1</p> },
|
|
138
|
+
{ id: 'tab2', label: 'Tab 2', content: (
|
|
139
|
+
<>
|
|
140
|
+
<p>Content for Tab 2</p>
|
|
141
|
+
<p>Lorem ipsum dolor sit amet...</p>
|
|
142
|
+
</>
|
|
143
|
+
)},
|
|
144
|
+
{ id: 'tab3', label: 'Tab 3', content: <p>Content for Tab 3</p> },
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
play: async ({ canvasElement }: StoryContext) => {
|
|
148
|
+
const canvas = within(canvasElement);
|
|
149
|
+
|
|
150
|
+
// Check that tab 2 content has multiple paragraphs
|
|
151
|
+
const tab2 = canvas.getByRole('tab', { name: 'Tab 2' });
|
|
152
|
+
await userEvent.click(tab2);
|
|
153
|
+
|
|
154
|
+
expect(canvas.getByText(/Content for Tab 2/)).toBeVisible();
|
|
155
|
+
expect(canvas.getByText(/Lorem ipsum dolor sit amet/)).toBeVisible();
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const HoverIconTest: Story = {
|
|
160
|
+
args: {
|
|
161
|
+
variant: 'outline',
|
|
162
|
+
tabs: [
|
|
163
|
+
{
|
|
164
|
+
id: 'tab1',
|
|
165
|
+
label: 'Left Icon',
|
|
166
|
+
content: <p>Left Content</p>,
|
|
167
|
+
activeIcon: <span data-testid="active-left">Active Left</span>,
|
|
168
|
+
inactiveIcon: <span data-testid="inactive-left">Inactive Left</span>,
|
|
169
|
+
iconPosition: IconPosition.Left,
|
|
170
|
+
},
|
|
171
|
+
// You can add additional tabs if needed
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
play: async ({ canvasElement }: StoryContext) => {
|
|
175
|
+
const canvas = within(canvasElement);
|
|
176
|
+
|
|
177
|
+
// Get all tab buttons and select the first one.
|
|
178
|
+
const tabButtons = canvas.getAllByRole('tab');
|
|
179
|
+
const firstTab = tabButtons[0];
|
|
180
|
+
|
|
181
|
+
// Verify that initially the active icon is displayed.
|
|
182
|
+
expect(canvas.getByTestId('active-left')).toBeInTheDocument();
|
|
183
|
+
|
|
184
|
+
// Hover over the first tab to trigger onMouseEnter.
|
|
185
|
+
await userEvent.hover(firstTab);
|
|
186
|
+
// With hover active, the inactive icon should now appear.
|
|
187
|
+
expect(canvas.getByTestId('inactive-left')).toBeInTheDocument();
|
|
188
|
+
|
|
189
|
+
// Unhover the tab to trigger onMouseLeave (this is the line we want to cover).
|
|
190
|
+
await userEvent.unhover(firstTab);
|
|
191
|
+
// After unhovering, the active icon should be rendered again.
|
|
192
|
+
expect(canvas.getByTestId('active-left')).toBeInTheDocument();
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const IconPositionsAndOutline: Story = {
|
|
197
|
+
args: {
|
|
198
|
+
variant: 'outline',
|
|
199
|
+
tabs: [
|
|
200
|
+
{
|
|
201
|
+
id: 'tab1',
|
|
202
|
+
label: 'Left Icon',
|
|
203
|
+
content: <p>Left Content</p>,
|
|
204
|
+
activeIcon: <span data-testid="active-left">Active Left</span>,
|
|
205
|
+
inactiveIcon: <span data-testid="inactive-left">Inactive Left</span>,
|
|
206
|
+
iconPosition: IconPosition.Left,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'tab2',
|
|
210
|
+
label: 'Icon Only',
|
|
211
|
+
content: <p>Icon Only Content</p>,
|
|
212
|
+
activeIcon: <span data-testid="active-icononly">Active IconOnly</span>,
|
|
213
|
+
inactiveIcon: <span data-testid="inactive-icononly">Inactive IconOnly</span>,
|
|
214
|
+
iconPosition: "iconOnly",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'tab3',
|
|
218
|
+
label: 'Right Icon',
|
|
219
|
+
content: <p>Right Content</p>,
|
|
220
|
+
activeIcon: <span data-testid="active-right">Active Right</span>,
|
|
221
|
+
inactiveIcon: <span data-testid="inactive-right">Inactive Right</span>,
|
|
222
|
+
iconPosition: "right",
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: 'tab4',
|
|
226
|
+
label: 'No Icon',
|
|
227
|
+
content: <p>No Icon Content</p>,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
play: async ({ canvasElement }: StoryContext) => {
|
|
232
|
+
const canvas = within(canvasElement);
|
|
233
|
+
const tabButtons = canvas.getAllByRole('tab');
|
|
234
|
+
|
|
235
|
+
// ─── Verify Outline Variant Button Classes ──────────────────────────────
|
|
236
|
+
// First tab should have left border classes.
|
|
237
|
+
expect(tabButtons[0].className).toContain('border-l-2');
|
|
238
|
+
expect(tabButtons[0].className).toContain('rounded-l-md');
|
|
239
|
+
|
|
240
|
+
// Last tab should have right border classes.
|
|
241
|
+
expect(tabButtons[3].className).toContain('border-r-2');
|
|
242
|
+
expect(tabButtons[3].className).toContain('rounded-r-md');
|
|
243
|
+
|
|
244
|
+
// A middle tab should have top and bottom border classes (and not left/right-specific).
|
|
245
|
+
expect(tabButtons[1].className).toContain('border-t-2');
|
|
246
|
+
expect(tabButtons[1].className).toContain('border-b-2');
|
|
247
|
+
expect(tabButtons[1].className).not.toContain('border-l-2');
|
|
248
|
+
expect(tabButtons[1].className).not.toContain('border-r-2');
|
|
249
|
+
|
|
250
|
+
// ─── Verify Icon Rendering Based on Icon Position and Active State ──────
|
|
251
|
+
// Initially, the first tab (index 0) is active.
|
|
252
|
+
expect(canvas.getByTestId('active-left')).toBeInTheDocument();
|
|
253
|
+
|
|
254
|
+
// For tab2 (Icon Only): since it's inactive, it should display the inactive icon.
|
|
255
|
+
expect(canvas.getByTestId('inactive-icononly')).toBeInTheDocument();
|
|
256
|
+
|
|
257
|
+
// For tab3 (Right Icon): inactive initially, so it should display the inactive icon.
|
|
258
|
+
expect(canvas.getByTestId('inactive-right')).toBeInTheDocument();
|
|
259
|
+
|
|
260
|
+
// For tab4 (No Icon): should display its label.
|
|
261
|
+
expect(canvas.getByText('No Icon')).toBeInTheDocument();
|
|
262
|
+
|
|
263
|
+
// ─── Simulate Tab Clicks to Check Active/Inactive Icon Updates ─────────
|
|
264
|
+
|
|
265
|
+
// Click on tab2 (Icon Only) so it becomes active.
|
|
266
|
+
await userEvent.click(tabButtons[1]);
|
|
267
|
+
// Simulate moving the mouse off the tab to clear the hover state.
|
|
268
|
+
await userEvent.unhover(tabButtons[1]);
|
|
269
|
+
expect(tabButtons[1]).toHaveAttribute('aria-selected', 'true');
|
|
270
|
+
|
|
271
|
+
// Now, tab2 should display its active icon.
|
|
272
|
+
// Using waitFor to ensure the component has updated.
|
|
273
|
+
await waitFor(() => {
|
|
274
|
+
expect(canvas.getByTestId('active-icononly')).toBeInTheDocument();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// And tab1 should now display its inactive icon.
|
|
278
|
+
expect(canvas.getByTestId('inactive-left')).toBeInTheDocument();
|
|
279
|
+
|
|
280
|
+
// Click on tab3 (Right Icon) so it becomes active.
|
|
281
|
+
await userEvent.click(tabButtons[2]);
|
|
282
|
+
await userEvent.unhover(tabButtons[2]);
|
|
283
|
+
expect(tabButtons[2]).toHaveAttribute('aria-selected', 'true');
|
|
284
|
+
await waitFor(() => {
|
|
285
|
+
expect(canvas.getByTestId('active-right')).toBeInTheDocument();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Click on tab4 (No Icon) so it becomes active.
|
|
289
|
+
await userEvent.click(tabButtons[3]);
|
|
290
|
+
await userEvent.unhover(tabButtons[3]);
|
|
291
|
+
expect(tabButtons[3]).toHaveAttribute('aria-selected', 'true');
|
|
292
|
+
expect(canvas.getByText('No Icon')).toBeInTheDocument();
|
|
293
|
+
},
|
|
294
|
+
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { axe } from "vitest-axe";
|
|
4
|
+
import { IconPosition, Tabs } from "./Tabs";
|
|
5
|
+
|
|
6
|
+
describe("Tabs Component", () => {
|
|
7
|
+
const tabs = [
|
|
8
|
+
{ id: "tab1", label: "Tab 1", content: <p>Content for Tab 1</p> },
|
|
9
|
+
{ id: "tab2", label: "Tab 2", content: <p>Content for Tab 2</p> },
|
|
10
|
+
{ id: "tab3", label: "Tab 3", content: <p>Content for Tab 3</p> },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
it("renders tabs and panels", () => {
|
|
14
|
+
render(<Tabs variant={''} tabs={tabs} />);
|
|
15
|
+
|
|
16
|
+
expect(screen.getByText("Tab 1")).toBeInTheDocument();
|
|
17
|
+
expect(screen.getByText("Content for Tab 1")).toBeVisible();
|
|
18
|
+
expect(screen.queryByText("Content for Tab 2")).not.toBeVisible();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("switches tabs on click", () => {
|
|
22
|
+
render(<Tabs variant={''} tabs={tabs} />);
|
|
23
|
+
fireEvent.click(screen.getByText("Tab 2"));
|
|
24
|
+
|
|
25
|
+
expect(screen.getByText("Content for Tab 2")).toBeVisible();
|
|
26
|
+
expect(screen.queryByText("Content for Tab 1")).not.toBeVisible();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("has no accessibility violations", async () => {
|
|
30
|
+
const { container } = render(<Tabs variant={''} tabs={tabs} />);
|
|
31
|
+
const results = await axe(container);
|
|
32
|
+
|
|
33
|
+
expect(results).toHaveNoViolations();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// it("navigates tabs with Arrow keys", () => {
|
|
37
|
+
// render(<Tabs variant={''} tabs={tabs} />);
|
|
38
|
+
// const tab1 = screen.getByText("Tab 1");
|
|
39
|
+
// const tab2 = screen.getByText("Tab 2");
|
|
40
|
+
|
|
41
|
+
// tab1.focus();
|
|
42
|
+
// fireEvent.keyDown(tab1, { key: "ArrowRight" });
|
|
43
|
+
// expect(tab2).toHaveFocus();
|
|
44
|
+
// });
|
|
45
|
+
|
|
46
|
+
// it("navigates to the first and last tabs using Home and End keys", () => {
|
|
47
|
+
// render(<Tabs variant={''} tabs={tabs} />);
|
|
48
|
+
// const tab2 = screen.getByText("Tab 2");
|
|
49
|
+
// const tab1 = screen.getByText("Tab 1");
|
|
50
|
+
// const tab3 = screen.getByText("Tab 3");
|
|
51
|
+
|
|
52
|
+
// tab2.focus();
|
|
53
|
+
// fireEvent.keyDown(tab2, { key: "Home" });
|
|
54
|
+
// expect(tab1).toHaveFocus();
|
|
55
|
+
|
|
56
|
+
// fireEvent.keyDown(tab2, { key: "End" });
|
|
57
|
+
// expect(tab3).toHaveFocus();
|
|
58
|
+
// });
|
|
59
|
+
|
|
60
|
+
// it("activates a tab with Enter or Space key", () => {
|
|
61
|
+
// render(<Tabs variant={''} tabs={tabs} />);
|
|
62
|
+
// const tab2 = screen.getByText("Tab 2");
|
|
63
|
+
|
|
64
|
+
// tab2.focus();
|
|
65
|
+
// fireEvent.keyDown(tab2, { key: "Enter" });
|
|
66
|
+
// expect(screen.getByText("Content for Tab 2")).toBeVisible();
|
|
67
|
+
|
|
68
|
+
// tab2.focus();
|
|
69
|
+
// fireEvent.keyDown(tab2, { key: " " }); // Space key
|
|
70
|
+
// expect(screen.getByText("Content for Tab 2")).toBeVisible();
|
|
71
|
+
// });
|
|
72
|
+
|
|
73
|
+
it("sets correct ARIA attributes for tabs and panels", () => {
|
|
74
|
+
render(<Tabs variant={''} tabs={tabs} />);
|
|
75
|
+
|
|
76
|
+
const tab1 = screen.getByRole("tab", { name: "Tab 1" });
|
|
77
|
+
const panel1 = screen.getByRole("tabpanel", { name: "Tab 1" });
|
|
78
|
+
|
|
79
|
+
expect(tab1).toHaveAttribute("aria-selected", "true");
|
|
80
|
+
expect(panel1).not.toHaveAttribute("hidden");
|
|
81
|
+
|
|
82
|
+
const tab2 = screen.getByRole("tab", { name: "Tab 2" });
|
|
83
|
+
expect(tab2).toHaveAttribute("aria-selected", "false");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
// it("focuses content inside the active tab panel", () => {
|
|
88
|
+
// render(
|
|
89
|
+
// <Tabs
|
|
90
|
+
// variant={''}
|
|
91
|
+
// tabs={[
|
|
92
|
+
// { id: "tab1", label: "Tab 1", content: <button>Focus Button</button> },
|
|
93
|
+
// { id: "tab2", label: "Tab 2", content: <p>Content for Tab 2</p> },
|
|
94
|
+
// ]}
|
|
95
|
+
// />
|
|
96
|
+
// );
|
|
97
|
+
|
|
98
|
+
// const button = screen.getByText("Focus Button");
|
|
99
|
+
// expect(button).toBeFocusable();
|
|
100
|
+
// });
|
|
101
|
+
|
|
102
|
+
it("renders correctly with no tabs", () => {
|
|
103
|
+
render(<Tabs variant={''} tabs={[]} />);
|
|
104
|
+
expect(screen.queryByRole("tab")).not.toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("sets the previous tab as active and focuses it when ArrowLeft is pressed", () => {
|
|
108
|
+
render(<Tabs variant="default" tabs={tabs} />);
|
|
109
|
+
|
|
110
|
+
// Get references to the tab elements
|
|
111
|
+
const tab1 = screen.getByRole("tab", { name: "Tab 1" });
|
|
112
|
+
const tab2 = screen.getByRole("tab", { name: "Tab 2" });
|
|
113
|
+
|
|
114
|
+
// Focus the second tab and press ArrowLeft
|
|
115
|
+
tab2.focus();
|
|
116
|
+
fireEvent.keyDown(tab2, { key: "ArrowLeft" });
|
|
117
|
+
|
|
118
|
+
// Verify that Tab 1 is now active and focused
|
|
119
|
+
expect(tab1).toHaveAttribute("aria-selected", "true");
|
|
120
|
+
expect(tab1).toHaveFocus();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("applies default variant when empty variant prop is provided", () => {
|
|
124
|
+
render(<Tabs variant="" tabs={tabs} />);
|
|
125
|
+
// The default container style should be applied:
|
|
126
|
+
// "flex border-b border-gray-200" from the default variant
|
|
127
|
+
const tabList = screen.getByRole("tablist");
|
|
128
|
+
expect(tabList.className).toMatch(/flex/);
|
|
129
|
+
expect(tabList.className).toMatch(/border-b/);
|
|
130
|
+
expect(tabList.className).toMatch(/border-gray-200/);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("navigates tabs with ArrowRight key", () => {
|
|
134
|
+
render(<Tabs variant="default" tabs={tabs} />);
|
|
135
|
+
const tab1 = screen.getByRole("tab", { name: "Tab 1" });
|
|
136
|
+
const tab2 = screen.getByRole("tab", { name: "Tab 2" });
|
|
137
|
+
tab1.focus();
|
|
138
|
+
fireEvent.keyDown(tab1, { key: "ArrowRight" });
|
|
139
|
+
expect(tab2).toHaveFocus();
|
|
140
|
+
expect(tab2).toHaveAttribute("aria-selected", "true");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("navigates to the first tab using Home key", () => {
|
|
144
|
+
render(<Tabs variant="default" tabs={tabs} />);
|
|
145
|
+
const tab1 = screen.getByRole("tab", { name: "Tab 1" });
|
|
146
|
+
const tab2 = screen.getByRole("tab", { name: "Tab 2" });
|
|
147
|
+
tab2.focus();
|
|
148
|
+
fireEvent.keyDown(tab2, { key: "Home" });
|
|
149
|
+
expect(tab1).toHaveFocus();
|
|
150
|
+
expect(tab1).toHaveAttribute("aria-selected", "true");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("navigates to the last tab using End key", () => {
|
|
154
|
+
render(<Tabs variant="default" tabs={tabs} />);
|
|
155
|
+
const tab2 = screen.getByRole("tab", { name: "Tab 2" });
|
|
156
|
+
const tab3 = screen.getByRole("tab", { name: "Tab 3" });
|
|
157
|
+
tab2.focus();
|
|
158
|
+
fireEvent.keyDown(tab2, { key: "End" });
|
|
159
|
+
expect(tab3).toHaveFocus();
|
|
160
|
+
expect(tab3).toHaveAttribute("aria-selected", "true");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("renders outline variant tabs with proper border classes", () => {
|
|
164
|
+
render(<Tabs variant="outline" tabs={tabs} />);
|
|
165
|
+
const tabButtons = screen.getAllByRole("tab");
|
|
166
|
+
// For the outline variant, the first tab should have a left border class (rounded-l-md)
|
|
167
|
+
expect(tabButtons[0].className).toMatch(/rounded-l-md/);
|
|
168
|
+
// The last tab should have a right border class (rounded-r-md)
|
|
169
|
+
expect(tabButtons[tabButtons.length - 1].className).toMatch(/rounded-r-md/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("renders iconOnly tab with icon instead of label", () => {
|
|
173
|
+
const iconOnlyTabs = [
|
|
174
|
+
{
|
|
175
|
+
id: "iconTab",
|
|
176
|
+
label: "Should not show",
|
|
177
|
+
content: <p>Icon Only Content</p>,
|
|
178
|
+
activeIcon: <span data-testid="active-icon-iconOnly">ActiveIcon</span>,
|
|
179
|
+
inactiveIcon: <span data-testid="inactive-icon-iconOnly">InactiveIcon</span>,
|
|
180
|
+
iconPosition: IconPosition.IconOnly,
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
render(<Tabs variant="default" tabs={iconOnlyTabs} />);
|
|
184
|
+
// By default, the first (and only) tab is active so it should display the activeIcon
|
|
185
|
+
expect(screen.getByTestId("active-icon-iconOnly")).toBeInTheDocument();
|
|
186
|
+
// The label should not be rendered because iconOnly is true
|
|
187
|
+
expect(screen.queryByText("Should not show")).not.toBeInTheDocument();
|
|
188
|
+
// Simulate hover to trigger icon toggle
|
|
189
|
+
const tabButton = screen.getByRole("tab");
|
|
190
|
+
fireEvent.mouseEnter(tabButton);
|
|
191
|
+
// When hovered, the active tab should display the inactiveIcon per the component logic
|
|
192
|
+
expect(screen.getByTestId("inactive-icon-iconOnly")).toBeInTheDocument();
|
|
193
|
+
});
|
|
194
|
+
it("renders tab with left icon and toggles icon on hover", () => {
|
|
195
|
+
const leftIconTabs = [
|
|
196
|
+
{
|
|
197
|
+
id: "leftTab",
|
|
198
|
+
label: "Left Icon Tab",
|
|
199
|
+
content: <p>Left Icon Content</p>,
|
|
200
|
+
activeIcon: <span data-testid="active-icon-left">ActiveIcon</span>,
|
|
201
|
+
inactiveIcon: <span data-testid="inactive-icon-left">InactiveIcon</span>,
|
|
202
|
+
iconPosition: IconPosition.Left,
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
render(<Tabs variant="default" tabs={leftIconTabs} />);
|
|
206
|
+
// Use a regex that matches a part of the accessible name (which includes the icon text)
|
|
207
|
+
const tabButton = screen.getByRole("tab", { name: /Left Icon Tab/ });
|
|
208
|
+
// When active and not hovered, it should show the active icon.
|
|
209
|
+
expect(screen.getByTestId("active-icon-left")).toBeInTheDocument();
|
|
210
|
+
// Simulate mouse enter (hover)
|
|
211
|
+
fireEvent.mouseEnter(tabButton);
|
|
212
|
+
expect(screen.getByTestId("inactive-icon-left")).toBeInTheDocument();
|
|
213
|
+
// Simulate mouse leave so that hover resets.
|
|
214
|
+
fireEvent.mouseLeave(tabButton);
|
|
215
|
+
expect(screen.getByTestId("active-icon-left")).toBeInTheDocument();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("renders tab with right icon and toggles icon on hover", () => {
|
|
219
|
+
const rightIconTabs = [
|
|
220
|
+
{
|
|
221
|
+
id: "rightTab",
|
|
222
|
+
label: "Right Icon Tab",
|
|
223
|
+
content: <p>Right Icon Content</p>,
|
|
224
|
+
activeIcon: <span data-testid="active-icon-right">ActiveIcon</span>,
|
|
225
|
+
inactiveIcon: <span data-testid="inactive-icon-right">InactiveIcon</span>,
|
|
226
|
+
iconPosition: IconPosition.Right,
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
render(<Tabs variant="default" tabs={rightIconTabs} />);
|
|
230
|
+
// Use a regex that matches a part of the accessible name (which includes both the label and icon text)
|
|
231
|
+
const tabButton = screen.getByRole("tab", { name: /Right Icon Tab/ });
|
|
232
|
+
// When active and not hovered, it should show the active icon.
|
|
233
|
+
expect(screen.getByTestId("active-icon-right")).toBeInTheDocument();
|
|
234
|
+
// Simulate mouse enter (hover)
|
|
235
|
+
fireEvent.mouseEnter(tabButton);
|
|
236
|
+
expect(screen.getByTestId("inactive-icon-right")).toBeInTheDocument();
|
|
237
|
+
// Simulate mouse leave so that hover resets.
|
|
238
|
+
fireEvent.mouseLeave(tabButton);
|
|
239
|
+
expect(screen.getByTestId("active-icon-right")).toBeInTheDocument();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("renders tab panels with correct aria-labelledby attributes", () => {
|
|
243
|
+
render(<Tabs variant="default" tabs={tabs} />);
|
|
244
|
+
const panels = screen.getAllByRole("tabpanel");
|
|
245
|
+
panels.forEach((panel, index) => {
|
|
246
|
+
expect(panel).toHaveAttribute("aria-labelledby", `tab-${tabs[index].id}`);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|