Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const docs: ComponentDocs = {
</Stack>,
),
alternatives: [],
accessibility: [],
};

export default docs;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { style } from '@vanilla-extract/css';
import { vars } from 'braid-design-system/css';

export const searchButton = style({
':hover': {
background: vars.backgroundColor.neutralSoft,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we change the background colour outside of React Context we need to manage the colour mode change manually. So here need to use the colorModeStyle utility and specify the right token for dark mode.

You can test it on the docs site by focusing the window and typing braiddark 🥸

},
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add a changeset for this, as its new API for the docs-ui component

Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { Box, Hidden, HiddenVisually, Link, Text } from 'braid-design-system';
import {
Bleed,
Box,
Hidden,
HiddenVisually,
IconSearch,
Link,
Stack,
Text,
} from 'braid-design-system';
import type { ReactNode } from 'react';

import { KeyboardShortcut } from '../KeyboardShortcut/KeyboardShortcut';
import { MenuButton } from '../MenuButton/MenuButton';

import { searchButton } from './HeaderNavigation.css';

interface HeaderNavigationProps {
menuOpen?: boolean;
menuClick?: () => void;
onSearchClick?: () => void;
logo: ReactNode;
logoLabel: string;
logoHref?: string;
Expand All @@ -15,6 +28,7 @@ interface HeaderNavigationProps {
export const HeaderNavigation = ({
menuOpen = false,
menuClick = () => {},
onSearchClick = () => {},
logo,
logoLabel,
logoHref = '/',
Expand All @@ -41,6 +55,31 @@ export const HeaderNavigation = ({
</Link>
</Text>
</Box>
{themeToggle}
<Stack space="none">
<Box>{themeToggle}</Box>
<Bleed horizontal="xxsmall" bottom="xxsmall">
<Box
component="button"
padding="xxsmall"
paddingRight="xsmall"
borderRadius="standard"
className={searchButton}
onClick={onSearchClick}
>
<KeyboardShortcut
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A subtle one, but can you ensure all elements rendered in here use span elements. Given we wrap it in a button ideally we dont use div elements inside.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep good pickup

keys={[
navigator.platform.startsWith('Mac') ||
navigator.platform === 'iPhone' ||
navigator.platform === 'iPad' ||
navigator.platform === 'iPod'
? '⌘'
: 'Ctrl',
'K',
]}
shortcutLabel={<IconSearch />}
/>
</Box>
</Bleed>
</Stack>
Comment on lines +58 to +83
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based one where this landed we can remove the wrapping Stack and the Box around the themeToggle.

</Box>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Box, Text } from 'braid-design-system';
import type { ReactElement } from 'react';

interface KeyboardShortcutProps {
keys: string[];
shortcutLabel: ReactElement | string;
}

export const KeyboardIcon = ({ children }: { children: React.ReactNode }) => (
<Box
display="flex"
padding="xxsmall"
background="neutralLight"
borderRadius="standard"
alignItems="center"
justifyContent="center"
>
<Text tone="secondary" size="xsmall">
{children}
</Text>
</Box>
);

export const KeyboardShortcut = ({
keys,
shortcutLabel,
}: KeyboardShortcutProps) => (
<Box display="flex" alignItems="center" gap="xxsmall">
{keys.map((key) => (
<KeyboardIcon key={key}>{key}</KeyboardIcon>
))}
<Text tone="secondary" size="xsmall">
{shortcutLabel}
</Text>
</Box>
);
4 changes: 4 additions & 0 deletions packages/docs-ui/src/index.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add a changeset for this new component

Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ export { LinkableHeading } from './components/LinkableHeading/LinkableHeading';
export { MenuButton } from './components/MenuButton/MenuButton';
export { SideNavigationSection } from './components/SideNavigation/SideNavigationSection';
export { HeaderNavigation } from './components/HeaderNavigation/HeaderNavigation';
export {
KeyboardShortcut,
KeyboardIcon,
} from './components/KeyboardShortcut/KeyboardShortcut';
13 changes: 13 additions & 0 deletions site/src/App/CategoryHeading/CategoryHeading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Box, Text } from 'braid-src/index';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import path is not right, we might have some setup issues to work through.


export const CategoryHeading = ({
children,
}: {
children: React.ReactNode;
}) => (
<Box style={{ textTransform: 'uppercase' }} component="li">
<Text size="xsmall" weight="medium" component="h2">
{children}
</Text>
</Box>
Comment on lines +8 to +12
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think these elements are correct for all usages. I would suggest flipping the Text and Box around and exposing the component prop on the outer Text component, so the element can be set from the call site.

Suggested change
<Box style={{ textTransform: 'uppercase' }} component="li">
<Text size="xsmall" weight="medium" component="h2">
{children}
</Text>
</Box>
<Text component={component} size="xsmall" weight="medium">
<Box component="span" style={{ textTransform: 'uppercase' }}>
{children}
</Box>
</Text>

);
4 changes: 2 additions & 2 deletions site/src/App/DocNavigation/DocDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export const DocDetails = () => {
</PlayroomStateProvider>
) : null}

{'accessibility' in docs ? (
{'accessibility' in docs && docs.accessibility ? (
<Stack space={headingSpacing}>
<LinkableHeading level="3">Accessibility</LinkableHeading>
{docs.accessibility}
Expand Down Expand Up @@ -201,7 +201,7 @@ export const DocDetails = () => {
/>
))}

{'alternatives' in docs ? (
{'alternatives' in docs && docs.alternatives.length > 0 ? (
<Stack space={headingSpacing}>
<LinkableHeading level="3">Alternatives</LinkableHeading>
<List space="medium">
Expand Down
177 changes: 177 additions & 0 deletions site/src/App/JumpToModal/JumpToModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import {
Box,
Dialog,
TextField,
IconSearch,
Bleed,
} from 'braid-src/lib/components';
import { ScrollContainer } from 'braid-src/lib/components/private/ScrollContainer/ScrollContainer';
import { useEffect, useRef, useState, useMemo } from 'react';
import { useNavigate } from 'react-router';

import { SearchResults } from './SearchResults';
import {
getSearchItems,
groupSearchResults,
type SearchItem,
} from './getSearchItems';

interface JumpToModalProps {
isOpen: boolean;
onClose: () => void;
}

export const JumpToModal = ({ isOpen, onClose }: JumpToModalProps) => {
const [searchQuery, setSearchQuery] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const resultsRef = useRef<HTMLElement>(null);
const navigate = useNavigate();

const searchItems = useMemo(() => getSearchItems(), []);

const filteredItems = useMemo(() => {
if (!searchQuery.trim()) {
return [];
}

const query = searchQuery.toLowerCase();
return searchItems.filter((item) =>
item.name.toLowerCase().includes(query),
);
}, [searchQuery, searchItems]);

const groupedResults = useMemo(
() => groupSearchResults(filteredItems),
[filteredItems],
);

const flatResults = useMemo(() => {
const results: SearchItem[] = [];
const categoryOrder = [
'Foundations',
'Components',
'CSS',
'Logic',
] as const;

categoryOrder.forEach((category) => {
results.push(...groupedResults[category]);
});

return results;
}, [groupedResults]);

useEffect(() => {
if (isOpen) {
setSearchQuery('');
setSelectedIndex(0);
// Wait for Dialog animation to complete before focusing
const timer = setTimeout(() => {
inputRef.current?.focus();
}, 100);
return () => clearTimeout(timer);
}
}, [isOpen]);

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!isOpen) {
return;
}

switch (e.key) {
case 'Escape':
onClose();
e.preventDefault();
break;
case 'ArrowDown':
e.preventDefault();
setSelectedIndex((prev) =>
flatResults.length === 0 ? 0 : (prev + 1) % flatResults.length,
);
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex((prev) => {
if (flatResults.length === 0) {
return 0;
}
if (prev === 0) {
return flatResults.length - 1;
}
return prev - 1;
});
break;
case 'Enter':
if (flatResults[selectedIndex]) {
const selectedItem = flatResults[selectedIndex];
const targetPath =
e.shiftKey && selectedItem.hasProps
? `${selectedItem.path}/props`
: selectedItem.path;
navigate(targetPath);
onClose();
}
e.preventDefault();
break;
}
};

window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, flatResults, selectedIndex, navigate, onClose]);

// Scroll the selected item into view
useEffect(() => {
if (resultsRef.current && flatResults.length > 0) {
const selectedElement = resultsRef.current.querySelector(
`[data-index="${selectedIndex}"]`,
);
if (selectedElement) {
selectedElement.scrollIntoView({ block: 'nearest' });
}
}
}, [selectedIndex, flatResults.length]);

return (
<Dialog
open={isOpen}
onClose={onClose}
width="medium"
title="Jump to a page"
closeLabel="Close search"
>
<TextField
icon={<IconSearch />}
ref={inputRef}
label=""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use aria-label here instead of a blank label

placeholder="Jump to Foundations, Components, CSS, Logic..."
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setSelectedIndex(0);
}}
/>
<Bleed horizontal="large">
<ScrollContainer direction="vertical">
<Box ref={resultsRef} paddingY="small" style={{ height: '40vh' }}>
<Box paddingX="large" height="full">
<SearchResults
searchQuery={searchQuery}
groupedResults={groupedResults}
flatResults={flatResults}
selectedIndex={selectedIndex}
onSelectIndex={setSelectedIndex}
onNavigate={(path) => {
navigate(path);
onClose();
}}
/>
</Box>
</Box>
</ScrollContainer>
</Bleed>
</Dialog>
);
};
Loading
Loading