Skip to content

Commit

Permalink
feat(TreeSelect): add tree select component
Browse files Browse the repository at this point in the history
  • Loading branch information
ej9x committed Mar 15, 2019
1 parent 3fc338b commit 00a5f4e
Show file tree
Hide file tree
Showing 25 changed files with 747 additions and 12 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions e2e/__tests__/tree-select-field.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { baisy } from '../setup/TestSuiter';


const SUITES = [
baisy.suite('Components/TreeSelectField', 'common'),
];


SUITES.map(suite => {
it(suite.getTestName(), suite.testStory, 20000);
});

73 changes: 73 additions & 0 deletions e2e/__tests__/tree-select.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { baisy } from '../setup/TestSuiter';


const expandAllTree = async(iframe) => {
const selectTrigger = await iframe.waitForXPath('(//*[contains(@class,"dropdown-trigger")])[2]');
await selectTrigger.click();

const firstToggle = await iframe.waitForXPath('//*[contains(@class,"dropdown-content")]//*[contains(@class,"toggle collapsed")]');
await firstToggle.click();

const secondToggle = await iframe.waitForXPath('//*[contains(@class,"dropdown-content")]//*[contains(@class,"toggle collapsed")]');
await secondToggle.click();
};


const SUITES = [
baisy.suite('Components/TreeSelect', 'common'),
baisy.suite('Components/TreeSelect', 'common', 'open')
.addRootHeight(500)
.setEnhancer(async (iframe) => {
const selectTrigger = await iframe.waitForXPath('(//*[contains(@class,"dropdown-trigger")])[2]');

await selectTrigger.click();

const firstToggle = await iframe.waitForXPath('//*[contains(@class,"dropdown-content")]//*[contains(@class,"toggle collapsed")]');
await firstToggle.click();
}),

baisy.suite('Components/TreeSelect', 'common', 'select item')
.addRootHeight(500)
.setEnhancer(async (iframe) => {
await expandAllTree(iframe);

const lastCheckbox = await iframe.waitForXPath('//*[contains(@class,"dropdown-content")]//*[contains(text(),"Search me too 5")]');
await lastCheckbox.click();
}),

baisy.suite('Components/TreeSelect', 'common', 'select all')
.addRootHeight(500)
.setEnhancer(async (iframe) => {
await expandAllTree(iframe);

const firstCheckbox = await iframe.waitForXPath('//*[contains(@class,"dropdown-content")]//*[contains(text(),"Search me")]');
await firstCheckbox.click();
}),

baisy.suite('Components/TreeSelect', 'common', 'unselect item')
.addRootHeight(500)
.setEnhancer(async (iframe) => {
await expandAllTree(iframe);

const firstCheckbox = await iframe.waitForXPath('//*[contains(@class,"dropdown-content")]//*[contains(text(),"Search me")]');
await firstCheckbox.click();

const innerCheckbox = await iframe.waitForXPath('//*[contains(@class,"dropdown-content")]//*[contains(text(),"No one can get me")]');
await innerCheckbox.click();
}),

baisy.suite('Components/TreeSelect', 'common', 'remove item')
.addRootHeight(500)
.setEnhancer(async (iframe) => {
await expandAllTree(iframe);

const removeButton = await iframe.waitForXPath('//*[contains(@class,"tag-item")]//button');
await removeButton.click();
}),
];


SUITES.map(suite => {
it(suite.getTestName(), suite.testStory, 20000);
});

12 changes: 12 additions & 0 deletions e2e/setup/TestSuiter.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ class TestSuiter {
return this;
}

setRootWidth = (width: number) => {
this.enhancers.push(
async () => {
await this.iframe.evaluate((root, width) => {
root.style.width = `${width}px`;
}, this.root, width);
},
);

return this;
}

addRootHeight = (height: number) => {
this.enhancers.push(
async () => {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"luxon": "^1.7.1",
"prop-types": "^15.6.1",
"react-datepicker": "^2.0.0",
"react-dropdown-tree-select": "1.13.0",
"react-input-mask": "^2.0.4",
"react-onclickoutside": "^6.7.1",
"react-popper": "^1.3.2",
Expand Down
8 changes: 6 additions & 2 deletions src/EightBaseBoostProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ class EightBaseBoostProvider extends React.Component<EightBaseBoostProviderProps
resetGlobal(this.theme);

Object.keys(this.theme.components).forEach((name) => {
if (this.theme.components[name].globals) {
injectGlobal(this.theme.components[name].globals);
const { globals } = this.theme.components[name];

if (globals) {
typeof globals === 'function'
? injectGlobal(globals(this.theme))
: injectGlobal(globals);
}
});
}
Expand Down
1 change: 1 addition & 0 deletions src/components/Checkbox/Checkbox.theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const [CheckboxSquareTag, themeSquare] = createThemeTag(`${name}Square`, ({ COLO
},
disabled: {
backgroundColor: COLORS.LIGHT_GRAY4,
cursor: 'auto',
},
},
}));
Expand Down
2 changes: 1 addition & 1 deletion src/components/DateInput/DateInput.theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createThemeTag } from '../../theme/createThemeTag';


// eslint-disable-next-line
const [_, theme] = createThemeTag(name, ({ COLORS }: *) => ({
const [_, theme] = createThemeTag('dateInput', ({ COLORS }: *) => ({
globals: `
.react-datepicker {
border: 1px solid ${COLORS.LIGHT_GRAY1};
Expand Down
149 changes: 149 additions & 0 deletions src/components/TreeSelect/TreeSelect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// @flow
import React from 'react';
import fp from 'lodash/fp';
import DropdownTreeSelect from 'react-dropdown-tree-select';

import { TreeSelectWrapperTag, WRAPPER_CLASS_NAME } from './TreeSelect.theme';

type TreeData = {
label: string,
value: string,
checked?: boolean,
children: Array<TreeData>,
}

type TreeNode = {
label: string,
value: string,
checked?: boolean,
_id?: string,
}


type ToggledNode = {
label: string,
value: string,
expanded: boolean,
_id: string,
}

type TreeSelectProps = {
data: TreeData,
value: TreeNode[],
onChange: (currentNode: TreeNode, selectedNodes: TreeNode[]) => void,
onNodeToggle?: (ToggledNode) => void,
stretch: boolean,
placeholder?: string,
}


const getDeepKeys = (obj: Object) => {
let keys = [];
for (const key in obj) {
keys.push(key);
if (typeof obj[key] === 'object') {
const subkeys = getDeepKeys(obj[key]);
keys = keys.concat(subkeys.map((subkey) => {
return `${key}.${subkey}`;
}));
}
}
return keys;
};


const getNodePath = (treeData: TreeData, value: string): null | string[] => {
const treePaths = getDeepKeys(treeData).map(key => key.split('.'));

let nodePath = null;

[[], ...treePaths].forEach(path => {
const node = fp.equals(path, [])
? treeData
: fp.get(path, treeData);

if (node.value === value) {
nodePath = path;
}
});

return nodePath;
};

class TreeSelect extends React.PureComponent<TreeSelectProps> {
toggledNodes: { [string]: ToggledNode } = {};

static defaultProps = {
stretch: true,
value: [],
placeholder: 'Select...',
}


getSelectedData = fp.memoize(
({ value, toggledNodes, data }: *) => {

const selectedData = value
.reduce(
(accum, node: TreeNode) => {
const nodePath = getNodePath(data, node.value);

if (nodePath === null) return accum;

return fp.pipe(
fp.set([...nodePath, 'checked'], true),
)(accum);
},
data,
);

const expandedData = Object.keys(toggledNodes).reduce(
(accum, key: string) => {
const node = toggledNodes[key];
const nodePath = getNodePath(data, node.value);

if (nodePath === null) return accum;
return fp.set([...nodePath, 'expanded'], node.expanded, accum);
},
selectedData,
);

return expandedData;
},
)

onNodeToggle = (currentNode: ToggledNode) => {
const { onNodeToggle } = this.props;

onNodeToggle && onNodeToggle(currentNode);

this.toggledNodes = {
...this.toggledNodes,
[currentNode._id]: currentNode,
};
}

render () {
const { stretch, value, data, placeholder, ...rest } = this.props;

const selectedData = this.getSelectedData({ value, data, toggledNodes: this.toggledNodes });

return (
<TreeSelectWrapperTag tagName="div" stretch={ stretch }>
<DropdownTreeSelect
{ ...rest }
data={ selectedData }
className={ WRAPPER_CLASS_NAME }
onNodeToggle={ this.onNodeToggle }
placeholderText={ placeholder }
/>
</TreeSelectWrapperTag>
);
}
}


export {
TreeSelect,
};

84 changes: 84 additions & 0 deletions src/components/TreeSelect/TreeSelect.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react';

const OPTIONS = {
label: 'Search me',
value: 'searchme',
children: [
{
label: 'Search me too',
value: 'searchmetoo',
children: [
{
label: 'No one can get me',
value: 'anonymous',
},
],
},
{
label: 'Search me too 2',
value: 'searchmetoo2',
},
{
label: 'Search me too 3',
value: 'searchmetoo3',
},
{
label: 'Search me too 4',
value: 'searchmetoo4',
},
{
label: 'Search me too 5',
value: 'searchmetoo5',
},
],
};


const LONG_OPTIONS = [{
label: 'Geoglossaceae nontransportation laemodipodiform gluttonously spaework ankylorrhinia zain carlet ironheartedness geoglossaceae nontransportation laemodipodiform gluttonously spaework ankylorrhinia zain carlet ironheartedness topia antiorthodox cerebropedal Sothis whispered basilica idealizer outvalue thwacking unafraid coining nak friskily renishly stringsman',
value: 'ovenlike',
}, {
label: 'backhander unpersecuted platch antisymmetrical fumaroid chromitite Microthelyphonida epigraphically myope supramechanical pageant ankle camphory nitronaphthalene thieve umquhile mornings gynomonoecism unvulgarize rickmatic saltless sternoglossal pungi pronumber',
value: 'serjeant',
}, {
label: 'juju tattlery nonperpetual nonexternal vocabularied umber lichenological repressure unpoled blepharosynechia peragration reduplicature acarid citizenism nongelatinizing splenoptosia unpoisoned tympanic tachogram unhardness dovetail transonic cuinage tributariness',
value: 'wiseheartedly',
}];


export default (asStory) => {
asStory('Components/TreeSelect', module, (story, { Column, StateContainer, TreeSelect }) => {
story
.add('common', () => (
<Column>
<StateContainer value={ [] }>
{ ({ value, onChange }) => (
<TreeSelect
value={ value }
data={ OPTIONS }
onChange={ (_, selectedNodes) => onChange(selectedNodes) }
/>
) }
</StateContainer>
<StateContainer value={ [OPTIONS.children[1], OPTIONS.children[2], OPTIONS.children[0].children[0]] }>
{ ({ value, onChange }) => (
<TreeSelect
value={ value }
data={ OPTIONS }
onChange={ (_, selectedNodes) => onChange(selectedNodes) }
/>
) }
</StateContainer>
<StateContainer value={ [LONG_OPTIONS[0], LONG_OPTIONS[2]] }>
{ ({ value, onChange }) => (
<TreeSelect
value={ value }
data={ LONG_OPTIONS }
onChange={ (_, selectedNodes) => onChange(selectedNodes) }
/>
) }
</StateContainer>
</Column>
));
});
};
Loading

0 comments on commit 00a5f4e

Please sign in to comment.