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:
lenahong 2021-05-19 15:53:59 +09:00 коммит произвёл GitHub
Родитель 27c0673e30
Коммит 06aa14bd6c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
28 изменённых файлов: 721 добавлений и 45 удалений

Просмотреть файл

@ -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 clients 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;
}
}