diff --git a/app/docs/components/dropdown/dropdown.mdx b/app/docs/components/dropdown/dropdown.mdx index 5e550e84a..f64e07be7 100644 --- a/app/docs/components/dropdown/dropdown.mdx +++ b/app/docs/components/dropdown/dropdown.mdx @@ -1,6 +1,7 @@ import { CodePreview } from '~/app/components/code-preview'; import { Dropdown, theme } from '~/src'; import { HiCog, HiCurrencyDollar, HiLogout, HiViewGrid } from 'react-icons/hi'; +import Link from 'next/link'; The dropdown component is a UI component built with React that allows you to show a list of items when clicking on a trigger element (ie. a button) that you can use to build dropdown menus, lists, and more. @@ -189,6 +190,32 @@ To customize the trigger element you can use `renderTrigger` property. +## Custom item + +To customize the `Dropdown.Item` base element you can use the `as` property. + + + + Home + + + External link + +`} +> + + + Home + + + External link + + + + ## Theme To learn more about how to customize the appearance of components, please see the [Theme docs](/docs/customize/theme). diff --git a/package.json b/package.json index ce9921a0d..cafa3476e 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,9 @@ "react-dom": "^18", "tailwindcss": "^3" }, + "resolutions": { + "nwsapi": "2.2.2" + }, "private": false, "eslintConfig": { "extends": [ diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 8fed0f210..046929adf 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,4 +1,4 @@ -import { createElement, forwardRef, type ComponentProps, type ElementType, type ReactNode } from 'react'; +import { forwardRef, type ReactNode } from 'react'; import { twMerge } from 'tailwind-merge'; import type { DeepPartial, @@ -10,6 +10,7 @@ import type { } from '../../'; import { Spinner, useTheme } from '../../'; import { mergeDeep } from '../../helpers/merge-deep'; +import { ButtonBase, type ButtonBaseProps } from './ButtonBase'; import type { PositionInButtonGroup } from './ButtonGroup'; import ButtonGroup from './ButtonGroup'; @@ -61,13 +62,11 @@ export interface ButtonSizes extends Pick, 'color' | 'ref'> { +export interface ButtonProps extends ButtonBaseProps { color?: keyof FlowbiteColors; fullSized?: boolean; gradientDuoTone?: keyof ButtonGradientDuoToneColors; gradientMonochrome?: keyof ButtonGradientColors; - as?: ElementType; - href?: string; target?: string; isProcessing?: boolean; processingLabel?: string; @@ -95,8 +94,6 @@ const ButtonComponent = forwardRef processingSpinner: SpinnerComponent = , gradientDuoTone, gradientMonochrome, - as: Component = 'button', - href, label, outline = false, pill = false, @@ -110,18 +107,13 @@ const ButtonComponent = forwardRef const { buttonGroup: groupTheme, button: buttonTheme } = useTheme().theme; const theme = mergeDeep(buttonTheme, customTheme); - const BaseComponent = href ? 'a' : Component ?? 'button'; - const theirProps = props as object; - return createElement( - BaseComponent, - { - disabled, - href, - type: Component === 'button' ? 'button' : undefined, - ref: ref as never, - className: twMerge( + return ( + fullSized && theme.fullSized, groupTheme.position[positionInGroup], className, - ), - ...theirProps, - }, - - <> - {isProcessing && {SpinnerComponent}} - {typeof children !== 'undefined' ? ( - children - ) : ( - - {isProcessing ? processingLabel : label} - + - , + > + <> + {isProcessing && {SpinnerComponent}} + {typeof children !== 'undefined' ? ( + children + ) : ( + + {isProcessing ? processingLabel : label} + + )} + + + ); }, ); diff --git a/src/components/Button/ButtonBase.tsx b/src/components/Button/ButtonBase.tsx new file mode 100644 index 000000000..60d5a7077 --- /dev/null +++ b/src/components/Button/ButtonBase.tsx @@ -0,0 +1,18 @@ +import { createElement, forwardRef, type ComponentProps, type ElementType } from 'react'; + +export interface ButtonBaseProps extends Omit, 'color' | 'ref'> { + as?: ElementType; + href?: string; +} + +interface Props extends ButtonBaseProps, Record {} + +export const ButtonBase = forwardRef( + ({ children, as: Component = 'button', href, ...props }, ref) => { + const BaseComponent = href ? 'a' : Component ?? 'button'; + const type = Component === 'button' ? 'button' : undefined; + + return createElement(BaseComponent, { ref, href, type, ...props }, children); + }, +); +ButtonBase.displayName = 'Button'; diff --git a/src/components/Button/theme.ts b/src/components/Button/theme.ts index 4bbee1341..b46ad84ea 100644 --- a/src/components/Button/theme.ts +++ b/src/components/Button/theme.ts @@ -2,7 +2,7 @@ import type { FlowbiteButtonTheme } from './Button'; import type { FlowbiteButtonGroupTheme } from './ButtonGroup'; export const buttonTheme: FlowbiteButtonTheme = { - base: 'group flex h-min items-center justify-center p-0.5 text-center font-medium focus:z-10', + base: 'group flex h-min items-center justify-center p-0.5 text-center font-medium focus:z-10 focus:outline-none', fullSized: 'w-full', color: { dark: 'text-white bg-gray-800 border border-transparent enabled:hover:bg-gray-900 focus:ring-4 focus:ring-gray-300 dark:bg-gray-800 dark:enabled:hover:bg-gray-700 dark:focus:ring-gray-800 dark:border-gray-700', diff --git a/src/components/Dropdown/Dropdown.spec.tsx b/src/components/Dropdown/Dropdown.spec.tsx index 6c6339d7a..c5f1895c8 100644 --- a/src/components/Dropdown/Dropdown.spec.tsx +++ b/src/components/Dropdown/Dropdown.spec.tsx @@ -1,72 +1,159 @@ -import { act, render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import type { FC, ReactNode } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import { describe, expect, it } from 'vitest'; -import type { FlowbiteDropdownTheme } from './Dropdown'; +import type { DropdownProps } from './Dropdown'; import { Dropdown } from './Dropdown'; +const delay = async (delayTime: number) => await new Promise((r) => setTimeout(r, delayTime)); + describe('Components / Dropdown', () => { + describe('A11y', async () => { + it('should use `role="menu"` in menu container', async () => { + const user = userEvent.setup(); + render(); + + await act(() => user.click(button())); + + expect(screen.getByRole('menu')).toBe(dropdown()); + }); + + it('should use `role="menuitem"` in dropdown items', async () => { + const user = userEvent.setup(); + render(); + + await act(() => user.click(button())); + + expect(screen.getAllByRole('menuitem')).toHaveLength(4); + }); + + it('should not open when `disabled={true}`', async () => { + const user = userEvent.setup(); + const { rerender } = render(); + + expect(button()).toBeDisabled(); + + await act(() => user.click(button())); + expect(dropdown()).not.toBeInTheDocument(); + + rerender(); + + await act(() => user.hover(button())); + expect(dropdown()).not.toBeInTheDocument(); + }); + }); + describe('Keyboard interactions', () => { it('should collapse if expanded when `Space` is pressed', async () => { const user = userEvent.setup(); render(); + expect(dropdown()).not.toBeInTheDocument(); - expect(dropdown()).toHaveClass('invisible'); + await act(() => user.click(button())); - await user.click(button()); - - expect(dropdown()).not.toHaveClass('invisible'); + expect(dropdown()).toBeInTheDocument(); }); it('should expand if collapsed when `Space` is pressed', async () => { const user = userEvent.setup(); render(); - await user.click(button()); - await user.click(button()); + await act(() => user.click(button())); + await act(() => user.click(button())); + + expect(dropdown()).not.toBeInTheDocument(); + }); + + it('should expand when focus button and press arrow down key', async () => { + const user = userEvent.setup(); + render(); + + await act(() => user.tab()); + expect(button()).toHaveFocus(); + expect(dropdown()).not.toBeInTheDocument(); + + await act(() => fireEvent.keyDown(button(), { key: 'ArrowDown', code: 'ArrowDown' })); + expect(dropdown()).toBeInTheDocument(); + }); + + it('should focus matching item when user types the first option char and dropdown is open', async () => { + const user = userEvent.setup(); + render(); + + await act(() => user.click(button())); + expect(dropdown()).toBeInTheDocument(); - expect(dropdown()).toHaveClass('invisible'); + await act(() => fireEvent.keyDown(button(), { key: 'S', code: 'KeyS' })); + await delay(20); + + const item = screen.getByText('Settings'); + expect(item).toHaveFocus(); }); }); + describe('Mouse interactions', () => { it('should collapse if item is clicked', async () => { const user = userEvent.setup(); render(); - act(() => { - user.click(button()); - userEvent.click(dropdownItem()); - }); + await act(() => user.click(button())); + await act(() => userEvent.click(dropdownItem())); - expect(dropdown()).toHaveClass('invisible'); + expect(dropdown()).not.toBeInTheDocument(); }); + it('should collapse if CustomTriggerItem is clicked', async () => { const user = userEvent.setup(); render( } />); - act(() => { - user.click(button()); - userEvent.click(dropdownItem()); - }); + await act(() => user.click(screen.getByRole('button'))); + await act(() => userEvent.click(dropdownItem())); + + expect(dropdown()).not.toBeInTheDocument(); + }); + + it('should always collapse when item is clicked', async () => { + const user = userEvent.setup(); + render(); + + await act(() => user.click(button())); + await act(() => userEvent.click(dropdownItem())); + + expect(dropdown()).not.toBeInTheDocument(); + + await act(() => user.click(button())); + await act(() => userEvent.click(dropdownItem())); - expect(dropdown()).toHaveClass('invisible'); + expect(dropdown()).not.toBeInTheDocument(); }); it('should not collapse in case item is clicked if dismissOnClick = false', async () => { const user = userEvent.setup(); render(); - expect(dropdown()).toHaveClass('invisible'); + expect(dropdown()).not.toBeInTheDocument(); - await user.click(button()); + await act(() => user.click(button())); - expect(dropdown()).not.toHaveClass('invisible'); + expect(dropdown()).toBeInTheDocument(); - await user.click(dropdownItem()); + await act(() => userEvent.click(dropdownItem())); - expect(dropdown()).not.toHaveClass('invisible'); + expect(dropdown()).toBeInTheDocument(); + }); + + it('should open on hover when `trigger="hover"`', async () => { + const user = userEvent.setup(); + render(); + + expect(dropdown()).not.toBeInTheDocument(); + + await act(() => user.hover(button())); + + expect(dropdown()).toBeInTheDocument(); }); }); + describe('Type of button', async () => { it('should be of type `button`', async () => { render(); @@ -78,18 +165,43 @@ describe('Components / Dropdown', () => { expect(button()).toHaveAttribute('type', 'button'); }); }); + + describe('Dropdown item render', async () => { + it('should override Dropdownn.Item base component when using `as` prop', async () => { + const user = userEvent.setup(); + + const CustomBaseItem = ({ children }: PropsWithChildren) => { + return {children}; + }; + + render( + + Settings + , + ); + + await act(() => user.click(button())); + + const item = screen.getByText('Settings'); + expect(screen.getByRole('link')).toBe(item); + }); + }); }); -const TestDropdown: FC<{ - dismissOnClick?: boolean; - inline?: boolean; - renderTrigger?: (theme: FlowbiteDropdownTheme) => ReactNode; -}> = ({ dismissOnClick = true, inline = false, renderTrigger }) => ( +const TestDropdown: FC> = ({ + dismissOnClick = true, + inline = false, + disabled, + trigger, + renderTrigger, +}) => ( @@ -104,8 +216,8 @@ const TestDropdown: FC<{ ); -const button = () => screen.getByRole('button'); +const button = () => screen.getByRole('button', { name: /Dropdown button/i }); -const dropdown = () => screen.getByTestId('flowbite-tooltip'); +const dropdown = () => screen.queryByTestId('flowbite-dropdown'); const dropdownItem = () => screen.getByText('Dashboard'); diff --git a/src/components/Dropdown/Dropdown.stories.tsx b/src/components/Dropdown/Dropdown.stories.tsx index 6058120e7..21bfe2891 100644 --- a/src/components/Dropdown/Dropdown.stories.tsx +++ b/src/components/Dropdown/Dropdown.stories.tsx @@ -10,6 +10,7 @@ export default { title: 'Dropdown example', label: 'Dropdown button', placement: 'auto', + disabled: false, }, } as Meta; @@ -85,6 +86,20 @@ CustomTrigger.args = { ), }; +export const CustomItem = Template.bind({}); +CustomItem.args = { + children: ( + <> + Default button + As span + + + As link + + + ), +}; + export const ItemClickHandler = Template.bind({}); ItemClickHandler.storyName = 'Item click handlers'; ItemClickHandler.args = { diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 44e4fa3c2..ec90ddbce 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -1,13 +1,21 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -/* eslint-disable @typescript-eslint/ban-ts-comment */ - -import type { ComponentProps, Dispatch, FC, PropsWithChildren, ReactElement, ReactNode, SetStateAction } from 'react'; -import React, { Children, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; +import type { ExtendedRefs, useInteractions } from '@floating-ui/react'; +import { FloatingFocusManager, FloatingList, useListNavigation, useTypeahead } from '@floating-ui/react'; +import type { + ComponentProps, + Dispatch, + FC, + HTMLProps, + MutableRefObject, + PropsWithChildren, + ReactElement, + ReactNode, + SetStateAction, +} from 'react'; +import { cloneElement, createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { HiOutlineChevronDown, HiOutlineChevronLeft, HiOutlineChevronRight, HiOutlineChevronUp } from 'react-icons/hi'; import type { ButtonProps, DeepPartial } from '../../'; import { Button, useTheme } from '../../'; import type { FloatingProps, FlowbiteFloatingTheme } from '../../components/Floating'; -import { Floating } from '../../components/Floating'; import { mergeDeep } from '../../helpers/merge-deep'; import type { FlowbiteDropdownDividerTheme } from './DropdownDivider'; import { DropdownDivider } from './DropdownDivider'; @@ -16,6 +24,9 @@ import { DropdownHeader } from './DropdownHeader'; import type { FlowbiteDropdownItemTheme } from './DropdownItem'; import { DropdownItem } from './DropdownItem'; +import { twMerge } from 'tailwind-merge'; +import { useBaseFLoating, useFloatingInteractions } from '../../helpers/use-floating'; + export interface FlowbiteDropdownFloatingTheme extends FlowbiteFloatingTheme, FlowbiteDropdownDividerTheme, @@ -40,11 +51,8 @@ export interface DropdownProps inline?: boolean; label: ReactNode; theme?: DeepPartial; - renderTrigger?: (theme: FlowbiteDropdownTheme) => ReactNode; -} - -export interface TriggerWrapperProps extends ButtonProps { - setButtonWidth?: Dispatch>; + renderTrigger?: (theme: FlowbiteDropdownTheme) => ReactElement; + 'data-testid'?: string; } const icons: Record>> = { @@ -54,6 +62,60 @@ const icons: Record>> = { left: HiOutlineChevronLeft, }; +export interface TriggerProps extends Omit { + refs: ExtendedRefs; + inline?: boolean; + theme: FlowbiteDropdownTheme; + setButtonWidth?: Dispatch>; + getReferenceProps: (userProps?: HTMLProps | undefined) => Record; + renderTrigger?: (theme: FlowbiteDropdownTheme) => ReactElement; +} + +const Trigger = ({ + refs, + children, + inline, + theme, + disabled, + setButtonWidth, + getReferenceProps, + renderTrigger, + ...buttonProps +}: TriggerProps) => { + const ref = refs.reference as MutableRefObject; + const a11yProps = getReferenceProps(); + + useEffect(() => { + if (ref.current) { + setButtonWidth?.(ref.current.clientWidth); + } + }, [ref, setButtonWidth]); + + if (renderTrigger) { + const triggerElement = renderTrigger(theme); + return cloneElement(triggerElement, { ref: refs.setReference, disabled, ...a11yProps, ...triggerElement.props }); + } + + return inline ? ( + + ) : ( + + ); +}; + +interface DropdownContextValue { + activeIndex: number | null; + dismissOnClick?: boolean; + getItemProps: ReturnType['getItemProps']; + handleSelect: (index: number | null) => void; +} + +export const DropdownContext = createContext({} as DropdownContextValue); + const DropdownComponent: FC = ({ children, className, @@ -62,96 +124,125 @@ const DropdownComponent: FC = ({ renderTrigger, ...props }) => { - const id = useId(); + const [open, setOpen] = useState(false); + const [activeIndex, setActiveIndex] = useState(null); + const [selectedIndex, setSelectedIndex] = useState(null); + const [buttonWidth, setButtonWidth] = useState(undefined); + const elementsRef = useRef>([]); + const labelsRef = useRef>([]); + const theme = mergeDeep(useTheme().theme.dropdown, customTheme); const theirProps = props as Omit; + const dataTestId = props['data-testid'] || 'flowbite-dropdown-target'; const { placement = props.inline ? 'bottom-start' : 'bottom', trigger = 'click', label, inline, - floatingArrow = false, arrowIcon = true, ...buttonProps } = theirProps; - const Icon = useMemo(() => { - const [p] = placement.split('-'); - return icons[p] ?? HiOutlineChevronDown; - }, [placement]); + const handleSelect = useCallback((index: number | null) => { + setSelectedIndex(index); + setOpen(false); + }, []); - const [closeRequestKey, setCloseRequestKey] = useState(undefined); - const [buttonWidth, setButtonWidth] = useState(undefined); - - // Extends DropdownItem's onClick to trigger a close request to the Floating component - const attachCloseListener = useCallback( - // @ts-ignore TODO: Rewrite Dropdown - (node: ReactNode) => { - if (!React.isValidElement(node)) return node; - if ((node as ReactElement).type === DropdownItem) - return React.cloneElement(node, { - // @ts-ignore TODO: Rewrite Dropdown - onClick: () => { - node.props.onClick?.(); - dismissOnClick && setCloseRequestKey(id); - }, - }); - if (node.props.children && typeof node.props.children === 'object') { - return React.cloneElement(node, { - // @ts-ignore TODO: Rewrite Dropdown - children: Children.map(node.props.children, attachCloseListener), - }); + const handleTypeaheadMatch = useCallback( + (index: number | null) => { + if (open) { + setActiveIndex(index); + } else { + handleSelect(index); } - return node; }, - [dismissOnClick, id], + [open, handleSelect], ); - const content = useMemo( - () =>
    {Children.map(children, attachCloseListener)}
, - [attachCloseListener, children, theme.content], - ); + const { context, floatingStyles, refs } = useBaseFLoating({ + open, + setOpen, + placement, + }); + + const listNav = useListNavigation(context, { + listRef: elementsRef, + activeIndex, + selectedIndex, + onNavigate: setActiveIndex, + }); - const TriggerWrapper: FC = ({ children, setButtonWidth }): JSX.Element => { - const ref = useRef(null); + const typeahead = useTypeahead(context, { + listRef: labelsRef, + activeIndex, + selectedIndex, + onMatch: handleTypeaheadMatch, + }); - useEffect(() => { - if (ref.current) setButtonWidth?.(ref.current.clientWidth); - }, [ref]); + const { getReferenceProps, getFloatingProps, getItemProps } = useFloatingInteractions({ + context, + role: 'menu', + trigger, + interactions: [listNav, typeahead], + }); - return inline ? ( - - ) : ( - - ); - }; + const Icon = useMemo(() => { + const [p] = placement.split('-'); + return icons[p] ?? HiOutlineChevronDown; + }, [placement]); return ( - - {renderTrigger ? ( - renderTrigger(theme) - ) : ( - - {label} - {arrowIcon && } - - )} - + <> + + {label} + {arrowIcon && } + + + {open && ( + +
+ +
    + {children} +
+
+
+
+ )} +
+ ); }; diff --git a/src/components/Dropdown/DropdownItem.tsx b/src/components/Dropdown/DropdownItem.tsx index 123230eef..e1571cf46 100644 --- a/src/components/Dropdown/DropdownItem.tsx +++ b/src/components/Dropdown/DropdownItem.tsx @@ -1,24 +1,28 @@ -/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ - +import { useListItem } from '@floating-ui/react'; import type { ComponentProps, FC, PropsWithChildren } from 'react'; +import { useContext } from 'react'; import { twMerge } from 'tailwind-merge'; import type { DeepPartial } from '../../'; import { useTheme } from '../../'; import { mergeDeep } from '../../helpers/merge-deep'; +import type { ButtonBaseProps } from '../Button/ButtonBase'; +import { ButtonBase } from '../Button/ButtonBase'; +import { DropdownContext } from './Dropdown'; export interface FlowbiteDropdownItemTheme { + container: string; base: string; icon: string; } -export interface DropdownItemProps extends PropsWithChildren, ComponentProps<'li'> { +export interface DropdownItemProps extends PropsWithChildren, ButtonBaseProps { icon?: FC>; onClick?: () => void; theme?: DeepPartial; } +interface Props extends DropdownItemProps, Record {} -export const DropdownItem: FC = ({ +export const DropdownItem: FC = ({ children, className, icon: Icon, @@ -26,12 +30,29 @@ export const DropdownItem: FC = ({ theme: customTheme = {}, ...props }) => { + const { ref, index } = useListItem({ label: typeof children === 'string' ? children : undefined }); + const { activeIndex, dismissOnClick, getItemProps, handleSelect } = useContext(DropdownContext); + const isActive = activeIndex === index; + const theme = mergeDeep(useTheme().theme.dropdown.floating.item, customTheme); return ( -
  • - {Icon && } - {children} +
  • + { + onClick && onClick(); + dismissOnClick && handleSelect(null); + }, + })} + tabIndex={isActive ? 0 : -1} + > + {Icon && } + {children} +
  • ); }; diff --git a/src/components/Dropdown/theme.ts b/src/components/Dropdown/theme.ts index 541338ba2..ea43d1701 100644 --- a/src/components/Dropdown/theme.ts +++ b/src/components/Dropdown/theme.ts @@ -2,7 +2,7 @@ import type { FlowbiteDropdownTheme } from './Dropdown'; export const dropdownTheme: FlowbiteDropdownTheme = { arrowIcon: 'ml-2 h-4 w-4', - content: 'py-1', + content: 'py-1 focus:outline-none', floating: { animation: 'transition-opacity', arrow: { @@ -14,13 +14,14 @@ export const dropdownTheme: FlowbiteDropdownTheme = { }, placement: '-4px', }, - base: 'z-10 w-fit rounded divide-y divide-gray-100 shadow', + base: 'z-10 w-fit rounded divide-y divide-gray-100 shadow focus:outline-none', content: 'py-1 text-sm text-gray-700 dark:text-gray-200', divider: 'my-1 h-px bg-gray-100 dark:bg-gray-600', header: 'block py-2 px-4 text-sm text-gray-700 dark:text-gray-200', hidden: 'invisible opacity-0', item: { - base: 'flex items-center justify-start py-2 px-4 text-sm text-gray-700 cursor-pointer hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-600 dark:hover:text-white', + container: '', + base: 'flex items-center justify-start py-2 px-4 text-sm text-gray-700 cursor-pointer w-full hover:bg-gray-100 focus:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-600 focus:outline-none dark:hover:text-white dark:focus:bg-gray-600 dark:focus:text-white', icon: 'mr-2 h-4 w-4', }, style: { diff --git a/src/components/Floating/Floating.tsx b/src/components/Floating/Floating.tsx index 1a4ca16d6..7f1d12a28 100644 --- a/src/components/Floating/Floating.tsx +++ b/src/components/Floating/Floating.tsx @@ -1,18 +1,10 @@ import type { Placement } from '@floating-ui/core'; -import { - autoUpdate, - safePolygon, - useClick, - useFloating, - useFocus, - useHover, - useInteractions, - useRole, -} from '@floating-ui/react'; +import { autoUpdate, useFocus } from '@floating-ui/react'; import type { ComponentProps, FC, PropsWithChildren, ReactNode } from 'react'; import { useEffect, useRef, useState } from 'react'; import { twMerge } from 'tailwind-merge'; -import { getArrowPlacement, getMiddleware, getPlacement } from '../../helpers/floating'; +import { getArrowPlacement } from '../../helpers/floating'; +import { useBaseFLoating, useFloatingInteractions } from '../../helpers/use-floating'; export interface FlowbiteFloatingTheme { arrow: FlowbiteFloatingArrowTheme; @@ -38,13 +30,14 @@ export interface FlowbiteFloatingArrowTheme { }; } +export type FloatingStyle = 'dark' | 'light' | 'auto'; + export interface FloatingProps extends PropsWithChildren, Omit, 'content' | 'style'> { animation?: false | `duration-${number}`; arrow?: boolean; - closeRequestKey?: string; content: ReactNode; placement?: 'auto' | Placement; - style?: 'dark' | 'light' | 'auto'; + style?: FloatingStyle; theme: FlowbiteFloatingTheme; trigger?: 'hover' | 'click'; minWidth?: number; @@ -58,7 +51,6 @@ export const Floating: FC = ({ arrow = true, children, className, - closeRequestKey, content, placement = 'top', style = 'dark', @@ -70,11 +62,11 @@ export const Floating: FC = ({ const arrowRef = useRef(null); const [open, setOpen] = useState(false); - const floatingTooltip = useFloating({ - middleware: getMiddleware({ arrowRef, placement }), - onOpenChange: setOpen, + const floatingProperties = useBaseFLoating({ open, - placement: getPlacement({ placement }), + placement, + arrowRef, + setOpen, }); const { @@ -85,17 +77,15 @@ export const Floating: FC = ({ update, x, y, - } = floatingTooltip; + } = floatingProperties; - const { getFloatingProps, getReferenceProps } = useInteractions([ - useClick(context, { enabled: trigger === 'click' }), - useFocus(context), - useHover(context, { - enabled: trigger === 'hover', - handleClose: safePolygon(), - }), - useRole(context, { role: 'tooltip' }), - ]); + const focus = useFocus(context); + const { getFloatingProps, getReferenceProps } = useFloatingInteractions({ + context, + role: 'tooltip', + trigger, + interactions: [focus], + }); useEffect(() => { if (refs.reference.current && refs.floating.current && open) { @@ -103,10 +93,6 @@ export const Floating: FC = ({ } }, [open, refs.floating, refs.reference, update]); - useEffect(() => { - if (closeRequestKey !== undefined) setOpen(false); - }, [closeRequestKey]); - return ( <>
    = ({ left: arrowX ?? ' ', right: ' ', bottom: ' ', - [getArrowPlacement({ placement: floatingTooltip.placement })]: theme.arrow.placement, + [getArrowPlacement({ placement: floatingProperties.placement })]: theme.arrow.placement, }} >   diff --git a/src/helpers/floating.ts b/src/helpers/floating.ts index 55670804b..1ff1d0171 100644 --- a/src/helpers/floating.ts +++ b/src/helpers/floating.ts @@ -9,7 +9,7 @@ export const getMiddleware = ({ arrowRef, placement, }: { - arrowRef: RefObject; + arrowRef?: RefObject; placement: 'auto' | Placement; }): Middleware[] => { const middleware = []; @@ -18,7 +18,7 @@ export const getMiddleware = ({ middleware.push(placement === 'auto' ? autoPlacement() : flip()); middleware.push(shift({ padding: 8 })); - if (arrowRef.current) { + if (arrowRef?.current) { middleware.push(arrow({ element: arrowRef.current })); } diff --git a/src/helpers/use-floating.ts b/src/helpers/use-floating.ts new file mode 100644 index 000000000..97eb01aaf --- /dev/null +++ b/src/helpers/use-floating.ts @@ -0,0 +1,62 @@ +import type { ElementProps, Placement, ReferenceType, UseRoleProps } from '@floating-ui/react'; +import { + autoUpdate, + safePolygon, + useClick, + useDismiss, + useFloating, + useHover, + useInteractions, + useRole, +} from '@floating-ui/react'; + +import type { Dispatch, RefObject, SetStateAction } from 'react'; + +import { getMiddleware, getPlacement } from './floating'; + +export type UseBaseFloatingParams = { + placement?: 'auto' | Placement; + open: boolean; + arrowRef?: RefObject; + setOpen: Dispatch>; +}; + +export const useBaseFLoating = ({ + open, + arrowRef, + placement = 'top', + setOpen, +}: UseBaseFloatingParams) => { + return useFloating({ + placement: getPlacement({ placement }), + open, + onOpenChange: setOpen, + whileElementsMounted: autoUpdate, + middleware: getMiddleware({ placement, arrowRef }), + }); +}; + +export type UseFloatingInteractionsParams = { + context: ReturnType['context']; + trigger?: 'hover' | 'click'; + role?: UseRoleProps['role']; + interactions?: ElementProps[]; +}; + +export const useFloatingInteractions = ({ + context, + trigger, + role = 'tooltip', + interactions = [], +}: UseFloatingInteractionsParams) => { + return useInteractions([ + useClick(context, { enabled: trigger === 'click' }), + useHover(context, { + enabled: trigger === 'hover', + handleClose: safePolygon(), + }), + useDismiss(context), + useRole(context, { role }), + ...interactions, + ]); +}; diff --git a/yarn.lock b/yarn.lock index f77ca63ff..f75960387 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1546,12 +1546,12 @@ "@floating-ui/dom" "^1.3.0" "@floating-ui/react@^0.24.3": - version "0.24.3" - resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.24.3.tgz#4f11f09c7245555724f5167dd6925133457db89c" - integrity sha512-wWC9duiog4HmbgKSKObDRuXqMjZR/6m75MIG+slm5CVWbridAjK9STcnCsGYmdpK78H/GmzYj4ADVP8paZVLYQ== + version "0.24.5" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.24.5.tgz#a5ba451c308ce112e98c59dcb89b28d100236cde" + integrity sha512-p/cOvACHFooJX5yTaim8PZgMAt67IIBAkynZfWiLsor5aUE6all1OJ73eVpjATUxFP5l8gqOszvP1Zr22T2UgQ== dependencies: "@floating-ui/react-dom" "^2.0.1" - aria-hidden "^1.1.3" + aria-hidden "^1.2.3" tabbable "^6.0.1" "@hapi/hoek@^9.0.0": @@ -3714,40 +3714,39 @@ test-exclude "^6.0.0" v8-to-istanbul "^9.1.0" -"@vitest/expect@0.32.2": - version "0.32.2" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.32.2.tgz#8111f6ab1ff3b203efbe3a25e8bb2d160ce4b720" - integrity sha512-6q5yzweLnyEv5Zz1fqK5u5E83LU+gOMVBDuxBl2d2Jfx1BAp5M+rZgc5mlyqdnxquyoiOXpXmFNkcGcfFnFH3Q== +"@vitest/expect@0.32.4": + version "0.32.4" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.32.4.tgz#4aa4eec78112cdbe299834b965420d4fb3afa91d" + integrity sha512-m7EPUqmGIwIeoU763N+ivkFjTzbaBn0n9evsTOcde03ugy2avPs3kZbYmw3DkcH1j5mxhMhdamJkLQ6dM1bk/A== dependencies: - "@vitest/spy" "0.32.2" - "@vitest/utils" "0.32.2" + "@vitest/spy" "0.32.4" + "@vitest/utils" "0.32.4" chai "^4.3.7" -"@vitest/runner@0.32.2": - version "0.32.2" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.32.2.tgz#18dd979ce4e8766bcc90948d11b4c8ae6ed90b89" - integrity sha512-06vEL0C1pomOEktGoLjzZw+1Fb+7RBRhmw/06WkDrd1akkT9i12su0ku+R/0QM69dfkIL/rAIDTG+CSuQVDcKw== +"@vitest/runner@0.32.4": + version "0.32.4" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.32.4.tgz#2872c697994745f1b70e2bd6568236ad2d9eade6" + integrity sha512-cHOVCkiRazobgdKLnczmz2oaKK9GJOw6ZyRcaPdssO1ej+wzHVIkWiCiNacb3TTYPdzMddYkCgMjZ4r8C0JFCw== dependencies: - "@vitest/utils" "0.32.2" - concordance "^5.0.4" + "@vitest/utils" "0.32.4" p-limit "^4.0.0" - pathe "^1.1.0" + pathe "^1.1.1" -"@vitest/snapshot@0.32.2": - version "0.32.2" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.32.2.tgz#500b6453e88e4c50a0aded39839352c16b519b9e" - integrity sha512-JwhpeH/PPc7GJX38vEfCy9LtRzf9F4er7i4OsAJyV7sjPwjj+AIR8cUgpMTWK4S3TiamzopcTyLsZDMuldoi5A== +"@vitest/snapshot@0.32.4": + version "0.32.4" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.32.4.tgz#75166b1c772d018278a7f0e79f43f3eae813f5ae" + integrity sha512-IRpyqn9t14uqsFlVI2d7DFMImGMs1Q9218of40bdQQgMePwVdmix33yMNnebXcTzDU5eiV3eUsoxxH5v0x/IQA== dependencies: magic-string "^0.30.0" - pathe "^1.1.0" - pretty-format "^27.5.1" + pathe "^1.1.1" + pretty-format "^29.5.0" -"@vitest/spy@0.32.2": - version "0.32.2" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.32.2.tgz#f3ef7afe0d34e863b90df7c959fa5af540a6aaf9" - integrity sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug== +"@vitest/spy@0.32.4": + version "0.32.4" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.32.4.tgz#c3212bc60c1430c3b5c39d6a384a75458b8f1e80" + integrity sha512-oA7rCOqVOOpE6rEoXuCOADX7Lla1LIa4hljI2MSccbpec54q+oifhziZIJXxlE/CvI2E+ElhBHzVu0VEvJGQKQ== dependencies: - tinyspy "^2.1.0" + tinyspy "^2.1.1" "@vitest/ui@^0.32.2": version "0.32.2" @@ -3771,6 +3770,15 @@ loupe "^2.3.6" pretty-format "^27.5.1" +"@vitest/utils@0.32.4": + version "0.32.4" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.32.4.tgz#36283e3aa3f3b1a378e19493c7b3b9107dc4ea71" + integrity sha512-Gwnl8dhd1uJ+HXrYyV0eRqfmk9ek1ASE/LWfTCuWMw+d07ogHqp4hEAV28NiecimK6UY9DpSEPh+pXBA5gtTBg== + dependencies: + diff-sequences "^29.4.3" + loupe "^2.3.6" + pretty-format "^29.5.0" + "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" @@ -3967,6 +3975,11 @@ acorn@^8.0.0, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8 resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.9.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59" + integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ== + add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" @@ -4143,7 +4156,7 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-hidden@^1.1.3: +aria-hidden@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ== @@ -4502,11 +4515,6 @@ bluebird@3.7.2, bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -blueimp-md5@^2.10.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0" - integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w== - bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" @@ -5181,20 +5189,6 @@ concat-stream@^2.0.0: readable-stream "^3.0.2" typedarray "^0.0.6" -concordance@^5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/concordance/-/concordance-5.0.4.tgz#9896073261adced72f88d60e4d56f8efc4bbbbd2" - integrity sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw== - dependencies: - date-time "^3.1.0" - esutils "^2.0.3" - fast-diff "^1.2.0" - js-string-escape "^1.0.1" - lodash "^4.17.15" - md5-hex "^3.0.1" - semver "^7.3.2" - well-known-symbols "^2.0.0" - console-browserify@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" @@ -5694,13 +5688,6 @@ data-urls@^4.0.0: whatwg-mimetype "^3.0.0" whatwg-url "^12.0.0" -date-time@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/date-time/-/date-time-3.1.0.tgz#0d1e934d170579f481ed8df1e2b8ff70ee845e1e" - integrity sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg== - dependencies: - time-zone "^1.0.0" - dateformat@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" @@ -6672,7 +6659,7 @@ estree-walker@^3.0.0: dependencies: "@types/estree" "^1.0.0" -esutils@^2.0.2, esutils@^2.0.3: +esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== @@ -6875,11 +6862,6 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-diff@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" - integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== - fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" @@ -8640,11 +8622,6 @@ joi@^17.7.0: "@sideway/formula" "^3.0.1" "@sideway/pinpoint" "^2.0.0" -js-string-escape@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" - integrity sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -9186,13 +9163,6 @@ markdown-to-jsx@^7.1.8: resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.2.0.tgz#e7b46b65955f6a04d48a753acd55874a14bdda4b" integrity sha512-3l4/Bigjm4bEqjCR6Xr+d4DtM1X6vvtGsMGSjJYyep8RjjIvcWtrXBS8Wbfe1/P+atKNMccpsraESIaWVplzVg== -md5-hex@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-3.0.1.tgz#be3741b510591434b2784d79e556eefc2c9a8e5c" - integrity sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw== - dependencies: - blueimp-md5 "^2.10.0" - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -9977,6 +9947,16 @@ mlly@^1.2.0: pkg-types "^1.0.3" ufo "^1.1.2" +mlly@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.0.tgz#830c10d63f1f97bd8785377b24dc2a15d972832b" + integrity sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg== + dependencies: + acorn "^8.9.0" + pathe "^1.1.1" + pkg-types "^1.0.3" + ufo "^1.1.2" + modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" @@ -10209,10 +10189,10 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" -nwsapi@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.4.tgz#fd59d5e904e8e1f03c25a7d5a15cfa16c714a1e5" - integrity sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g== +nwsapi@2.2.2, nwsapi@^2.2.4: + version "2.2.2" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" + integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" @@ -10641,6 +10621,11 @@ pathe@^1.1.0: resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.0.tgz#e2e13f6c62b31a3289af4ba19886c230f295ec03" integrity sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w== +pathe@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a" + integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== + pathval@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" @@ -12229,7 +12214,7 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -std-env@^3.3.2: +std-env@^3.3.2, std-env@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.3.tgz#a54f06eb245fdcfef53d56f3c0251f1d5c3d01fe" integrity sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg== @@ -12695,11 +12680,6 @@ through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8, through@~2.3, t resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== -time-zone@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/time-zone/-/time-zone-1.0.0.tgz#99c5bf55958966af6d06d83bdf3800dc82faec5d" - integrity sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA== - timers-browserify@^2.0.12: version "2.0.12" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" @@ -12717,10 +12697,10 @@ tinypool@^0.5.0: resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.5.0.tgz#3861c3069bf71e4f1f5aa2d2e6b3aaacc278961e" integrity sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ== -tinyspy@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.1.0.tgz#bd6875098f988728e6456cfd5ab8cc06498ecdeb" - integrity sha512-7eORpyqImoOvkQJCSkL0d0mB4NHHIFAy4b1u8PHdDa7SjGS2njzl6/lyGoZLm+eyYEtlUmFGE0rFj66SWxZgQQ== +tinyspy@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.1.1.tgz#9e6371b00c259e5c5b301917ca18c01d40ae558c" + integrity sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w== titleize@^3.0.0: version "3.0.0" @@ -13325,15 +13305,15 @@ vfile@^5.0.0: unist-util-stringify-position "^3.0.0" vfile-message "^3.0.0" -vite-node@0.32.2: - version "0.32.2" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.32.2.tgz#bfccdfeb708b2309ea9e5fe424951c75bb9c0096" - integrity sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA== +vite-node@0.32.4: + version "0.32.4" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.32.4.tgz#7b3f94af5a87c631fbc380ba662914bafbd04d80" + integrity sha512-L2gIw+dCxO0LK14QnUMoqSYpa9XRGnTTTDjW2h19Mr+GR0EFj4vx52W41gFXfMLqpA00eK4ZjOVYo1Xk//LFEw== dependencies: cac "^6.7.14" debug "^4.3.4" - mlly "^1.2.0" - pathe "^1.1.0" + mlly "^1.4.0" + pathe "^1.1.1" picocolors "^1.0.0" vite "^3.0.0 || ^4.0.0" @@ -13369,34 +13349,33 @@ vite@^4.3.9: fsevents "~2.3.2" vitest@^0.32.2: - version "0.32.2" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.32.2.tgz#758ce2220f609e240ac054eca7ad11a5140679ab" - integrity sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ== + version "0.32.4" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.32.4.tgz#a0558ae44c2ccdc254eece0365f16c4ffc5231bb" + integrity sha512-3czFm8RnrsWwIzVDu/Ca48Y/M+qh3vOnF16czJm98Q/AN1y3B6PBsyV8Re91Ty5s7txKNjEhpgtGPcfdbh2MZg== dependencies: "@types/chai" "^4.3.5" "@types/chai-subset" "^1.3.3" "@types/node" "*" - "@vitest/expect" "0.32.2" - "@vitest/runner" "0.32.2" - "@vitest/snapshot" "0.32.2" - "@vitest/spy" "0.32.2" - "@vitest/utils" "0.32.2" - acorn "^8.8.2" + "@vitest/expect" "0.32.4" + "@vitest/runner" "0.32.4" + "@vitest/snapshot" "0.32.4" + "@vitest/spy" "0.32.4" + "@vitest/utils" "0.32.4" + acorn "^8.9.0" acorn-walk "^8.2.0" cac "^6.7.14" chai "^4.3.7" - concordance "^5.0.4" debug "^4.3.4" local-pkg "^0.4.3" magic-string "^0.30.0" - pathe "^1.1.0" + pathe "^1.1.1" picocolors "^1.0.0" - std-env "^3.3.2" + std-env "^3.3.3" strip-literal "^1.0.1" tinybench "^2.5.0" tinypool "^0.5.0" vite "^3.0.0 || ^4.0.0" - vite-node "0.32.2" + vite-node "0.32.4" why-is-node-running "^2.2.2" vm-browserify@^1.1.2: @@ -13514,11 +13493,6 @@ webpack@5: watchpack "^2.4.0" webpack-sources "^3.2.3" -well-known-symbols@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/well-known-symbols/-/well-known-symbols-2.0.0.tgz#e9c7c07dbd132b7b84212c8174391ec1f9871ba5" - integrity sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q== - whatwg-encoding@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53"