+ Tabbar Component
This commit is contained in:
parent
58c0038f84
commit
26b9de9051
|
@ -0,0 +1,28 @@
|
|||
import {
|
||||
TabProps as BaseTabProps,
|
||||
TabsManagedClasses as BaseTabsManagedClasses,
|
||||
} from "@microsoft/fast-components-react-base";
|
||||
import { Subtract } from "utility-types";
|
||||
import { IconType } from "react-icons/lib";
|
||||
import { TabClassNameContract as BaseTabClassNameContract } from "@microsoft/fast-components-class-name-contracts-base";
|
||||
import { ManagedClasses } from "@microsoft/fast-jss-manager-react";
|
||||
|
||||
/**
|
||||
* Class name contract for the component
|
||||
*/
|
||||
export interface TabClassNameContract extends BaseTabClassNameContract {
|
||||
tab_title: string;
|
||||
tab_icon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the component
|
||||
*/
|
||||
export interface TabProps
|
||||
extends ManagedClasses<TabClassNameContract>,
|
||||
Subtract<BaseTabProps, BaseTabsManagedClasses> {
|
||||
/**
|
||||
* Custom icon prepended to the tab title
|
||||
*/
|
||||
icon?: IconType;
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import React from "react";
|
||||
import {
|
||||
DesignSystem,
|
||||
neutralForegroundRest,
|
||||
} from "@microsoft/fast-components-styles-msft";
|
||||
import manageJss, { ComponentStyles } from "@microsoft/fast-jss-manager-react";
|
||||
import { Tab as BaseTab } from "@microsoft/fast-components-react-base";
|
||||
import { TabProps, TabClassNameContract } from "./Tab.props";
|
||||
|
||||
const styles: ComponentStyles<TabClassNameContract, DesignSystem> = {
|
||||
tab: {
|
||||
color: neutralForegroundRest,
|
||||
margin: "0px 10px",
|
||||
padding: "5px 0 3px",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
tab__active: {
|
||||
cursor: "default",
|
||||
},
|
||||
tab_title: {},
|
||||
tab_icon: {
|
||||
marginRight: "5px",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom tab element containing an optional icon prop
|
||||
*/
|
||||
const Tab: React.ComponentType<TabProps> = ({
|
||||
icon: Icon,
|
||||
children,
|
||||
managedClasses: { tab_title, tab_icon, ...managedClasses },
|
||||
...props
|
||||
}) => (
|
||||
<BaseTab {...props} managedClasses={managedClasses}>
|
||||
{Icon && <Icon className={tab_icon} tabIndex={-1} />}
|
||||
<span className={tab_title} tabIndex={-1}>
|
||||
{children}
|
||||
</span>
|
||||
</BaseTab>
|
||||
);
|
||||
|
||||
export default manageJss(styles)(Tab);
|
|
@ -0,0 +1,54 @@
|
|||
import { IconType } from "react-icons/lib";
|
||||
import { ManagedClasses } from "@microsoft/fast-jss-manager-react";
|
||||
import { Orientation } from "@microsoft/fast-web-utilities";
|
||||
|
||||
/**
|
||||
* Class name contract for the component
|
||||
*/
|
||||
export interface TabBarClassNameContract {
|
||||
tabBar: string;
|
||||
tab?: string;
|
||||
|
||||
/**
|
||||
* Indicator for the currently selected item
|
||||
*/
|
||||
tabBar_indicator: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Object containing all the necessary data for a tab to be displayed
|
||||
*/
|
||||
export interface TabItem {
|
||||
/**
|
||||
* Common identifier of the tab.
|
||||
* This is also used to determine the corresponding tab panel.
|
||||
*/
|
||||
id: string;
|
||||
title: React.ReactText | React.ReactNode;
|
||||
icon: IconType; //| React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the component
|
||||
*/
|
||||
export interface TabBarProps extends ManagedClasses<TabBarClassNameContract> {
|
||||
/**
|
||||
* Unique identifier used to establish connection between tabbar and tab-listeners
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The list of tabs on the tabbar
|
||||
*/
|
||||
items: TabItem[];
|
||||
|
||||
/**
|
||||
* The aria-label applied to the tabbar
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* The orientation for the tabbar
|
||||
*/
|
||||
orientation?: Orientation;
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { TabBarProps, TabBarClassNameContract, TabItem } from "./TabBar.props";
|
||||
import {
|
||||
DesignSystem,
|
||||
accentFillActive,
|
||||
accentFillHover,
|
||||
neutralFillHover,
|
||||
neutralFillActive,
|
||||
} from "@microsoft/fast-components-styles-msft";
|
||||
import manageJss, { ComponentStyles } from "@microsoft/fast-jss-manager-react";
|
||||
import Tab from "../Tab/Tab";
|
||||
import { TabsSlot, TabLocation } from "@microsoft/fast-components-react-base";
|
||||
import {
|
||||
keyCodeArrowLeft,
|
||||
Orientation,
|
||||
keyCodeArrowRight,
|
||||
keyCodeArrowUp,
|
||||
keyCodeArrowDown,
|
||||
keyCodeHome,
|
||||
keyCodeEnd,
|
||||
} from "@microsoft/fast-web-utilities";
|
||||
import tabEventEmitter from "../TabEvents";
|
||||
import { TabClassNameContract } from "../Tab/Tab.props";
|
||||
import { parseColorHexRGBA } from "@microsoft/fast-colors";
|
||||
|
||||
const styles: ComponentStyles<TabBarClassNameContract, DesignSystem> = {
|
||||
tabBar: {
|
||||
display: "inline-flex",
|
||||
position: "relative",
|
||||
paddingBottom: "3px",
|
||||
"&:not(.focus-visible), & *": {
|
||||
outline: "none",
|
||||
},
|
||||
},
|
||||
tabBar_indicator: {
|
||||
background: accentFillActive,
|
||||
position: "absolute",
|
||||
bottom: "0",
|
||||
left: "0",
|
||||
width: "1px",
|
||||
height: "3px",
|
||||
transition: "all .2s",
|
||||
transformOrigin: "left",
|
||||
},
|
||||
};
|
||||
|
||||
const tabStyles: ComponentStyles<TabClassNameContract, DesignSystem> = {
|
||||
tab_title: {},
|
||||
tab: {
|
||||
position: "relative",
|
||||
"&::after": {
|
||||
content: "''",
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "3px",
|
||||
opacity: "0",
|
||||
background: neutralFillHover,
|
||||
bottom: "-3px",
|
||||
transition: "opacity .1s",
|
||||
},
|
||||
"&:hover::after": {
|
||||
opacity: "1",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom tabbar which can be connected to a seperate tabpanel.
|
||||
* Some parts of the code were borrowed from FAST's <Tabs/>
|
||||
*/
|
||||
const TabBar: React.ComponentType<TabBarProps> = React.memo(
|
||||
({ managedClasses, id, items, orientation = Orientation.horizontal, ...props }) => {
|
||||
/**
|
||||
* Id of the currently selected tab
|
||||
*/
|
||||
const [activeId, setActiveId] = useState<string>(null);
|
||||
|
||||
/**
|
||||
* Stylesheet for the active tab indicator
|
||||
*/
|
||||
const [activeIndicatorStyles, setAactiveIndicatorStyles] = useState<
|
||||
React.CSSProperties
|
||||
>({});
|
||||
|
||||
/**
|
||||
* Ref of the main tabbar
|
||||
*/
|
||||
const tabBarRef = useRef<HTMLDivElement>();
|
||||
|
||||
/**
|
||||
* Looks up whether the given tab is currently selected or not
|
||||
*/
|
||||
const getActiveState = useCallback(
|
||||
(tabItem: TabItem, index: number): boolean => {
|
||||
if (activeId) return activeId === tabItem.id;
|
||||
return index === 0;
|
||||
},
|
||||
[activeId]
|
||||
);
|
||||
|
||||
/**
|
||||
* Activates a tab and focuses on the new element
|
||||
*/
|
||||
const activateTab = (location: TabLocation): void => {
|
||||
const count = items.length;
|
||||
const currentItemIndex: number = items.findIndex(getActiveState);
|
||||
let itemIndex: number;
|
||||
|
||||
switch (location) {
|
||||
case TabLocation.first:
|
||||
itemIndex = 0;
|
||||
break;
|
||||
case TabLocation.last:
|
||||
itemIndex = count - 1;
|
||||
break;
|
||||
case TabLocation.previous:
|
||||
itemIndex = currentItemIndex > 0 ? currentItemIndex - 1 : count - 1;
|
||||
break;
|
||||
case TabLocation.next:
|
||||
itemIndex = currentItemIndex < count - 1 ? currentItemIndex + 1 : 0;
|
||||
break;
|
||||
}
|
||||
|
||||
const nextActiveId = items[itemIndex].id;
|
||||
setActiveId(nextActiveId);
|
||||
(Array.from(tabBarRef.current.children)[itemIndex] as HTMLButtonElement).focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the click event of a tab element
|
||||
*/
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>): void => {
|
||||
setActiveId(e.currentTarget.getAttribute("aria-controls"));
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the keydown event on a tab element
|
||||
*/
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
const keyCode: number = e.keyCode;
|
||||
|
||||
if (orientation === Orientation.horizontal) {
|
||||
switch (keyCode) {
|
||||
case keyCodeArrowLeft:
|
||||
e.preventDefault();
|
||||
activateTab(TabLocation.previous);
|
||||
break;
|
||||
case keyCodeArrowRight:
|
||||
e.preventDefault();
|
||||
activateTab(TabLocation.next);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (e.keyCode) {
|
||||
case keyCodeArrowUp:
|
||||
e.preventDefault();
|
||||
activateTab(TabLocation.previous);
|
||||
break;
|
||||
case keyCodeArrowDown:
|
||||
e.preventDefault();
|
||||
activateTab(TabLocation.next);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (keyCode) {
|
||||
case keyCodeHome:
|
||||
activateTab(TabLocation.first);
|
||||
break;
|
||||
case keyCodeEnd:
|
||||
activateTab(TabLocation.last);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Emitting events on changes in the currently selected tab,
|
||||
* so that other components can react to the changes. (e.g.: TabPanel)
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!activeId) return;
|
||||
const activeItem = items.find(getActiveState);
|
||||
tabEventEmitter.emit(`${id}-change`, activeItem);
|
||||
|
||||
/**
|
||||
* Sends necessary tabbar data to new subscribers.
|
||||
*/
|
||||
const subHandler = () => tabEventEmitter.emit(`${id}-subPub`, activeItem);
|
||||
tabEventEmitter.on(`${id}-sub`, subHandler);
|
||||
return () => {
|
||||
tabEventEmitter.off(`${id}-sub`, subHandler);
|
||||
};
|
||||
}, [activeId, getActiveState, id, items]);
|
||||
|
||||
const renderTabs = (): JSX.Element[] => {
|
||||
if (items) {
|
||||
return items.map((tabItem: TabItem, index: number) => {
|
||||
const isActive = getActiveState(tabItem, index);
|
||||
return (
|
||||
<Tab
|
||||
slot={TabsSlot.tab}
|
||||
key={tabItem.id}
|
||||
aria-controls={tabItem.id}
|
||||
active={isActive}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
icon={tabItem.icon}
|
||||
jssStyleSheet={tabStyles}
|
||||
>
|
||||
{tabItem.title}
|
||||
</Tab>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (tabBarRef.current && Array.isArray(items)) {
|
||||
const tabBar: HTMLDivElement = tabBarRef.current;
|
||||
const selectedTab: HTMLElement = tabBar.querySelector("[aria-selected='true']");
|
||||
|
||||
// Default values
|
||||
let width: number = 0;
|
||||
let posX: number = 0;
|
||||
|
||||
if (selectedTab !== null) {
|
||||
width = selectedTab.getBoundingClientRect().width;
|
||||
posX =
|
||||
(selectedTab.getBoundingClientRect().left -
|
||||
tabBar.getBoundingClientRect().left) /
|
||||
width;
|
||||
}
|
||||
|
||||
setAactiveIndicatorStyles({
|
||||
transform: `scaleX(${width}) translateX(${posX}px)`,
|
||||
});
|
||||
}
|
||||
}, [items, activeId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tablist"
|
||||
ref={tabBarRef}
|
||||
className={managedClasses.tabBar}
|
||||
aria-label={props.label}
|
||||
aria-orientation={orientation}
|
||||
>
|
||||
{renderTabs()}
|
||||
<div
|
||||
className={managedClasses.tabBar_indicator}
|
||||
tabIndex={-1}
|
||||
style={activeIndicatorStyles}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default manageJss(styles)(TabBar);
|
|
@ -0,0 +1,15 @@
|
|||
import EventEmitter from "onfire.js";
|
||||
|
||||
/**
|
||||
* Cross-component event event manager for tabs
|
||||
*/
|
||||
const tabEventEmitter: EventEmitter = new EventEmitter();
|
||||
|
||||
/*
|
||||
const origOnFunction = tabEventEmitter.on;
|
||||
tabEventEmitter.on = (eventName: string, cb: Function, once?: boolean) => {
|
||||
origOnFunction(eventName, cb, once);
|
||||
}
|
||||
*/
|
||||
|
||||
export default tabEventEmitter;
|
|
@ -16,6 +16,7 @@ import ProgressIcon from "./ProgressIcon/ProgressIcon";
|
|||
import useMotionValueFactory from "./Hooks/useMotionValueFactory";
|
||||
import { useScaleFactor } from "./Hooks/useScaleFactor";
|
||||
import { ToastManager, toast } from "./Toasts/toast";
|
||||
import TabBar from "./Tabs/TabBar/TabBar";
|
||||
|
||||
export {
|
||||
Button,
|
||||
|
@ -37,4 +38,5 @@ export {
|
|||
useScaleFactor,
|
||||
ToastManager,
|
||||
toast,
|
||||
TabBar,
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue