From f837474ff8d60edaa3399a15a53c52b543597629 Mon Sep 17 00:00:00 2001 From: Peter Oesteritz Date: Thu, 11 Aug 2022 09:21:02 +0200 Subject: [PATCH 01/15] =?UTF-8?q?Added=20prop=20to=20render=20Radix?= =?UTF-8?q?=E2=80=99=20Dialog.Trigger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmdk/src/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cmdk/src/index.tsx b/cmdk/src/index.tsx index e0c05c6..8c757a5 100644 --- a/cmdk/src/index.tsx +++ b/cmdk/src/index.tsx @@ -14,7 +14,12 @@ type SeparatorProps = DivProps & { /** Whether this separator should always be rendered. Useful if you disable automatic filtering. */ alwaysRender?: boolean } -type DialogProps = RadixDialog.DialogProps & CommandProps +type DialogProps = RadixDialog.DialogProps & CommandProps & { + /** + * Pass an element which will be used as Dialog.Trigger inside Dialog.Root + */ + trigger?: React.ReactNode +} type ListProps = Children & DivProps & {} type ItemProps = Children & Omit & { @@ -748,9 +753,10 @@ const List = React.forwardRef((props, forwardedRef) = * Renders the command menu in a Radix Dialog. */ const Dialog = React.forwardRef((props, forwardedRef) => { - const { open, onOpenChange, ...etc } = props + const { open, onOpenChange, trigger, ...etc } = props return ( + {trigger && {trigger}} From f6745609ff9a855e6a14f64552479aed8bf36600 Mon Sep 17 00:00:00 2001 From: Peter Oesteritz Date: Wed, 31 Aug 2022 10:42:07 +0200 Subject: [PATCH 02/15] [WIP] extracted dialog portal --- cmdk/src/index.tsx | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/cmdk/src/index.tsx b/cmdk/src/index.tsx index 8c757a5..84c2b57 100644 --- a/cmdk/src/index.tsx +++ b/cmdk/src/index.tsx @@ -14,12 +14,8 @@ type SeparatorProps = DivProps & { /** Whether this separator should always be rendered. Useful if you disable automatic filtering. */ alwaysRender?: boolean } -type DialogProps = RadixDialog.DialogProps & CommandProps & { - /** - * Pass an element which will be used as Dialog.Trigger inside Dialog.Root - */ - trigger?: React.ReactNode -} +type DialogProps = RadixDialog.DialogProps & CommandProps +type DialogPortalProps = RadixDialog.DialogPortalProps & CommandProps type ListProps = Children & DivProps & {} type ItemProps = Children & Omit & { @@ -749,14 +745,27 @@ const List = React.forwardRef((props, forwardedRef) = ) }) +/** + * Dialog Portal + */ +const DialogPortal = React.forwardRef((props, forwardedRef) => { + return ( + + + + + + + ) +}) + /** * Renders the command menu in a Radix Dialog. */ const Dialog = React.forwardRef((props, forwardedRef) => { - const { open, onOpenChange, trigger, ...etc } = props + const { open, onOpenChange, ...etc } = props return ( - {trigger && {trigger}} @@ -811,6 +820,7 @@ const pkg = Object.assign(Command, { Group, Separator, Dialog, + DialogPortal, Empty, Loading, }) From 64bef04a0fbf47f706ed12b8136779344ffffe69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Magalh=C3=A3es?= Date: Mon, 17 Apr 2023 15:59:23 -0300 Subject: [PATCH 03/15] Use `offsetHeight` to calculate list height (#121) chore: use offsetHeight to calculate list height --- cmdk/src/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmdk/src/index.tsx b/cmdk/src/index.tsx index 51103c9..763b55a 100644 --- a/cmdk/src/index.tsx +++ b/cmdk/src/index.tsx @@ -765,7 +765,7 @@ const List = React.forwardRef((props, forwardedRef) = let animationFrame const observer = new ResizeObserver(() => { animationFrame = requestAnimationFrame(() => { - const height = el.getBoundingClientRect().height + const height = el.offsetHeight wrapper.style.setProperty(`--cmdk-list-height`, height.toFixed(1) + 'px') }) }) From 0d74730944c5d286d0b99ec40d726b4913145f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Magalh=C3=A3es?= Date: Tue, 18 Apr 2023 11:39:47 -0300 Subject: [PATCH 04/15] Add `data-disabled` to `Item` and recommend using data attributes (#122) * chore: add data-disabled to item * docs: use data-attributes in docs and examples --- README.md | 2 +- cmdk/src/index.tsx | 1 + website/styles/cmdk/framer.scss | 4 ++-- website/styles/cmdk/linear.scss | 4 ++-- website/styles/cmdk/raycast.scss | 4 ++-- website/styles/cmdk/vercel.scss | 4 ++-- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 40fdc65..9c11f1a 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ To scroll item into view earlier near the edges of the viewport, use scroll-padd } ``` -### Item `[cmdk-item]` `[aria-disabled?]` `[aria-selected?]` +### Item `[cmdk-item]` `[data-disabled?]` `[data-selected?]` Item that becomes active on pointer enter. You should provide a unique `value` for each item, but it will be automatically inferred from the `.textContent`. diff --git a/cmdk/src/index.tsx b/cmdk/src/index.tsx index 763b55a..fb7ba0b 100644 --- a/cmdk/src/index.tsx +++ b/cmdk/src/index.tsx @@ -632,6 +632,7 @@ const Item = React.forwardRef((props, forwardedRef) = role="option" aria-disabled={disabled || undefined} aria-selected={selected || undefined} + data-disabled={disabled || undefined} data-selected={selected || undefined} onPointerMove={disabled ? undefined : select} onClick={disabled ? undefined : onSelect} diff --git a/website/styles/cmdk/framer.scss b/website/styles/cmdk/framer.scss index 687306c..639007c 100644 --- a/website/styles/cmdk/framer.scss +++ b/website/styles/cmdk/framer.scss @@ -63,7 +63,7 @@ transition: all 150ms ease; transition-property: none; - &[aria-selected='true'] { + &[data-selected='true'] { background: var(--blue9); color: #ffffff; @@ -72,7 +72,7 @@ } } - &[aria-disabled='true'] { + &[data-disabled='true'] { color: var(--gray8); cursor: not-allowed; } diff --git a/website/styles/cmdk/linear.scss b/website/styles/cmdk/linear.scss index 89493df..f344f87 100644 --- a/website/styles/cmdk/linear.scss +++ b/website/styles/cmdk/linear.scss @@ -75,7 +75,7 @@ transition-property: none; position: relative; - &[aria-selected='true'] { + &[data-selected='true'] { background: var(--gray3); svg { @@ -93,7 +93,7 @@ } } - &[aria-disabled='true'] { + &[data-disabled='true'] { color: var(--gray8); cursor: not-allowed; } diff --git a/website/styles/cmdk/raycast.scss b/website/styles/cmdk/raycast.scss index 07283f5..b69a411 100644 --- a/website/styles/cmdk/raycast.scss +++ b/website/styles/cmdk/raycast.scss @@ -148,12 +148,12 @@ transition: all 150ms ease; transition-property: none; - &[aria-selected='true'] { + &[data-selected='true'] { background: var(--gray4); color: var(--gray12); } - &[aria-disabled='true'] { + &[data-disabled='true'] { color: var(--gray8); cursor: not-allowed; } diff --git a/website/styles/cmdk/vercel.scss b/website/styles/cmdk/vercel.scss index 9d3e55e..78baf0f 100644 --- a/website/styles/cmdk/vercel.scss +++ b/website/styles/cmdk/vercel.scss @@ -66,12 +66,12 @@ transition: all 150ms ease; transition-property: none; - &[aria-selected='true'] { + &[data-selected='true'] { background: var(--grayA3); color: var(--gray12); } - &[aria-disabled='true'] { + &[data-disabled='true'] { color: var(--gray8); cursor: not-allowed; } From bbcc8cfd526afcd81f0125c2a269512648eacf0a Mon Sep 17 00:00:00 2001 From: Daniel Gabriel <56942108+revogabe@users.noreply.github.com> Date: Wed, 3 May 2023 11:13:08 -0300 Subject: [PATCH 05/15] feat: add default value prop (#123) * feat: add default value prop * fix: revert example * fix: check for undefined toLowerCase * fix: prettier test format --- cmdk/src/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmdk/src/index.tsx b/cmdk/src/index.tsx index fb7ba0b..7bf45fb 100644 --- a/cmdk/src/index.tsx +++ b/cmdk/src/index.tsx @@ -74,6 +74,10 @@ type CommandProps = Children & * By default, uses the `command-score` library. */ filter?: (value: string, search: string) => number + /** + * Optional default item value when it is initially rendered. + */ + defaultValue?: string /** * Optional controlled state of the selected command menu item. */ @@ -137,7 +141,7 @@ const Command = React.forwardRef((props, forwarded /** Value of the search query. */ search: '', /** Currently selected item value. */ - value: props.value ?? '', + value: props.value ?? props.defaultValue?.toLowerCase() ?? '', filtered: { /** The count of all visible items. */ count: 0, From 53da94a3a3e816ff3d21a3d9b159ed2d888fbdfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Magalh=C3=A3es?= Date: Wed, 3 May 2023 11:14:22 -0300 Subject: [PATCH 06/15] chore: use forceMount from group as fallback (#119) * chore: use forceMount from group as fallback * fix: undefined forceMount * fix: check for null/undefined for group id * fix: add forceMount to deps Co-authored-by: Paco <34928425+pacocoursey@users.noreply.github.com> --------- Co-authored-by: Paco <34928425+pacocoursey@users.noreply.github.com> --- cmdk/src/index.tsx | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/cmdk/src/index.tsx b/cmdk/src/index.tsx index 7bf45fb..a57f005 100644 --- a/cmdk/src/index.tsx +++ b/cmdk/src/index.tsx @@ -115,6 +115,10 @@ type Store = { setState: (key: K, value: State[K], opts?: any) => void emit: () => void } +type Group = { + id: string + forceMount?: boolean +} const LIST_SELECTOR = `[cmdk-list-sizer=""]` const GROUP_SELECTOR = `[cmdk-group=""]` @@ -133,7 +137,7 @@ const useCommand = () => React.useContext(CommandContext) const StoreContext = React.createContext(undefined) const useStore = () => React.useContext(StoreContext) // @ts-ignore -const GroupContext = React.createContext(undefined) +const GroupContext = React.createContext(undefined) const Command = React.forwardRef((props, forwardedRef) => { const ref = React.useRef(null) @@ -585,12 +589,13 @@ const Command = React.forwardRef((props, forwarded const Item = React.forwardRef((props, forwardedRef) => { const id = React.useId() const ref = React.useRef(null) - const groupId = React.useContext(GroupContext) + const groupContext = React.useContext(GroupContext) const context = useCommand() const propsRef = useAsRef(props) + const forceMount = propsRef.current?.forceMount ?? groupContext?.forceMount useLayoutEffect(() => { - return context.item(id, groupId) + return context.item(id, groupContext?.id) }, []) const value = useValue(id, ref, [props.value, props.children, ref]) @@ -598,13 +603,7 @@ const Item = React.forwardRef((props, forwardedRef) = const store = useStore() const selected = useCmdk((state) => state.value && state.value === value.current) const render = useCmdk((state) => - props.forceMount - ? true - : context.filter() === false - ? true - : !state.search - ? true - : state.filtered.items.get(id) > 0, + forceMount ? true : context.filter() === false ? true : !state.search ? true : state.filtered.items.get(id) > 0, ) React.useEffect(() => { @@ -651,14 +650,14 @@ const Item = React.forwardRef((props, forwardedRef) = * Grouped items are always shown together. */ const Group = React.forwardRef((props, forwardedRef) => { - const { heading, children, ...etc } = props + const { heading, children, forceMount, ...etc } = props const id = React.useId() const ref = React.useRef(null) const headingRef = React.useRef(null) const headingId = React.useId() const context = useCommand() const render = useCmdk((state) => - props.forceMount ? true : context.filter() === false ? true : !state.search ? true : state.filtered.groups.has(id), + forceMount ? true : context.filter() === false ? true : !state.search ? true : state.filtered.groups.has(id), ) useLayoutEffect(() => { @@ -667,7 +666,8 @@ const Group = React.forwardRef((props, forwardedRef) useValue(id, ref, [props.value, props.heading, headingRef]) - const inner = {children} + const contextValue = React.useMemo(() => ({ id, forceMount }), [forceMount]) + const inner = {children} return (
Date: Wed, 3 May 2023 10:17:43 -0400 Subject: [PATCH 07/15] Fix: `selected` and `disabled` state (#84) * Add "data-selected" attribute to Item Component * fix:`selected` and `disabled` state --------- Co-authored-by: Paco <34928425+pacocoursey@users.noreply.github.com> --- cmdk/src/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmdk/src/index.tsx b/cmdk/src/index.tsx index a57f005..303ef29 100644 --- a/cmdk/src/index.tsx +++ b/cmdk/src/index.tsx @@ -633,10 +633,10 @@ const Item = React.forwardRef((props, forwardedRef) = id={id} cmdk-item="" role="option" - aria-disabled={disabled || undefined} - aria-selected={selected || undefined} - data-disabled={disabled || undefined} - data-selected={selected || undefined} + aria-disabled={disabled ?? undefined} + aria-selected={selected ?? undefined} + data-disabled={disabled ?? undefined} + data-selected={selected ?? undefined} onPointerMove={disabled ? undefined : select} onClick={disabled ? undefined : onSelect} > From 0f5bf586b1981782d9d21eb80128826056a01e93 Mon Sep 17 00:00:00 2001 From: Paco <34928425+pacocoursey@users.noreply.github.com> Date: Wed, 3 May 2023 10:38:18 -0400 Subject: [PATCH 08/15] Revert "Fix: `selected` and `disabled` state" (#131) Revert "Fix: `selected` and `disabled` state (#84)" This reverts commit aeeaf32e89563e10d15bbaef8da86bf9fd015467. --- cmdk/src/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmdk/src/index.tsx b/cmdk/src/index.tsx index 303ef29..a57f005 100644 --- a/cmdk/src/index.tsx +++ b/cmdk/src/index.tsx @@ -633,10 +633,10 @@ const Item = React.forwardRef((props, forwardedRef) = id={id} cmdk-item="" role="option" - aria-disabled={disabled ?? undefined} - aria-selected={selected ?? undefined} - data-disabled={disabled ?? undefined} - data-selected={selected ?? undefined} + aria-disabled={disabled || undefined} + aria-selected={selected || undefined} + data-disabled={disabled || undefined} + data-selected={selected || undefined} onPointerMove={disabled ? undefined : select} onClick={disabled ? undefined : onSelect} > From 45be6deb983e052e04de0abb47764f2a22b77d57 Mon Sep 17 00:00:00 2001 From: Paco <34928425+pacocoursey@users.noreply.github.com> Date: Wed, 3 May 2023 19:25:33 -0400 Subject: [PATCH 09/15] Bring in local dependency of command-score (#130) * move command-score into local dep * remove command-score dep * add tests from #16 * update lockfile --- cmdk/package.json | 3 +- cmdk/src/command-score.ts | 161 ++++++++++++++++++++++++++++++++++++++ cmdk/src/index.tsx | 2 +- pnpm-lock.yaml | 6 -- test/numeric.test.ts | 25 ++++++ test/pages/numeric.tsx | 22 ++++++ 6 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 cmdk/src/command-score.ts create mode 100644 test/numeric.test.ts create mode 100644 test/pages/numeric.tsx diff --git a/cmdk/package.json b/cmdk/package.json index 8fcec53..f4b5a87 100644 --- a/cmdk/package.json +++ b/cmdk/package.json @@ -19,8 +19,7 @@ "react-dom": "^18.0.0" }, "dependencies": { - "@radix-ui/react-dialog": "1.0.0", - "command-score": "0.1.2" + "@radix-ui/react-dialog": "1.0.0" }, "devDependencies": { "@types/react": "18.0.15" diff --git a/cmdk/src/command-score.ts b/cmdk/src/command-score.ts new file mode 100644 index 0000000..f79cccc --- /dev/null +++ b/cmdk/src/command-score.ts @@ -0,0 +1,161 @@ +// The scores are arranged so that a continuous match of characters will +// result in a total score of 1. +// +// The best case, this character is a match, and either this is the start +// of the string, or the previous character was also a match. +var SCORE_CONTINUE_MATCH = 1, + // A new match at the start of a word scores better than a new match + // elsewhere as it's more likely that the user will type the starts + // of fragments. + // NOTE: We score word jumps between spaces slightly higher than slashes, brackets + // hyphens, etc. + SCORE_SPACE_WORD_JUMP = 0.9, + SCORE_NON_SPACE_WORD_JUMP = 0.8, + // Any other match isn't ideal, but we include it for completeness. + SCORE_CHARACTER_JUMP = 0.17, + // If the user transposed two letters, it should be significantly penalized. + // + // i.e. "ouch" is more likely than "curtain" when "uc" is typed. + SCORE_TRANSPOSITION = 0.1, + // The goodness of a match should decay slightly with each missing + // character. + // + // i.e. "bad" is more likely than "bard" when "bd" is typed. + // + // This will not change the order of suggestions based on SCORE_* until + // 100 characters are inserted between matches. + PENALTY_SKIPPED = 0.999, + // The goodness of an exact-case match should be higher than a + // case-insensitive match by a small amount. + // + // i.e. "HTML" is more likely than "haml" when "HM" is typed. + // + // This will not change the order of suggestions based on SCORE_* until + // 1000 characters are inserted between matches. + PENALTY_CASE_MISMATCH = 0.9999, + // Match higher for letters closer to the beginning of the word + PENALTY_DISTANCE_FROM_START = 0.9, + // If the word has more characters than the user typed, it should + // be penalised slightly. + // + // i.e. "html" is more likely than "html5" if I type "html". + // + // However, it may well be the case that there's a sensible secondary + // ordering (like alphabetical) that it makes sense to rely on when + // there are many prefix matches, so we don't make the penalty increase + // with the number of tokens. + PENALTY_NOT_COMPLETE = 0.99 + +var IS_GAP_REGEXP = /[\\\/_+.#"@\[\(\{&]/, + COUNT_GAPS_REGEXP = /[\\\/_+.#"@\[\(\{&]/g, + IS_SPACE_REGEXP = /[\s-]/, + COUNT_SPACE_REGEXP = /[\s-]/g + +function commandScoreInner( + string, + abbreviation, + lowerString, + lowerAbbreviation, + stringIndex, + abbreviationIndex, + memoizedResults, +) { + if (abbreviationIndex === abbreviation.length) { + if (stringIndex === string.length) { + return SCORE_CONTINUE_MATCH + } + return PENALTY_NOT_COMPLETE + } + + var memoizeKey = `${stringIndex},${abbreviationIndex}` + if (memoizedResults[memoizeKey] !== undefined) { + return memoizedResults[memoizeKey] + } + + var abbreviationChar = lowerAbbreviation.charAt(abbreviationIndex) + var index = lowerString.indexOf(abbreviationChar, stringIndex) + var highScore = 0 + + var score, transposedScore, wordBreaks, spaceBreaks + + while (index >= 0) { + score = commandScoreInner( + string, + abbreviation, + lowerString, + lowerAbbreviation, + index + 1, + abbreviationIndex + 1, + memoizedResults, + ) + if (score > highScore) { + if (index === stringIndex) { + score *= SCORE_CONTINUE_MATCH + } else if (IS_GAP_REGEXP.test(string.charAt(index - 1))) { + score *= SCORE_NON_SPACE_WORD_JUMP + wordBreaks = string.slice(stringIndex, index - 1).match(COUNT_GAPS_REGEXP) + if (wordBreaks && stringIndex > 0) { + score *= Math.pow(PENALTY_SKIPPED, wordBreaks.length) + } + } else if (IS_SPACE_REGEXP.test(string.charAt(index - 1))) { + score *= SCORE_SPACE_WORD_JUMP + spaceBreaks = string.slice(stringIndex, index - 1).match(COUNT_SPACE_REGEXP) + if (spaceBreaks && stringIndex > 0) { + score *= Math.pow(PENALTY_SKIPPED, spaceBreaks.length) + } + } else { + score *= SCORE_CHARACTER_JUMP + if (stringIndex > 0) { + score *= Math.pow(PENALTY_SKIPPED, index - stringIndex) + } + } + + if (string.charAt(index) !== abbreviation.charAt(abbreviationIndex)) { + score *= PENALTY_CASE_MISMATCH + } + } + + if ( + (score < SCORE_TRANSPOSITION && + lowerString.charAt(index - 1) === lowerAbbreviation.charAt(abbreviationIndex + 1)) || + (lowerAbbreviation.charAt(abbreviationIndex + 1) === lowerAbbreviation.charAt(abbreviationIndex) && // allow duplicate letters. Ref #7428 + lowerString.charAt(index - 1) !== lowerAbbreviation.charAt(abbreviationIndex)) + ) { + transposedScore = commandScoreInner( + string, + abbreviation, + lowerString, + lowerAbbreviation, + index + 1, + abbreviationIndex + 2, + memoizedResults, + ) + + if (transposedScore * SCORE_TRANSPOSITION > score) { + score = transposedScore * SCORE_TRANSPOSITION + } + } + + if (score > highScore) { + highScore = score + } + + index = lowerString.indexOf(abbreviationChar, index + 1) + } + + memoizedResults[memoizeKey] = highScore + return highScore +} + +function formatInput(string) { + // convert all valid space characters to space so they match each other + return string.toLowerCase().replace(COUNT_SPACE_REGEXP, ' ') +} + +export function commandScore(string: string, abbreviation: string): number { + /* NOTE: + * in the original, we used to do the lower-casing on each recursive call, but this meant that toLowerCase() + * was the dominating cost in the algorithm, passing both is a little ugly, but considerably faster. + */ + return commandScoreInner(string, abbreviation, formatInput(string), formatInput(abbreviation), 0, 0, {}) +} diff --git a/cmdk/src/index.tsx b/cmdk/src/index.tsx index a57f005..bd5c229 100644 --- a/cmdk/src/index.tsx +++ b/cmdk/src/index.tsx @@ -1,6 +1,6 @@ import * as RadixDialog from '@radix-ui/react-dialog' import * as React from 'react' -import commandScore from 'command-score' +import { commandScore } from './command-score' type Children = { children?: React.ReactNode } type DivProps = React.HTMLAttributes diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 324b651..870b215 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,10 +18,8 @@ importers: specifiers: '@radix-ui/react-dialog': 1.0.0 '@types/react': 18.0.15 - command-score: 0.1.2 dependencies: '@radix-ui/react-dialog': 1.0.0_@types+react@18.0.15 - command-score: 0.1.2 devDependencies: '@types/react': 18.0.15 @@ -1440,10 +1438,6 @@ packages: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} dev: true - /command-score/0.1.2: - resolution: {integrity: sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w==} - dev: false - /commander/4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} diff --git a/test/numeric.test.ts b/test/numeric.test.ts new file mode 100644 index 0000000..28a0a10 --- /dev/null +++ b/test/numeric.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@playwright/test' + +test.describe('behavior for numeric values', async () => { + test.beforeEach(async ({ page }) => { + await page.goto('/numeric') + }) + + test('items filter correctly on numeric inputs', async ({ page }) => { + const input = page.locator(`[cmdk-input]`) + await input.type('112') + const removed = page.locator(`[cmdk-item][data-value="removed"]`) + const remains = page.locator(`[cmdk-item][data-value="foo.bar112.value"]`) + await expect(removed).toHaveCount(0) + await expect(remains).toHaveCount(1) + }) + + test('items filter correctly on non-numeric inputs', async ({ page }) => { + const input = page.locator(`[cmdk-input]`) + await input.type('bar') + const removed = page.locator(`[cmdk-item][data-value="removed"]`) + const remains = page.locator(`[cmdk-item][data-value="foo.bar112.value"]`) + await expect(removed).toHaveCount(0) + await expect(remains).toHaveCount(1) + }) +}) diff --git a/test/pages/numeric.tsx b/test/pages/numeric.tsx new file mode 100644 index 0000000..e05ad7e --- /dev/null +++ b/test/pages/numeric.tsx @@ -0,0 +1,22 @@ +import { Command } from 'cmdk' + +const Page = () => { + return ( +
+ + + + No results. + + To be removed + + + Not to be removed + + + +
+ ) +} + +export default Page From 563c2bd9317ae00d2ce7c38bba0fc06064142531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Magalh=C3=A3es?= Date: Wed, 10 May 2023 10:34:14 -0300 Subject: [PATCH 10/15] docs: add forceMount prop (#134) * docs: add forceMount prop * Apply suggestions from code review --------- Co-authored-by: Paco <34928425+pacocoursey@users.noreply.github.com> --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 9c11f1a..4134418 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,8 @@ Item that becomes active on pointer enter. You should provide a unique `value` f ``` +You can force an item to always render, regardless of filtering, by passing the `forceMount` prop. + ### Group `[cmdk-group]` `[hidden?]` Groups items together with the given `heading` (`[cmdk-group-heading]`). @@ -221,6 +223,8 @@ Groups items together with the given `heading` (`[cmdk-group-heading]`). Groups will not unmount from the DOM, rather the `hidden` attribute is applied to hide it from view. This may be relevant in your styling. +You can force a group to always render, regardless of filtering, by passing the `forceMount` prop. + ### Separator `[cmdk-separator]` Visible when the search query is empty or `alwaysRender` is true, hidden otherwise. From db1e29aa7ee173bf3ac269f9f815ee45ebbabd56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Magalh=C3=A3es?= Date: Mon, 15 May 2023 11:26:27 -0300 Subject: [PATCH 11/15] docs: update dialog example (#136) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4134418..6a83a76 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ const CommandMenu = () => { // Toggle the menu when ⌘K is pressed React.useEffect(() => { const down = (e) => { - if (e.key === 'k' && e.metaKey) { + if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { + e.preventDefault() setOpen((open) => !open) } } From a5e4004970e3b46cd9d5c153213fba2060bf2fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=E2=80=94Rysana?= <51100181+jrysana@users.noreply.github.com> Date: Wed, 5 Jul 2023 12:48:14 -0400 Subject: [PATCH 12/15] Add links to title badges (#151) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a83a76..b397494 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

-# ⌘K ![cmdk minzip package size](https://img.shields.io/bundlephobia/minzip/cmdk) ![cmdk package version](https://img.shields.io/npm/v/cmdk.svg?colorB=green) +# ⌘K [![cmdk minzip package size](https://img.shields.io/bundlephobia/minzip/cmdk)](https://www.npmjs.com/package/cmdk?activeTab=code) [![cmdk package version](https://img.shields.io/npm/v/cmdk.svg?colorB=green)](https://www.npmjs.com/package/cmdk) ⌘K is a command menu React component that can also be used as an accessible combobox. You render items, it filters and sorts them automatically. ⌘K supports a fully composable API [How?](/ARCHITECTURE.md), so you can wrap items in other components or even as static JSX. From 986e06ba2fd9febb72f9deb1068669d578d0e704 Mon Sep 17 00:00:00 2001 From: Chase Adams Date: Fri, 14 Jul 2023 10:09:04 -0700 Subject: [PATCH 13/15] docs: tiny fix (#153) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b397494..9056e81 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,9 @@ Or disable filtering and sorting entirely: You can make the arrow keys wrap around the list (when you reach the end, it goes back to the first item) by setting the `loop` prop: +```tsx +``` ### Dialog `[cmdk-dialog]` `[cmdk-overlay]` From ba2e20035ad86a2cb0fae0c2c296796e48a0181c Mon Sep 17 00:00:00 2001 From: Chase Adams Date: Fri, 14 Jul 2023 10:09:46 -0700 Subject: [PATCH 14/15] fix: added div props type to loading (#154) --- cmdk/src/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmdk/src/index.tsx b/cmdk/src/index.tsx index bd5c229..fcbd83c 100644 --- a/cmdk/src/index.tsx +++ b/cmdk/src/index.tsx @@ -5,7 +5,7 @@ import { commandScore } from './command-score' type Children = { children?: React.ReactNode } type DivProps = React.HTMLAttributes -type LoadingProps = Children & { +type LoadingProps = Children & DivProps & { /** Estimated progress of loading asynchronous options. */ progress?: number } From 13912b2b5f924cf0c8405df6781722b93a43f0f2 Mon Sep 17 00:00:00 2001 From: Peter Oesteritz Date: Mon, 24 Jul 2023 16:20:29 +0200 Subject: [PATCH 15/15] export DialogPortal as single component --- cmdk/src/index.tsx | 58 +++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/cmdk/src/index.tsx b/cmdk/src/index.tsx index fcbd83c..35f56b6 100644 --- a/cmdk/src/index.tsx +++ b/cmdk/src/index.tsx @@ -5,24 +5,29 @@ import { commandScore } from './command-score' type Children = { children?: React.ReactNode } type DivProps = React.HTMLAttributes -type LoadingProps = Children & DivProps & { - /** Estimated progress of loading asynchronous options. */ - progress?: number -} +type LoadingProps = Children & + DivProps & { + /** Estimated progress of loading asynchronous options. */ + progress?: number + } type EmptyProps = Children & DivProps & {} type SeparatorProps = DivProps & { /** Whether this separator should always be rendered. Useful if you disable automatic filtering. */ alwaysRender?: boolean } -type DialogProps = RadixDialog.DialogProps & - CommandProps & { - /** Provide a className to the Dialog overlay. */ - overlayClassName?: string - /** Provide a className to the Dialog content. */ - contentClassName?: string - /** Provide a custom element the Dialog should portal into. */ - container?: HTMLElement - } +type CommonDialogProps = { + /** Provide a className to the Dialog overlay. */ + overlayClassName?: string + /** Provide a className to the Dialog content. */ + contentClassName?: string + /** Provide a custom element the Dialog should portal into. */ + container?: HTMLElement +} +type DialogProps = RadixDialog.DialogProps & CommandProps & CommonDialogProps +type DialogPortalWrapperProps = RadixDialog.DialogPortalProps & CommandProps & CommonDialogProps +type DialogPortalProps = { + ref: React.ForwardedRef +} & (DialogProps | DialogPortalWrapperProps) type ListProps = Children & DivProps & {} type ItemProps = Children & Omit & { @@ -799,6 +804,17 @@ const List = React.forwardRef((props, forwardedRef) = ) }) +const DialogPortal = ({ overlayClassName, contentClassName, container, label, ref, ...etc }: DialogPortalProps) => { + return ( + + + + + + + ) +} + /** * Renders the command menu in a Radix Dialog. */ @@ -806,16 +822,19 @@ const Dialog = React.forwardRef((props, forwardedRe const { open, onOpenChange, overlayClassName, contentClassName, container, ...etc } = props return ( - - - - - - + ) }) +/** + * Renders the command menu in a Radix Dialog without the root element + */ +const DialogPortalWrapper = React.forwardRef((props, forwardedRef) => { + const { overlayClassName, contentClassName, container, ...etc } = props + return +}) + /** * Automatically renders when there are no results for the search query. */ @@ -860,6 +879,7 @@ const pkg = Object.assign(Command, { Group, Separator, Dialog, + DialogPortal: DialogPortalWrapper, Empty, Loading, })