Junior Garcia 67347d8713
v2.4.0 (#3101)
* chore(root): reat-aria packages updated (#2889)

* chore(storybook): common colors enabled (#2902)

* fix(range-calendar): hide only dates outside the month (#2906)

* fix(range-calendar): hide only dates outside the month #2890

* fix(range-calendar): corrected spelling mistake in changeset description

* fix(range-calendar): corrected capitalization in changeset description

* chore(changeset): patch @nextui-org/theme

---------

Co-authored-by: shrinidhi.upadhyaya <shrinidhi.upadhyaya@stud.uni-bamberg.de>
Co-authored-by: աɨռɢӄաօռɢ <wingkwong.code@gmail.com>

* fix(date-picker): keep date picker style consistent for different variants (#2908)

* fix: add missing TableRowProps export (#2866)

* fix: add missing TableRowProps export

* feat(changeset): add changeset for PR2866

* chore(changeset): revise changeset message

---------

Co-authored-by: աɨռɢӄաօռɢ <wingkwong.code@gmail.com>

* fix(input): correct label margin for RTL required inputs (#2781)

* fix(input): correct label margin for RTL required inputs

* fix(theme): add changeset fr theme

* docs(core): add storybook and canary release info (#2914)

* Cn utility refactor (#2915)

* refactor(core): cn utility adjusted and moved to the theme package

* chore(root): changeset

* fix(storybook): stories that used cn

* docs(date-picker): change to jsx instead (#2919)

* fix(switch): support uncontrolled switch in react-hook-form (#2924)

* feat(switch): add @nextui-org/use-safe-layout-effect

* chore(deps): add @nextui-org/use-safe-layout-effect

* fix(switch): react-hook-form uncontrolled switch component

* fix(switch): react-hook-form uncontrolled switch component

* feat(switch): add rect-hook-form in dev dep

* feat(switch): add WithReactHookFormTemplate

* refactor(root): react aria packages fixed (#2944)

* feat(docs): docs changes (#2868)

* feat(docs): add example how to set locale (#2867)

* docs(guide): add an explanation for the installation guide (#2769)

* docs(guide): add an explanation for the installation guide

* docs(guide): add an explanation for the cli guide

* docs(guide): add support for cli output

* fix: change sort priority - cmdk (#2873)

* docs: remove unsupported props in range calendar and date range picker (#2881)

* chore(calendar): remove showMonthAndYearPickers from range calendar story

* docs(date-range-picker): remove showMonthAndYearPickers info

* docs(range-calendar): remove unsupported props

* docs: refactor typing in form.ts (#2882)

* chore(docs): supplement errorMessage behaviour in input (#2892)

* refactor(docs): revise NextUI Provider structure

* chore(docs): add updated tag

---------

Co-authored-by: Nozomi-Hijikata <116155762+Nozomi-Hijikata@users.noreply.github.com>
Co-authored-by: HaRuki <soccer_haruki15@me.com>
Co-authored-by: Kaben <carnoxen@gmail.com>

* fix(slider): missing marks when hideThumb is true & revise slider styles (#2883)

* chore(slider): include marks in hideThumb

* fix(slider): revise slider styles

* feat(changeset): add changeset

* feat(slider): add tests with marks and hideThumb

* feat(test): react hook form tests & stories (#2931)

* feat(input): add Input with React Hook Form tests

* refactor(input): add missing types

* feat(checkbox): add checkbox with React Hook Form tests

* feat(select): add react-hook-form to dev dep

* feat(select): add react hook form story

* feat(select): react hook form tests

* fix(select): incorrect button reference

* feat(deps): add react-hook-form to dev dep in autocomplete

* feat(autocomplete): react hook form story

* feat(autocomplete): react hook form tests

* fix(autocomplete): rollback wrapper type

* feat(switch): add react hook form tests

* refactor(stories): reorder stories items

* fix: update accordion item heading tag to be customizable (#2265)

* fix: update accordion item heading tag to be customizable

* Update .changeset/heavy-hairs-join.md

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

* Update .changeset/heavy-hairs-join.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* chore(accordion): lint

* chore(changeset): add issue number

* feat(docs): add HeadingComponent prop

---------

Co-authored-by: Shawn Dong <shawn.dong@flybuys.com.au>
Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: աɨռɢӄաօռɢ <wingkwong.code@gmail.com>

* fix(theme): add pointer-events-none to skeleton base (#2972)

* feat(tabs): add `destroyInactiveTabPanel` prop for Tabs component (#2973)

* feat(tabs): add destroyInactiveTabPanel and set default to false

* feat(tabs): integrate with destroyInactiveTabPanel

* feat(theme): hidden inert tab panel

* feat(changeset): add changeset

* chore(changeset): add issue number

* feat(docs): add `destroyInactiveTabPanel` prop to tabs page

* chore(docs): set destroyInactiveTabPanel to true by default

* chore(tabs): set destroyInactiveTabPanel to true by default

* chore(tabs): revise destroyInactiveTabPanel logic

* feat(tabs): add tests for destroyInactiveTabPanel

* chore(tabs): change the default value of destroyInactiveTabPanel to true

* refactor: add support for disabling the animation globally (#2929)

* refactor: add support for disabling the animation globally

* chore(docs): disableAnimation removed from global provider

* feat(docs): nextui provider api updated, storybook preview adjusted

* chore(theme): button is scalable when disabled, tooltip animation improved

* fix(theme): remove origin-bottom from button (#2990)

* fix(skeleton): overflow issue in skeleton (#2986)

* fix(theme): set overflow visible after skeleton loaded

* feat(changeset): add changeset

* fix(table): v2 input/textarea don't allow spaces inside a table (#3020)

* fix(table): set onKeyDownCapture to undefined

* feat(changeset): add changeset

* fix(slider): calculate the correct value on mark click (#3017)

* fix(slider): calculate the correct value on mark click

* refactor(slider): remove the tests inside describe block

* feat(slider): add tests for thumb move on mark click

* refactor(slider): use val instead of pos

* fix(theme): revise input isInvalid styles (#3010)

* fix(theme): revise isInvalid input styles

* feat(changeset): add changeset

* feat(date-picker): add missing ref to input wrapper (#3011)

* fix(date-picker): add missing ref to input wrapper

* feat(changeset): add changeset

* fix(core): incorrect tailwind classnames (#3018)

* fix(dropdown): focus behaviour on press / enter keydown (#2970)

* fix(dropdown): set focus on the first item

* feat(dropdown): add keyboard interactions tests

* feat(changeset): add changeset

* fix(dropdown): use fireEvent.keyDown instead

* chore(deps): add @nextui-org/test-utils to dropdown

* refactor(dropdown): pass onKeyDown to menu trigger and don't hardcode autoFocus

* chore(dropdown): remove autoFocus

* fix(menu): pass userMenuProps to useTreeState and useAriaMenu and remove from getListProps

* chore(changeset): add menu package

* fix(component): update type definition to prevent primitive values as items (#2953)

* fix: update type definition to prevent primitive values as items

* fix: typecheck

* fix(select): onSelectionChange can handle number (#2937)

* fix: onSelectionChange type for dynamic items in Select component

* docs: remove unnecessary properties

* docs: update highlightedLines

* chore: add changeset

* fix(calendar): scrolling is hidden when changing the month (#2949)

* fix(calendar): scrolling is hidden when changing the month

* chore(changeset): correct package name

---------

Co-authored-by: Poli Sour <polisour.work@gmail.com>
Co-authored-by: WK Wong <wingkwong.code@gmail.com>

* fix: make VisuallyHidden's element type as span when it's inside phrasing element (#3013)

* fix(checkbox): make VisuallyHidden's element type as span

* feat(changeset): add changeset

* fix(radio): make the VisuallyHidden element type as span

* fix(switch): make the VisuallyHidden element type as span

* fix(select): make the VisuallyHidden element type as span

* feat(changeset): replace changeset

* chore: fix formatting

* docs: sync nextui-cli  api (#3035)

* docs: sync nextui-cli  api

* docs: update

* chore: update routes.json with new path and set updated flag

---------

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

* feat: switch default validationBehavior to aria and allow switching via props (#2987)

* chore: add support validationBehavior aria

* chore: add validationBehavior to Provider

* chore: add autocomplete validation test

* chore: add checkbox validation test

* fix(input): require condition

* docs: add description of validationBehavior props

* chore: add support validationBehavior props for date components

* docs(dates): add description of validationBehavior props

* chore: add changeset

* chore: format

* chore: fix test

* fix: select validationBehavior is not support yet

* fix: select validationBehavior not supported yet

* chore(docs): validation behavior prop added to nextui-provider

---------

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

* fix: popover-based focus behaviour (#2854)

* fix(autocomplete): autocomplete focus behaviour

* feat(autocomplete): add test case for catching blur cases

* refactor(autocomplete): use isOpen instead

* feat(autocomplete): add "should focus when clicking autocomplete" test case

* feat(autocomplete): add should set the input after selection

* fix(autocomplete): remove shouldUseVirtualFocus

* fix(autocomplete): uncomment blur logic

* refactor(autocomplete): remove state as it is in getPopoverProps

* refactor(autocomplete): remove unnecessary blur

* refactor(select): remove unncessary props

* fix(popover): use domRef instead

* fix(popover): revise isNonModal and isDismissable

* fix(popover): use dialogRef back

* fix(popover): rollback

* fix(autocomplete): onFocus logic

* feat(popover): set disableFocusManagement to overlay

* feat(modal): set disableFocusManagement to overlay

* fix(autocomplete): set disableFocusManagement for autocomplete

* feat(popover): include disableFocusManagement prop

* refactor(autocomplete): revise type in selectorButton

* fix(autocomplete): revise focus logic

* feat(autocomplete): add internal focus state and add shouldCloseOnInteractOutside

* feat(autocomplete): handle selectedItem change

* feat(autocomplete): add clear button test

* feat(changeset): add changeset

* refactor(components): use the original order

* refactor(autocomplete): add more comments

* fix(autocomplete): revise focus behaviours

* refactor(autocomplete): rename to listbox

* chore(popover): remove disableFocusManagement from popover

* chore(autocomplete): remove disableFocusManagement from autocomplete

* chore(changeset): add issue number

* fix(popover): don't set default value to transformOrigin

* fix(autocomplete): revise shouldCloseOnInteractOutside logic

* feat(autocomplete): should close listbox by clicking another autocomplete

* fix(popover): add disableFocusManagement to overlay

* refactor(autocomplete): revise comments and refactor shouldCloseOnInteractOutside

* feat(changeset): add issue number

* fix(autocomplete): merge with selectorButtonProps.onClick

* refactor(autocomplete): remove extra line

* refactor(autocomplete): revise comment

* feat(select): add shouldCloseOnInteractOutside

* feat(dropdown): add shouldCloseOnInteractOutside

* feat(date-picker): add shouldCloseOnInteractOutside

* feat(changeset): add dropdown and date-picker

* fix(popover): revise shouldCloseOnInteractOutside

* feat(date-picker): integrate with ariaShouldCloseOnInteractOutside

* feat(select): integrate with ariaShouldCloseOnInteractOutside

* feat(dropdown): integrate with ariaShouldCloseOnInteractOutside

* feat(popover): integrate with ariaShouldCloseOnInteractOutside

* feat(aria-utils): ariaShouldCloseOnInteractOutside

* chore(deps): update pnpm-lock.yaml

* feat(autocomplete): integrate with ariaShouldCloseOnInteractOutside

* feat(aria-utils): handle setShouldFocus logic

* feat(changeset): add @nextui-org/aria-utils

* chore(autocomplete): put the test into correct group

* feat(select): should close listbox by clicking another select

* feat(dropdown): should close listbox by clicking another dropdown

* feat(popover): should close listbox by clicking another popover

* feat(date-picker): should close listbox by clicking another datepicker

* chore(changeset): add issue numbers and revise changeset message

* refactor(autocomplete): change to useRef instead

* refactor(autocomplete): change to useRef instead

* refactor(aria-utils): revise comments and format code

* chore(changeset): add issue number

* chore: take popoverProps.shouldCloseOnInteractOutside first

* refactor(autocomplete): remove unnecessary logic

* refactor(autocomplete): focus management logic

* fix(components): Fix 'Tap to click' behavior on macOS with Edge/Chrome for Accordion and Tab (#2725)

* fix(components): fix 'Tap to click' behavior on macOS

* Add change file for accordion, menu, and tabs

* Remove 'fix(components)' from the .changeset file

* fix(components): undo dropdown change now that it's no longer applicable

* fix(components): update changeset file now that we are no longer modifying the dropdown component

* fix(date-picker): corrected inert value for true condition (#3054)

* fix(date-picker): corrected inert value for true condition #3044

* refactor(calendar): add todo comment

* feat(changeset): add changeset

---------

Co-authored-by: shrinidhi.upadhyaya <shrinidhi.upadhyaya@stud.uni-bamberg.de>
Co-authored-by: WK Wong <wingkwong.code@gmail.com>

* fix(hooks): resolve type error in onKeyDown event handler (#3064)

* fix(hooks): resolve type error in onKeyDown event handler

* chore(changeset): revise changeset

---------

Co-authored-by: WK Wong <wingkwong.code@gmail.com>

* Update dependency array on setPage useCallback hook (#3029)

Changes:
Add the onChangeActivePage function to the dependency array of the setPage useCallback hook to ensure it always reflects the latest state.

Impact:
This fix ensures that the pagination component accurately reflects the current state when triggering onChangeActivePage.

* fix: error peerDep in pkg (#3014)

* fix: error peerDep in pkg

* docs: changeset

* Fix DatePicker Time Input (#2845)

* fix(date-picker): set `isCalendarHeaderExpanded` to `false` when DatePicker is closed

* fix(date-picker): calendar header controlled state on DatePicker

* chore(date-picker): update test

* chore(date-picker): remove unnecessary `async` in test

* Update packages/components/date-picker/__tests__/date-picker.test.tsx

---------

Co-authored-by: WK Wong <wingkwong.code@gmail.com>
Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

* fix(date-picker): test

* fix(hooks): optimize useScrollPosition with useCallback and useRef (#3049)

* fix(hooks): optimize useScrollPosition with useCallback and useRef

* Update .changeset/lucky-cobras-jog.md

* Update packages/hooks/use-scroll-position/src/index.ts

* Update packages/hooks/use-scroll-position/src/index.ts

---------

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

* fix(select): placeholder text display for controlled component (#3081)

* fix: return placeholder when selectedItems is empty

* chore: add test and changeset

* chore(docs): v2.4.0 (#3084)

* chore(docs): v2.4.0

* chore(docs): v2.4.0 blog

* chore(docs): revise typos based on coderabbitai

* chore(docs): adjust navbar

---------

Co-authored-by: WK Wong <wingkwong.code@gmail.com>

* chore(changese): update @nextui-org/react dependency to minor version

* docs: update cli docs (#3096)

* ci(changesets): version packages (#2903)

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

---------

Co-authored-by: Shrinidhi Upadhyaya <shrinidhiupadhyaya1195@gmail.com>
Co-authored-by: shrinidhi.upadhyaya <shrinidhi.upadhyaya@stud.uni-bamberg.de>
Co-authored-by: աɨռɢӄաօռɢ <wingkwong.code@gmail.com>
Co-authored-by: Paul Tiedtke <PaulTiedtke@web.de>
Co-authored-by: Mohammad Reza Badri <85818966+mrbadri@users.noreply.github.com>
Co-authored-by: Nozomi-Hijikata <116155762+Nozomi-Hijikata@users.noreply.github.com>
Co-authored-by: HaRuki <soccer_haruki15@me.com>
Co-authored-by: Kaben <carnoxen@gmail.com>
Co-authored-by: Shawn Dong <dsknight@live.com.au>
Co-authored-by: Shawn Dong <shawn.dong@flybuys.com.au>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com>
Co-authored-by: Poli Sour <57824881+novsource@users.noreply.github.com>
Co-authored-by: Poli Sour <polisour.work@gmail.com>
Co-authored-by: Artem Pitikin <git@kosmotema.dev>
Co-authored-by: winches <329487092@qq.com>
Co-authored-by: Eric Abreu <ericfabreu@gmail.com>
Co-authored-by: Minsu <52266597+Gaic4o@users.noreply.github.com>
Co-authored-by: Jesus Perdomo Lampignano <38929969+jesuzon@users.noreply.github.com>
Co-authored-by: chirokas <157580465+chirokas@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-05-27 19:51:30 -03:00

477 lines
13 KiB
TypeScript

/* eslint-disable jsx-a11y/no-autofocus */
"use client";
import {Command} from "cmdk";
import {useEffect, useState, FC, useMemo, useCallback, useRef} from "react";
import {matchSorter} from "match-sorter";
import {Button, ButtonProps, Kbd, Modal, ModalContent} from "@nextui-org/react";
import {CloseIcon} from "@nextui-org/shared-icons";
import {tv} from "tailwind-variants";
import {usePathname, useRouter} from "next/navigation";
import MultiRef from "react-multi-ref";
import {clsx} from "@nextui-org/shared-utils";
import scrollIntoView from "scroll-into-view-if-needed";
import {isAppleDevice, isWebKit} from "@react-aria/utils";
import {create} from "zustand";
import {intersectionBy, isEmpty} from "lodash";
import {writeStorage, useLocalStorage} from "@rehooks/local-storage";
import {
DocumentCodeBoldIcon,
HashBoldIcon,
ChevronRightLinearIcon,
SearchLinearIcon,
} from "./icons";
import searchData from "@/config/search-meta.json";
import {useUpdateEffect} from "@/hooks/use-update-effect";
import {trackEvent} from "@/utils/va";
const hideOnPaths = ["examples"];
export interface CmdkStore {
isOpen: boolean;
onClose: () => void;
onOpen: () => void;
}
export const useCmdkStore = create<CmdkStore>((set) => ({
isOpen: false,
onClose: () => set({isOpen: false}),
onOpen: () => set({isOpen: true}),
}));
const cmdk = tv({
slots: {
base: "max-h-full overflow-y-auto",
header: [
"flex",
"items-center",
"w-full",
"px-4",
"border-b",
"border-default-400/50",
"dark:border-default-100",
],
searchIcon: "text-default-400 text-lg",
input: [
"w-full",
"px-2",
"h-14",
"font-sans",
"text-lg",
"outline-none",
"rounded-none",
"bg-transparent",
"text-default-700",
"placeholder-default-500",
"dark:text-default-500",
"dark:placeholder:text-default-300",
],
list: ["px-4", "mt-2", "pb-4", "overflow-y-auto", "max-h-[50vh]"],
itemWrapper: [
"px-4",
"mt-2",
"group",
"flex",
"h-16",
"justify-between",
"items-center",
"rounded-lg",
"shadow",
"bg-content2/50",
"active:opacity-70",
"cursor-pointer",
"transition-opacity",
"data-[active=true]:bg-primary",
"data-[active=true]:text-primary-foreground",
],
leftWrapper: ["flex", "gap-3", "items-center", "w-full", "max-w-full"],
leftIcon: [
"text-default-500 dark:text-default-300",
"group-data-[active=true]:text-primary-foreground",
],
itemContent: ["flex", "flex-col", "gap-0", "justify-center", "max-w-[80%]"],
itemParentTitle: [
"text-default-400",
"text-xs",
"group-data-[active=true]:text-primary-foreground",
"select-none",
],
itemTitle: [
"truncate",
"text-default-500",
"group-data-[active=true]:text-primary-foreground",
"select-none",
],
emptyWrapper: ["flex", "flex-col", "text-center", "items-center", "justify-center", "h-32"],
},
});
interface SearchResultItem {
content: string;
objectID: string;
url: string;
type: "lvl1" | "lvl2" | "lvl3";
hierarchy: {
lvl1: string | null;
lvl2?: string | null;
lvl3?: string | null;
};
}
const MATCH_KEYS = ["hierarchy.lvl1", "hierarchy.lvl2", "hierarchy.lvl3", "content"];
const RECENT_SEARCHES_KEY = "recent-searches";
const MAX_RECENT_SEARCHES = 10;
const MAX_RESULTS = 20;
export const Cmdk: FC<{}> = () => {
const [query, setQuery] = useState("");
const [activeItem, setActiveItem] = useState(0);
const [menuNodes] = useState(() => new MultiRef<number, HTMLElement>());
const slots = useMemo(() => cmdk(), []);
const pathname = usePathname();
const eventRef = useRef<"mouse" | "keyboard">();
const listRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const {isOpen, onClose, onOpen} = useCmdkStore();
const [recentSearches] = useLocalStorage<SearchResultItem[]>(RECENT_SEARCHES_KEY);
const addToRecentSearches = (item: SearchResultItem) => {
let searches = recentSearches ?? [];
// Avoid adding the same search again
if (!searches.find((i) => i.objectID === item.objectID)) {
writeStorage(RECENT_SEARCHES_KEY, [item, ...searches].slice(0, MAX_RECENT_SEARCHES));
} else {
// Move the search to the top
searches = searches.filter((i) => i.objectID !== item.objectID);
writeStorage(RECENT_SEARCHES_KEY, [item, ...searches].slice(0, MAX_RECENT_SEARCHES));
}
};
const prioritizeFirstLevelItems = (a: SearchResultItem, b: SearchResultItem) => {
if (a.type === "lvl1") {
return -1;
} else if (b.type === "lvl1") {
return 1;
}
return 0;
};
const results = useMemo<SearchResultItem[]>(
function getResults() {
if (query.length < 2) return [];
const data = searchData as SearchResultItem[];
const words = query.split(" ");
if (words.length === 1) {
return matchSorter(data, query, {
keys: MATCH_KEYS,
sorter: (matches) => {
matches.sort((a, b) => prioritizeFirstLevelItems(a.item, b.item));
return matches;
},
}).slice(0, MAX_RESULTS);
}
const matchesForEachWord = words.map((word) =>
matchSorter(data, word, {
keys: MATCH_KEYS,
sorter: (matches) => {
matches.sort((a, b) => prioritizeFirstLevelItems(a.item, b.item));
return matches;
},
}),
);
const matches = intersectionBy(...matchesForEachWord, "objectID").slice(0, MAX_RESULTS);
trackEvent("Cmdk - Search", {
name: "cmdk - search",
action: "search",
category: "cmdk",
data: {query, words, matches: matches?.map((match) => match.url).join(", ")},
});
return matches;
},
[query],
);
const items = !isEmpty(results) ? results : recentSearches ?? [];
// Toggle the menu when ⌘K / CTRL K is pressed
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
const hotkey = isAppleDevice() ? "metaKey" : "ctrlKey";
if (e?.key?.toLowerCase() === "k" && e[hotkey]) {
e.preventDefault();
isOpen ? onClose() : onOpen();
trackEvent("Cmdk - Open/Close", {
name: "cmdk - open/close",
action: "keydown",
category: "cmdk",
data: isOpen ? "close" : "open",
});
}
};
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [isOpen]);
const onItemSelect = useCallback(
(item: SearchResultItem) => {
onClose();
router.push(item.url);
addToRecentSearches(item);
trackEvent("Cmdk - ItemSelect", {
name: item.content,
action: "click",
category: "cmdk",
data: item.url,
});
},
[router, recentSearches],
);
const onInputKeyDown = useCallback(
(e: React.KeyboardEvent) => {
eventRef.current = "keyboard";
switch (e.key) {
case "ArrowDown": {
e.preventDefault();
if (activeItem + 1 < items.length) {
setActiveItem(activeItem + 1);
}
break;
}
case "ArrowUp": {
e.preventDefault();
if (activeItem - 1 >= 0) {
setActiveItem(activeItem - 1);
}
break;
}
case "Control":
case "Alt":
case "Shift": {
e.preventDefault();
break;
}
case "Enter": {
if (items?.length <= 0) {
break;
}
onItemSelect(items[activeItem]);
break;
}
}
},
[activeItem, items, router],
);
useUpdateEffect(() => {
setActiveItem(0);
}, [query]);
useUpdateEffect(() => {
if (!listRef.current || eventRef.current === "mouse") return;
const node = menuNodes.map.get(activeItem);
if (!node) return;
scrollIntoView(node, {
scrollMode: "if-needed",
behavior: "smooth",
block: "end",
inline: "end",
boundary: listRef.current,
});
}, [activeItem]);
const CloseButton = useCallback(
({
onPress,
className,
}: {
onPress?: ButtonProps["onPress"];
className?: ButtonProps["className"];
}) => {
return (
<Button
isIconOnly
className={clsx(
"border data-[hover=true]:bg-content2 border-default-400 dark:border-default-100",
className,
)}
radius="full"
size="sm"
variant="bordered"
onPress={onPress}
>
<CloseIcon />
</Button>
);
},
[],
);
const renderItem = useCallback(
(item: SearchResultItem, index: number, isRecent = false) => {
const isLvl1 = item.type === "lvl1";
const mainIcon = isRecent ? (
<SearchLinearIcon className={slots.leftIcon()} size={20} strokeWidth={2} />
) : isLvl1 ? (
<DocumentCodeBoldIcon className={slots.leftIcon()} />
) : (
<HashBoldIcon className={slots.leftIcon()} />
);
return (
<Command.Item
key={item.objectID}
ref={menuNodes.ref(index)}
className={slots.itemWrapper()}
data-active={index === activeItem}
value={item.content}
onMouseEnter={() => {
eventRef.current = "mouse";
setActiveItem(index);
}}
onSelect={() => {
if (eventRef.current === "keyboard") {
return;
}
onItemSelect(item);
}}
>
<div className={slots.leftWrapper()}>
{mainIcon}
<div className={slots.itemContent()}>
{!isLvl1 && <span className={slots.itemParentTitle()}>{item.hierarchy.lvl1}</span>}
<p className={slots.itemTitle()}>{item.content}</p>
</div>
</div>
<ChevronRightLinearIcon size={14} />
</Command.Item>
);
},
[activeItem, onItemSelect, CloseButton, slots],
);
const shouldOpen = !hideOnPaths.some((path) => pathname.includes(path));
return (
<Modal
hideCloseButton
backdrop="opaque"
classNames={{
base: [
"mt-[20vh]",
"border-small",
"dark:border-default-100",
"supports-[backdrop-filter]:bg-background/80",
"dark:supports-[backdrop-filter]:bg-background/30",
"supports-[backdrop-filter]:backdrop-blur-md",
"supports-[backdrop-filter]:backdrop-saturate-150",
],
backdrop: ["bg-black/80"],
}}
isOpen={isOpen && shouldOpen}
motionProps={{
onAnimationComplete: () => {
if (!isOpen) {
setQuery("");
}
},
}}
placement="top-center"
scrollBehavior="inside"
size="xl"
onClose={() => onClose()}
>
<ModalContent>
<Command className={slots.base()} label="Quick search command" shouldFilter={false}>
<div className={slots.header()}>
<SearchLinearIcon className={slots.searchIcon()} strokeWidth={2} />
<Command.Input
autoFocus={!isWebKit()}
className={slots.input()}
placeholder="Search documentation"
value={query}
onKeyDown={onInputKeyDown}
onValueChange={setQuery}
/>
{query.length > 0 && <CloseButton onPress={() => setQuery("")} />}
<Kbd className="hidden md:block border-none px-2 py-1 ml-2 font-medium text-[0.6rem]">
ESC
</Kbd>
</div>
<Command.List ref={listRef} className={slots.list()} role="listbox">
<Command.Empty>
{query.length > 0 && (
<div className={slots.emptyWrapper()}>
<div>
<p>No results for &quot;{query}&quot;</p>
{query.length === 1 ? (
<p className="text-default-400">
Try adding more characters to your search term.
</p>
) : (
<p className="text-default-400">Try searching for something else.</p>
)}
</div>
</div>
)}
</Command.Empty>
{isEmpty(query) &&
(isEmpty(recentSearches) ? (
<div className={slots.emptyWrapper()}>
<p className="text-default-400">No recent searches</p>
</div>
) : (
recentSearches &&
recentSearches.length > 0 && (
<Command.Group
heading={
<div className="flex items-center justify-between">
<p className="text-default-600">Recent</p>
</div>
}
>
{recentSearches.map((item, index) => renderItem(item, index, true))}
</Command.Group>
)
))}
{results.map((item, index) => renderItem(item, index))}
</Command.List>
</Command>
</ModalContent>
</Modal>
);
};