Support submenu for ContextualMenu component (#667)
* add submenu component to menu * Add nested menu example to Fluentui tester * use single quote for string * Change files * Add menu E2E test page * formatting fix for submenu * fix hover behavior on submenu items * resolve dependency errors * fix typos * disable no-var-requires * Change files
This commit is contained in:
Родитель
27c0673e30
Коммит
06aa14bd6c
|
@ -1,6 +1,7 @@
|
|||
import { HOMEPAGE_CHECKBOX_BUTTON } from '../../../FluentTester/TestComponents/Checkbox/consts';
|
||||
import { HOMEPAGE_BUTTON_BUTTON } from '../../../FluentTester/TestComponents/Button/consts';
|
||||
import { HOMEPAGE_CALLOUT_BUTTON } from '../../../FluentTester/TestComponents/Callout/consts';
|
||||
import { HOMEPAGE_CONTEXTUALMENU_BUTTON } from '../../../FluentTester/TestComponents/ContextualMenu/consts';
|
||||
import { HOMEPAGE_FOCUSTRAPZONE_BUTTON } from '../../../FluentTester/TestComponents/FocusTrapZone/consts';
|
||||
import { HOMEPAGE_FOCUSZONE_BUTTON } from '../../../FluentTester/TestComponents/FocusZone/consts';
|
||||
import { HOMEPAGE_ICON_BUTTON } from '../../../FluentTester/TestComponents/Icon/consts';
|
||||
|
@ -29,6 +30,10 @@ class BootTestPage extends BasePage {
|
|||
this.checkboxPage.click();
|
||||
}
|
||||
|
||||
clickAndGoToContextualMenuPage() {
|
||||
this.contextualMenuPage.click();
|
||||
}
|
||||
|
||||
clickAndGoToFocusTrapZonePage() {
|
||||
this.focusTrapZonePage.click();
|
||||
}
|
||||
|
@ -97,6 +102,10 @@ class BootTestPage extends BasePage {
|
|||
return By(HOMEPAGE_CHECKBOX_BUTTON);
|
||||
}
|
||||
|
||||
private get contextualMenuPage() {
|
||||
return By(HOMEPAGE_CONTEXTUALMENU_BUTTON);
|
||||
}
|
||||
|
||||
private get focusTrapZonePage() {
|
||||
return By(HOMEPAGE_FOCUSTRAPZONE_BUTTON);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import ButtonTestPage from '../../Button/pages/ButtonTestPage.win';
|
||||
import CalloutTestPage from '../../Callout/pages/CalloutTestPage.win';
|
||||
import CheckboxTestPage from '../../Checkbox/pages/CheckboxTestPage.win';
|
||||
import ContextualMenuTestPage from '../../ContextualMenu/pages/ContextualMenuTestPage.win';
|
||||
import FocusTrapZoneTestPage from '../../FocusTrapZone/pages/FocusTrapZonePage.win';
|
||||
import FocusZoneTestPage from '../../FocusZone/pages/FocusZoneTestPage.win';
|
||||
import IconTestPage from '../../Icon/pages/IconTestPage.win';
|
||||
|
@ -45,6 +46,12 @@ describe('Click on each test page and check if it renders', function () {
|
|||
expect(CheckboxTestPage.isPageLoaded()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('ContextualMenu Test Page', () => {
|
||||
BootTestPage.clickAndGoToContextualMenuPage();
|
||||
ContextualMenuTestPage.waitForPageDisplayed(PAGE_TIMEOUT);
|
||||
expect(ContextualMenuTestPage.isPageLoaded()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FocusTrapZone Test Page', () => {
|
||||
BootTestPage.clickAndGoToFocusTrapZonePage();
|
||||
FocusTrapZoneTestPage.waitForPageDisplayed(PAGE_TIMEOUT);
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { CONTEXTUALMENU_TESTPAGE } from '../../../FluentTester/TestComponents/ContextualMenu/consts';
|
||||
import { BasePage, By } from '../../common/BasePage';
|
||||
|
||||
class ContextualMenuPage extends BasePage {
|
||||
get _testPage() {
|
||||
return By(CONTEXTUALMENU_TESTPAGE);
|
||||
}
|
||||
|
||||
get _pageName() {
|
||||
return CONTEXTUALMENU_TESTPAGE;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ContextualMenuPage();
|
|
@ -1,8 +1,11 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import * as React from 'react';
|
||||
import { Text, View, Switch } from 'react-native';
|
||||
import { Button, ContextualMenu, ContextualMenuItem, Separator } from '@fluentui/react-native';
|
||||
import { Button, ContextualMenu, ContextualMenuItem, Submenu, SubmenuItem, Separator } from '@fluentui/react-native';
|
||||
import { CONTEXTUALMENU_TESTPAGE } from './consts';
|
||||
import { Test, TestSection, PlatformStatus } from '../Test';
|
||||
import { SvgIconProps, FontIconProps } from '@fluentui-react-native/icon';
|
||||
import TestSvg from '../Button/test.svg';
|
||||
|
||||
const contextualMenu: React.FunctionComponent<{}> = () => {
|
||||
const stdBtnRef = React.useRef(null);
|
||||
|
@ -95,12 +98,148 @@ const contextualMenu: React.FunctionComponent<{}> = () => {
|
|||
);
|
||||
}
|
||||
|
||||
const nestedContextualMenu: React.FunctionComponent<{}> = () => {
|
||||
const testImage = require('../Button/icon_24x24.png');
|
||||
const testTtf = require('../Button/Font Awesome 5 Free-Solid-900.otf');
|
||||
|
||||
const fontProps: FontIconProps = {
|
||||
fontFamily: `Font Awesome 5 Free`,
|
||||
fontSrcFile: testTtf,
|
||||
codepoint: 0xf083,
|
||||
fontSize: 16,
|
||||
};
|
||||
|
||||
const svgProps: SvgIconProps = {
|
||||
src: TestSvg,
|
||||
viewBox: '0 0 500 500',
|
||||
};
|
||||
|
||||
const stdBtnRef = React.useRef(null);
|
||||
|
||||
const [showContextualMenu, setShowContextualMenu] = React.useState(false);
|
||||
const [isContextualMenuVisible, setIsContextualMenuVisible] = React.useState(false);
|
||||
|
||||
const [focusOnMount, setShouldFocusOnMount] = React.useState(true);
|
||||
const toggleFocusOnMount = React.useCallback((value) => setShouldFocusOnMount(value), [setShouldFocusOnMount]);
|
||||
|
||||
const [focusOnContainer, setShouldFocusOnContainer] = React.useState(false);
|
||||
const toggleFocusOnContainer = React.useCallback((value) => setShouldFocusOnContainer(value), [setShouldFocusOnContainer]);
|
||||
|
||||
const toggleShowContextualMenu = React.useCallback(() => {
|
||||
setShowContextualMenu(!showContextualMenu);
|
||||
setIsContextualMenuVisible(!isContextualMenuVisible);
|
||||
}, [showContextualMenu, isContextualMenuVisible, setShowContextualMenu, setIsContextualMenuVisible]);
|
||||
|
||||
const onShowContextualMenu = React.useCallback(() => {
|
||||
setIsContextualMenuVisible(true);
|
||||
}, [setIsContextualMenuVisible]);
|
||||
|
||||
const onDismissContextualMenu = React.useCallback(() => {
|
||||
setShowContextualMenu(false);
|
||||
setIsContextualMenuVisible(false);
|
||||
}, [setShowContextualMenu]);
|
||||
|
||||
const stdMenuItemRef = React.useRef(null);
|
||||
|
||||
const [showSubmenu, setShowSubmenu] = React.useState(false);
|
||||
const [isSubmenuVisible, setIsSubmenuVisible] = React.useState(false);
|
||||
|
||||
const toggleShowSubmenu = React.useCallback(() => {
|
||||
setShowSubmenu(!showSubmenu);
|
||||
setIsSubmenuVisible(!isSubmenuVisible);
|
||||
}, [showSubmenu, isSubmenuVisible, setShowSubmenu, setIsSubmenuVisible]);
|
||||
|
||||
const onShowSubmenu = React.useCallback(() => {
|
||||
setIsSubmenuVisible(true);
|
||||
}, [setIsSubmenuVisible]);
|
||||
|
||||
const onDismissSubmenu = React.useCallback(() => {
|
||||
setShowSubmenu(false);
|
||||
}, [setShowSubmenu]);
|
||||
|
||||
const onClick = React.useCallback(
|
||||
() => {
|
||||
console.log('submenu item clicked');
|
||||
}, []
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={{ flexDirection: 'row', paddingVertical: 5 }}>
|
||||
|
||||
<View style={{ flexDirection: 'column', paddingHorizontal: 5 }}>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<Text>Should Focus on Mount</Text>
|
||||
<Switch value={focusOnMount} onValueChange={toggleFocusOnMount} />
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<Text>Should Focus on Container</Text>
|
||||
<Switch value={focusOnContainer} onValueChange={toggleFocusOnContainer} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Separator vertical />
|
||||
|
||||
<View style={{ flexDirection: 'column', paddingHorizontal: 5 }}>
|
||||
<Text>
|
||||
<Text>Menu Visibility: </Text>
|
||||
{isContextualMenuVisible ? <Text style={{ color: 'green' }}>Visible</Text> : <Text style={{ color: 'red' }}>Not Visible</Text>}
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
<Text>Submenu Visibility: </Text>
|
||||
{isSubmenuVisible ? <Text style={{ color: 'green' }}>Visible</Text> : <Text style={{ color: 'red' }}>Not Visible</Text>}
|
||||
</Text>
|
||||
<Button content="Press for ContextualMenu" onClick={toggleShowContextualMenu} componentRef={stdBtnRef} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{showContextualMenu && (
|
||||
<ContextualMenu
|
||||
target={stdBtnRef}
|
||||
onDismiss={onDismissContextualMenu}
|
||||
onShow={onShowContextualMenu}
|
||||
accessibilityLabel="Standard ContextualMenu"
|
||||
setShowMenu={toggleShowContextualMenu}
|
||||
shouldFocusOnMount={focusOnMount}
|
||||
shouldFocusOnContainer={focusOnContainer}
|
||||
>
|
||||
<ContextualMenuItem icon={testImage} text="Menu item with png Icon" itemKey="1" />
|
||||
<ContextualMenuItem icon={{ fontSource: fontProps, color: 'blue' }} text="Menu item with font icon" itemKey="2" />
|
||||
<ContextualMenuItem text="Disabled Menu Item" itemKey="3" disabled />
|
||||
<SubmenuItem text="Nested Menu" itemKey="4" onHoverIn={toggleShowSubmenu} componentRef={stdMenuItemRef} />
|
||||
{showSubmenu && (
|
||||
<Submenu
|
||||
target={stdMenuItemRef}
|
||||
onDismiss={onDismissSubmenu}
|
||||
onShow={onShowSubmenu}
|
||||
setShowMenu={toggleShowSubmenu}
|
||||
>
|
||||
<ContextualMenuItem icon={{ svgSource: svgProps, width: 20, height: 20, color: 'red' }} text="SubmenuItem svg icon" itemKey="4" onClick={onClick} />
|
||||
<ContextualMenuItem text="SubmenuItem 2" itemKey="2" />
|
||||
<ContextualMenuItem text="Disabled Menu Item" itemKey="3" disabled />
|
||||
</Submenu>
|
||||
)}
|
||||
<ContextualMenuItem text="Menuitem 5" itemKey="5" />
|
||||
</ContextualMenu>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
const contextualMenuSections: TestSection[] = [
|
||||
{
|
||||
name: 'Standard ContextualMenu',
|
||||
testID: CONTEXTUALMENU_TESTPAGE,
|
||||
component: contextualMenu,
|
||||
},
|
||||
{
|
||||
name: 'Nested ContextualMenu',
|
||||
component: nestedContextualMenu,
|
||||
}
|
||||
];
|
||||
|
||||
export const ContextualMenuTest: React.FunctionComponent<{}> = () => {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "none",
|
||||
"comment": "disable no-var-requires",
|
||||
"packageName": "@fluentui/react-native",
|
||||
"email": "lehon@microsoft.com",
|
||||
"dependentChangeType": "none",
|
||||
"date": "2021-05-19T06:14:24.359Z"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "none",
|
||||
"comment": "disable no-var-requires",
|
||||
"packageName": "@fluentui-react-native/android-theme",
|
||||
"email": "lehon@microsoft.com",
|
||||
"dependentChangeType": "none",
|
||||
"date": "2021-05-19T06:14:27.918Z"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "minor",
|
||||
"comment": "create iconProps hook",
|
||||
"packageName": "@fluentui-react-native/button",
|
||||
"email": "lehon@microsoft.com",
|
||||
"dependentChangeType": "patch",
|
||||
"date": "2021-05-19T06:13:49.828Z"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "minor",
|
||||
"comment": "add submenu component to menu",
|
||||
"packageName": "@fluentui-react-native/contextual-menu",
|
||||
"email": "lehon@microsoft.com",
|
||||
"dependentChangeType": "patch",
|
||||
"date": "2021-04-27T11:41:17.601Z"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "none",
|
||||
"comment": "disable no-var-requires",
|
||||
"packageName": "@fluentui-react-native/experimental-shimmer",
|
||||
"email": "lehon@microsoft.com",
|
||||
"dependentChangeType": "none",
|
||||
"date": "2021-05-19T06:13:59.973Z"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "minor",
|
||||
"comment": "add iconProps hook",
|
||||
"packageName": "@fluentui-react-native/interactive-hooks",
|
||||
"email": "lehon@microsoft.com",
|
||||
"dependentChangeType": "patch",
|
||||
"date": "2021-05-19T06:14:41.137Z"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "none",
|
||||
"comment": "disable no-var-requires",
|
||||
"packageName": "@fluentui-react-native/radio-group",
|
||||
"email": "lehon@microsoft.com",
|
||||
"dependentChangeType": "none",
|
||||
"date": "2021-05-19T06:13:55.912Z"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "minor",
|
||||
"comment": "add submenu component to menu",
|
||||
"packageName": "@fluentui-react-native/tester",
|
||||
"email": "lehon@microsoft.com",
|
||||
"dependentChangeType": "patch",
|
||||
"date": "2021-04-27T11:41:07.403Z"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "minor",
|
||||
"comment": "fix hover behavior on submenu items",
|
||||
"packageName": "@fluentui-react-native/tester-win32",
|
||||
"email": "lehon@microsoft.com",
|
||||
"dependentChangeType": "patch",
|
||||
"date": "2021-05-19T06:13:22.066Z"
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/** @jsx withSlots */
|
||||
import * as React from 'react';
|
||||
import { Image, View } from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
import { IButtonSlotProps, IButtonState, IButtonProps, IButtonRenderData, buttonName, IButtonType } from './Button.types';
|
||||
import { compose, IUseComposeStyling } from '@uifabricshared/foundation-compose';
|
||||
import { ISlots, withSlots } from '@uifabricshared/foundation-composable';
|
||||
|
@ -10,29 +10,8 @@ import { backgroundColorTokens, borderTokens, textTokens, foregroundColorTokens,
|
|||
import { filterViewProps } from '@fluentui-react-native/adapters';
|
||||
import { mergeSettings } from '@uifabricshared/foundation-settings';
|
||||
import { useAsPressable, useKeyCallback, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks';
|
||||
import { Icon, RasterImageIconProps, IconProps } from '@fluentui-react-native/icon';
|
||||
|
||||
function createIconProps(src: number | string | IconProps) {
|
||||
if (src === undefined) return null;
|
||||
|
||||
if (typeof src === 'number') {
|
||||
const rasterProps: RasterImageIconProps = { src: src };
|
||||
const asset = Image.resolveAssetSource(+src);
|
||||
|
||||
return {
|
||||
rasterImageSource: rasterProps,
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
};
|
||||
}
|
||||
else if (typeof src === 'string') {
|
||||
const rasterProps: RasterImageIconProps = { src: { uri: src as string } };
|
||||
return { rasterImageSource: rasterProps };
|
||||
}
|
||||
else {
|
||||
return src as IconProps;
|
||||
}
|
||||
}
|
||||
import { Icon } from '@fluentui-react-native/icon';
|
||||
import { createIconProps } from '@fluentui-react-native/interactive-hooks';
|
||||
|
||||
export const Button = compose<IButtonType>({
|
||||
displayName: buttonName,
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"dependencies": {
|
||||
"@uifabricshared/foundation-compose": "^1.8.1",
|
||||
"@fluentui-react-native/callout": ">=0.14.1 <1.0.0",
|
||||
"@fluentui-react-native/icon": "0.5.1",
|
||||
"@fluentui-react-native/interactive-hooks": ">=0.9.0 <1.0.0",
|
||||
"@fluentui-react-native/text": ">=0.9.1 <1.0.0",
|
||||
"@fluentui-react-native/tokens": ">=0.8.0 <1.0.0",
|
||||
|
|
|
@ -14,7 +14,18 @@ export interface ContextualMenuContext {
|
|||
** Updates the clicked menu item and calls the client’s onItemClick callback
|
||||
*/
|
||||
onItemClick?: (key: string) => void;
|
||||
/*
|
||||
** Parent menu's onDismiss callback that is passed into submenu to call when submenu item is clicked
|
||||
*/
|
||||
onDismissMenu?: () => void;
|
||||
/*
|
||||
** Checks if any child menus are open
|
||||
*/
|
||||
isSubmenuOpen?: boolean;
|
||||
/*
|
||||
** ContextualMenuItems will call this submenu dismissal when they are hovered
|
||||
*/
|
||||
dismissSubmenu?: () => void;
|
||||
}
|
||||
|
||||
export interface ContextualMenuState {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/** @jsx withSlots */
|
||||
import * as React from 'react';
|
||||
import { Image, View } from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
import {
|
||||
ContextualMenuItemSlotProps,
|
||||
ContextualMenuItemState,
|
||||
|
@ -13,10 +13,12 @@ import { compose, IUseComposeStyling } from '@uifabricshared/foundation-compose'
|
|||
import { ISlots, withSlots } from '@uifabricshared/foundation-composable';
|
||||
import { Text } from '@fluentui-react-native/text';
|
||||
import { settings } from './ContextualMenuItem.settings';
|
||||
import { backgroundColorTokens, borderTokens, textTokens, foregroundColorTokens } from '@fluentui-react-native/tokens';
|
||||
import { backgroundColorTokens, borderTokens, textTokens, foregroundColorTokens, getPaletteFromTheme } from '@fluentui-react-native/tokens';
|
||||
import { mergeSettings } from '@uifabricshared/foundation-settings';
|
||||
import { useAsPressable, useKeyCallback, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks';
|
||||
import { CMContext } from './ContextualMenu';
|
||||
import { Icon } from '@fluentui-react-native/icon';
|
||||
import { createIconProps } from '@fluentui-react-native/interactive-hooks';
|
||||
|
||||
export const ContextualMenuItem = compose<ContextualMenuItemType>({
|
||||
displayName: contextualMenuItemName,
|
||||
|
@ -40,11 +42,7 @@ export const ContextualMenuItem = compose<ContextualMenuItemType>({
|
|||
(e) => {
|
||||
if (!disabled) {
|
||||
context ?.onDismissMenu();
|
||||
if (onClick) {
|
||||
onClick();
|
||||
} else {
|
||||
context.onItemClick && context.onItemClick(itemKey);
|
||||
}
|
||||
onClick ? onClick() : context.onItemClick(itemKey);
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
@ -52,11 +50,16 @@ export const ContextualMenuItem = compose<ContextualMenuItemType>({
|
|||
);
|
||||
|
||||
const cmRef = useViewCommandFocus(componentRef);
|
||||
//const cmRef = React.useRef(null);
|
||||
|
||||
const onItemHoverIn = React.useCallback(
|
||||
() => {
|
||||
componentRef.current.focus();
|
||||
}, [componentRef]);
|
||||
// dismiss submenu
|
||||
if (!disabled && context.isSubmenuOpen)
|
||||
{
|
||||
context.dismissSubmenu();
|
||||
}
|
||||
}, [componentRef, disabled, context]);
|
||||
|
||||
const pressable = useAsPressable({ ...rest, onPress: onItemClick, onHoverIn: onItemHoverIn });
|
||||
|
||||
|
@ -104,7 +107,7 @@ export const ContextualMenuItem = compose<ContextualMenuItemType>({
|
|||
accessibilityLabel: accessibilityLabel
|
||||
},
|
||||
content: { children: text, testID },
|
||||
icon: { source: icon },
|
||||
icon: createIconProps(icon),
|
||||
});
|
||||
|
||||
return { slotProps, state };
|
||||
|
@ -115,7 +118,7 @@ export const ContextualMenuItem = compose<ContextualMenuItemType>({
|
|||
return (
|
||||
<Slots.root>
|
||||
<Slots.stack>
|
||||
{renderData!.state.icon && <Slots.icon source={renderData.slotProps!.icon.source} />}
|
||||
{renderData!.state.icon && <Slots.icon />}
|
||||
{renderData!.state.content && <Slots.content />}
|
||||
{children}
|
||||
</Slots.stack>
|
||||
|
@ -125,13 +128,13 @@ export const ContextualMenuItem = compose<ContextualMenuItemType>({
|
|||
slots: {
|
||||
root: View,
|
||||
stack: { slotType: View },
|
||||
icon: { slotType: Image as React.ComponentType<object> },
|
||||
icon: { slotType: Icon as React.ComponentType<object> },
|
||||
content: Text,
|
||||
},
|
||||
styles: {
|
||||
root: [backgroundColorTokens, borderTokens],
|
||||
stack: [],
|
||||
icon: [foregroundColorTokens],
|
||||
icon: [{ source: 'iconColor', lookup: getPaletteFromTheme, target: 'color' }],
|
||||
content: [textTokens, foregroundColorTokens],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import * as React from 'react';
|
||||
import { ViewProps, ImageProps } from 'react-native';
|
||||
import { ViewProps } from 'react-native';
|
||||
import { IRenderData } from '@uifabricshared/foundation-composable';
|
||||
import { ITextProps } from '@fluentui-react-native/text';
|
||||
import { IPressableProps } from '@fluentui-react-native/pressable';
|
||||
import { FontTokens, IForegroundColorTokens, IBackgroundColorTokens, IBorderTokens } from '@fluentui-react-native/tokens';
|
||||
import { IFocusable, IPressableState } from '@fluentui-react-native/interactive-hooks';
|
||||
import { IconProps } from '@fluentui-react-native/icon';
|
||||
|
||||
export const contextualMenuItemName = 'ContextualMenuItem';
|
||||
|
||||
|
@ -19,7 +20,32 @@ export interface ContextualMenuItemState extends IPressableState {
|
|||
icon?: boolean;
|
||||
}
|
||||
|
||||
export interface ContextualMenuItemTokens extends FontTokens, IForegroundColorTokens, IBackgroundColorTokens, IBorderTokens { }
|
||||
export interface ContextualMenuItemTokens extends FontTokens, IForegroundColorTokens, IBackgroundColorTokens, IBorderTokens {
|
||||
/**
|
||||
* The icon color.
|
||||
*/
|
||||
iconColor?: string;
|
||||
|
||||
/**
|
||||
* The icon color when hovering over the Button.
|
||||
*/
|
||||
iconColorHovered?: string;
|
||||
|
||||
/**
|
||||
* The icon color when the Button is being pressed.
|
||||
*/
|
||||
iconColorPressed?: string;
|
||||
|
||||
/**
|
||||
* The size of the icon.
|
||||
*/
|
||||
iconSize?: number | string;
|
||||
|
||||
/**
|
||||
* The weight of the lines used when drawing the icon.
|
||||
*/
|
||||
iconWeight?: number;
|
||||
}
|
||||
|
||||
export interface ContextualMenuItemProps extends Omit<IPressableProps, 'onPress'> {
|
||||
/*
|
||||
|
@ -31,10 +57,10 @@ export interface ContextualMenuItemProps extends Omit<IPressableProps, 'onPress'
|
|||
*/
|
||||
text?: string;
|
||||
|
||||
/*
|
||||
* Source URL or name of the icon to show on the ContextualMenuItem.
|
||||
/**
|
||||
* Source URL or name of the icon to show on the Button.
|
||||
*/
|
||||
icon?: string;
|
||||
icon?: number | string | IconProps;
|
||||
/**
|
||||
* A RefObject to access the IContextualMenuItem interface. Use this to access the public methods and properties of the component.
|
||||
*/
|
||||
|
@ -56,7 +82,7 @@ export interface ContextualMenuItemProps extends Omit<IPressableProps, 'onPress'
|
|||
export interface ContextualMenuItemSlotProps {
|
||||
root: React.PropsWithRef<ViewProps>;
|
||||
stack: ViewProps;
|
||||
icon: ImageProps;
|
||||
icon: IconProps;
|
||||
content: ITextProps;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { submenuName, SubmenuType } from './Submenu.types';
|
||||
import { IComposeSettings } from '@uifabricshared/foundation-compose';
|
||||
|
||||
export const settings: IComposeSettings<SubmenuType> = [
|
||||
{
|
||||
tokens: {
|
||||
backgroundColor: 'menuBackground',
|
||||
beakWidth: 20,
|
||||
borderColor: 'buttonBorder',
|
||||
borderWidth: 1,
|
||||
gapSpace: 0,
|
||||
minPadding: 0
|
||||
},
|
||||
root: {
|
||||
accessibilityRole: 'menu',
|
||||
directionalHint: 'rightTopEdge'
|
||||
},
|
||||
container: {
|
||||
style: {
|
||||
padding: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
submenuName
|
||||
];
|
|
@ -0,0 +1,111 @@
|
|||
/** @jsx withSlots */
|
||||
import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import {
|
||||
submenuName,
|
||||
SubmenuProps,
|
||||
SubmenuSlotProps,
|
||||
SubmenuType,
|
||||
SubmenuRenderData,
|
||||
SubmenuState
|
||||
} from './Submenu.types';
|
||||
import { settings } from './Submenu.settings';
|
||||
import { IUseComposeStyling, compose } from '@uifabricshared/foundation-compose';
|
||||
import { useSelectedKey } from '@fluentui-react-native/interactive-hooks';
|
||||
import { mergeSettings } from '@uifabricshared/foundation-settings';
|
||||
import { backgroundColorTokens, borderTokens } from '@fluentui-react-native/tokens';
|
||||
import { Callout } from '@fluentui-react-native/callout';
|
||||
import { ISlots, withSlots } from '@uifabricshared/foundation-composable';
|
||||
import { CMContext } from './ContextualMenu';
|
||||
|
||||
export const Submenu = compose<SubmenuType>({
|
||||
displayName: submenuName,
|
||||
usePrepareProps: (userProps: SubmenuProps, useStyling: IUseComposeStyling<SubmenuType>) => {
|
||||
const {
|
||||
setShowMenu,
|
||||
shouldFocusOnMount = true,
|
||||
shouldFocusOnContainer = true,
|
||||
...rest
|
||||
} = userProps;
|
||||
|
||||
// Grabs the context information from ContextualMenu (onDismissMenu callback)
|
||||
const context = React.useContext(CMContext);
|
||||
|
||||
// This hook updates the Selected Button and calls the customer's onClick function. This gets called after a button is pressed.
|
||||
const data = useSelectedKey(null, userProps.onItemClick);
|
||||
|
||||
const onShow = React.useCallback(
|
||||
() => {
|
||||
userProps ?.onShow();
|
||||
context.isSubmenuOpen = true;
|
||||
}, [context]
|
||||
)
|
||||
const onDismiss = React.useCallback(
|
||||
() => {
|
||||
userProps ?.onDismiss();
|
||||
setShowMenu(false);
|
||||
context.isSubmenuOpen = false;
|
||||
}, [context, setShowMenu]
|
||||
)
|
||||
const dismissCallback = React.useCallback(
|
||||
() => {
|
||||
onDismiss();
|
||||
context ?.onDismissMenu();
|
||||
}, [onDismiss, context]);
|
||||
|
||||
context.dismissSubmenu = onDismiss;
|
||||
|
||||
const [containerFocus, setContainerFocus] = React.useState(true);
|
||||
const toggleContainerFocus = React.useCallback(() => {
|
||||
setContainerFocus(false);
|
||||
}, [setContainerFocus]);
|
||||
|
||||
const state: SubmenuState = {
|
||||
context: {
|
||||
selectedKey: data.selectedKey,
|
||||
onItemClick: data.onKeySelect,
|
||||
onDismissMenu: dismissCallback,
|
||||
}
|
||||
};
|
||||
|
||||
const styleProps = useStyling(userProps, (override: string) => state[override] || userProps[override]);
|
||||
|
||||
const slotProps = mergeSettings<SubmenuSlotProps>(styleProps, {
|
||||
root: {
|
||||
...rest,
|
||||
onShow: onShow,
|
||||
onDismiss: onDismiss,
|
||||
setInitialFocus: shouldFocusOnMount
|
||||
},
|
||||
container: {
|
||||
accessible: shouldFocusOnContainer,
|
||||
focusable: shouldFocusOnContainer && containerFocus,
|
||||
onBlur: toggleContainerFocus,
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return { slotProps, state };
|
||||
},
|
||||
settings: settings,
|
||||
slots: {
|
||||
root: Callout,
|
||||
container: View
|
||||
},
|
||||
styles: {
|
||||
root: [backgroundColorTokens, borderTokens],
|
||||
container: []
|
||||
},
|
||||
render: (Slots: ISlots<SubmenuSlotProps>, renderData: SubmenuRenderData, ...children: React.ReactNode[]) => {
|
||||
if (renderData.state == undefined) {
|
||||
return null;
|
||||
}
|
||||
return<CMContext.Provider value={renderData.state.context}>
|
||||
<Slots.root>
|
||||
<Slots.container>{children}</Slots.container>
|
||||
</Slots.root>
|
||||
</CMContext.Provider>;
|
||||
}
|
||||
});
|
||||
|
||||
export default Submenu;
|
|
@ -0,0 +1,23 @@
|
|||
import { ContextualMenuProps, ContextualMenuTokens, ContextualMenuState } from './ContextualMenu.types';
|
||||
import { ViewProps } from 'react-native';
|
||||
import { IRenderData } from '@uifabricshared/foundation-composable';
|
||||
|
||||
export const submenuName = 'Submenu';
|
||||
|
||||
export type SubmenuState = ContextualMenuState;
|
||||
export type SubmenuTokens = ContextualMenuTokens;
|
||||
export type SubmenuProps = ContextualMenuProps;
|
||||
|
||||
export type SubmenuSlotProps = {
|
||||
root: SubmenuProps;
|
||||
container: ViewProps;
|
||||
};
|
||||
|
||||
export type SubmenuRenderData = IRenderData<SubmenuSlotProps, SubmenuState>;
|
||||
|
||||
export interface SubmenuType {
|
||||
props: SubmenuProps;
|
||||
slotProps: SubmenuSlotProps;
|
||||
tokens: SubmenuTokens;
|
||||
state: SubmenuState;
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import { submenuItemName, SubmenuItemType } from './SubmenuItem.types';
|
||||
import { IComposeSettings } from '@uifabricshared/foundation-compose';
|
||||
|
||||
export const settings: IComposeSettings<SubmenuItemType> = [
|
||||
{
|
||||
tokens: {
|
||||
backgroundColor: 'menuBackground',
|
||||
color: 'menuItemText',
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 1
|
||||
},
|
||||
root: {
|
||||
accessible: true,
|
||||
accessibilityRole: 'menuitem',
|
||||
focusable: true,
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'flex-start',
|
||||
width: '100%'
|
||||
}
|
||||
},
|
||||
content: {},
|
||||
icon: {},
|
||||
stack: {
|
||||
style: {
|
||||
display: 'flex',
|
||||
paddingStart: 16,
|
||||
paddingEnd: 16,
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'flex-start',
|
||||
minHeight: 32,
|
||||
minWidth: 80,
|
||||
justifyContent: 'center'
|
||||
}
|
||||
},
|
||||
_precedence: ['focused', 'hovered', 'pressed', 'disabled'],
|
||||
_overrides: {
|
||||
disabled: {
|
||||
tokens: {
|
||||
backgroundColor: 'menuBackground',
|
||||
color: 'disabledText',
|
||||
}
|
||||
},
|
||||
pressed: {
|
||||
tokens: {
|
||||
backgroundColor: 'menuItemBackgroundHovered',
|
||||
color: 'menuItemTextHovered',
|
||||
}
|
||||
},
|
||||
focused: {
|
||||
tokens: {
|
||||
color: 'menuItemTextHovered',
|
||||
backgroundColor: 'menuItemBackgroundHovered',
|
||||
},
|
||||
_overrides: {
|
||||
disabled: {
|
||||
tokens: {
|
||||
borderColor: 'focusBorder'
|
||||
}
|
||||
},
|
||||
hovered: {
|
||||
_overrides: {
|
||||
disabled: {
|
||||
tokens: {
|
||||
borderColor: 'transparent'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
submenuItemName
|
||||
];
|
|
@ -0,0 +1,119 @@
|
|||
/** @jsx withSlots */
|
||||
import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import {
|
||||
SubmenuItemSlotProps,
|
||||
SubmenuItemState,
|
||||
SubmenuItemProps,
|
||||
SubmenuItemRenderData,
|
||||
submenuItemName,
|
||||
SubmenuItemType,
|
||||
} from './SubmenuItem.types';
|
||||
import { compose, IUseComposeStyling } from '@uifabricshared/foundation-compose';
|
||||
import { ISlots, withSlots } from '@uifabricshared/foundation-composable';
|
||||
import { Text } from '@fluentui-react-native/text';
|
||||
import { settings } from './SubmenuItem.settings';
|
||||
import { backgroundColorTokens, borderTokens, textTokens, foregroundColorTokens, getPaletteFromTheme } from '@fluentui-react-native/tokens';
|
||||
import { mergeSettings } from '@uifabricshared/foundation-settings';
|
||||
import { useAsPressable, useKeyCallback, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks';
|
||||
import { CMContext } from './ContextualMenu';
|
||||
import { Icon } from '@fluentui-react-native/icon';
|
||||
import { createIconProps } from '@fluentui-react-native/interactive-hooks';
|
||||
|
||||
export const SubmenuItem = compose<SubmenuItemType>({
|
||||
displayName: submenuItemName,
|
||||
usePrepareProps: (userProps: SubmenuItemProps, useStyling: IUseComposeStyling<SubmenuItemType>) => {
|
||||
const {
|
||||
disabled,
|
||||
itemKey,
|
||||
icon,
|
||||
text,
|
||||
accessibilityLabel = userProps.text,
|
||||
onClick,
|
||||
testID,
|
||||
componentRef = React.useRef(null),
|
||||
...rest
|
||||
} = userProps;
|
||||
|
||||
// Grabs the context information from Submenu (currently selected menuItem and client's onItemClick callback)
|
||||
const context = React.useContext(CMContext);
|
||||
|
||||
const onItemClick = React.useCallback(
|
||||
(e) => {
|
||||
if (!disabled ) {
|
||||
context ?.onDismissMenu();
|
||||
onClick ? onClick() : context.onItemClick(itemKey);
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
[context, disabled, itemKey, onClick],
|
||||
);
|
||||
|
||||
const cmRef = useViewCommandFocus(componentRef);
|
||||
|
||||
const onItemHoverIn = React.useCallback(
|
||||
(e) => {
|
||||
componentRef.current.focus();
|
||||
userProps.onHoverIn(e);
|
||||
}, [componentRef]);
|
||||
|
||||
const pressable = useAsPressable({ ...rest, onPress: onItemClick, onHoverIn: onItemHoverIn});
|
||||
|
||||
// set up state
|
||||
const state: SubmenuItemState = {
|
||||
...pressable.state,
|
||||
selected: context.selectedKey === userProps.itemKey,
|
||||
disabled: userProps.disabled,
|
||||
content: !!text,
|
||||
icon: !!icon,
|
||||
};
|
||||
|
||||
/*
|
||||
* For SubmenuItem, menu is shown on hover.
|
||||
*/
|
||||
const onKeyUp = useKeyCallback(onItemHoverIn, ' ', 'Enter', 'ArrowRight');
|
||||
|
||||
// grab the styling information, referencing the state as well as the props
|
||||
const styleProps = useStyling(userProps, (override: string) => state[override] || userProps[override]);
|
||||
// create the merged slot props
|
||||
const slotProps = mergeSettings<SubmenuItemSlotProps>(styleProps, {
|
||||
root: {
|
||||
...pressable.props,
|
||||
ref: cmRef,
|
||||
onKeyUp: onKeyUp,
|
||||
accessibilityLabel: accessibilityLabel
|
||||
},
|
||||
content: { children: text, testID },
|
||||
icon: createIconProps(icon),
|
||||
});
|
||||
|
||||
return { slotProps, state };
|
||||
},
|
||||
settings,
|
||||
render: (Slots: ISlots<SubmenuItemSlotProps>, renderData: SubmenuItemRenderData, ...children: React.ReactNode[]) => {
|
||||
// We shouldn't have to specify the source prop on Slots.icon, here, but we need another drop from @uifabricshared
|
||||
return (
|
||||
<Slots.root>
|
||||
<Slots.stack>
|
||||
{renderData!.state.icon && <Slots.icon />}
|
||||
{renderData!.state.content && <Slots.content />}
|
||||
{children}
|
||||
</Slots.stack>
|
||||
</Slots.root>
|
||||
);
|
||||
},
|
||||
slots: {
|
||||
root: View,
|
||||
stack: { slotType: View },
|
||||
icon: { slotType: Icon as React.ComponentType<object> },
|
||||
content: Text,
|
||||
},
|
||||
styles: {
|
||||
root: [backgroundColorTokens, borderTokens],
|
||||
stack: [],
|
||||
icon: [{ source: 'iconColor', lookup: getPaletteFromTheme, target: 'color' }],
|
||||
content: [textTokens, foregroundColorTokens],
|
||||
},
|
||||
});
|
||||
|
||||
export default SubmenuItem;
|
|
@ -0,0 +1,28 @@
|
|||
import * as React from 'react';
|
||||
import { ViewProps } from 'react-native';
|
||||
import { IRenderData } from '@uifabricshared/foundation-composable';
|
||||
import { ITextProps } from '@fluentui-react-native/text';
|
||||
import { ContextualMenuItemProps, ContextualMenuItemTokens, ContextualMenuItemState } from './ContextualMenuItem.types';
|
||||
import { IconProps } from '@fluentui-react-native/icon';
|
||||
|
||||
export const submenuItemName = 'submenuItem';
|
||||
export type SubmenuItemTokens = ContextualMenuItemTokens;
|
||||
export type SubmenuItemProps = ContextualMenuItemProps;
|
||||
export type SubmenuItemState = ContextualMenuItemState;
|
||||
|
||||
export interface SubmenuItemSlotProps {
|
||||
root: React.PropsWithRef<ViewProps>;
|
||||
stack: ViewProps;
|
||||
icon: IconProps;
|
||||
content: ITextProps;
|
||||
dropArrow: IconProps;
|
||||
}
|
||||
|
||||
export type SubmenuItemRenderData = IRenderData<SubmenuItemSlotProps, SubmenuItemState>;
|
||||
|
||||
export interface SubmenuItemType {
|
||||
props: SubmenuItemProps;
|
||||
tokens: SubmenuItemTokens;
|
||||
slotProps: SubmenuItemSlotProps;
|
||||
state: SubmenuItemState;
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
export * from './ContextualMenu.types';
|
||||
export * from './ContextualMenu';
|
||||
export * from './ContextualMenuItem.types';
|
||||
export * from './ContextualMenuItem';
|
||||
export * from './ContextualMenuItem';
|
||||
export * from './Submenu.types';
|
||||
export * from './Submenu';
|
||||
export * from './SubmenuItem.types';
|
||||
export * from './SubmenuItem';
|
|
@ -29,6 +29,7 @@
|
|||
"@types/react-native": "^0.63.0",
|
||||
"@uifabricshared/build-native": "^0.1.1",
|
||||
"@uifabricshared/eslint-config-rules": "^0.1.1",
|
||||
"@fluentui-react-native/icon": "0.5.1",
|
||||
"react": "16.13.1",
|
||||
"react-native": "^0.63.4"
|
||||
},
|
||||
|
|
|
@ -3,6 +3,7 @@ export * from './useAsPressable';
|
|||
export * from './usePressability';
|
||||
export * from './useViewCommandFocus';
|
||||
export * from './useSelectedKey.hooks';
|
||||
export * from './useIconProps.hooks';
|
||||
export * from './useAsToggle';
|
||||
export * from './Pressability/Pressability.types';
|
||||
export * from './Pressability/InternalTypes';
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { Image } from 'react-native';
|
||||
import { RasterImageIconProps, IconProps } from '@fluentui-react-native/icon';
|
||||
|
||||
// this hook creates icon props from given source
|
||||
export function createIconProps(src: number | string | IconProps) {
|
||||
if (src === undefined) return null;
|
||||
|
||||
if (typeof src === 'number') {
|
||||
const rasterProps: RasterImageIconProps = { src: src };
|
||||
const asset = Image.resolveAssetSource(+src);
|
||||
|
||||
return {
|
||||
rasterImageSource: rasterProps,
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
};
|
||||
}
|
||||
else if (typeof src === 'string') {
|
||||
const rasterProps: RasterImageIconProps = { src: { uri: src as string } };
|
||||
return { rasterImageSource: rasterProps };
|
||||
}
|
||||
else {
|
||||
return src as IconProps;
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче