Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion apps/www/src/content/docs/components/navbar/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Navbar } from "@raystack/apsara";

<Navbar>
<Navbar.Start />
<Navbar.Center />
<Navbar.End />
</Navbar>
```
Expand All @@ -41,6 +42,12 @@ The start section is a container component that accepts all `div` props. It's co

<auto-type-table path="./props.ts" name="NavbarStartProps" />

### Center

The center section sits between Start and End and centers its content. It accepts all `div` props.

<auto-type-table path="./props.ts" name="NavbarCenterProps" />

### End

The end section is a container component that accepts all `div` props. It's commonly used for search inputs, action buttons, user menus, or secondary navigation.
Expand Down Expand Up @@ -73,7 +80,7 @@ The Navbar implements the following accessibility features:

- Proper ARIA roles and attributes
- `role="navigation"` for the main navbar
- `role="group"` for Start and End sections when `aria-label` is provided
- `role="group"` for Start, Center, and End sections when `aria-label` is provided
- Customizable `aria-label` and `aria-labelledby` support

- Semantic HTML
Expand Down
13 changes: 13 additions & 0 deletions apps/www/src/content/docs/components/navbar/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ export interface NavbarRootProps {
*/
sticky?: boolean;

/**
* Show the bottom shadow.
* @default true
*/
shadow?: boolean;

/**
* Accessible label for the navigation.
* Use this to provide a description of the navbar's purpose.
Expand All @@ -27,6 +33,13 @@ export interface NavbarStartProps {
'aria-label'?: string;
}

export interface NavbarCenterProps {
/**
* Accessible label for the center section. When provided, the section will have `role="group"`.
*/
'aria-label'?: string;
}

export interface NavbarEndProps {
/**
* Accessible label for the end section. Use this to describe the purpose
Expand Down
48 changes: 46 additions & 2 deletions packages/raystack/components/navbar/__tests__/navbar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { Navbar } from '../navbar';
import { NavbarRootProps } from '../navbar-root';
import styles from '../navbar.module.css';
import { NavbarRootProps } from '../navbar-root';

const START_TEXT = 'Explore';
const END_BUTTON_TEXT = 'Action';
Expand Down Expand Up @@ -118,6 +118,22 @@ describe('Navbar', () => {
});
});

describe('Shadow', () => {
it('shows shadow by default', () => {
render(<BasicNavbar />);

const nav = screen.getByRole('navigation');
expect(nav).toHaveAttribute('data-shadow', 'true');
});

it('hides shadow when shadow is false', () => {
render(<BasicNavbar shadow={false} />);

const nav = screen.getByRole('navigation');
expect(nav).toHaveAttribute('data-shadow', 'false');
});
});

describe('Navbar.Start', () => {
it('renders start section content', () => {
render(
Expand Down Expand Up @@ -204,6 +220,31 @@ describe('Navbar', () => {
});
});

describe('Navbar.Center', () => {
it('renders center section content', () => {
render(
<BasicNavbar>
<Navbar.Center>
<span>Center</span>
</Navbar.Center>
</BasicNavbar>
);

expect(screen.getByText('Center')).toBeInTheDocument();
});

it('applies center styles', () => {
const { container } = render(
<BasicNavbar>
<Navbar.Center data-testid='center-section' />
</BasicNavbar>
);

const center = container.querySelector(`.${styles.center}`);
expect(center).toBeInTheDocument();
});
});

describe('Navbar.End', () => {
it('renders end section content', () => {
render(
Expand Down Expand Up @@ -369,18 +410,21 @@ describe('Navbar', () => {
expect(containerEl).toBeInTheDocument();
});

it('positions Start and End correctly', () => {
it('positions Start, Center, and End correctly', () => {
const { container } = render(
<BasicNavbar>
<Navbar.Start data-testid='start'>Start</Navbar.Start>
<Navbar.Center data-testid='center'>Center</Navbar.Center>
<Navbar.End data-testid='end'>End</Navbar.End>
</BasicNavbar>
);

const start = container.querySelector(`.${styles.start}`);
const center = container.querySelector(`.${styles.center}`);
const end = container.querySelector(`.${styles.end}`);

expect(start).toBeInTheDocument();
expect(center).toBeInTheDocument();
expect(end).toBeInTheDocument();
});
});
Expand Down
4 changes: 3 additions & 1 deletion packages/raystack/components/navbar/navbar-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import styles from './navbar.module.css';

export interface NavbarRootProps extends ComponentPropsWithoutRef<'nav'> {
sticky?: boolean;
shadow?: boolean;
}

export const NavbarRoot = forwardRef<ComponentRef<'nav'>, NavbarRootProps>(
({ className, sticky = false, children, ...props }, ref) => {
({ className, sticky = false, shadow = true, children, ...props }, ref) => {
return (
<nav
ref={ref}
className={cx(styles.root, className)}
data-sticky={sticky}
data-shadow={shadow}
role='navigation'
{...props}
>
Expand Down
21 changes: 21 additions & 0 deletions packages/raystack/components/navbar/navbar-sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ export const NavbarStart = forwardRef<HTMLDivElement, NavbarStartProps>(

NavbarStart.displayName = 'Navbar.Start';

export interface NavbarCenterProps
extends ComponentPropsWithoutRef<typeof Flex> {}

export const NavbarCenter = forwardRef<HTMLDivElement, NavbarCenterProps>(
({ className, children, 'aria-label': ariaLabel, ...props }, ref) => (
<Flex
ref={ref}
align='center'
gap={5}
className={cx(styles.center, className)}
role={ariaLabel ? 'group' : undefined}
aria-label={ariaLabel}
{...props}
>
{children}
</Flex>
)
);

NavbarCenter.displayName = 'Navbar.Center';

export interface NavbarEndProps extends ComponentPropsWithoutRef<typeof Flex> {}

export const NavbarEnd = forwardRef<HTMLDivElement, NavbarEndProps>(
Expand Down
14 changes: 11 additions & 3 deletions packages/raystack/components/navbar/navbar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
padding: var(--rs-space-4) var(--rs-space-7);
background: var(--rs-color-background-base-primary);
border-bottom: 0.5px solid var(--rs-color-border-base-primary);
box-shadow: var(--rs-shadow-feather);
box-sizing: border-box;
min-height: 48px;
min-height: var(--rs-space-11);
}

.root[data-shadow='true'] {
box-shadow: var(--rs-shadow-feather);
}

.root[data-sticky="true"] {
Expand All @@ -24,7 +27,12 @@
flex: 0 0 auto;
}

.center {
flex: 1 1 auto;
justify-content: center;
}

.end {
flex: 0 0 auto;
margin-left: auto;
}
}
3 changes: 2 additions & 1 deletion packages/raystack/components/navbar/navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use client';

import { NavbarRoot } from './navbar-root';
import { NavbarEnd, NavbarStart } from './navbar-sections';
import { NavbarCenter, NavbarEnd, NavbarStart } from './navbar-sections';

export const Navbar = Object.assign(NavbarRoot, {
Start: NavbarStart,
Center: NavbarCenter,
End: NavbarEnd
});