Skip to content
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

Add basic grid component #210

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
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
84 changes: 80 additions & 4 deletions cmdk/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ type DialogProps = RadixDialog.DialogProps &
/** Provide a custom element the Dialog should portal into. */
container?: HTMLElement
}

type GridProps = ListProps &
Children &
DivProps & {
/** Amount of columns for the grid */
columns: number
}
type ListProps = Children &
DivProps & {
/**
Expand Down Expand Up @@ -549,6 +556,36 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>((props, forwarded
}
}

const gridEl = listInnerRef.current?.closest('[data-columns]')
const gridColumns = gridEl ? Number(gridEl.getAttribute('data-columns')) : undefined

const prevRow = (e: React.KeyboardEvent) => {
e.preventDefault()
const selected = getSelectedItem()
const items = getValidItems()
const index = items.findIndex((item) => item === selected)
const newIndex = index - gridColumns

if (newIndex >= 0) {
updateSelectedToIndex(newIndex)
}
}

const nextRow = (e: React.KeyboardEvent) => {
e.preventDefault()
const selected = getSelectedItem()
const items = getValidItems()
const index = items.findIndex((item) => item === selected)
const newIndex = index + gridColumns

// TODO: should go to the last item of uneven columns
// TODO: should handle grouping correctly

if (newIndex < items.length) {
updateSelectedToIndex(newIndex)
}
}

return (
<Primitive.div
ref={forwardedRef}
Expand All @@ -560,28 +597,51 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>((props, forwarded

if (!e.defaultPrevented) {
switch (e.key) {
// emacs keybind next
case 'n':
// vim keybind down
case 'j': {
// vim keybind down
if (vimBindings && e.ctrlKey) {
next(e)
}
break
}
case 'ArrowLeft': {
if (gridColumns) {
prev(e)
}
break
}
case 'ArrowRight': {
if (gridColumns) {
next(e)
}
break
}
case 'ArrowDown': {
next(e)
if (gridColumns) {
nextRow(e)
} else {
next(e)
}
break
}
// emacs keybind previous
case 'p':
// vim keybind up
case 'k': {
// vim keybind up
if (vimBindings && e.ctrlKey) {
prev(e)
}
break
}
case 'ArrowUp': {
prev(e)
if (gridColumns) {
prevRow(e)
} else {
prev(e)
}

break
}
case 'Home': {
Expand Down Expand Up @@ -818,6 +878,7 @@ const List = React.forwardRef<HTMLDivElement, ListProps>((props, forwardedRef) =
const { children, label = 'Suggestions', ...etc } = props
const ref = React.useRef<HTMLDivElement>(null)
const height = React.useRef<HTMLDivElement>(null)

const context = useCommand()

React.useEffect(() => {
Expand Down Expand Up @@ -857,6 +918,20 @@ const List = React.forwardRef<HTMLDivElement, ListProps>((props, forwardedRef) =
)
})

/**
* Contains `Item`, `Group`, and `Separator`.
* Use the `--cmdk-list-height` CSS variable to animate height based on the number of results.
*/
const Grid = React.forwardRef<HTMLDivElement, GridProps>((props, forwardedRef) => {
const { children, columns, ...etc } = props

return (
<List {...etc} ref={forwardedRef} cmdk-grid="" data-columns={columns}>
{children}
</List>
)
})

/**
* Renders the command menu in a Radix Dialog.
*/
Expand Down Expand Up @@ -910,6 +985,7 @@ const Loading = React.forwardRef<HTMLDivElement, LoadingProps>((props, forwarded

const pkg = Object.assign(Command, {
List,
Grid,
Item,
Input,
Group,
Expand Down
23 changes: 23 additions & 0 deletions test/grid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { expect, test } from '@playwright/test'

test.describe('grid', async () => {
test.beforeEach(async ({ page }) => {
await page.goto('/grid')
})

test('correct attributes are applied', async ({ page }) => {
await expect(page.locator(`[cmdk-grid]`)).toBeDefined()
await expect(page.locator(`[data-columns]`)).toBeDefined()
})

test('arrow up/down changes selected item', async ({ page }) => {
await page.locator(`[cmdk-input]`).press('ArrowDown')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'C')
await page.locator(`[cmdk-input]`).press('ArrowLeft')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'B')
await page.locator(`[cmdk-input]`).press('ArrowRight')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'C')
await page.locator(`[cmdk-input]`).press('ArrowUp')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A')
})
})
21 changes: 21 additions & 0 deletions test/pages/grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Command } from 'cmdk'

const Page = () => {
return (
<div>
<Command className="root">
<Command.Input placeholder="Search…" className="input" />
<Command.Grid columns={2} className="list">
<Command.Empty className="empty">No results.</Command.Empty>
<Command.Item className="item">A</Command.Item>
<Command.Item className="item">B</Command.Item>
<Command.Item className="item">C</Command.Item>
<Command.Item className="item">D</Command.Item>
<Command.Item className="item">E</Command.Item>
</Command.Grid>
</Command>
</div>
)
}

export default Page
65 changes: 65 additions & 0 deletions website/components/cmdk/raycast-grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as React from 'react'
import { Command } from 'cmdk'
import { useTheme } from 'next-themes'

// Alphabet
const list = `ABCDEFGHIJKLMNOPQRSTUVWXYZ`.split('')
const list2 = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig', 'Grape', 'Honeydew', 'Icaco', 'Jujube']

const Asset = () => {
return (
<div
style={{
width: 100,
height: 100,
borderRadius: 4,
background: 'red',
flexShrink: 0,
}}
/>
)
}

export function RaycastCMDKGrid() {
const [value, setValue] = React.useState('linear')
const inputRef = React.useRef<HTMLInputElement | null>(null)
const listRef = React.useRef(null)

React.useEffect(() => {
inputRef?.current?.focus()
}, [])

return (
<div className="raycast-grid">
<Command value={value} onValueChange={(v) => setValue(v)}>
<div cmdk-raycast-top-shine="" />
<Command.Input ref={inputRef} autoFocus placeholder="Search for apps and commands..." />
<hr cmdk-raycast-loader="" />
<Command.Grid columns={5} ref={listRef}>
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Alphabet">
{list.map((item) => {
return (
<Command.Item key={item} value={item}>
<Asset />
<span>{item}</span>
</Command.Item>
)
})}
</Command.Group>

<Command.Group heading="Fruits">
{list2.map((item) => {
return (
<Command.Item key={item} value={item}>
<Asset />
<span>{item}</span>
</Command.Item>
)
})}
</Command.Group>
</Command.Grid>
</Command>
</div>
)
}
1 change: 1 addition & 0 deletions website/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from './cmdk/framer'
export * from './cmdk/linear'
export * from './cmdk/vercel'
export * from './cmdk/raycast'
export * from './cmdk/raycast-grid'
export * from './icons'
export * from './code'
1 change: 1 addition & 0 deletions website/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'styles/globals.scss'
import 'styles/cmdk/vercel.scss'
import 'styles/cmdk/linear.scss'
import 'styles/cmdk/raycast.scss'
import 'styles/cmdk/raycast-grid.scss'
import 'styles/cmdk/framer.scss'

import type { AppProps } from 'next/app'
Expand Down
42 changes: 22 additions & 20 deletions website/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
VercelCMDK,
VercelIcon,
RaycastCMDK,
RaycastCMDKGrid,
RaycastIcon,
CopyIcon,
FramerIcon,
Expand Down Expand Up @@ -63,7 +64,8 @@ export default function Index() {
)}
{theme === 'raycast' && (
<CMDKWrapper key="raycast">
<RaycastCMDK />
{/* <RaycastCMDK /> */}
<RaycastCMDKGrid />
</CMDKWrapper>
)}
</AnimatePresence>
Expand Down Expand Up @@ -164,25 +166,25 @@ function ThemeSwitcher() {
function listener(e: KeyboardEvent) {
const themeNames = themes.map((t) => t.key)

if (e.key === 'ArrowRight') {
const currentIndex = themeNames.indexOf(theme)
const nextIndex = currentIndex + 1
const nextItem = themeNames[nextIndex]

if (nextItem) {
setTheme(nextItem)
}
}

if (e.key === 'ArrowLeft') {
const currentIndex = themeNames.indexOf(theme)
const prevIndex = currentIndex - 1
const prevItem = themeNames[prevIndex]

if (prevItem) {
setTheme(prevItem)
}
}
// if (e.key === 'ArrowRight') {
// const currentIndex = themeNames.indexOf(theme)
// const nextIndex = currentIndex + 1
// const nextItem = themeNames[nextIndex]

// if (nextItem) {
// setTheme(nextItem)
// }
// }

// if (e.key === 'ArrowLeft') {
// const currentIndex = themeNames.indexOf(theme)
// const prevIndex = currentIndex - 1
// const prevItem = themeNames[prevIndex]

// if (prevItem) {
// setTheme(prevItem)
// }
// }
}

document.addEventListener('keydown', listener)
Expand Down
Loading
Loading