Skip to content

Commit 2eb13be

Browse files
committed
Move breadcrumb styles to CSS module + move tests their own file
1 parent 65c1a83 commit 2eb13be

File tree

6 files changed

+85
-72
lines changed

6 files changed

+85
-72
lines changed

src/components/Breadcrumb.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { ReactNode } from 'react'
22
import { useConfig } from '../hooks/useConfig.js'
33
import type { Source } from '../lib/sources/types.js'
4+
import { cn } from '../lib/utils.js'
5+
import styles from '../styles/Breadcrumb.module.css'
46

57
interface BreadcrumbProps {
68
source: Source,
@@ -11,10 +13,10 @@ interface BreadcrumbProps {
1113
* Breadcrumb navigation
1214
*/
1315
export default function Breadcrumb({ source, children }: BreadcrumbProps) {
14-
const { routes } = useConfig()
16+
const { routes, customClass } = useConfig()
1517

16-
return <nav className='top-header top-header-divided'>
17-
<div className='path'>
18+
return <nav className={cn(styles.breadcrumb, customClass?.breadcrumb)}>
19+
<div className={cn(styles.path, customClass?.path)}>
1820
{source.sourceParts.map((part, depth) =>
1921
<a href={routes?.getSourceRouteUrl?.({ sourceId: part.sourceId }) ?? ''} key={depth}>{part.text}</a>
2022
)}

src/hooks/useConfig.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import { createContext, useContext } from 'react'
1010
export interface Config {
1111
customClass?: {
1212
brand?: string
13+
breadcrumb?: string
1314
contentWrapper?: string
1415
errorBar?: string
1516
highTable?: string
1617
imageView?: string
1718
markdownView?: string
19+
path?: string
1820
progressBar?: string
1921
sideBar?: string
2022
slideCloseButton?: string

src/styles/Breadcrumb.module.css

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
.breadcrumb {
2+
/* top navbar */
3+
align-items: center;
4+
display: flex;
5+
font-size: 18px;
6+
height: 32px;
7+
justify-content: space-between;
8+
min-height: 32px;
9+
padding-left: 20px;
10+
padding-right: 10px;
11+
border-bottom: 1px solid #ddd;
12+
background: #eee;
13+
/* TODO(SL): forbid overflow? */
14+
15+
h1 {
16+
font-size: 18px;
17+
margin: 4px 0 0 0; /* top */
18+
user-select: none;
19+
}
20+
}
21+
22+
/* file path */
23+
.path {
24+
margin: 0 2px;
25+
margin-right: 4px;
26+
min-width: 0;
27+
overflow: auto;
28+
/* TODO(SL): forbid wrap + use an ellipsis instead? */
29+
30+
&::-webkit-scrollbar {
31+
display: none;
32+
}
33+
a {
34+
color: #222622;
35+
font-family: "Courier New", Courier, monospace;
36+
font-weight: 600;
37+
font-size: 18px;
38+
text-overflow: ellipsis;
39+
white-space: nowrap;
40+
text-decoration-thickness: 1px;
41+
}
42+
/* hide all but the last path link on small screens */
43+
@media (max-width: 360px) {
44+
& a:not(:last-child) {
45+
display: none;
46+
}
47+
}
48+
}

src/styles/app.css

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -98,28 +98,6 @@ main {
9898
overflow: hidden;
9999
}
100100

101-
/* top navbar */
102-
.top-header {
103-
align-items: center;
104-
display: flex;
105-
font-size: 18px;
106-
height: 32px;
107-
justify-content: space-between;
108-
min-height: 32px;
109-
padding-left: 20px;
110-
padding-right: 10px;
111-
}
112-
.top-header h1 {
113-
font-size: 18px;
114-
margin: 4px 0 0 0; /* top */
115-
user-select: none;
116-
}
117-
118-
.top-header-divided {
119-
border-bottom: 1px solid #ddd;
120-
background: #eee;
121-
}
122-
123101
.top-actions {
124102
left: auto;
125103
}
@@ -146,32 +124,6 @@ input.search:focus {
146124
width: 180px;
147125
}
148126

149-
/* file path */
150-
.path {
151-
margin: 0 2px;
152-
margin-right: 4px;
153-
min-width: 0;
154-
overflow: auto;
155-
}
156-
.path::-webkit-scrollbar {
157-
display: none;
158-
}
159-
.path a {
160-
color: #222622;
161-
font-family: 'Courier New', Courier, monospace;
162-
font-weight: 600;
163-
font-size: 18px;
164-
text-overflow: ellipsis;
165-
white-space: nowrap;
166-
text-decoration-thickness: 1px;
167-
}
168-
/* hide all but the last path link on small screens */
169-
@media (max-width: 360px) {
170-
.path a:not(:last-child) {
171-
display: none;
172-
}
173-
}
174-
175127
/* file list */
176128
.file-list {
177129
flex: 1;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { render } from '@testing-library/react'
2+
import { strict as assert } from 'assert'
3+
import React from 'react'
4+
import { describe, expect, it } from 'vitest'
5+
import { Config, ConfigProvider } from '../../src/hooks/useConfig.js'
6+
import { Breadcrumb, getHyperparamSource } from '../../src/index.js'
7+
8+
const endpoint = 'http://localhost:3000'
9+
10+
describe('Breadcrumb Component', () => {
11+
it('renders breadcrumbs correctly', () => {
12+
const source = getHyperparamSource('subdir1/subdir2/', { endpoint })
13+
assert(source !== undefined)
14+
15+
const config: Config = {
16+
routes: {
17+
getSourceRouteUrl: ({ sourceId }) => `/files?key=${sourceId}`,
18+
},
19+
}
20+
const { getByText } = render(<ConfigProvider value={config}>
21+
<Breadcrumb source={source} />
22+
</ConfigProvider>)
23+
24+
const subdir1Link = getByText('subdir1/')
25+
expect(subdir1Link.closest('a')?.getAttribute('href')).toBe('/files?key=subdir1/')
26+
27+
const subdir2Link = getByText('subdir2/')
28+
expect(subdir2Link.closest('a')?.getAttribute('href')).toBe('/files?key=subdir1/subdir2/')
29+
})
30+
})

test/components/Folder.test.tsx

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -82,27 +82,6 @@ describe('Folder Component', () => {
8282
expect(queryByText('folder1/')).toBeNull()
8383
})
8484

85-
it('renders breadcrumbs correctly', async () => {
86-
vi.mocked(fetch).mockResolvedValueOnce({
87-
json: () => Promise.resolve(mockFiles),
88-
ok: true,
89-
} as Response)
90-
91-
const source = getHyperparamSource('subdir1/subdir2/', { endpoint })
92-
assert(source?.kind === 'directory')
93-
94-
const { findByText, getByText } = render(<ConfigProvider value={config}>
95-
<Folder source={source} />
96-
</ConfigProvider>)
97-
await waitFor(() => { expect(fetch).toHaveBeenCalled() })
98-
99-
const subdir1Link = await findByText('subdir1/')
100-
expect(subdir1Link.closest('a')?.getAttribute('href')).toBe('/files?key=subdir1/')
101-
102-
const subdir2Link = getByText('subdir2/')
103-
expect(subdir2Link.closest('a')?.getAttribute('href')).toBe('/files?key=subdir1/subdir2/')
104-
})
105-
10685
it('filters files based on search query', async () => {
10786
const mockFiles: FileMetadata[] = [
10887
{ sourceId: 'folder1', name: 'folder1/', kind: 'directory', lastModified: '2023-01-01T00:00:00Z' },

0 commit comments

Comments
 (0)