diff --git a/packages/core/src/menubar/menubar-menu.tsx b/packages/core/src/menubar/menubar-menu.tsx index d2f17f09..bffc4e11 100644 --- a/packages/core/src/menubar/menubar-menu.tsx +++ b/packages/core/src/menubar/menubar-menu.tsx @@ -5,16 +5,6 @@ import { MenuRoot, MenuRootOptions } from "../menu"; import { useMenubarContext } from "./menubar-context"; export interface MenubarMenuOptions extends MenuRootOptions { - /** - * Whether the menu should be the only visible content for screen readers. - * When set to `true`: - * - interaction with outside elements will be disabled. - * - scroll will be locked. - * - focus will be locked inside the menu content. - * - elements outside the menu content will not be visible for screen readers. - * Default false - */ - modal?: boolean; } export interface MenubarMenuProps extends ParentProps {} diff --git a/packages/core/src/menubar/menubar-root.tsx b/packages/core/src/menubar/menubar-root.tsx index cfb776e9..9d79a707 100644 --- a/packages/core/src/menubar/menubar-root.tsx +++ b/packages/core/src/menubar/menubar-root.tsx @@ -22,6 +22,7 @@ import { createUniqueId, onCleanup, splitProps, + Setter, } from "solid-js"; import { isServer } from "solid-js/web"; @@ -48,6 +49,9 @@ export interface MenubarRootOptions { /** When true, click on alt by itsef will focus this Menubar (some browsers interfere) */ focusOnAlt?: boolean; + + autoFocusMenu?: boolean; + onAutoFocusMenuChange?: Setter; } export interface MenubarRootCommonProps { @@ -84,6 +88,8 @@ export function MenubarRoot( "onValueChange", "loop", "focusOnAlt", + "autoFocusMenu", + "onAutoFocusMenuChange", ]); const [value, setValue] = createControllableSignal({ @@ -103,7 +109,11 @@ export function MenubarRoot( "data-closed": value() === undefined ? "" : undefined, })); - const [autoFocusMenu, setAutoFocusMenu] = createSignal(false); + const [autoFocusMenu, setAutoFocusMenu] = createControllableSignal({ + value: () => local.autoFocusMenu, + defaultValue: () => false, + onChange: local.onAutoFocusMenuChange, + }) const context: MenubarContextValue = { dataset, @@ -163,7 +173,7 @@ export function MenubarRoot( setAutoFocusMenu(false); setValue(undefined); }, - autoFocusMenu, + autoFocusMenu: () => autoFocusMenu()!, setAutoFocusMenu, generateId: createGenerateId(() => others.id!), }; diff --git a/packages/core/src/navigation-menu/index.tsx b/packages/core/src/navigation-menu/index.tsx new file mode 100644 index 00000000..2aa92f10 --- /dev/null +++ b/packages/core/src/navigation-menu/index.tsx @@ -0,0 +1,228 @@ +import { + MenuCheckboxItem as CheckboxItem, + type MenuCheckboxItemCommonProps as NavigationMenuCheckboxItemCommonProps, + type MenuCheckboxItemOptions as NavigationMenuCheckboxItemOptions, + type MenuCheckboxItemProps as NavigationMenuCheckboxItemProps, + type MenuCheckboxItemRenderProps as NavigationMenuCheckboxItemRenderProps, + MenuGroup as Group, + type MenuGroupCommonProps as NavigationMenuGroupCommonProps, + MenuGroupLabel as GroupLabel, + type MenuGroupLabelCommonProps as NavigationMenuGroupLabelCommonProps, + type MenuGroupLabelOptions as NavigationMenuGroupLabelOptions, + type MenuGroupLabelProps as NavigationMenuGroupLabelProps, + type MenuGroupLabelRenderProps as NavigationMenuGroupLabelRenderProps, + type MenuGroupOptions as NavigationMenuGroupOptions, + type MenuGroupProps as NavigationMenuGroupProps, + type MenuGroupRenderProps as NavigationMenuGroupRenderProps, + MenuIcon as Icon, + type MenuIconCommonProps as NavigationMenuIconCommonProps, + type MenuIconOptions as NavigationMenuIconOptions, + type MenuIconProps as NavigationMenuIconProps, + type MenuIconRenderProps as NavigationMenuIconRenderProps, + MenuItem as Item, + type MenuItemCommonProps as NavigationMenuItemCommonProps, + MenuItemDescription as ItemDescription, + type MenuItemDescriptionCommonProps as NavigationMenuItemDescriptionCommonProps, + type MenuItemDescriptionOptions as NavigationMenuItemDescriptionOptions, + type MenuItemDescriptionProps as NavigationMenuItemDescriptionProps, + type MenuItemDescriptionRenderProps as NavigationMenuItemDescriptionRenderProps, + MenuItemIndicator as ItemIndicator, + type MenuItemIndicatorCommonProps as NavigationMenuItemIndicatorCommonProps, + type MenuItemIndicatorOptions as NavigationMenuItemIndicatorOptions, + type MenuItemIndicatorProps as NavigationMenuItemIndicatorProps, + type MenuItemIndicatorRenderProps as NavigationMenuItemIndicatorRenderProps, + MenuItemLabel as ItemLabel, + type MenuItemLabelCommonProps as NavigationMenuItemLabelCommonProps, + type MenuItemLabelOptions as NavigationMenuItemLabelOptions, + type MenuItemLabelProps as NavigationMenuItemLabelProps, + type MenuItemLabelRenderProps as NavigationMenuItemLabelRenderProps, + type MenuItemOptions as NavigationMenuItemOptions, + type MenuItemProps as NavigationMenuItemProps, + type MenuItemRenderProps as NavigationMenuItemRenderProps, + MenuPortal as Portal, + type MenuPortalProps as NavigationMenuPortalProps, + MenuRadioGroup as RadioGroup, + type MenuRadioGroupCommonProps as NavigationMenuRadioGroupCommonProps, + type MenuRadioGroupOptions as NavigationMenuRadioGroupOptions, + type MenuRadioGroupProps as NavigationMenuRadioGroupProps, + type MenuRadioGroupRenderProps as NavigationMenuRadioGroupRenderProps, + MenuRadioItem as RadioItem, + type MenuRadioItemCommonProps as NavigationMenuRadioItemCommonProps, + type MenuRadioItemOptions as NavigationMenuRadioItemOptions, + type MenuRadioItemProps as NavigationMenuRadioItemProps, + type MenuRadioItemRenderProps as NavigationMenuRadioItemPRenderrops, + MenuSub as Sub, + MenuSubContent as SubContent, + type MenuSubContentCommonProps as NavigationMenuSubContentCommonProps, + type MenuSubContentOptions as NavigationMenuSubContentOptions, + type MenuSubContentProps as NavigationMenuSubContentProps, + type MenuSubContentRenderProps as NavigationMenuSubContentRenderProps, + type MenuSubOptions as NavigationMenuSubOptions, + type MenuSubProps as NavigationMenuSubProps, + MenuSubTrigger as SubTrigger, + type MenuSubTriggerCommonProps as NavigationMenuSubTriggerCommonProps, + type MenuSubTriggerOptions as NavigationMenuSubTriggerOptions, + type MenuSubTriggerProps as NavigationMenuSubTriggerProps, + type MenuSubTriggerRenderProps as NavigationMenuSubTriggerRenderProps, +} from "../menu"; +import { + Arrow, + type PopperArrowCommonProps as NavigationMenuArrowCommonProps, + type PopperArrowOptions as NavigationMenuArrowOptions, + type PopperArrowProps as NavigationMenuArrowProps, + type PopperArrowRenderProps as NavigationMenuArrowRenderProps, +} from "../popper"; +import { + Root as Separator, + type SeparatorRootCommonProps as NavigationMenuSeparatorCommonProps, + type SeparatorRootOptions as NavigationMenuSeparatorOptions, + type SeparatorRootProps as NavigationMenuSeparatorProps, + type SeparatorRootRenderProps as NavigationMenuSeparatorRenderProps, +} from "../separator"; +import { + NavigationMenuMenu as Menu, + type NavigationMenuMenuOptions, + type NavigationMenuMenuProps, +} from "./navigation-menu-menu"; +import { + NavigationMenuRoot as Root, + type NavigationMenuRootCommonProps, + type NavigationMenuRootOptions, + type NavigationMenuRootProps, + type NavigationMenuRootRenderProps, +} from "./navigation-menu-root"; +import { + NavigationMenuTrigger as Trigger, + type NavigationMenuTriggerOptions, + type NavigationMenuTriggerCommonProps, + type NavigationMenuTriggerRenderProps, + type NavigationMenuTriggerProps, +} from "./navigation-menu-trigger"; +import { + NavigationMenuContent as Content, + type NavigationMenuContentCommonProps, + type NavigationMenuContentOptions, + type NavigationMenuContentProps, + type NavigationMenuContentRenderProps, +} from "./navigation-menu-content"; + +export type { + NavigationMenuRootOptions, + NavigationMenuRootCommonProps, + NavigationMenuRootRenderProps, + NavigationMenuRootProps, + NavigationMenuMenuOptions, + NavigationMenuMenuProps, + NavigationMenuArrowOptions, + NavigationMenuArrowCommonProps, + NavigationMenuArrowRenderProps, + NavigationMenuArrowProps, + NavigationMenuCheckboxItemOptions, + NavigationMenuCheckboxItemCommonProps, + NavigationMenuCheckboxItemRenderProps, + NavigationMenuCheckboxItemProps, + NavigationMenuContentOptions, + NavigationMenuContentCommonProps, + NavigationMenuContentRenderProps, + NavigationMenuContentProps, + NavigationMenuGroupLabelOptions, + NavigationMenuGroupLabelCommonProps, + NavigationMenuGroupLabelRenderProps, + NavigationMenuGroupLabelProps, + NavigationMenuGroupOptions, + NavigationMenuGroupCommonProps, + NavigationMenuGroupRenderProps, + NavigationMenuGroupProps, + NavigationMenuIconOptions, + NavigationMenuIconCommonProps, + NavigationMenuIconRenderProps, + NavigationMenuIconProps, + NavigationMenuItemDescriptionOptions, + NavigationMenuItemDescriptionCommonProps, + NavigationMenuItemDescriptionRenderProps, + NavigationMenuItemDescriptionProps, + NavigationMenuItemIndicatorOptions, + NavigationMenuItemIndicatorCommonProps, + NavigationMenuItemIndicatorRenderProps, + NavigationMenuItemIndicatorProps, + NavigationMenuItemLabelOptions, + NavigationMenuItemLabelCommonProps, + NavigationMenuItemLabelRenderProps, + NavigationMenuItemLabelProps, + NavigationMenuItemOptions, + NavigationMenuItemCommonProps, + NavigationMenuItemRenderProps, + NavigationMenuItemProps, + NavigationMenuPortalProps, + NavigationMenuRadioGroupOptions, + NavigationMenuRadioGroupCommonProps, + NavigationMenuRadioGroupRenderProps, + NavigationMenuRadioGroupProps, + NavigationMenuRadioItemOptions, + NavigationMenuRadioItemCommonProps, + NavigationMenuRadioItemPRenderrops, + NavigationMenuRadioItemProps, + NavigationMenuSeparatorOptions, + NavigationMenuSeparatorCommonProps, + NavigationMenuSeparatorRenderProps, + NavigationMenuSeparatorProps, + NavigationMenuSubContentOptions, + NavigationMenuSubContentCommonProps, + NavigationMenuSubContentRenderProps, + NavigationMenuSubContentProps, + NavigationMenuSubOptions, + NavigationMenuSubProps, + NavigationMenuSubTriggerOptions, + NavigationMenuSubTriggerCommonProps, + NavigationMenuSubTriggerRenderProps, + NavigationMenuSubTriggerProps, + NavigationMenuTriggerOptions, + NavigationMenuTriggerCommonProps, + NavigationMenuTriggerRenderProps, + NavigationMenuTriggerProps, +}; + +export { + Arrow, + CheckboxItem, + Content, + Group, + GroupLabel, + Icon, + Item, + ItemDescription, + ItemIndicator, + ItemLabel, + Portal, + RadioGroup, + RadioItem, + Root, + Menu, + Separator, + Sub, + SubContent, + SubTrigger, + Trigger, +}; + +export const NavigationMenu = Object.assign(Root, { + Arrow, + CheckboxItem, + Content, + Group, + GroupLabel, + Icon, + Item, + ItemDescription, + ItemIndicator, + ItemLabel, + Portal, + RadioGroup, + RadioItem, + Menu, + Separator, + Sub, + SubContent, + SubTrigger, + Trigger, +}); diff --git a/packages/core/src/navigation-menu/navigation-menu-content.tsx b/packages/core/src/navigation-menu/navigation-menu-content.tsx new file mode 100644 index 00000000..682db9f7 --- /dev/null +++ b/packages/core/src/navigation-menu/navigation-menu-content.tsx @@ -0,0 +1,55 @@ +import { callHandler } from "@kobalte/utils"; +import { Component, ValidComponent, splitProps, JSX } from "solid-js"; + +import { MenuContent, MenuContentCommonProps, MenuContentOptions, MenuContentRenderProps } from "../menu"; +import { PolymorphicProps } from "../polymorphic"; +import { useNavigationMenuContext } from "./navigation-menu-context"; +import { useMenuContext } from "../menu/menu-context"; +export interface NavigationMenuContentOptions extends MenuContentOptions {} + +export interface NavigationMenuContentCommonProps extends MenuContentCommonProps { + onPointerEnter: JSX.EventHandlerUnion; + onPointerLeave: JSX.EventHandlerUnion; +} + +export interface NavigationMenuContentRenderProps extends MenuContentRenderProps, NavigationMenuContentCommonProps {} + +export type NavigationMenuContentProps = NavigationMenuContentOptions & + Partial; + +export function NavigationMenuContent( + props: PolymorphicProps, +) { + const context = useNavigationMenuContext(); + const menuContext = useMenuContext(); + + const [local, others] = splitProps(props as NavigationMenuContentProps, ["onPointerEnter", "onPointerLeave"]); + + const onPointerEnter: JSX.EventHandlerUnion = ( + e, + ) => { + callHandler(e, local.onPointerEnter); + + context.cancelLeaveTimer(); + }; + + const onPointerLeave: JSX.EventHandlerUnion = ( + e, + ) => { + callHandler(e, local.onPointerLeave); + + context.startLeaveTimer(); + + menuContext.close(true); + }; + + return ( + > + > + onPointerEnter={onPointerEnter} + onPointerLeave={onPointerLeave} + {...others} + /> + ); +} diff --git a/packages/core/src/navigation-menu/navigation-menu-context.tsx b/packages/core/src/navigation-menu/navigation-menu-context.tsx new file mode 100644 index 00000000..580e5ece --- /dev/null +++ b/packages/core/src/navigation-menu/navigation-menu-context.tsx @@ -0,0 +1,28 @@ +import { Accessor, createContext, Setter, useContext } from "solid-js"; + +export interface NavigationMenuContextValue { + delayDuration: Accessor; + skipDelayDuration: Accessor; + autoFocusMenu: Accessor; + setAutoFocusMenu: Setter; + startLeaveTimer: () => void; + cancelLeaveTimer: () => void; +} + +export const NavigationMenuContext = createContext(); + +export function useOptionalNavigationMenuContext() { + return useContext(NavigationMenuContext); +} + +export function useNavigationMenuContext() { + const context = useOptionalNavigationMenuContext(); + + if (context === undefined) { + throw new Error( + "[kobalte]: `useNavigationMenuContext` must be used within a `NavigationMenu` component", + ); + } + + return context; +} diff --git a/packages/core/src/navigation-menu/navigation-menu-menu.tsx b/packages/core/src/navigation-menu/navigation-menu-menu.tsx new file mode 100644 index 00000000..6199e42d --- /dev/null +++ b/packages/core/src/navigation-menu/navigation-menu-menu.tsx @@ -0,0 +1,17 @@ +import { MenubarMenuOptions, MenubarMenuProps } from "../menubar"; +import { MenubarMenu } from "../menubar/menubar-menu"; +import { useNavigationMenuContext } from "./navigation-menu-context"; + +export interface NavigationMenuMenuOptions extends MenubarMenuOptions { +} + +export interface NavigationMenuMenuProps extends MenubarMenuProps {} + +/** + * Displays a menu to the user —such as a set of actions or functions— triggered by a button. + */ +export function NavigationMenuMenu(props: NavigationMenuMenuProps) { + const menubarContext = useNavigationMenuContext(); + + return ; +} diff --git a/packages/core/src/navigation-menu/navigation-menu-root.tsx b/packages/core/src/navigation-menu/navigation-menu-root.tsx new file mode 100644 index 00000000..69ac100b --- /dev/null +++ b/packages/core/src/navigation-menu/navigation-menu-root.tsx @@ -0,0 +1,77 @@ +import { callHandler, mergeDefaultProps } from "@kobalte/utils"; +import { ValidComponent, splitProps, Component, createSignal, JSX } from "solid-js"; +import { MenubarRootCommonProps, MenubarRootOptions, MenubarRootRenderProps } from "../menubar"; +import { MenubarRoot } from "../menubar/menubar-root"; + +import { PolymorphicProps } from "../polymorphic"; +import { NavigationMenuContext, NavigationMenuContextValue } from "./navigation-menu-context"; + +export interface NavigationMenuRootOptions extends MenubarRootOptions { + /** + * Delay before the menu opens on hover (default 200). + */ + delayDuration?: number; + + /** + * Open immediately if hovered again within delay (default 300). + */ + skipDelayDuration?: number; +} + +export interface NavigationMenuRootCommonProps extends MenubarRootCommonProps { +} + +export interface NavigationMenuRootRenderProps extends NavigationMenuRootCommonProps, MenubarRootRenderProps { +} + +export type NavigationMenuRootProps = NavigationMenuRootOptions & + Partial; + +/** + * A visually persistent menu common in desktop applications that provides quick access to a consistent set of commands. + */ +export function NavigationMenuRoot( + props: PolymorphicProps, +) { + const mergedProps = mergeDefaultProps( + { + delayDuration: 200, + skipDelayDuration: 300, + }, + props as NavigationMenuRootProps, + ); + + const [local, others] = splitProps(mergedProps, [ + "delayDuration", + "skipDelayDuration", + ]); + + const [autoFocusMenu, setAutoFocusMenu] = createSignal(false); + + let timeoutId: number | undefined; + + const context: NavigationMenuContextValue = { + delayDuration: () => local.delayDuration, + skipDelayDuration: () => local.skipDelayDuration, + autoFocusMenu, + setAutoFocusMenu, + startLeaveTimer: () => { + timeoutId = window.setTimeout(() => { + context.setAutoFocusMenu(false); + }, context.skipDelayDuration()); + }, + cancelLeaveTimer: () => { + if (timeoutId) clearTimeout(timeoutId); + }, + }; + + return ( + + >> + autoFocusMenu={autoFocusMenu()} + onAutoFocusMenuChange={setAutoFocusMenu} + {...others} + /> + + ); +} diff --git a/packages/core/src/navigation-menu/navigation-menu-trigger.tsx b/packages/core/src/navigation-menu/navigation-menu-trigger.tsx new file mode 100644 index 00000000..99e18da6 --- /dev/null +++ b/packages/core/src/navigation-menu/navigation-menu-trigger.tsx @@ -0,0 +1,73 @@ +import { callHandler } from "@kobalte/utils"; +import { Component, JSX, splitProps, ValidComponent } from "solid-js"; + +import { + MenuTrigger, + MenuTriggerCommonProps, + MenuTriggerOptions, + MenuTriggerProps, + MenuTriggerRenderProps, +} from "../menu"; +import { useMenuContext, useOptionalMenuContext } from "../menu/menu-context"; +import { PolymorphicProps } from "../polymorphic"; +import { useNavigationMenuContext } from "./navigation-menu-context"; +import { useMenubarContext } from "../menubar/menubar-context"; + +export interface NavigationMenuTriggerOptions extends MenuTriggerOptions {} + +export interface NavigationMenuTriggerCommonProps extends MenuTriggerCommonProps { + onPointerEnter: JSX.EventHandlerUnion; + onPointerLeave: JSX.EventHandlerUnion; +} + +export interface NavigationMenuTriggerRenderProps + extends NavigationMenuTriggerCommonProps, MenuTriggerRenderProps { +} + +export type NavigationMenuTriggerProps = NavigationMenuTriggerOptions & + Partial; + +/** + * The button that toggles the menubar menu or a menubar link. + */ +export function NavigationMenuTrigger( + props: PolymorphicProps, +) { + const context = useNavigationMenuContext();\ + const menuContext = useMenuContext(); + + const [local, others] = splitProps(props as NavigationMenuTriggerProps, ["onPointerEnter", "onPointerLeave", "onClick"]); + + let timeoutId: number | undefined; + + const onClick: JSX.EventHandlerUnion = (e) => { + callHandler(e, local.onClick); + + if (timeoutId) clearTimeout(timeoutId); + }; + + + const onPointerEnter: JSX.EventHandlerUnion = ( + e, + ) => { + callHandler(e, local.onPointerEnter); + + context.cancelLeaveTimer(); + + timeoutId = window.setTimeout(() => { + menuContext.open(true); + context.setAutoFocusMenu(true); + }, context.delayDuration()); + }; + + const onPointerLeave: JSX.EventHandlerUnion = ( + e, + ) => { + callHandler(e, local.onPointerLeave); + + context.startLeaveTimer(); + if (timeoutId) clearTimeout(timeoutId); + }; + + return >> onClick={onClick} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave} {...others} />; +}