Skip to content

Commit 396bfd3

Browse files
BN-19 | Add. Basic Clinical Layout (#15)
* BN-19 | Refactor. Extract header out of clinical layout * BN-19 | Refactor. Call ClinicalLayout From ConsultationPage * BN-19 | Add. CSS Module Import Setup * BN-19 | Add. Basic Clinical Layout * BN-19 | Add. Sidebar and SidebarItem Component * BN-19 | Add. Basic Consulatation Page Layout Setup --------- Co-authored-by: arshiyaTW2021 <arshiya.shehzad@thoughtworks.com>
1 parent 4f40e84 commit 396bfd3

File tree

22 files changed

+1359
-143
lines changed

22 files changed

+1359
-143
lines changed

declarations.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module '*.module.scss' {
2+
const classes: { [key: string]: string };
3+
export = classes;
4+
}

src/App.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
import React from 'react';
22
import { Routes, Route } from 'react-router-dom';
33
import { Content } from '@carbon/react';
4-
import ClinicalLayout from './layouts/clinical/ClinicalLayout';
54
import HomePage from './pages/ConsultationPage';
65
import NotFoundPage from './pages/NotFoundPage';
76

87
const App: React.FC = () => {
98
return (
10-
<ClinicalLayout>
11-
<Content>
12-
<Routes>
13-
<Route path="/" element={<HomePage />} />
14-
<Route path="/clinical/:patientUuid" element={<HomePage />} />
15-
<Route path="*" element={<NotFoundPage />} />
16-
</Routes>
17-
</Content>
18-
</ClinicalLayout>
9+
<Content>
10+
<Routes>
11+
<Route path="/" element={<HomePage />} />
12+
<Route path="/clinical/:patientUuid" element={<HomePage />} />
13+
<Route path="*" element={<NotFoundPage />} />
14+
</Routes>
15+
</Content>
1916
);
2017
};
2118

src/__tests__/App.test.tsx

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,9 @@ import React from 'react';
22
import { render, screen } from '@testing-library/react';
33
import { MemoryRouter } from 'react-router-dom';
44
import App from '../App';
5-
import ClinicalLayout from '@layouts/clinical/ClinicalLayout';
65
import ConsultationPage from '@pages/ConsultationPage';
76
import NotFoundPage from '@pages/NotFoundPage';
87

9-
// Mock dependencies
10-
jest.mock('@layouts/clinical/ClinicalLayout', () => {
11-
return jest.fn(({ children }) => (
12-
<div data-testid="mock-main-layout">{children}</div>
13-
));
14-
});
15-
168
jest.mock('@pages/ConsultationPage', () => {
179
return jest.fn(() => <div data-testid="mock-home-page">Home Page</div>);
1810
});
@@ -41,7 +33,6 @@ describe('App Component', () => {
4133
</MemoryRouter>,
4234
);
4335

44-
expect(screen.getByTestId('mock-main-layout')).toBeInTheDocument();
4536
expect(screen.getByTestId('mock-carbon-content')).toBeInTheDocument();
4637
});
4738

@@ -83,21 +74,6 @@ describe('App Component', () => {
8374
expect(screen.queryByTestId('mock-home-page')).not.toBeInTheDocument();
8475
});
8576

86-
it('should wrap content with ClinicalLayout', () => {
87-
render(
88-
<MemoryRouter>
89-
<App />
90-
</MemoryRouter>,
91-
);
92-
93-
expect(ClinicalLayout).toHaveBeenCalled();
94-
const clinicalLayout = screen.getByTestId('mock-main-layout');
95-
expect(clinicalLayout).toBeInTheDocument();
96-
expect(clinicalLayout).toContainElement(
97-
screen.getByTestId('mock-carbon-content'),
98-
);
99-
});
100-
10177
it('should render Routes component correctly', () => {
10278
render(
10379
<MemoryRouter>

src/__tests__/__snapshots__/App.test.tsx.snap

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,12 @@
33
exports[`App Component should match snapshot 1`] = `
44
<DocumentFragment>
55
<div
6-
data-testid="mock-main-layout"
6+
data-testid="mock-carbon-content"
77
>
88
<div
9-
data-testid="mock-carbon-content"
9+
data-testid="mock-home-page"
1010
>
11-
<div
12-
data-testid="mock-home-page"
13-
>
14-
Home Page
15-
</div>
11+
Home Page
1612
</div>
1713
</div>
1814
</DocumentFragment>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
import {
3+
Header as CarbonHeader,
4+
HeaderName,
5+
HeaderNavigation,
6+
HeaderMenuItem,
7+
} from '@carbon/react';
8+
import BahmniIcon from '@components/common/bahmniIcon/BahmniIcon';
9+
import { ICON_SIZE } from '@constants/icon';
10+
11+
/**
12+
* Header component for the Bahmni Clinical application
13+
* Provides navigation and branding elements
14+
*
15+
* @returns {React.ReactElement} The Header component
16+
*/
17+
const Header: React.FC = () => {
18+
return (
19+
<CarbonHeader aria-label="Bahmni Clinical">
20+
<HeaderName href="/" prefix="">
21+
Bahmni Clinical
22+
</HeaderName>
23+
<HeaderNavigation aria-label="Main Navigation">
24+
<HeaderMenuItem href="/" aria-label="Bahmni Home">
25+
<BahmniIcon
26+
name="fa-home"
27+
size={ICON_SIZE.LG}
28+
id="homeIcon"
29+
></BahmniIcon>
30+
Home
31+
</HeaderMenuItem>
32+
</HeaderNavigation>
33+
</CarbonHeader>
34+
);
35+
};
36+
37+
export default Header;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { BrowserRouter } from 'react-router-dom';
4+
import Header from '../Header';
5+
6+
// Mock FontAwesomeIcon for BahmniIcon
7+
jest.mock('@fortawesome/react-fontawesome', () => ({
8+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9+
FontAwesomeIcon: ({ icon, size, color, ...props }: any) => (
10+
<svg
11+
data-testid={props['data-testid']}
12+
data-icon={icon[1]}
13+
data-prefix={icon[0]}
14+
data-size={size}
15+
data-color={color}
16+
{...props}
17+
/>
18+
),
19+
}));
20+
21+
describe('Header Component', () => {
22+
// Helper function to render the component with router
23+
const renderWithRouter = () =>
24+
render(
25+
<BrowserRouter>
26+
<Header />
27+
</BrowserRouter>,
28+
);
29+
30+
// Happy Path Tests
31+
describe('Happy Path', () => {
32+
test('renders the header with correct aria-label', () => {
33+
renderWithRouter();
34+
const header = screen.getByRole('banner');
35+
expect(header).toHaveAttribute('aria-label', 'Bahmni Clinical');
36+
});
37+
38+
test('renders the Bahmni Clinical header text', () => {
39+
renderWithRouter();
40+
const headerText = screen.getByText('Bahmni Clinical');
41+
expect(headerText).toBeInTheDocument();
42+
expect(headerText.tagName).toBe('A'); // Should be a link
43+
expect(headerText).toHaveAttribute('href', '/');
44+
});
45+
46+
test('renders the Home menu item with icon', () => {
47+
renderWithRouter();
48+
const homeMenuItem = screen.getByText('Home');
49+
expect(homeMenuItem).toBeInTheDocument();
50+
51+
// Check if it's a link to the home page
52+
const homeLink = homeMenuItem.closest('a');
53+
expect(homeLink).toHaveAttribute('href', '/');
54+
55+
// Check if the icon is rendered
56+
const homeIcon = screen.getByTestId('homeIcon');
57+
expect(homeIcon).toBeInTheDocument();
58+
});
59+
60+
test('renders the HeaderNavigation with correct aria-label', () => {
61+
renderWithRouter();
62+
const navigation = screen.getByRole('navigation');
63+
expect(navigation).toHaveAttribute('aria-label', 'Main Navigation');
64+
});
65+
});
66+
67+
// Edge Cases
68+
describe('Edge Cases', () => {
69+
test('supports keyboard navigation', () => {
70+
renderWithRouter();
71+
72+
// Get all interactive elements
73+
const interactiveElements = screen.getAllByRole('link');
74+
75+
// Ensure they have proper tab index
76+
interactiveElements.forEach((element) => {
77+
expect(element.tabIndex).not.toBe(-1);
78+
});
79+
});
80+
});
81+
82+
// Accessibility Tests
83+
describe('Accessibility', () => {
84+
test('all interactive elements have accessible names', () => {
85+
renderWithRouter();
86+
87+
// Check all buttons and links have accessible names
88+
const links = screen.getAllByRole('link', { hidden: true });
89+
90+
links.forEach((element) => {
91+
expect(element).toHaveAccessibleName();
92+
});
93+
});
94+
});
95+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import SidebarItem, { SidebarItemProps } from './SidebarItem';
3+
import * as styles from './styles/Sidebar.module.scss';
4+
5+
/**
6+
* Sidebar component that renders a vertical list of sidebar items.
7+
*
8+
* @component
9+
* @param {SidebarItemProps[]} items - Array of sidebar items to render
10+
* @param {string} [className] - Optional CSS class name for additional styling
11+
*/
12+
interface SidebarProps {
13+
items: SidebarItemProps[];
14+
className?: string;
15+
}
16+
17+
const Sidebar: React.FC<SidebarProps> = ({ items, className = '' }) => {
18+
return (
19+
<div className={`${styles.sidebar} ${className}`} data-testid="sidebar">
20+
{items.map((item) => (
21+
<SidebarItem
22+
key={item.id}
23+
id={item.id}
24+
icon={item.icon}
25+
label={item.label}
26+
active={item.active}
27+
action={item.action}
28+
/>
29+
))}
30+
</div>
31+
);
32+
};
33+
34+
export default Sidebar;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from 'react';
2+
import BahmniIcon from '@components/common/bahmniIcon/BahmniIcon';
3+
import * as styles from './styles/Sidebar.module.scss';
4+
import { ICON_SIZE } from '@constants/icon';
5+
6+
/**
7+
* SidebarItem component displays a single item in the sidebar with an icon and label.
8+
* It can be in an active or inactive state and can handle click actions.
9+
*
10+
* @component
11+
* @param {string} id - Unique identifier for the item
12+
* @param {string} icon - Icon name in FontAwesome format (e.g., "fa-clipboard-list")
13+
* @param {string} label - Display text for the item
14+
* @param {boolean} [active=false] - Whether the item is currently active/selected
15+
* @param {function} [action] - Callback function executed when the item is clicked
16+
*/
17+
export interface SidebarItemProps {
18+
id: string;
19+
icon: string;
20+
label: string;
21+
active?: boolean;
22+
action?: () => void;
23+
}
24+
25+
const SidebarItem: React.FC<SidebarItemProps> = ({
26+
id,
27+
icon,
28+
label,
29+
active = false,
30+
action,
31+
}) => {
32+
const handleClick = () => {
33+
if (action) {
34+
action();
35+
}
36+
};
37+
38+
return (
39+
<div
40+
className={`${styles.sidebarItem} ${active ? styles.active : ''}`}
41+
onClick={handleClick}
42+
data-testid={`sidebar-item-${id}`}
43+
>
44+
<BahmniIcon
45+
name={icon}
46+
id={`sidebar-icon-${id}`}
47+
color={active ? 'var(--cds-link-primary)' : 'var(--cds-text-secondary)'}
48+
size={ICON_SIZE.SM}
49+
/>
50+
<span className={styles.label}>{label}</span>
51+
</div>
52+
);
53+
};
54+
55+
export default SidebarItem;

0 commit comments

Comments
 (0)