Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@

## [0.20.0](https://github.com/frontapp/front-ui-kit/compare/v0.19.0...v0.20.0) (2025-10-29)


### Features

* navigation dropdown ([#273](https://github.com/frontapp/front-ui-kit/issues/273)) ([3774b61](https://github.com/frontapp/front-ui-kit/commit/3774b6134c1797106b68b74d97f9aeb0be827a0f))
- navigation dropdown ([#273](https://github.com/frontapp/front-ui-kit/issues/273)) ([3774b61](https://github.com/frontapp/front-ui-kit/commit/3774b6134c1797106b68b74d97f9aeb0be827a0f))

## [0.19.0](https://github.com/frontapp/front-ui-kit/compare/v0.18.1...v0.19.0) (2025-10-24)


### Features

* empty state subtitle ([#298](https://github.com/frontapp/front-ui-kit/issues/298)) ([7c11bf4](https://github.com/frontapp/front-ui-kit/commit/7c11bf4510838df83d7367321fc3b2ed301fa382))
- empty state subtitle ([#298](https://github.com/frontapp/front-ui-kit/issues/298)) ([7c11bf4](https://github.com/frontapp/front-ui-kit/commit/7c11bf4510838df83d7367321fc3b2ed301fa382))

## [0.18.1](https://github.com/frontapp/front-ui-kit/compare/v0.18.0...v0.18.1) (2025-10-24)

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@
"@storybook/react-webpack5": "8.6.14",
"@svgr/webpack": "^8.1.0",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.202",
"@types/luxon": "^3.7.1",
"@types/mdx": "^2.0.10",
"@types/react": "^19.0.0",
"@types/react-dom": "^18.3.3",
"@types/react-dom": "^19.0.0",
"@types/react-is": "^19.2.0",
"@types/react-window": "^1.8.8",
"@types/react-window-infinite-loader": "^1.0.9",
Expand Down
14 changes: 7 additions & 7 deletions src/elements/dropdown/components/NavigationalSubmenuTrigger.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, {useCallback, useLayoutEffect} from 'react';
import React, { useCallback, useLayoutEffect } from 'react';
import styled from 'styled-components';

import {useNavigationalDropdown} from '../context/NavigationalDropdownContext';
import { useNavigationalDropdown } from '../context/NavigationalDropdownContext';

interface NavigationalSubmenuTriggerProps {
children: React.ReactNode;
Expand Down Expand Up @@ -42,7 +42,7 @@ export const NavigationalSubmenuTrigger: React.FC<NavigationalSubmenuTriggerProp
className,
onNavigate
}) => {
const {navigateTo, autoNavigateToSubmenuPath, viewStack} = useNavigationalDropdown();
const { navigateTo, autoNavigateToSubmenuPath, viewStack } = useNavigationalDropdown();

useLayoutEffect(() => {
// Check if this submenu is the first one in the auto-navigation path
Expand All @@ -51,16 +51,16 @@ export const NavigationalSubmenuTrigger: React.FC<NavigationalSubmenuTriggerProp
// Also check that we haven't already navigated to this level
const isAlreadyInStack = viewStack.some((view) => view.id === submenuId);

if (isFirstInPath && !isAlreadyInStack)
// Always navigate if we're first in path and not in stack
if (isFirstInPath && !isAlreadyInStack)
navigateTo(submenuId, getSubmenu, backTitle);

}, [autoNavigateToSubmenuPath, submenuId, getSubmenu, backTitle, navigateTo, viewStack]);

const handleClick = useCallback(
(event: React.MouseEvent) => {
if (disabled) return;

const {target} = event;
const { target } = event;
if (target instanceof HTMLElement && isInteractiveElement(target)) return;

event.preventDefault();
Expand All @@ -76,7 +76,7 @@ export const NavigationalSubmenuTrigger: React.FC<NavigationalSubmenuTriggerProp
(event: React.KeyboardEvent) => {
if (disabled) return;

const {target} = event;
const { target } = event;
if (target instanceof HTMLElement && isInteractiveElement(target)) return;

if (event.key === 'Enter' || event.key === ' ') {
Expand Down
50 changes: 33 additions & 17 deletions src/elements/dropdown/context/NavigationalDropdownContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,34 +52,50 @@ export const NavigationalDropdownProvider: React.FC<NavigationalDropdownProvider
]);

const prevContentVersionRef = React.useRef(contentVersion);
const viewStackRef = React.useRef(viewStack);
viewStackRef.current = viewStack;
const [autoNavigateToSubmenuPath, setAutoNavigateToSubmenuPath] = React.useState<string[]>([]);

// Reset stack when content changes, then restore submenu path via autoNavigateToSubmenuPath.
// Capture the path from viewStackRef during this effect (after the render where contentVersion
// changed) instead of reading stack inside setViewStack — avoids nested setState and keeps
// stack + path updates batching consistently.
useLayoutEffect(() => {
if (prevContentVersionRef.current !== contentVersion && prevContentVersionRef.current !== undefined)
setViewStack((currentStack) => {
// Store the entire navigation path, not just the last submenu ID
const navigationPath = currentStack.length > 1 ? currentStack.slice(1).map((view) => view.id) : [];

const newStack = [
{
id: rootId,
getContent: getRootContent,
level: 0
}
];

// Set the navigation path to auto-restore
if (navigationPath.length > 0) setAutoNavigateToSubmenuPath(navigationPath);
const prevVersion = prevContentVersionRef.current;
if (prevVersion !== contentVersion && prevVersion !== undefined) {
const navigationPath =
viewStackRef.current.length > 1 ? viewStackRef.current.slice(1).map((view) => view.id) : [];

setViewStack([
{
id: rootId,
getContent: getRootContent,
level: 0
}
]);

return newStack;
});
if (navigationPath.length > 0) setAutoNavigateToSubmenuPath(navigationPath);
}

prevContentVersionRef.current = contentVersion;
}, [contentVersion, getRootContent, rootId]);

const navigateTo = useCallback(
(id: string, getContent: () => React.ReactNode, parentTitle?: string) => {
setViewStack((prev) => {
const top = prev[prev.length - 1];
// Idempotent: same view already on top (fresh getContent / avoids duplicate stack entries).
if (top && top.id === id) {
const updatedView: NavigationalView = {
id,
getContent,
parentTitle,
level: top.level
};
onNavigate?.(updatedView.level, id);
return [...prev.slice(0, -1), updatedView];
}

const currentLevel = prev.length > 0 ? prev[prev.length - 1].level : -1;
const newView: NavigationalView = {
id,
Expand Down
6 changes: 4 additions & 2 deletions src/elements/dropdown/dropdownCoordinator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import {useMeasureElement} from '../../helpers/hookHelpers';
* Props.
*/

interface DropdownCoordinatorProps
extends Pick<RepositionPopoverProps, 'hasVisibleOverlay' | 'isExclusive' | 'placement'> {
interface DropdownCoordinatorProps extends Pick<
RepositionPopoverProps,
'hasVisibleOverlay' | 'isExclusive' | 'placement'
> {
/** Controls if the dropdown is disabled from opening. If disabled, we will not open anything. */
isDisabled?: boolean;
/** Controls if the overlay will close the dropdown. If disabled, we will not close when the overlay is clicked. */
Expand Down
Loading
Loading