diff --git a/docs/src/app/(private)/experiments/composite.module.css b/docs/src/app/(private)/experiments/composite.module.css new file mode 100644 index 0000000000..1ba9a65f80 --- /dev/null +++ b/docs/src/app/(private)/experiments/composite.module.css @@ -0,0 +1,46 @@ +.Grid { + display: flex; + flex-direction: column; + box-sizing: border-box; + padding-block: 0.25rem; + background-color: canvas; + color: oklch(12% 5% 264 / 90%); + transform-origin: var(--transform-origin); + transition: + transform 150ms, + opacity 150ms; + + &[data-starting-style], + &[data-ending-style] { + opacity: 0; + transform: scale(0.9); + } + + outline: 1px solid oklch(12% 9% 264 / 8%); + box-shadow: + 0px 4px 6px -3px oklch(12% 9% 264 / 8%), + 0px 4px 6px -4px oklch(12% 9% 264 / 8%); +} + +.GridRow { + display: flex; +} + +.GridItem { + outline: 0; + cursor: default; + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + &:hover { + background: oklch(12% 9.5% 264 / 5%); + } + + &[data-highlighted] { + z-index: 0; + position: relative; + background: oklch(12% 9.5% 264 / 5%); + } +} diff --git a/docs/src/app/(private)/experiments/composite.tsx b/docs/src/app/(private)/experiments/composite.tsx new file mode 100644 index 0000000000..af0b114e35 --- /dev/null +++ b/docs/src/app/(private)/experiments/composite.tsx @@ -0,0 +1,81 @@ +'use client'; +import * as React from 'react'; +import { Composite } from '@base-ui-components/react/composite'; +import styles from './composite.module.css'; + +export default function ExampleGrid() { + const [column, setColumn] = React.useState(0); + const [row, setRow] = React.useState(0); + + const rootHandleHighligtedIndexChange = (index: number) => { + setRow(index); + }; + + const handleHighligtedIndexChange = (index: number) => { + setColumn(index); + }; + + return ( + + + + A + B + C + D + + + + + E + F + G + H + + + + + I + J + K + L + + + + + M + N + + + + ); +} diff --git a/docs/src/app/(private)/experiments/menu-fluent-ui.module.css b/docs/src/app/(private)/experiments/menu-fluent-ui.module.css new file mode 100644 index 0000000000..9b3d9da892 --- /dev/null +++ b/docs/src/app/(private)/experiments/menu-fluent-ui.module.css @@ -0,0 +1,174 @@ +.Button { + width: fit-content; + box-sizing: border-box; + font-size: 0.875rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + height: 2rem; + padding: 0 1rem; + margin: 0; + outline: 0; + border: 1px solid oklch(12% 9% 264 / 8%); + background-color: oklch(98% 0.25% 264); + font-family: inherit; + font-weight: 500; + line-height: 1.5rem; + color: oklch(12% 5% 264 / 90%); + user-select: none; + + @media (hover: hover) { + &:hover { + background-color: oklch(12% 9.5% 264 / 5%); + } + } + + &:active { + background-color: oklch(12% 9.5% 264 / 5%); + } + + &[data-popup-open] { + background-color: oklch(12% 9.5% 264 / 5%); + } + + &:focus-visible { + outline: 2px solid oklch(45% 50% 264); + outline-offset: -1px; + } +} + +.Positioner { + outline: 0; +} + +.Popup, +.Grid { + box-sizing: border-box; + padding-block: 0.25rem; + background-color: canvas; + color: oklch(12% 5% 264 / 90%); + transform-origin: var(--transform-origin); + transition: + transform 150ms, + opacity 150ms; + + &[data-starting-style], + &[data-ending-style] { + opacity: 0; + transform: scale(0.9); + } + + outline: 1px solid oklch(12% 9% 264 / 8%); + box-shadow: + 0px 4px 6px -3px oklch(12% 9% 264 / 8%), + 0px 4px 6px -4px oklch(12% 9% 264 / 8%); +} + +.Arrow { + display: flex; + + &[data-side='top'] { + bottom: -8px; + rotate: 180deg; + } + &[data-side='bottom'] { + top: -8px; + rotate: 0deg; + } + &[data-side='left'] { + right: -13px; + rotate: 90deg; + } + &[data-side='right'] { + left: -13px; + rotate: -90deg; + } +} + +.ArrowFill { + fill: canvas; +} + +.ArrowOuterStroke { + fill: oklch(12% 9% 264 / 8%); +} + +.Separator { + margin: 0.375rem 0; + height: 1px; + background-color: oklch(12% 9% 264 / 8%); +} + +.Item, +.ItemWithSubmenu, +.SubmenuTriggerInsideItem, +.SplitButtonItem, +.SplitButtonSubmenuTrigger { + outline: 0; + cursor: default; + user-select: none; + padding-block: 0.5rem; + padding-left: 1rem; + padding-right: 2rem; + display: flex; + font-size: 0.875rem; + line-height: 1rem; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding-right: 1rem; + + &[data-highlighted] { + z-index: 0; + position: relative; + background: oklch(12% 9.5% 264 / 5%); + } +} + +.ItemWithSubmenu { + padding-right: 0; + padding-block: 0; +} + +.SubmenuTrigger { + padding: 0.75rem 1rem; + margin-left: auto; +} + +.SplitButtonItem { + display: inline-flex; + padding-left: 1rem; +} + +.SplitButtonSubmenuTrigger { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + display: inline-flex; +} + +.Grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.25rem; +} + +.GridRow { + display: flex; +} + +.GridItem { + outline: 0; + cursor: default; + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + + &[data-highlighted] { + z-index: 0; + position: relative; + background: oklch(12% 9.5% 264 / 5%); + } +} diff --git a/docs/src/app/(private)/experiments/menu-fluent-ui.tsx b/docs/src/app/(private)/experiments/menu-fluent-ui.tsx new file mode 100644 index 0000000000..ad1e84011f --- /dev/null +++ b/docs/src/app/(private)/experiments/menu-fluent-ui.tsx @@ -0,0 +1,159 @@ +'use client'; +import * as React from 'react'; +import { Menu } from '@base-ui-components/react/menu'; +import styles from './menu-fluent-ui.module.css'; + +export default function ExampleMenu() { + return ( +
+
+ Reference:{' '} + + ContextualMenu with subemnus + {' '} +
+ + + Contextual menu + + + + + {/* Submenu */} + + + New{' '} + + + + + + + + Email message + Calendar event + + + + + + {/* Submenu */} + + + Share{' '} + + + + + + + + Share to Twitter + + Share to Facebook + + + + + + + Share w/ split + + + + + + + + + Share to Twitter + + Share to Facebook + + + + + + + + + +
+ Reference:{' '} + + ContextualMenu with customized submenus and noWrap attributes + +
+ + + Contextual menu + + + + + New + Upload + + + + Charm + + + + + + + + {[...Array(16).keys()].map((i) => ( + + {i + 1} + + ))} + + + + + + + Categorized + + + + + + + + Share to Twitter + + Share to Facebook + + + + + + + + + +
+ ); +} + +function ChevronDownIcon(props: React.ComponentProps<'svg'>) { + return ( + + + + ); +} + +function ChevronRightIcon(props: React.ComponentProps<'svg'>) { + return ( + + + + ); +} diff --git a/packages/react/src/composite/index.parts.ts b/packages/react/src/composite/index.parts.ts new file mode 100644 index 0000000000..e42456f7d9 --- /dev/null +++ b/packages/react/src/composite/index.parts.ts @@ -0,0 +1,3 @@ +export { CompositeRoot as Root } from './root/CompositeRoot'; +export { CompositeList as List } from './list/CompositeList'; +export { CompositeItem as Item } from './item/CompositeItem'; diff --git a/packages/react/src/composite/index.ts b/packages/react/src/composite/index.ts new file mode 100644 index 0000000000..5709e5228e --- /dev/null +++ b/packages/react/src/composite/index.ts @@ -0,0 +1 @@ +export * as Composite from './index.parts'; diff --git a/packages/react/src/menu/root/MenuRoot.tsx b/packages/react/src/menu/root/MenuRoot.tsx index ce482cfe37..a7245ca58e 100644 --- a/packages/react/src/menu/root/MenuRoot.tsx +++ b/packages/react/src/menu/root/MenuRoot.tsx @@ -26,6 +26,7 @@ const MenuRoot: React.FC = function MenuRoot(props) { orientation = 'vertical', delay = 100, openOnHover: openOnHoverProp, + cols, } = props; const direction = useDirection(); @@ -54,6 +55,7 @@ const MenuRoot: React.FC = function MenuRoot(props) { delay, onTypingChange, modal, + cols, }); const context: MenuRootContext = React.useMemo( @@ -141,6 +143,7 @@ namespace MenuRoot { * Defaults to `true` for nested menus. */ openOnHover?: boolean; + cols?: number; } } diff --git a/packages/react/src/menu/root/useMenuRoot.ts b/packages/react/src/menu/root/useMenuRoot.ts index 058d927800..4844b5f3ed 100644 --- a/packages/react/src/menu/root/useMenuRoot.ts +++ b/packages/react/src/menu/root/useMenuRoot.ts @@ -44,6 +44,7 @@ export function useMenuRoot(parameters: useMenuRoot.Parameters): useMenuRoot.Ret openOnHover, onTypingChange, modal, + cols, } = parameters; const [triggerElement, setTriggerElement] = React.useState(null); @@ -192,6 +193,7 @@ export function useMenuRoot(parameters: useMenuRoot.Parameters): useMenuRoot.Ret rtl: direction === 'rtl', disabledIndices: EMPTY_ARRAY, onNavigate: setActiveIndex, + cols, }); const typeahead = useTypeahead(floatingRootContext, { @@ -287,7 +289,7 @@ export function useMenuRoot(parameters: useMenuRoot.Parameters): useMenuRoot.Ret ); } -export type MenuOrientation = 'horizontal' | 'vertical'; +export type MenuOrientation = 'horizontal' | 'vertical' | 'both'; export namespace useMenuRoot { export interface Parameters { @@ -347,6 +349,7 @@ export namespace useMenuRoot { */ onTypingChange: (typing: boolean) => void; modal: boolean; + cols?: number; } export interface ReturnValue {