Skip to content

Commit

Permalink
feat(react-keytips): add overflowSequence prop (#304)
Browse files Browse the repository at this point in the history
  • Loading branch information
mainframev authored Feb 21, 2025
1 parent 6f13f82 commit 763f850
Show file tree
Hide file tree
Showing 21 changed files with 436 additions and 407 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "BREAKING: removed isShortcut prop, added overflowSequence",
"packageName": "@fluentui-contrib/react-keytips",
"email": "[email protected]",
"dependentChangeType": "patch"
}
8 changes: 4 additions & 4 deletions packages/react-keytips/src/components/Keytip/Keytip.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ export type KeytipProps = ComponentProps<KeytipSlots> & {
* Common cases are a Tabs or Modal. Or if the keytip opens a menu.
*/
dynamic?: boolean;
/**
* Whether this Keytip can be accessed at the root level.
*/
isShortcut?: boolean;
/**
* Whether or not this Keytip belongs to a component that has a menu. Keytip mode will stay on when a menu is opened,
* even if the items in that menu have no keytips.
*/
hasMenu?: boolean;
/**
* If this keytip is inside OverflowMenu, you might need to add the overflowSequence to make shortcuts available.
*/
overflowSequence?: string[];
};

/** @internal */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,20 @@ test.describe('test keytip navigation', () => {
mount,
page,
}) => {
page.setViewportSize({ width: 400, height: 500 });

const component = await mount(
<KeytipsOverflowMenuExample startSequence="alt+meta" />
);
await component.press('Alt+Meta');

await expect(page.getByRole('tooltip', { name: 'A' })).toBeVisible();
await expect(page.getByRole('tooltip', { name: 'R' })).toBeVisible();
// should open nested menus and show next keytip
await component.press('a');
await expect(page.getByRole('menuitem', { name: 'Item 7' })).toBeVisible();
await expect(page.getByRole('tooltip', { name: 'B' })).toBeVisible();
await component.press('r');
await expect(page.getByRole('menuitem', { name: 'Item 4' })).toBeVisible();

await component.press('b');
await expect(page.getByRole('menuitem', { name: '8' })).toBeVisible();
await component.press('y');
await expect(page.getByRole('menuitem', { name: '6' })).toBeVisible();
});

test('should be hidden with overflow menu, if the overflow menu is not available', async ({
Expand All @@ -148,18 +149,21 @@ test.describe('test keytip navigation', () => {
<KeytipsOverflowMenuExample startSequence="alt+meta" />
);
await component.press('Alt+Meta');
await expect(page.getByRole('tooltip', { name: 'A' })).toBeHidden();
await expect(page.getByRole('tooltip', { name: 'R' })).toBeHidden();
});

test('should have shortcut keytip', async ({ mount, page }) => {
page.setViewportSize({ width: 400, height: 500 });

const component = await mount(
<KeytipsOverflowMenuExample startSequence="alt+meta" />
);

await component.press('Alt+Meta');

await expect(page.getByRole('tooltip', { name: 'A' })).toBeVisible();
await component.press('b');
await expect(page.getByRole('menuitem', { name: '8' })).toBeVisible();
await expect(page.getByRole('tooltip', { name: 'R' })).toBeVisible();
await component.press('y');
await expect(page.getByRole('menuitem', { name: '6' })).toBeVisible();
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import * as React from 'react';
import { useKeytipRef } from '../../hooks/useKeytipRef';
import type { ExecuteKeytipEventHandler } from '../Keytip/Keytip.types';
import type {
ExecuteKeytipEventHandler,
KeytipProps,
} from '../Keytip/Keytip.types';
import {
Button,
FluentProvider,
Overflow,
OverflowItem,
OverflowItemProps,
useMergedRefs,
mergeClasses,
useOverflowMenu,
Menu,
MenuItem,
MenuTrigger,
MenuButton,
Toolbar,
ToolbarButton,
MenuPopover,
MenuList,
Tab,
Expand All @@ -29,6 +31,7 @@ import {
useIsOverflowItemVisible,
TabValue,
} from '@fluentui/react-components';
import { MoreHorizontal20Filled } from '@fluentui/react-icons';
import { Keytips } from './Keytips';
import type { KeytipsProps } from './Keytips.types';

Expand Down Expand Up @@ -248,55 +251,125 @@ export const KeytipsTabsExample = (props: KeytipsProps) => {
);
};

const SubMenu = React.forwardRef<HTMLDivElement>((_, ref) => {
return (
<Menu>
<MenuTrigger disableButtonEnhancement>
<MenuItem ref={ref}>Sub Menu</MenuItem>
</MenuTrigger>

<MenuPopover>
<MenuList>
<MenuItem>8</MenuItem>
<MenuItem>9</MenuItem>
<MenuItem>10</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
);
});
type MenuItemType = KeytipProps & {
id: string;
overflowMenuItems?: Array<KeytipProps & { id: string }>;
};

const OverflowMenuItem: React.FC<Pick<OverflowItemProps, 'id'>> = (props) => {
const { id } = props;
const isVisible = useIsOverflowItemVisible(id);
const onExecute: ExecuteKeytipEventHandler = (_, { targetElement }) => {
if (targetElement) {
console.info(targetElement);
targetElement.focus();
targetElement.click();
}
};

if (isVisible) {
return null;
const menuItems = [
{
id: '1',
keySequences: ['q'],
content: 'Q',
onExecute,
},
{
id: '2',
keySequences: ['w'],
content: 'W',
onExecute,
},
{
id: '3',
keySequences: ['e'],
content: 'E',
onExecute,
},
{
id: '4',
keySequences: ['t'],
content: 'T',
onExecute,
},
{
id: '5',
keySequences: ['y'],
content: 'Y',
hasMenu: true,
onExecute,
overflowMenuItems: [
{
id: '6',
keySequences: ['y', 'h'],
content: 'H',
onExecute,
},
{ id: '7', keySequences: ['y', 'z'], content: 'Z' },
],
},
] satisfies MenuItemType[];

const OverflowItemWrapper = React.forwardRef<HTMLDivElement, MenuItemType>(
(keytipProps, ref) => {
const isVisible = useIsOverflowItemVisible(keytipProps.id);
const keytipRef = useKeytipRef<HTMLElement>({
...keytipProps,
overflowSequence: !isVisible ? ['r'] : [],
});
const mergedRefs = useMergedRefs(ref, keytipRef);

return (
<OverflowItem id={keytipProps.id}>
<ToolbarButton ref={mergedRefs}>Item {keytipProps.id}</ToolbarButton>
</OverflowItem>
);
}
);

return <MenuItem>Item {id}</MenuItem>;
};
const OverflowMenuItemWrapper = React.forwardRef<HTMLDivElement, MenuItemType>(
({ overflowMenuItems, ...keytipProps }, ref) => {
const isVisible = useIsOverflowItemVisible(keytipProps.id);

const OverflowMenu: React.FC<{ itemIds: string[] }> = ({ itemIds }) => {
const { ref, overflowCount, isOverflowing } =
useOverflowMenu<HTMLButtonElement>();
const keytipRef = useKeytipRef<HTMLDivElement>({
...keytipProps,
keySequences: !isVisible
? ['r', ...keytipProps.keySequences]
: keytipProps.keySequences,
});

const onExecute: ExecuteKeytipEventHandler = (_, el) => {
el.targetElement?.click();
};
const mergedRefs = useMergedRefs(ref, keytipRef);

const menuRef = useKeytipRef({
keySequences: ['a'],
content: 'A',
dynamic: true,
onExecute,
});
if (isVisible) {
return null;
}

const subMenuRef = useKeytipRef<HTMLDivElement>({
keySequences: ['a', 'b'],
content: 'B',
return overflowMenuItems && overflowMenuItems.length > 0 ? (
<Menu key={keytipProps.id}>
<MenuTrigger disableButtonEnhancement>
<MenuItem ref={mergedRefs}>Item {keytipProps.id}</MenuItem>
</MenuTrigger>

<MenuPopover>
<MenuList>
{overflowMenuItems.map((item) => (
<OverflowMenuItemWrapper key={item.id} {...item} />
))}
</MenuList>
</MenuPopover>
</Menu>
) : (
<MenuItem id={keytipProps.id} ref={mergedRefs}>
Item {keytipProps.id}
</MenuItem>
);
}
);

const OverflowMenu = ({ menuItems }: { menuItems: MenuItemType[] }) => {
const { ref, isOverflowing } = useOverflowMenu();

const menuRef = useKeytipRef<HTMLElement>({
hasMenu: true,
isShortcut: true,
keySequences: ['r'],
content: 'R',
onExecute,
});

Expand All @@ -309,39 +382,44 @@ const OverflowMenu: React.FC<{ itemIds: string[] }> = ({ itemIds }) => {
return (
<Menu>
<MenuTrigger disableButtonEnhancement>
<MenuButton ref={mergedRefs}>+{overflowCount} items</MenuButton>
<Button
ref={mergedRefs}
icon={<MoreHorizontal20Filled />}
aria-label="More items"
appearance="subtle"
/>
</MenuTrigger>

<MenuPopover>
<MenuList>
{itemIds.map((i) => {
return <OverflowMenuItem key={i} id={i} />;
})}
<SubMenu ref={subMenuRef} />
{menuItems.map(({ id, ...props }) => (
<OverflowMenuItemWrapper key={id} id={id} {...props} />
))}
</MenuList>
</MenuPopover>
</Menu>
);
};

export const KeytipsOverflowMenuExample = (props: KeytipsProps) => {
const styles = useStyles();

const itemIds = new Array(8).fill(0).map((_, i) => i.toString());

return (
<>
<Keytips {...props} />
<Overflow>
<div className={mergeClasses(styles.container, styles.resizableArea)}>
{itemIds.map((i) => (
<OverflowItem key={i} id={i}>
<Button>Item {i}</Button>
</OverflowItem>
))}
<OverflowMenu itemIds={itemIds} />
</div>
</Overflow>
<div
style={{
resize: 'horizontal',
overflow: 'hidden',
}}
>
<Overflow>
<Toolbar>
{menuItems.map(({ id, ...props }) => (
<OverflowItemWrapper key={id} id={id} {...props} />
))}
<OverflowMenu menuItems={menuItems} />
</Toolbar>
</Overflow>
</div>
</>
);
};
Expand Down
Loading

0 comments on commit 763f850

Please sign in to comment.