Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Story Hierarchy - keyboard accessibility #1427

Merged
merged 10 commits into from
Jul 14, 2017
2 changes: 1 addition & 1 deletion lib/ui/src/modules/ui/components/layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ class Layout extends React.Component {
showLeftPanel,
() =>
<div style={leftPanelStyle(leftPanelOnTop)}>
<div style={{ flexGrow: 1, height: '100%' }}>{leftPanel()}</div>
<div style={{ flexGrow: 1, height: '100%', width: '100%' }}>{leftPanel()}</div>
<USplit shift={5} split={storiesSplit} />
</div>,
() => <span />
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/src/modules/ui/components/left_panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import TextFilter from './text_filter';
const scrollStyle = {
height: 'calc(100vh - 105px)',
marginTop: 10,
overflowY: 'auto',
overflow: 'auto',
};

const mainStyle = {
Expand Down
13 changes: 11 additions & 2 deletions lib/ui/src/modules/ui/components/left_panel/stories_tree/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import PropTypes from 'prop-types';
import React from 'react';
import deepEqual from 'deep-equal';
import treeNodeTypes from './tree_node_type';
import treeDecorators from './tree_decorators';
import createTreeDecorators from './tree_decorators';
import treeStyle from './tree_style';

const namespaceSeparator = '@';
const keyCodeEnter = 13;

function createNodeKey({ namespaces, type }) {
return [...namespaces, [type]].join(namespaceSeparator);
Expand Down Expand Up @@ -39,12 +40,14 @@ class Stories extends React.Component {
constructor(...args) {
super(...args);
this.onToggle = this.onToggle.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);

const { selectedHierarchy } = this.props;

this.state = {
nodes: getSelectedNodes(selectedHierarchy),
};
this.treeDecorators = createTreeDecorators(this);
}

componentWillReceiveProps(nextProps) {
Expand Down Expand Up @@ -82,6 +85,12 @@ class Stories extends React.Component {
}));
}

onKeyDown(event, node) {
if (event.keyCode === keyCodeEnter) {
this.onToggle(node, !node.toggled);
}
}

fireOnKind(kind) {
const { onSelectStory } = this.props;
if (onSelectStory) onSelectStory(kind, null);
Expand Down Expand Up @@ -144,7 +153,7 @@ class Stories extends React.Component {
style={treeStyle}
data={data}
onToggle={this.onToggle}
decorators={treeDecorators}
decorators={this.treeDecorators}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,5 +313,46 @@ describe('manager.ui.components.left_panel.stories', () => {

expect(onSelectStory).toHaveBeenCalledWith('another.space.20', 'b2');
});

test('should call the onSelectStory prop when a story is selected with enter key', () => {
const data = createHierarchy(
[
{ kind: 'some.name.item1', stories: ['a1', 'a2'] },
{ kind: 'another.space.20', stories: ['b1', 'b2'] },
],
'\\.'
);

const onSelectStory = jest.fn();
const wrap = mount(
<Stories
storiesHierarchy={data}
selectedKind="some.name.item1"
selectedStory="a2"
selectedHierarchy={['some', 'name', 'item1']}
onSelectStory={onSelectStory}
/>
);

wrap
.find('a')
.filterWhere(el => el.text() === 'another')
.last()
.simulate('keyDown', { keyCode: 13 });

wrap
.find('a')
.filterWhere(el => el.text() === 'space')
.last()
.simulate('keyDown', { keyCode: 13 });

wrap
.find('a')
.filterWhere(el => el.text() === '20')
.last()
.simulate('keyDown', { keyCode: 13 });

expect(onSelectStory).toHaveBeenCalledWith('another.space.20', null);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,41 +34,61 @@ ContainerDecorator.propTypes = {
}).isRequired,
};

function HeaderDecorator(props) {
const { style, node } = props;
function createHeaderDecoratorScope(parent) {
class HeaderDecorator extends React.Component {
constructor(...args) {
super(...args);
this.onKeyDown = this.onKeyDown.bind(this);
}

const newStyleTitle = {
...style.title,
};
onKeyDown(event) {
const { onKeyDown } = parent;
const { node } = this.props;

onKeyDown(event, node);
}

render() {
const { style, node } = this.props;

const newStyleTitle = {
...style.title,
};

const Icon = iconsMap[node.type];
const Icon = iconsMap[node.type];

if (!node.children || !node.children.length) {
newStyleTitle.fontSize = '13px';
if (!node.children || !node.children.length) {
newStyleTitle.fontSize = '13px';
}

return (
<div style={style.base} role="menuitem" tabIndex="0" onKeyDown={this.onKeyDown}>
{Icon && <Icon color={iconsColor} />}
<a style={newStyleTitle}>
{node.name}
</a>
</div>
);
}
}

return (
<div style={style.base}>
{Icon && <Icon color={iconsColor} />}
<a style={newStyleTitle}>
{node.name}
</a>
</div>
);
}
HeaderDecorator.propTypes = {
style: PropTypes.shape({
title: PropTypes.object.isRequired,
base: PropTypes.object.isRequired,
}).isRequired,
node: PropTypes.shape({
name: PropTypes.string.isRequired,
}).isRequired,
};

HeaderDecorator.propTypes = {
style: PropTypes.shape({
title: PropTypes.object,
base: PropTypes.object,
}).isRequired,
node: PropTypes.shape({
name: PropTypes.string,
}).isRequired,
};
return HeaderDecorator;
}

export default {
...decorators,
Header: HeaderDecorator,
Container: ContainerDecorator,
};
export default function(parent) {
return {
...decorators,
Header: createHeaderDecoratorScope(parent),
Container: ContainerDecorator,
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { baseFonts } from '../../theme';

const toggleWidth = '24px';

export default {
tree: {
base: {
Expand All @@ -8,6 +10,7 @@ export default {
padding: 0,
fontFamily: baseFonts.fontFamily,
fontSize: '15px',
minWidth: '200px',
},
node: {
base: {
Expand All @@ -30,7 +33,7 @@ export default {
verticalAlign: 'top',
marginLeft: '-5px',
height: '24px',
width: '24px',
width: toggleWidth,
},
wrapper: {
position: 'absolute',
Expand All @@ -49,6 +52,7 @@ export default {
base: {
display: 'inline-block',
verticalAlign: 'top',
maxWidth: `calc(100% - ${toggleWidth})`,
},
connector: {
width: '2px',
Expand Down