Skip to content
32 changes: 28 additions & 4 deletions src/elements/dropdown/__docs__/stories/nestedDropdown.stories.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -123,7 +124,11 @@ const departmentsData = [
}
];

const Template: StoryFn = () => {
type NestedDropdownStoryArgs = {
placement: Placement;
};

const Template: StoryFn<NestedDropdownStoryArgs> = () => {
const [selectedValue, setSelectedValue] = useState('');
const [submenuSearchValues, setSubmenuSearchValues] = useState<Record<string, string>>({});
const [filteredSubmenuItems, setFilteredSubmenuItems] = useState<Record<string, string[]>>({});
Expand Down Expand Up @@ -229,7 +234,7 @@ CustomConfigDropdown.decorators = [
];

// Multi-select nested dropdown example
const MultiSelectTemplate: StoryFn = () => {
const MultiSelectTemplate: StoryFn = ({placement, offset}) => {
const [selectedItems, setSelectedItems] = useState<string[]>([]);
const [submenuSearchValues, setSubmenuSearchValues] = useState<Record<string, string>>({});

Expand Down Expand Up @@ -258,7 +263,7 @@ const MultiSelectTemplate: StoryFn = () => {
openDelay: 200,
closeDelay: 400,
maxDepth: 3,
placement: 'right-start'
placement
}}>
<DropdownCoordinator
placement="bottom-start"
Expand Down Expand Up @@ -326,7 +331,8 @@ const MultiSelectTemplate: StoryFn = () => {
</DropdownItem>
))}
</Dropdown>
}>
}
submenuConfig={{offset}}>
<DropdownItemIcon
color={palette.purple.shade40}
iconName={category.icon as 'Archive' | 'Star' | 'Calendar'}
Expand All @@ -346,6 +352,24 @@ const MultiSelectTemplate: StoryFn = () => {

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 = () => (
Expand Down
16 changes: 13 additions & 3 deletions src/elements/dropdown/components/SubmenuTrigger.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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';

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;
Expand Down Expand Up @@ -115,7 +116,8 @@ export const SubmenuTrigger: React.FC<SubmenuTriggerProps> = ({
submenuId: effectiveSubmenuId,
level,
placement: effectiveConfig.placement,
layerIdPrefix: effectiveConfig.layerIdPrefix
layerIdPrefix: effectiveConfig.layerIdPrefix,
offset: effectiveConfig.offset
});

const timeoutRef = useRef<NodeJS.Timeout | null>(null);
Expand Down Expand Up @@ -244,6 +246,7 @@ interface SubmenuPortalProps {
placement: Placement;
zIndex: number;
layerId: string;
offset?: SubmenuOffset;
};
onMouseEnter: () => void;
onMouseLeave: () => void;
Expand All @@ -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.
*
Expand Down Expand Up @@ -295,7 +305,7 @@ const SubmenuPortal: React.FC<SubmenuPortalProps> = ({
{
name: 'offset',
options: {
offset: [0, 8] // 8px gap between trigger and submenu
offset: toPopperOffset(positioning.offset) // 8px gap between trigger and submenu
}
},
{
Expand Down
12 changes: 8 additions & 4 deletions src/elements/dropdown/hooks/useSubmenuPositioning.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -27,6 +27,8 @@ interface UseSubmenuPositioningProps {
placement?: Placement;
/** Prefix for generating unique layer IDs */
layerIdPrefix?: string;
/** Offset for submenu positioning */
offset?: SubmenuOffset;
}

/**
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down
12 changes: 11 additions & 1 deletion src/elements/dropdown/types/nestedDropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface NestedDropdownConfig {
enableKeyboardNavigation: boolean;
/** Custom layer root ID prefix */
layerIdPrefix: string;
/** Offset for submenu positioning */
offset?: SubmenuOffset;
}

/**
Expand All @@ -28,7 +30,7 @@ export const DEFAULT_NESTED_CONFIG: NestedDropdownConfig = {
maxDepth: 3,
placement: 'right-start',
enableKeyboardNavigation: true,
layerIdPrefix: 'nested-dropdown'
layerIdPrefix: 'nested-dropdown',
};

/**
Expand Down Expand Up @@ -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;
}
Loading