Skip to content

Commit

Permalink
[BREAKING] feat(react-nav-preview): Allow controlled behavior for nav…
Browse files Browse the repository at this point in the history
… categories, add controlled example and other cleanup (#32489)

Co-authored-by: Mitch-At-Work <mifraser@microsoft.com>
  • Loading branch information
mltejera and Mitch-At-Work committed Sep 19, 2024
1 parent f275490 commit 8506c11
Show file tree
Hide file tree
Showing 69 changed files with 430 additions and 618 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": " [BREAKING] Removed non functional reserveSelectedNavItemSpace prop. Added defaultOpenCategories and openCategories prop and example. Updated icon selection logic for NavCategoryItem. Exports OnNavItemSelectData.",
"packageName": "@fluentui/react-nav-preview",
"email": "matejera@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export type NavCategoryState = NavCategoryContextValue & Required<NavCategoryPro
export const navClassNames: SlotClassNames<NavSlots>;

// @public (undocumented)
export type NavContextValue = Pick<NavProps, 'onNavItemSelect' | 'selectedValue' | 'selectedCategoryValue' | 'reserveSelectedNavItemSpace' | 'size'> & {
export type NavContextValue = Pick<NavProps, 'onNavItemSelect' | 'selectedValue' | 'selectedCategoryValue' | 'size'> & {
onRegister: RegisterNavItemEventHandler;
onUnregister: RegisterNavItemEventHandler;
onSelect: EventHandler<OnNavItemSelectData>;
Expand Down Expand Up @@ -253,13 +253,14 @@ export type NavItemState = ComponentState<NavItemSlots> & Pick<NavItemProps, 'va
};

// @public
export type NavItemValue = unknown;
export type NavItemValue = string;

// @public
export type NavProps = ComponentProps<NavSlots> & {
reserveSelectedNavItemSpace?: boolean;
defaultSelectedValue?: NavItemValue;
defaultSelectedCategoryValue?: NavItemValue;
defaultOpenCategories?: NavItemValue[];
openCategories?: NavItemValue[];
onNavItemSelect?: EventHandler<OnNavItemSelectData>;
selectedValue?: NavItemValue;
selectedCategoryValue?: NavItemValue;
Expand Down Expand Up @@ -288,7 +289,7 @@ export type NavSectionHeaderSlots = {
// @public
export type NavSectionHeaderState = ComponentState<NavSectionHeaderSlots>;

// @public (undocumented)
// @public
export type NavSize = 'small' | 'medium';

// @public (undocumented)
Expand Down Expand Up @@ -341,6 +342,12 @@ export type NavSubItemState = ComponentState<NavSubItemSlots> & Pick<NavSubItemP
size: NavSize;
};

// @public (undocumented)
export type OnNavItemSelectData = EventData<'click', React_2.MouseEvent<HTMLButtonElement | HTMLAnchorElement>> & {
value: NavItemValue;
categoryValue?: NavItemValue;
};

// @public (undocumented)
export type RegisterNavItemEventHandler = (data: NavItemRegisterData) => void;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,60 @@ export type NavSlots = {
root: NonNullable<Slot<'div'>>;
};

/***
* Indicates the vertical size of the Nav content.
*/
export type NavSize = 'small' | 'medium';

/**
* Nav Props
*/
export type NavProps = ComponentProps<NavSlots> & {
/**
* Nav size may change between unselected and selected states.
* The default scenario is a selected NavItem has bold text.
*
* When true, this property requests navItems be the same size whether unselected or selected.
* @default true
*/
reserveSelectedNavItemSpace?: boolean;

/**
* The value of the navItem to be selected by default.
* Typically useful when the selectedValue is uncontrolled.
* Mutually exclusive with selectedValue.
* Mutually exclusive with selectedValue.
* Empty string indicates no selection.
*/
defaultSelectedValue?: NavItemValue;

/**
* The value of the navCategory to be selected by default.
* Typically useful when the selectedValue is uncontrolled.
* Mutually exclusive with selectedValue.
* Mutually exclusive with selectedValue.
* Empty string indicates no selection.
*/
defaultSelectedCategoryValue?: NavItemValue;

/**
* Set of categories that are opened by default.
* Typically useful when the openCategories is uncontrolled.
*/
defaultOpenCategories?: NavItemValue[];

/**
* Controls the open categories.
* For use in controlled scenarios.
*/
openCategories?: NavItemValue[];

/**
* Raised when a navItem is selected.
* If the navItem is child of a category, the categoryValue will be provided
*/
onNavItemSelect?: EventHandler<OnNavItemSelectData>;

/**
* The value of the currently selected navItem.
* Mutually exclusive with defaultSelectedValue.
* @default undefined
*/
selectedValue?: NavItemValue;

/**
* Indicates a category that has a selected child
* Will show the category as selected if it is closed.
* Null otherwise
* @default undefined
*/
selectedCategoryValue?: NavItemValue;

Expand All @@ -61,13 +71,12 @@ export type NavProps = ComponentProps<NavSlots> & {
multiple?: boolean;

/**
* Callback used by NavCategoryItem to request a change on it's own opened state
* Callback raised when a NavCategoryItem is toggled.
*/
onNavCategoryItemToggle?: EventHandler<OnNavItemSelectData>;

/**
* The size and density of the Nav and it's children
*
* @default 'medium'
*/
size?: NavSize;
Expand All @@ -76,11 +85,12 @@ export type NavProps = ComponentProps<NavSlots> & {
export type OnNavItemSelectData = EventData<'click', React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>> & {
/**
* The value of the selected navItem.
* In the case of a category selection, this will be the value of the selected category.
*/
value: NavItemValue;

/**
* The parent value of the selected navItem
* The parent value of the selected navSubItem
* Null if not a child of a category
*/
categoryValue?: NavItemValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,29 @@ import {
import type { NavProps, NavState, OnNavItemSelectData } from './Nav.types';
import type { NavItemRegisterData, NavItemValue } from '../NavContext.types';

// /**
// * Initial value for the uncontrolled case of the list of open indexes
// */
// function initializeUncontrolledOpenItems({ defaultOpenItems }: Pick<NavProps, 'defaultOpenItems'>): NavItemValue[] {
// if (defaultOpenItems !== undefined) {
// if (Array.isArray(defaultOpenItems)) {
// return [defaultOpenItems[0]];
// }
// return [defaultOpenItems];
// }
// return [];
// }

// /**
// * Normalizes Accordion index into an array of indexes
// */
// function normalizeValues(index?: NavItemValue | NavItemValue[]): NavItemValue[] | undefined {
// if (index === undefined) {
// return undefined;
// }
// return Array.isArray(index) ? index : [index];
// }

// temp implementation of the above function.
const normalizeValues = (index?: NavItemValue | NavItemValue[]): NavItemValue[] | undefined => {
/**
* Initial value for the uncontrolled case of the list of open indexes
*/
function initializeUncontrolledOpenCategories({
defaultOpenCategories,
multiple,
}: Pick<NavProps, 'defaultOpenCategories' | 'multiple'>): NavItemValue[] | undefined {
if (defaultOpenCategories !== undefined) {
if (Array.isArray(defaultOpenCategories)) {
return multiple ? defaultOpenCategories : [defaultOpenCategories[0]];
}
return [defaultOpenCategories];
}
return undefined;
};
}

/**
* Updates the list of open indexes based on an index that changes
* @param value - the index that will change
* @param previousOpenItems - list of current open indexes
* @param multiple - if Nav supports open categories at the same time
*/
const updateOpenItems = (value: NavItemValue, previousOpenItems: NavItemValue[], multiple: boolean) => {
const updateOpenCategories = (value: NavItemValue, previousOpenItems: NavItemValue[], multiple: boolean) => {
if (multiple) {
if (previousOpenItems.includes(value)) {
return previousOpenItems.filter(i => i !== value);
Expand All @@ -67,31 +55,36 @@ const updateOpenItems = (value: NavItemValue, previousOpenItems: NavItemValue[],
* @param ref - reference to root HTMLDivElement of Nav
*/
export const useNav_unstable = (props: NavProps, ref: React.Ref<HTMLDivElement>): NavState => {
const { onNavItemSelect, onNavCategoryItemToggle, multiple = true, size = 'medium' } = props;
const {
onNavItemSelect,
onNavCategoryItemToggle,
multiple = true,
size = 'medium',
openCategories: controlledOpenCategoryItems,
selectedCategoryValue: controlledSelectedCategoryValue,
selectedValue: controlledSelectedValue,
defaultOpenCategories,
defaultSelectedValue,
defaultSelectedCategoryValue,
} = props;

const innerRef = React.useRef<HTMLElement>(null);

const [openCategories, setOpenCategories] = useControllableState({
// normalizeValues(controlledOpenItems), [controlledOpenItems])
state: React.useMemo(() => normalizeValues(), []),
defaultState: () => [], // initializeUncontrolledOpenItems({ defaultOpenItems }),
state: controlledOpenCategoryItems,
defaultState: initializeUncontrolledOpenCategories({ defaultOpenCategories, multiple }),
initialState: [],
});

const onRequestNavCategoryItemToggle: EventHandler<OnNavItemSelectData> = useEventCallback((event, data) => {
const nextOpenItems = updateOpenItems(data.value, openCategories, multiple);
onNavCategoryItemToggle?.(event, data);
setOpenCategories(nextOpenItems);
});

const [selectedCategoryValue, setSelectedCategoryValue] = useControllableState({
state: props.selectedCategoryValue,
defaultState: props.defaultSelectedCategoryValue,
state: controlledSelectedCategoryValue,
defaultState: defaultSelectedCategoryValue,
initialState: undefined,
});

const [selectedValue, setSelectedValue] = useControllableState({
state: props.selectedValue,
defaultState: props.defaultSelectedValue,
state: controlledSelectedValue,
defaultState: defaultSelectedValue,
initialState: undefined,
});

Expand All @@ -105,20 +98,32 @@ export const useNav_unstable = (props: NavProps, ref: React.Ref<HTMLDivElement>)
const currentSelectedCategoryValue = React.useRef<NavItemValue | undefined>(undefined);
const previousSelectedCategoryValue = React.useRef<NavItemValue | undefined>(undefined);

React.useEffect(() => {
if (currentSelectedValue.current !== selectedValue) {
previousSelectedValue.current = currentSelectedValue.current;
currentSelectedValue.current = selectedValue;
}

if (currentSelectedCategoryValue.current !== selectedCategoryValue) {
previousSelectedCategoryValue.current = currentSelectedCategoryValue.current;
currentSelectedCategoryValue.current = selectedCategoryValue;
}, [selectedValue, selectedCategoryValue]);
}

// used for NavItems and NavSubItems
const onSelect: EventHandler<OnNavItemSelectData> = useEventCallback((event, data) => {
setSelectedValue(data.value);
setSelectedCategoryValue(data.categoryValue);
setSelectedCategoryValue(data.categoryValue ? data.categoryValue : '');
onNavItemSelect?.(event, data);
});

// used for NavCategoryItems
const onRequestNavCategoryItemToggle: EventHandler<OnNavItemSelectData> = useEventCallback((event, data) => {
if (data.categoryValue !== undefined) {
const nextOpenCategories = updateOpenCategories(data.categoryValue, openCategories ?? [], multiple);
onNavCategoryItemToggle?.(event, data);
setOpenCategories(nextOpenCategories);
}
});

const registeredNavItems = React.useRef<Record<string, NavItemRegisterData>>({});

const onRegister = React.useCallback((data: NavItemRegisterData) => {
Expand Down Expand Up @@ -150,14 +155,14 @@ export const useNav_unstable = (props: NavProps, ref: React.Ref<HTMLDivElement>)
}),
{ elementType: 'div' },
),
openCategories,
selectedValue,
selectedCategoryValue,
onRegister,
onUnregister,
onSelect,
getRegisteredNavItems,
onRequestNavCategoryItemToggle,
openCategories,
multiple,
size,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const NavCategoryContext = React.createContext<NavCategoryContextValue | undefin

const navCategoryContextDefaultValue: NavCategoryContextValue = {
open: false,
value: undefined,
value: '',
};

export const { Provider: NavCategoryProvider } = NavCategoryContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ export const useNavCategoryItem_unstable = (
const { onRequestNavCategoryItemToggle, selectedCategoryValue, size = 'medium' } = useNavContext_unstable();

const onNavCategoryItemClick = useEventCallback(
mergeCallbacks(onClick, event => onRequestNavCategoryItemToggle(event, { type: 'click', event, value })),
mergeCallbacks(onClick, event =>
onRequestNavCategoryItemToggle(event, { type: 'click', event, value: '', categoryValue: value }),
),
);

const selected = selectedCategoryValue === value;
// don't fill the icon when it's open
const selected = selectedCategoryValue === value && !open;
// there's more than 2 possible values for aria-current, but this is the only one that's used in this component
const validAriaCurrent: 'page' | 'false' = selected && !open ? 'page' : 'false';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as React from 'react';
import { NavContextValue } from './NavContext.types';

const navContextDefaultValue: NavContextValue = {
reserveSelectedNavItemSpace: true,
selectedValue: undefined,
selectedCategoryValue: undefined,
onRegister: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { EventHandler } from '@fluentui/react-utilities';

import type { NavProps, OnNavItemSelectData } from './Nav/Nav.types';

export type NavContextValue = Pick<
NavProps,
'onNavItemSelect' | 'selectedValue' | 'selectedCategoryValue' | 'reserveSelectedNavItemSpace' | 'size'
> & {
export type NavContextValue = Pick<NavProps, 'onNavItemSelect' | 'selectedValue' | 'selectedCategoryValue' | 'size'> & {
/** A callback to allow a navItem to register itself with the navItem list. */
onRegister: RegisterNavItemEventHandler;

Expand Down Expand Up @@ -46,7 +43,7 @@ export type NavContextValue = Pick<
/**
* Any value that identifies a specific Item.
*/
export type NavItemValue = unknown;
export type NavItemValue = string;

/**
* Context values used in rendering navItemList.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const NavSubItemContext = React.createContext<NavSubItemContextValue | undefined

const NavSubItemContextDefaultValue: NavSubItemContextValue = {
open: false,
value: undefined,
value: '',
};

export const { Provider: NavSubItemProvider } = NavSubItemContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { NavCategoryContextValue, NavCategoryProvider } from '../NavCategoryCont
export function mockNavCategoryContextValue(partialValue?: Partial<NavCategoryContextValue>): NavCategoryContextValue {
return {
open: false,
value: undefined,
value: '',
...partialValue,
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { Nav, renderNav_unstable, useNav_unstable, useNavStyles_unstable, navClassNames } from './components/Nav/index';
export type { NavProps, NavSlots, NavState, NavSize } from './components/Nav/index';
export type { NavProps, NavSlots, NavState, NavSize, OnNavItemSelectData } from './components/Nav/index';

export { NavCategory, renderNavCategory_unstable, useNavCategory_unstable } from './components/NavCategory/index';
export type { NavCategoryProps, NavCategoryState } from './components/NavCategory/index';
Expand Down
Loading

0 comments on commit 8506c11

Please sign in to comment.