Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Feat(react-menu-grid-preview): Detect invalid MenuGrid nesting",
"packageName": "@fluentui/react-menu-grid-preview",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import * as React from 'react';
import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities';
import { useMergedRefs, getIntrinsicElementProps, slot } from '@fluentui/react-utilities';

import { useTableCompositeNavigation } from '@fluentui/react-table';
import type { MenuGridProps, MenuGridState } from './MenuGrid.types';
import { useMenuContext_unstable } from '@fluentui/react-menu';
import { useValidateMenuGridNesting } from './useValidateMenuGridNesting';

/**
* Returns the props and state required to render the component
*/
export const useMenuGrid_unstable = (props: MenuGridProps, ref: React.Ref<HTMLDivElement>): MenuGridState => {
const validateNestingRef = useValidateMenuGridNesting();
const triggerId = useMenuContext_unstable(context => context.triggerId);
const { tableRowTabsterAttribute, tableTabsterAttribute, onTableKeyDown } = useTableCompositeNavigation();

Expand All @@ -17,7 +20,7 @@ export const useMenuGrid_unstable = (props: MenuGridProps, ref: React.Ref<HTMLDi
},
root: slot.always(
getIntrinsicElementProps('div', {
ref,
ref: useMergedRefs(ref, validateNestingRef),
role: 'grid',
'aria-labelledby': triggerId,
onKeyDown: onTableKeyDown,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as React from 'react';

import { useValidateNesting } from '../../utils/useValidateNesting';

export const useValidateMenuGridNesting = (): React.RefObject<HTMLElement> => {
return useValidateNesting('MenuGrid');
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from 'react';
import { slot } from '@fluentui/react-utilities';
import { useMergedRefs, slot } from '@fluentui/react-utilities';

import { MenuGridItemProps, MenuGridItemState } from './MenuGridItem.types';
import { MenuGridCell } from './../MenuGridCell/MenuGridCell';
import { MenuGridRow } from './../MenuGridRow/MenuGridRow';
import { useValidateMenuGridItemNesting } from './useValidateMenuGridItemNesting';

/**
* Given user props, returns state and render function for a MenuGridItem.
Expand All @@ -17,6 +18,7 @@ export function useMenuGridItem_unstable(props: MenuGridItemProps, ref: React.Re
secondSubAction,
...rest
} = props;
const validateNestingRef = useValidateMenuGridItemNesting();

return {
components: {
Expand All @@ -29,7 +31,7 @@ export function useMenuGridItem_unstable(props: MenuGridItemProps, ref: React.Re
},
root: slot.always(
{
ref,
ref: useMergedRefs(ref, validateNestingRef),
...rest,
},
{ elementType: MenuGridRow },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as React from 'react';

import { useValidateNesting } from '../../utils/useValidateNesting';

export const useValidateMenuGridItemNesting = (): React.RefObject<HTMLElement> => {
return useValidateNesting('MenuGridItem');
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import * as React from 'react';
import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities';
import { useMergedRefs, getIntrinsicElementProps, slot } from '@fluentui/react-utilities';

import { useMenuGridContext_unstable } from '../../contexts/menuGridContext';
import { MenuGridRowProps, MenuGridRowState } from './MenuGridRow.types';
import { useValidateMenuGridRowNesting } from './useValidateMenuGridRowNesting';

/**
* Given user props, returns state and render function for a MenuGridRow.
*/
export function useMenuGridRow_unstable(props: MenuGridRowProps, ref: React.Ref<HTMLDivElement>): MenuGridRowState {
const validateNestingRef = useValidateMenuGridRowNesting();
const { tableRowTabsterAttribute } = useMenuGridContext_unstable();

return {
Expand All @@ -16,7 +18,7 @@ export function useMenuGridRow_unstable(props: MenuGridRowProps, ref: React.Ref<
},
root: slot.always(
getIntrinsicElementProps('div', {
ref,
ref: useMergedRefs(ref, validateNestingRef),
role: 'row',
tabIndex: 0,
...tableRowTabsterAttribute,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as React from 'react';

import { useValidateNesting } from '../../utils/useValidateNesting';

export const useValidateMenuGridRowNesting = (): React.RefObject<HTMLElement> => {
return useValidateNesting('MenuGridRow');
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useValidateNesting } from './useValidateNesting';
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from 'react';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';

type NestingComponentName = 'MenuGrid' | 'MenuGridItem' | 'MenuGridRow';

export const useValidateNesting = (componentName: NestingComponentName): React.RefObject<HTMLElement> => {
'use no memo';

const ref = React.useRef<HTMLElement>(null);
const { targetDocument } = useFluent();

if (process.env.NODE_ENV !== 'production') {
// This check should run only in development mode
// It's okay to disable the ESLint rule because we ar checking env variable statically (not at runtime)
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
let current = ref.current?.parentElement;
let role = current?.getAttribute('role');
while (current !== targetDocument?.body) {
if (role === 'grid' || role === 'menuitem') {
break;
}
if (role === 'menu') {
let message = '';
switch (componentName) {
case 'MenuGrid':
message = 'MenuGrid is incorrectly nested within MenuList or within an element with the "menu" role.';
break;
case 'MenuGridItem':
message =
'MenuGridItem is incorrectly nested within MenuList or within an element with the "menu" role. You probably want to wrap it in a MenuGrid.';
break;
case 'MenuGridRow':
message =
'MenuGridRow is incorrectly nested within MenuList or within an element with the "menu" role. You probably want to wrap it in a MenuGrid.';
break;
}
throw new Error(message);
}
current = current?.parentElement ?? null;
role = current?.getAttribute('role');
}
}, [componentName, ref, targetDocument?.body]);
}
return ref;
};