diff --git a/apps/docs/components/copy-button.tsx b/apps/docs/components/copy-button.tsx index 72a4b871ff..d6c32efc7d 100644 --- a/apps/docs/components/copy-button.tsx +++ b/apps/docs/components/copy-button.tsx @@ -1,7 +1,8 @@ -import {FC} from "react"; -import {Button, ButtonProps} from "@nextui-org/react"; +import {ButtonProps} from "@nextui-org/react"; import {useClipboard} from "@nextui-org/use-clipboard"; -import {clsx} from "@nextui-org/shared-utils"; +import {memo} from "react"; + +import {PreviewButton} from "./preview-button"; import {CheckLinearIcon, CopyLinearIcon} from "@/components/icons"; @@ -9,35 +10,28 @@ export interface CopyButtonProps extends ButtonProps { value?: string; } -export const CopyButton: FC = ({value, className, ...buttonProps}) => { +export const CopyButton = memo(({value, className, ...buttonProps}) => { const {copy, copied} = useClipboard(); + const icon = copied ? ( + + ) : ( + + ); + const handleCopy = () => { copy(value); }; - return ( - - ); -}; + return ; +}); + +CopyButton.displayName = "CopyButton"; diff --git a/apps/docs/components/docs/components/code-demo/react-live-demo.tsx b/apps/docs/components/docs/components/code-demo/react-live-demo.tsx index 74332d7636..de8813491a 100644 --- a/apps/docs/components/docs/components/code-demo/react-live-demo.tsx +++ b/apps/docs/components/docs/components/code-demo/react-live-demo.tsx @@ -10,6 +10,8 @@ import {SandpackFiles} from "@codesandbox/sandpack-react/types"; import {BgGridContainer} from "@/components/bg-grid-container"; import {GradientBox, GradientBoxProps} from "@/components/gradient-box"; import {CopyButton} from "@/components/copy-button"; +import {StackblitzButton} from "@/components/stackblitz-button"; +import {PreviewButton} from "@/components/preview-button"; export interface ReactLiveDemoProps { code: string; @@ -21,6 +23,7 @@ export interface ReactLiveDemoProps { className?: string; gradientColor?: GradientBoxProps["color"]; overflow?: "auto" | "visible" | "hidden"; + typescriptStrict?: boolean; } // 🚨 Do not pass react-hook-form to scope, it will break the live preview since @@ -49,11 +52,18 @@ export const ReactLiveDemo: React.FC = ({ height, className, noInline, + typescriptStrict = false, }) => { const content = ( <> {files?.[DEFAULT_FILE] && ( -
+
+ } + className="before:hidden opacity-0 group-hover/code-demo:opacity-100 transition-opacity text-zinc-400" + files={files} + typescriptStrict={typescriptStrict} + /> = ({width = "1em", height = "1em", ); }; +const StackblitzIcon: React.FC = ({...props}) => { + return ( + + + + ); +}; + const JavascriptIcon: React.FC = ({width = "1em", height = "1em", ...props}) => { return ( ( + (props, ref) => { + const {icon, className, ...buttonProps} = props; + + return ( + + ); + }, +); + +PreviewButton.displayName = "PreviewButton"; diff --git a/apps/docs/components/sandpack/sandpack.tsx b/apps/docs/components/sandpack/sandpack.tsx index b818331151..030f949639 100644 --- a/apps/docs/components/sandpack/sandpack.tsx +++ b/apps/docs/components/sandpack/sandpack.tsx @@ -3,6 +3,8 @@ import {FC, useRef} from "react"; import {SandpackProvider, SandpackLayout, SandpackPreview} from "@codesandbox/sandpack-react"; +import {StackblitzButton} from "../stackblitz-button"; + import {SandpackCodeViewer} from "./code-viewer"; import {nextuiTheme} from "./theme"; import {UseSandpackProps, useSandpack} from "./use-sandpack"; @@ -72,6 +74,18 @@ export const Sandpack: FC = ({ {showReportBug && } {showCopyCode && } {!showPreview && showOpenInCodeSandbox && } + {!showPreview && showOpenInCodeSandbox && ( + + )}
{hasTypescript && sandpackTemplate && ( diff --git a/apps/docs/components/stackblitz-button.tsx b/apps/docs/components/stackblitz-button.tsx new file mode 100644 index 0000000000..e639984cac --- /dev/null +++ b/apps/docs/components/stackblitz-button.tsx @@ -0,0 +1,58 @@ +import React, {forwardRef} from "react"; +import stackblitzSdk from "@stackblitz/sdk"; +import {SandpackFiles} from "@codesandbox/sandpack-react/types"; + +import {StackblitzIcon} from "./icons"; + +import {useStackblitz} from "@/hooks/use-stackblitz"; +import {Tooltip} from "@/../../packages/components/tooltip/src"; +import {Button, ButtonProps} from "@/../../packages/components/button/src"; + +export interface StackblitzButtonProps extends ButtonProps { + files: SandpackFiles; + typescriptStrict?: boolean; + className?: string; + button?: React.ReactElement; + icon?: React.ReactNode; +} + +export const StackblitzButton = forwardRef( + (props, ref) => { + const { + files, + typescriptStrict = false, + className, + button = + , + ); + + const combobox = getByTestId("combobox") as HTMLInputElement; + const submit = getByTestId("submit"); + + expect(combobox).not.toHaveAttribute("aria-describedby"); + expect(combobox.validity.valid).toBe(false); + + await user.hover(combobox); + await user.click(submit); + + expect(onSubmit).toHaveBeenCalledTimes(0); + expect(combobox).toHaveAttribute("aria-describedby"); + expect( + document.getElementById(combobox.getAttribute("aria-describedby")!), + ).toHaveTextContent("Please select an animal"); + + await user.click(combobox); + await user.keyboard("pe"); + + const listbox = await findByRole("listbox"); + const items = within(listbox).getAllByRole("option"); + + await user.click(items[0]); + expect(combobox).toHaveAttribute("aria-describedby"); + + await user.click(submit); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(combobox).not.toHaveAttribute("aria-describedby"); + }); }); describe("validationBehavior=aria", () => { diff --git a/packages/components/autocomplete/package.json b/packages/components/autocomplete/package.json index b34fa7546a..e3feb19520 100644 --- a/packages/components/autocomplete/package.json +++ b/packages/components/autocomplete/package.json @@ -1,6 +1,6 @@ { "name": "@nextui-org/autocomplete", - "version": "2.3.8", + "version": "2.3.9", "description": "An autocomplete combines a text input with a listbox, allowing users to filter a list of options to items matching a query.", "keywords": [ "autocomplete" diff --git a/packages/components/autocomplete/src/use-autocomplete.ts b/packages/components/autocomplete/src/use-autocomplete.ts index 381809b85b..47ce1dfad4 100644 --- a/packages/components/autocomplete/src/use-autocomplete.ts +++ b/packages/components/autocomplete/src/use-autocomplete.ts @@ -418,13 +418,9 @@ export function useAutocomplete(originalProps: UseAutocomplete onPress: (e: PressEvent) => { slotsProps.clearButtonProps?.onPress?.(e); if (state.selectedItem) { - state.setInputValue(""); state.setSelectedKey(null); - } else { - if (allowsCustomValue) { - state.setInputValue(""); - } } + state.setInputValue(""); state.open(); }, "data-visible": !!state.selectedItem || state.inputValue?.length > 0, @@ -433,12 +429,19 @@ export function useAutocomplete(originalProps: UseAutocomplete }), } as ButtonProps); + // prevent use-input's useFormValidation hook from overwriting use-autocomplete's useFormValidation hook when there are uncommitted validation errors + // see https://github.com/nextui-org/nextui/pull/4452 + const hasUncommittedValidation = + validationBehavior === "native" && + state.displayValidation.isInvalid === false && + state.realtimeValidation.isInvalid === true; + const getInputProps = () => ({ ...otherProps, ...inputProps, ...slotsProps.inputProps, - isInvalid, + isInvalid: hasUncommittedValidation ? undefined : isInvalid, validationBehavior, errorMessage: typeof errorMessage === "function" @@ -461,6 +464,7 @@ export function useAutocomplete(originalProps: UseAutocomplete itemHeight, } : undefined, + scrollShadowProps: slotsProps.scrollShadowProps, ...mergeProps(slotsProps.listboxProps, listBoxProps, { shouldHighlightOnFocus: true, }), @@ -478,6 +482,7 @@ export function useAutocomplete(originalProps: UseAutocomplete triggerType: "listbox", ...popoverProps, classNames: { + ...slotsProps.popoverProps?.classNames, content: slots.popoverContent({ class: clsx( classNames?.popoverContent, diff --git a/packages/components/autocomplete/stories/autocomplete.stories.tsx b/packages/components/autocomplete/stories/autocomplete.stories.tsx index a63fdf61b4..9c48731f5e 100644 --- a/packages/components/autocomplete/stories/autocomplete.stories.tsx +++ b/packages/components/autocomplete/stories/autocomplete.stories.tsx @@ -1187,3 +1187,23 @@ export const CustomItemHeight = { itemHeight: 40, }, }; + +export const PopoverTopOrBottom = { + args: { + ...defaultProps, + }, + render: (args) => ( +
+
+
+