Skip to content

Commit

Permalink
feat(client): handle TabbedLayout fullWidth, elements focus-visible
Browse files Browse the repository at this point in the history
  • Loading branch information
Jozwiaczek committed Mar 20, 2021
1 parent 9f6a9ee commit ca01987
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 76 deletions.
9 changes: 1 addition & 8 deletions packages/client/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,11 @@ export const parameters = {
layout: 'centered',
};

const ViewportContainer = styled.div`
width: 1440px;
height: 900px;
`;

export const decorators = [
(Story) => (
<ThemeProvider theme={getTheme(ThemeType.dark)}>
<GlobalStyles />
<ViewportContainer>
<Story />
</ViewportContainer>
<Story />
</ThemeProvider>
),
];
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const StyledButton = styled.button<ButtonProps>(
}
}
:focus {
:focus-visible {
transition: box-shadow 150ms ease-in-out;
box-shadow: 0 0 0 3px ${
colorVariant === ThemeType.light ? palette.primary.dark : palette.primary.light
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const StyledButton = styled.button<{ color: string }>(
}
}
:focus {
:focus-visible {
background: ${hexToRgba(getCssColor({ color, theme }), 0.1)};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const StyledInput = styled.input`
height: 0;
width: 0;
&:focus ~ ${Checkmark} {
&:focus-visible ~ ${Checkmark} {
box-shadow: 0 0 0 1px ${({ theme }) => theme.palette.primary.light};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import TabbedLayout from '.';
import { TabProps, TabsProps } from './TabbedLayout.types';

const MockRoot = styled.div`
width: 100%;
height: 100%;
width: 1000px;
height: 568px;
border: 2px solid white;
`;

Expand Down Expand Up @@ -69,7 +69,7 @@ const Template: Story<TabsProps> = ({ options }) => {
<TabbedLayout.TabPanel value={value} index={2}>
<p>Settings panel</p>
</TabbedLayout.TabPanel>
<TabbedLayout.TabPanel value={value} index={3}>
<TabbedLayout.TabPanel value={value} index={6}>
<p>Admin panel - accessible only for logged admin users</p>
</TabbedLayout.TabPanel>
</MockRoot>
Expand All @@ -81,6 +81,7 @@ Default.args = {
options: {
tabWidth: 160,
tabIndicatorPosition: 'bottom',
tabIndicatorSize: 160,
tabIndicatorWidth: 160,
variant: 'fullWidth',
},
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import styled from 'styled-components';

import { TabMarkerPosition, TabsIndicatorProps } from './TabbedLayout.types';
import {
TabButtonProps,
TabLabelProps,
TabMarkerPosition,
TabsIndicatorProps,
TabsWrapperProps,
} from './TabbedLayout.types';

export const TabsWrapper = styled.div`
export const TabsWrapper = styled.div<TabsWrapperProps>`
position: relative;
overflow-x: auto;
display: flex;
height: 100%;
width: 100%;
justify-content: space-evenly;
${({ variant }) => {
if (variant === 'fullWidth') {
return 'justify-content: space-evenly;';
}
}}
`;

const getTabsIndicatorPosition = ({ position }: { position: TabMarkerPosition }) => {
Expand All @@ -31,17 +43,17 @@ export const TabsIndicator = styled.span<TabsIndicatorProps>`
border-radius: 12px;
`;

export const TabLabel = styled.p<{ isActive: boolean }>`
export const TabLabel = styled.p<TabLabelProps>`
font-size: 14px;
color: ${({ theme, isActive }) =>
isActive ? theme.palette.text.primary : theme.palette.text.secondary};
transition: color 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
`;

export const TabButton = styled.button<{ width: number; isActive: boolean }>`
export const TabButton = styled.button<TabButtonProps>`
position: relative;
overflow: hidden;
width: ${({ width }) => width}px;
width: ${({ width, variant }) => (variant === 'fullWidth' ? '100%' : `${width}px`)};
height: 100%;
display: flex;
background: transparent;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { MouseEvent, ReactElement, ReactNode } from 'react';

type TabMarkerPosition = 'top' | 'bottom' | 'left' | 'right';
type TabsVariant = 'scrollable' | 'fullWidth' | 'default';

interface TabsOpt {
variant?: TabsVariant;
tabIndicatorPosition?: TabMarkerPosition;
tabIndicatorSize?: number;
tabIndicatorWidth?: number;
tabWidth?: number;
}

Expand All @@ -29,10 +31,25 @@ interface TabProps {
onChange?: (event: MouseEvent, newValue: number) => void;
index?: number;
tabWidth?: number;
variant?: TabsVariant;
}

interface TabPanelProps {
value: number;
index: number;
children: ReactNode;
}

interface TabsWrapperProps {
variant: TabsVariant;
}

interface TabButtonProps {
width: number;
isActive: boolean;
variant: TabsVariant;
}

interface TabLabelProps {
isActive: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Children, isValidElement, ReactNode } from 'react';

import { Role } from '../../../enums/role.enum';
import { User } from '../../../providers/api/CurrentUserProvider/CurrentUserProvider.types';
import { TabsVariant } from './TabbedLayout.types';

export const getIndicatorPosition = (
value: number,
totalChildren: number,
containerWidth: number,
tabWidth: number,
tabIndicatorSize: number,
variant: TabsVariant,
widthOnFullSize: number,
): number => {
console.log('L:32 | tabIndicatorSize: ', tabIndicatorSize); // TODO: handle indicator width diff

const totalChildrenWidth = totalChildren * tabWidth;
const totalEmptyWidth = containerWidth - totalChildrenWidth;
const singleEmptyWidth = totalEmptyWidth / (totalChildren + 1);
const trimmedSingleEmptyWidth = singleEmptyWidth <= 0 ? 0 : singleEmptyWidth;

if (variant === 'fullWidth') {
return value * widthOnFullSize;
}

if (value === 0) {
return trimmedSingleEmptyWidth;
}

const emptyWidthForValue = (value + 1) * trimmedSingleEmptyWidth;
const tabsWidthForValue = value * tabWidth;

return emptyWidthForValue + tabsWidthForValue;
};

export const hasAccess = (onlyAdmin: boolean, user?: User) =>
!(onlyAdmin && !user?.roles.includes(Role.Admin));

export const countAvailableChildren = (children: ReactNode, user?: User) =>
Children.count(
Children.map(children, (child) => {
if (isValidElement(child)) {
const { onlyAdmin } = child.props;
if (hasAccess(onlyAdmin, user)) {
return child;
}
}
}),
);
96 changes: 42 additions & 54 deletions packages/client/src/elements/layouts/TabbedLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ import React, {
cloneElement,
isValidElement,
MouseEvent,
ReactNode,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';

import { Role } from '../../../enums/role.enum';
import { useCurrentUser } from '../../../hooks';
import { User } from '../../../providers/api/CurrentUserProvider/CurrentUserProvider.types';
import { RippleEffect } from '../../animations';
import {
TabButton,
Expand All @@ -22,75 +19,55 @@ import {
TabsWrapper,
} from './TabbedLayout.styled';
import { TabPanelProps, TabProps, TabsProps } from './TabbedLayout.types';

const getIndicatorPosition = (
value: number,
totalChildren: number,
containerWidth: number,
tabWidth: number,
tabIndicatorSize: number,
): number => {
console.log('L:32 | tabIndicatorSize: ', tabIndicatorSize); // TODO: handle indicator width diff

const totalChildrenWidth = totalChildren * tabWidth;
const totalEmptyWidth = containerWidth - totalChildrenWidth;
const singleEmptyWidth = totalEmptyWidth / (totalChildren + 1);
const trimmedSingleEmptyWidth = singleEmptyWidth <= 0 ? 0 : singleEmptyWidth;

if (value === 0) {
return trimmedSingleEmptyWidth;
}

const emptyWidthForValue = (value + 1) * trimmedSingleEmptyWidth;
const tabsWidthForValue = value * tabWidth;

return emptyWidthForValue + tabsWidthForValue;
};

const hasAccess = (onlyAdmin: boolean, user?: User) =>
!(onlyAdmin && !user?.roles.includes(Role.Admin));

const countAvailableChildren = (children: ReactNode, user?: User) =>
Children.count(
Children.map(children, (child) => {
if (isValidElement(child)) {
const { onlyAdmin } = child.props;
if (hasAccess(onlyAdmin, user)) {
return child;
}
}
}),
);
import { countAvailableChildren, getIndicatorPosition, hasAccess } from './TabbedLayout.utils';

const Tabs = ({ children, onChange, value, options = {} }: TabsProps) => {
const tabsWrapperRef = useRef<HTMLDivElement>(null);
const [indicatorLeft, setIndicatorLeft] = useState(0);
const [currentUser] = useCurrentUser();
const { tabIndicatorPosition = 'bottom', tabWidth = 160, tabIndicatorSize = tabWidth } = options;
const [indicatorWidth, setIndicatorWidth] = useState(0);

const {
tabIndicatorPosition = 'bottom',
tabWidth = 160,
tabIndicatorWidth: tabIndicatorWidthProp = tabWidth,
variant = 'default',
} = options;

// TODO: Create tabs items refs for dynamically calculating total children width, instead constant 'tabWidth'.
useLayoutEffect(() => {
const tabbedContainerWidth = tabsWrapperRef.current?.offsetWidth || 0;
const totalsChildren = countAvailableChildren(children, currentUser);
const indicatorWidthOnFullWidth = tabbedContainerWidth / totalsChildren;
setIndicatorLeft(
getIndicatorPosition(value, totalsChildren, tabbedContainerWidth, tabWidth, tabIndicatorSize),
getIndicatorPosition(
value,
totalsChildren,
tabbedContainerWidth,
tabWidth,
tabIndicatorWidthProp,
variant,
indicatorWidthOnFullWidth,
),
);
}, [children, currentUser, tabIndicatorSize, tabWidth, value]);

if (variant === 'fullWidth') {
setIndicatorWidth(indicatorWidthOnFullWidth);
} else {
setIndicatorWidth(tabIndicatorWidthProp);
}
}, [children, currentUser, tabIndicatorWidthProp, tabWidth, value, variant]);

return (
<TabsWrapper ref={tabsWrapperRef}>
<TabsIndicator
left={indicatorLeft}
position={tabIndicatorPosition}
width={tabIndicatorSize}
/>
<TabsWrapper ref={tabsWrapperRef} variant={variant}>
<TabsIndicator left={indicatorLeft} position={tabIndicatorPosition} width={indicatorWidth} />
{Children.map(children, (child, index) => {
if (isValidElement(child)) {
const tabInjectProps: TabProps = {
...child.props,
value,
onChange,
index,
variant,
tabWidth,
};

Expand All @@ -111,19 +88,30 @@ const Tab = ({
index,
onlyAdmin = false,
tabWidth = 160,
variant = 'default',
}: TabProps) => {
const { t } = useTranslation();
const [currentUser] = useCurrentUser();
const itemRef = useRef<HTMLButtonElement>(null);

if (!hasAccess(onlyAdmin, currentUser)) {
return null;
}

const onClick = (event: MouseEvent) => onChange && onChange(event, index as number);
const onClick = (event: MouseEvent) => {
onChange && onChange(event, index as number);
itemRef.current?.scrollIntoView({ behavior: 'smooth', inline: 'end' });
};
const isActive = value === index;

return (
<TabButton onClick={onClick} width={tabWidth} isActive={isActive}>
<TabButton
ref={itemRef}
onClick={onClick}
width={tabWidth}
isActive={isActive}
variant={variant}
>
{icon && icon}
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<TabLabel isActive={isActive}>{t(label as any)}</TabLabel>
Expand Down

0 comments on commit ca01987

Please sign in to comment.