Skip to content

Commit

Permalink
feat(tree-view): make list with show/hide
Browse files Browse the repository at this point in the history
  • Loading branch information
acoopa committed Jun 23, 2022
1 parent 8e137b5 commit 520a735
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 26 deletions.
94 changes: 76 additions & 18 deletions packages/react-magma-dom/src/components/TreeView/TreeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,65 +4,123 @@ import styled from '../../theme/styled';
import { ThemeContext } from '../../theme/ThemeContext';
import { ThemeInterface } from '../../theme/magma';
import { InverseContext, useIsInverse } from '../../inverse';
import { Transition } from '../Transition';

import { IconProps } from 'react-magma-icons';
import { ExpandLessIcon, ExpandMoreIcon, IconProps } from 'react-magma-icons';

/**
* @children required
*/
export interface TreeItemProps extends React.HTMLAttributes<HTMLDivElement>{
export interface TreeItemProps extends React.HTMLAttributes<HTMLLIElement>{
testId?: string;
isInverse?: boolean;
/**
* @internal
*/
theme?: ThemeInterface;
icon?: React.ReactElement<IconProps>;
hasOwnTreeItems?: boolean;
}

const StyledTreeItem = styled.div<TreeItemProps>`
const addPxStyleStrings = (
styleStrings: (string | number)[]
): string => {
const pxValues: number[] = styleStrings.map(styleString => {
return parseInt(styleString.toString().replace(/\s*px$/,''));
});
return pxValues.reduce((total, value) => total + value).toString()+'px';
}

const StyledTreeItem = styled.li<TreeItemProps>`
background: ${props =>
props.isInverse
? props.theme.colors.primary600
: props.theme.colors.neutral100};
props.isInverse
? props.theme.colors.primary600
: props.theme.colors.neutral100};
color: ${props =>
props.isInverse
? props.theme.colors.neutral100
: props.theme.colors.neutral};
props.isInverse
? props.theme.colors.neutral100
: props.theme.colors.neutral};
list-style-type: none;
margin-left: ${props =>
props.hasOwnTreeItems
? '0px'
: addPxStyleStrings([
props.theme.spaceScale.spacing05,
props.theme.iconSizes.medium
])};
`;

const IconWrapper = styled.span<{ isInverse?: boolean }>`
const IconWrapper = styled.span<{ noIconButton?: boolean; isInverse?: boolean }>`
color: ${props =>
props.isInverse
? props.theme.colors.neutral100
: props.theme.colors.neutral500};
display: inline-flex;
margin-right: ${props => props.theme.spaceScale.spacing03};
margin-left: ${props =>
props.noIconButton
? addPxStyleStrings([
props.theme.spaceScale.spacing05,
props.theme.iconSizes.medium])
: props.theme.spaceScale.spacing05};
svg {
height: ${props => props.theme.iconSizes.medium}px;
width: ${props => props.theme.iconSizes.medium}px;
vertical-align: middle;
}
`;

export const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
const StyledButton = styled.button<{ theme?: ThemeInterface; isInverse?: boolean; }>`
border: none;
color: inherit;
background: inherit;
padding: 0;
margin-right: ${props => props.theme.spaceScale.spacing05};
vertical-align: middle;
`;

export const TreeItem = React.forwardRef<HTMLLIElement, TreeItemProps>(
(props, ref) => {
const {children, testId, isInverse: isInverseProp, icon, ...rest} = props;
const theme = React.useContext(ThemeContext);
const isInverse = useIsInverse(isInverseProp);

const [expanded, setExpanded] = React.useState(true);

const hasOwnTreeItems = React.Children.toArray(children).filter((child: React.ReactElement<any>) => child.type === TreeItem).length > 0;

return (<InverseContext.Provider value={{ isInverse, }}>
<StyledTreeItem
theme={theme}
isInverse={isInverse}
ref={ref}
data-testid={props.testId}
{...rest} >
<IconWrapper isInverse={isInverse} theme={theme}>
{icon}
hasOwnTreeItems={hasOwnTreeItems}
{...rest}
>
{hasOwnTreeItems &&
<StyledButton isInverse={isInverse} theme={theme} onClick={() => setExpanded(state => !state)}>
{
expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />
}
</StyledButton>
}
{icon &&
<IconWrapper isInverse={isInverse} theme={theme} noIconButton={hasOwnTreeItems}>
{icon}
</IconWrapper>
{children}
}
{
React.Children.map(children, (child: React.ReactElement<any>, index) => {
return (child.type === TreeItem) ? (
<Transition isOpen={expanded} collapse>
<ul>
{child}
</ul>
</Transition>
) : child;
})
}
</StyledTreeItem>
</InverseContext.Provider>);
}
)
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,40 @@ export default {
export const Default = (args) => {
return (
<TreeView {...args}>
<TreeItem icon={<FolderIcon />} {...args}>Sample text</TreeItem>
<TreeItem {...args}>Home
<TreeItem {...args}>Bath
<TreeItem {...args}>Bathroom Storage
<TreeItem {...args}>Item 1</TreeItem>
<TreeItem {...args}>Item 2</TreeItem>
</TreeItem>
<TreeItem {...args}>Shower Curtains & Accessories
</TreeItem>
<TreeItem {...args}>Bath Towels
<TreeItem {...args}>Item 1</TreeItem>
<TreeItem {...args}>Item 2</TreeItem>
</TreeItem>
</TreeItem>
<TreeItem {...args}>Bedding
<TreeItem {...args}>Item 1</TreeItem>
<TreeItem {...args}>Item 2</TreeItem>
</TreeItem>
<TreeItem {...args}>Arts & Crafts
</TreeItem>
<TreeItem {...args}>Storage & Organization
<TreeItem {...args}>Item 1</TreeItem>
<TreeItem {...args}>Item 2</TreeItem>
</TreeItem>
</TreeItem>
<TreeItem {...args}>Furniture
<TreeItem {...args}>Item 1</TreeItem>
<TreeItem {...args}>Item 2</TreeItem>
</TreeItem>
<TreeItem {...args}>Kitchen & Dining
</TreeItem>
<TreeItem {...args}>Patio & Garden
<TreeItem {...args}>Item 1</TreeItem>
<TreeItem {...args}>Item 2</TreeItem>
</TreeItem>
</TreeView>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react';
import { axe } from '../../../axe-helper';
import { TreeView } from '.';
import { TreeView, TreeItem } from '.';
import { render } from '@testing-library/react';

const TEXT = 'Test Text';

describe('TreeView', () => {
it('should render the visually hidden component', () => {
const { container, getByText } = render(
<TreeView>{TEXT}</TreeView>
<TreeView><TreeItem>{TEXT}</TreeItem></TreeView>
);

expect(getByText(TEXT)).toBeInTheDocument();
Expand All @@ -17,14 +17,14 @@ describe('TreeView', () => {
it('should find element by testId', () => {
const testId = 'test-id';
const { getByTestId } = render(
<TreeView testId={testId}>{TEXT}</TreeView>
<TreeView testId={testId}><TreeItem>{TEXT}</TreeItem></TreeView>
);

expect(getByTestId(testId)).toBeInTheDocument();
});

it('Does not violate accessibility standards', () => {
const { container } = render(<TreeView>{TEXT}</TreeView>);
const { container } = render(<TreeView><TreeItem>{TEXT}</TreeItem></TreeView>);

return axe(container.innerHTML).then(result => {
return expect(result).toHaveNoViolations();
Expand Down
10 changes: 7 additions & 3 deletions packages/react-magma-dom/src/components/TreeView/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { InverseContext, useIsInverse } from '../../inverse';
/**
* @children required
*/
export interface TreeViewProps extends React.HTMLAttributes<HTMLDivElement>{
export interface TreeViewProps extends React.HTMLAttributes<HTMLUListElement>{
testId?: string;
isInverse?: boolean;
/**
Expand All @@ -17,7 +17,7 @@ export interface TreeViewProps extends React.HTMLAttributes<HTMLDivElement>{
theme?: ThemeInterface;
}

const StyledTreeView = styled.div<TreeViewProps>`
const StyledTreeView = styled.ul<TreeViewProps>`
background: ${props =>
props.isInverse
? props.theme.colors.primary600
Expand All @@ -26,9 +26,13 @@ const StyledTreeView = styled.div<TreeViewProps>`
props.isInverse
? props.theme.colors.neutral100
: props.theme.colors.neutral};
ul {
padding: 0px;
margin-left: ${props => props.theme.spaceScale.spacing06};
}
`;

export const TreeView = React.forwardRef<HTMLDivElement, TreeViewProps>(
export const TreeView = React.forwardRef<HTMLUListElement, TreeViewProps>(
(props, ref) => {
const {children, testId, isInverse: isInverseProp, ...rest} = props;
const theme = React.useContext(ThemeContext);
Expand Down

0 comments on commit 520a735

Please sign in to comment.