-
Notifications
You must be signed in to change notification settings - Fork 98
Add 'Jump to' functionality to Braid Docs #2029
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d4e3cd8
3713aba
bab9efb
eaf16f1
5e5d7a3
08366aa
8211795
9ed3175
29b1a2a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
| }, | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
|
@@ -15,6 +28,7 @@ interface HeaderNavigationProps { | |
| export const HeaderNavigation = ({ | ||
| menuOpen = false, | ||
| menuClick = () => {}, | ||
| onSearchClick = () => {}, | ||
| logo, | ||
| logoLabel, | ||
| logoHref = '/', | ||
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based one where this landed we can remove the wrapping |
||
| </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> | ||
| ); |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,13 @@ | ||||||||||||||||||||||
| import { Box, Text } from 'braid-src/index'; | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| 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="" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use |
||
| 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> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
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
colorModeStyleutility and specify the right token for dark mode.You can test it on the docs site by focusing the window and typing
braiddark🥸