diff --git a/src/elements/dropdown/__docs__/stories/nestedDropdown.stories.tsx b/src/elements/dropdown/__docs__/stories/nestedDropdown.stories.tsx index 31d078ce..f81e570e 100644 --- a/src/elements/dropdown/__docs__/stories/nestedDropdown.stories.tsx +++ b/src/elements/dropdown/__docs__/stories/nestedDropdown.stories.tsx @@ -1,3 +1,4 @@ +import {type Placement, placements} from '@popperjs/core'; import type {StoryFn} from '@storybook/react'; import React, {useState} from 'react'; import styled from 'styled-components'; @@ -123,7 +124,11 @@ const departmentsData = [ } ]; -const Template: StoryFn = () => { +type NestedDropdownStoryArgs = { + placement: Placement; +}; + +const Template: StoryFn = () => { const [selectedValue, setSelectedValue] = useState(''); const [submenuSearchValues, setSubmenuSearchValues] = useState>({}); const [filteredSubmenuItems, setFilteredSubmenuItems] = useState>({}); @@ -229,7 +234,7 @@ CustomConfigDropdown.decorators = [ ]; // Multi-select nested dropdown example -const MultiSelectTemplate: StoryFn = () => { +const MultiSelectTemplate: StoryFn = ({placement, offset}) => { const [selectedItems, setSelectedItems] = useState([]); const [submenuSearchValues, setSubmenuSearchValues] = useState>({}); @@ -258,7 +263,7 @@ const MultiSelectTemplate: StoryFn = () => { openDelay: 200, closeDelay: 400, maxDepth: 3, - placement: 'right-start' + placement }}> { ))} - }> + } + submenuConfig={{offset}}> { export const MultiSelectDropdown = MultiSelectTemplate.bind({}); MultiSelectDropdown.storyName = 'Multi-Select with Submenus'; +MultiSelectDropdown.args = { + placement: 'right-start', + offset: { + skidding: 0, + distance: 8 + } +}; + +MultiSelectDropdown.argTypes = { + placement: { + control: 'select', + options: placements + }, + offset: { + control: 'object', + description: 'Offset for the submenu positioning' + } +}; // Left-aligned submenu example const LeftAlignedTemplate: StoryFn = () => ( diff --git a/src/elements/dropdown/components/SubmenuTrigger.tsx b/src/elements/dropdown/components/SubmenuTrigger.tsx index f560a3d5..c3271b16 100644 --- a/src/elements/dropdown/components/SubmenuTrigger.tsx +++ b/src/elements/dropdown/components/SubmenuTrigger.tsx @@ -1,4 +1,5 @@ import {type Placement} from '@popperjs/core'; +import {type Options} from '@popperjs/core/lib/modifiers/offset'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {usePopper} from 'react-popper'; import {styled} from 'styled-components'; @@ -6,7 +7,7 @@ import {styled} from 'styled-components'; import {Layer} from '../../../components/layer/layer'; import {useNestedDropdown, useSubmenu} from '../context/NestedDropdownContext'; import {useSubmenuPositioning} from '../hooks/useSubmenuPositioning'; -import {SubmenuCapableProps} from '../types/nestedDropdown'; +import {SubmenuCapableProps, SubmenuOffset} from '../types/nestedDropdown'; interface SubmenuTriggerProps extends SubmenuCapableProps { children: React.ReactNode; @@ -115,7 +116,8 @@ export const SubmenuTrigger: React.FC = ({ submenuId: effectiveSubmenuId, level, placement: effectiveConfig.placement, - layerIdPrefix: effectiveConfig.layerIdPrefix + layerIdPrefix: effectiveConfig.layerIdPrefix, + offset: effectiveConfig.offset }); const timeoutRef = useRef(null); @@ -244,6 +246,7 @@ interface SubmenuPortalProps { placement: Placement; zIndex: number; layerId: string; + offset?: SubmenuOffset; }; onMouseEnter: () => void; onMouseLeave: () => void; @@ -254,6 +257,13 @@ interface SubmenuPortalProps { maxWidth: string; } +const DEFAULT_SKIDDING_OFFSET = 0; +const DEFAULT_DISTANCE_OFFSET = 8; +const toPopperOffset = (incomingOffset?: SubmenuOffset): Options['offset'] => [ + incomingOffset?.skidding ?? DEFAULT_SKIDDING_OFFSET, + incomingOffset?.distance ?? DEFAULT_DISTANCE_OFFSET +]; + /** * SubmenuPortal renders the submenu in a portal with custom Popper.js positioning. * @@ -295,7 +305,7 @@ const SubmenuPortal: React.FC = ({ { name: 'offset', options: { - offset: [0, 8] // 8px gap between trigger and submenu + offset: toPopperOffset(positioning.offset) // 8px gap between trigger and submenu } }, { diff --git a/src/elements/dropdown/hooks/useSubmenuPositioning.ts b/src/elements/dropdown/hooks/useSubmenuPositioning.ts index 50d70031..8ecfaab8 100644 --- a/src/elements/dropdown/hooks/useSubmenuPositioning.ts +++ b/src/elements/dropdown/hooks/useSubmenuPositioning.ts @@ -1,7 +1,7 @@ import {Placement} from '@popperjs/core'; import {useCallback, useMemo} from 'react'; -import {SubmenuPositioning} from '../types/nestedDropdown'; +import {SubmenuOffset, SubmenuPositioning} from '../types/nestedDropdown'; /** * Hook for calculating submenu positioning, z-index, and layer management. @@ -27,6 +27,8 @@ interface UseSubmenuPositioningProps { placement?: Placement; /** Prefix for generating unique layer IDs */ layerIdPrefix?: string; + /** Offset for submenu positioning */ + offset?: SubmenuOffset; } /** @@ -86,7 +88,8 @@ export const useSubmenuPositioning = ({ submenuId, level, placement = 'right-start', - layerIdPrefix = 'nested-dropdown' + layerIdPrefix = 'nested-dropdown', + offset }: UseSubmenuPositioningProps): UseSubmenuPositioningReturn => { /** * Calculates z-index based on nesting level to ensure proper stacking order. @@ -115,9 +118,10 @@ export const useSubmenuPositioning = ({ anchorElement, placement, zIndex: calculateZIndex(), - layerId: getLayerId() + layerId: getLayerId(), + offset }), - [placement, calculateZIndex, getLayerId] + [placement, calculateZIndex, getLayerId, offset] ); return useMemo( diff --git a/src/elements/dropdown/types/nestedDropdown.ts b/src/elements/dropdown/types/nestedDropdown.ts index d6aada09..64433df9 100644 --- a/src/elements/dropdown/types/nestedDropdown.ts +++ b/src/elements/dropdown/types/nestedDropdown.ts @@ -17,6 +17,8 @@ export interface NestedDropdownConfig { enableKeyboardNavigation: boolean; /** Custom layer root ID prefix */ layerIdPrefix: string; + /** Offset for submenu positioning */ + offset?: SubmenuOffset; } /** @@ -28,7 +30,7 @@ export const DEFAULT_NESTED_CONFIG: NestedDropdownConfig = { maxDepth: 3, placement: 'right-start', enableKeyboardNavigation: true, - layerIdPrefix: 'nested-dropdown' + layerIdPrefix: 'nested-dropdown', }; /** @@ -97,4 +99,12 @@ export interface SubmenuPositioning { zIndex: number; /** Layer root ID */ layerId: string; + /** Offset for submenu positioning */ + offset?: SubmenuOffset; } + + +export interface SubmenuOffset { + skidding?: number; + distance?: number; +} \ No newline at end of file