Skip to content

Commit

Permalink
Always render itemsContainer (DOM restructure)
Browse files Browse the repository at this point in the history
  • Loading branch information
moroshko committed Aug 9, 2016
1 parent 1ccd180 commit ecbb5a4
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 139 deletions.
2 changes: 1 addition & 1 deletion demo/src/components/App/App.less
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.container {
font-family: 'Roboto', sans-serif;
font-family: Helvetica, Arial, sans-serif;
margin-bottom: 300px;
}

Expand Down
2 changes: 2 additions & 0 deletions demo/src/components/App/components/Example6/Example6.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ function mapDispatchToProps(dispatch) {
dispatch(updateInputValue(exampleId, event.target.value));
},
onKeyDown: (event, { newFocusedSectionIndex, newFocusedItemIndex }) => {
event.preventDefault(); // Don't move the cursor to start/end

if (typeof newFocusedItemIndex !== 'undefined') {
dispatch(updateFocusedItem(exampleId, newFocusedSectionIndex, newFocusedItemIndex));
}
Expand Down
2 changes: 2 additions & 0 deletions demo/src/components/App/components/Example7/Example7.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ function mapDispatchToProps(dispatch) {
dispatch(updateInputValue(exampleId, event.target.value));
},
onKeyDown: (event, { newFocusedSectionIndex, newFocusedItemIndex }) => {
event.preventDefault(); // Don't move the cursor to start/end

if (typeof newFocusedItemIndex !== 'undefined') {
dispatch(updateFocusedItem(exampleId, newFocusedSectionIndex, newFocusedItemIndex));
}
Expand Down
1 change: 1 addition & 0 deletions demo/src/components/App/components/Example8/Example8.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ function mapDispatchToProps(dispatch) {
switch (event.key) {
case 'ArrowDown':
case 'ArrowUp':
event.preventDefault(); // Don't move the cursor to start/end
dispatch(updateFocusedItem(exampleId, newFocusedSectionIndex, newFocusedItemIndex));
break;

Expand Down
1 change: 1 addition & 0 deletions demo/src/components/App/components/Example9/Example9.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ Example.propTypes = {
focusedSectionIndex: PropTypes.number,
focusedItemIndex: PropTypes.number,
items: PropTypes.array.isRequired,

onChange: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
onBlur: PropTypes.func.isRequired,
Expand Down
41 changes: 22 additions & 19 deletions demo/src/components/App/components/theme.less
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
width: 240px;
height: 30px;
padding: 10px 20px;
font-size: 18px;
font-size: 16px;
font-family: Helvetica, Arial, sans-serif;
border: 1px solid @border-color;
border-radius: @border-radius;
box-sizing: content-box;
Expand All @@ -25,21 +26,29 @@
}

.itemsContainer {
position: relative;
top: -1px;
width: 280px;
display: none;

.containerOpen & {
display: block;
position: relative;
top: -1px;
width: 280px;
border: 1px solid @border-color;
background-color: #fff;
font-size: 16px;
line-height: 1.25;
border-bottom-left-radius: @border-radius;
border-bottom-right-radius: @border-radius;
z-index: 2;
max-height: 260px;
overflow-y: auto;
}
}

.itemsList {
margin: 0;
padding: 0;
list-style-type: none;
border: 1px solid @border-color;
background-color: #fff;
font-size: 16px;
line-height: 1.25;
border-bottom-left-radius: @border-radius;
border-bottom-right-radius: @border-radius;
z-index: 2;
max-height: 260px;
overflow-y: auto;
}

.item {
Expand All @@ -62,12 +71,6 @@
}
}

.sectionItemsContainer {
margin: 0;
padding: 0;
list-style-type: none;
}

.highlight {
color: #ee0000;
font-weight: 400;
Expand Down
1 change: 0 additions & 1 deletion demo/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
<meta name="description" content="Accessible rendering layer for Autosuggest and Autocomplete components">
<meta name="author" content="Misha Moroshko">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="app.css">
</head>
<body>
Expand Down
160 changes: 101 additions & 59 deletions src/Autowhatever.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ const defaultTheme = {
containerOpen: 'react-autowhatever__container--open',
input: 'react-autowhatever__input',
itemsContainer: 'react-autowhatever__items-container',
itemsList: 'react-autowhatever__items-list',
item: 'react-autowhatever__item',
itemFocused: 'react-autowhatever__item--focused',
sectionContainer: 'react-autowhatever__section-container',
sectionTitle: 'react-autowhatever__section-title',
sectionItemsContainer: 'react-autowhatever__section-items-container'
sectionTitle: 'react-autowhatever__section-title'
};

export default class Autowhatever extends Component {
Expand Down Expand Up @@ -62,16 +62,23 @@ export default class Autowhatever extends Component {
constructor(props) {
super(props);

this.focusedItem = null;

this.setSectionsItems(props);
this.setSectionIterator(props);
this.setTheme(props);

this.onKeyDown = this.onKeyDown.bind(this);
this.storeInputReference = this.storeInputReference.bind(this);
this.storeItemsListReference = this.storeItemsListReference.bind(this);
this.storeItemsContainerReference = this.storeItemsContainerReference.bind(this);
this.onFocusedItemChange = this.onFocusedItemChange.bind(this);
this.getItemId = this.getItemId.bind(this);
}

componentDidMount() {
this.ensureFocusedItemIsVisible();
}

componentWillReceiveProps(nextProps) {
if (nextProps.items !== this.props.items) {
this.setSectionsItems(nextProps);
Expand All @@ -86,6 +93,10 @@ export default class Autowhatever extends Component {
}
}

componentDidUpdate() {
this.ensureFocusedItemIsVisible();
}

setSectionsItems(props) {
if (props.multiSection) {
this.sectionsItems = props.items.map(section => props.getSectionItems(section));
Expand All @@ -111,13 +122,16 @@ export default class Autowhatever extends Component {
}
}

// Needed only for testing
storeItemsListReference(itemsList) {
if (itemsList !== null) {
this.itemsList = itemsList;
storeItemsContainerReference(itemsContainer) {
if (itemsContainer !== null) {
this.itemsContainer = itemsContainer;
}
}

onFocusedItemChange(focusedItem) {
this.focusedItem = focusedItem;
}

getItemId(sectionIndex, itemIndex) {
if (itemIndex === null) {
return null;
Expand All @@ -129,12 +143,6 @@ export default class Autowhatever extends Component {
return `react-autowhatever-${id}-${section}-item-${itemIndex}`;
}

getItemsContainerId() {
const { id } = this.props;

return `react-autowhatever-${id}`;
}

renderSections() {
if (this.allSectionsAreEmpty) {
return null;
Expand All @@ -146,46 +154,39 @@ export default class Autowhatever extends Component {
renderSectionTitle, focusedSectionIndex, focusedItemIndex, itemProps
} = this.props;

return (
<div
{...theme(`react-autowhatever-${id}-items-container`, 'itemsContainer')}>
{
items.map((section, sectionIndex) => {
if (!shouldRenderSection(section)) {
return null;
}

const keyPrefix = `react-autowhatever-${id}-`;
const sectionKeyPrefix = `${keyPrefix}section-${sectionIndex}-`;

// `key` is provided by theme()
/* eslint-disable react/jsx-key */
return (
<div {...theme(`${sectionKeyPrefix}container`, 'sectionContainer')}>
<SectionTitle
section={section}
renderSectionTitle={renderSectionTitle}
theme={theme}
sectionKeyPrefix={sectionKeyPrefix} />
<ItemsList
id={this.getItemsContainerId()}
items={this.sectionsItems[sectionIndex]}
itemProps={itemProps}
renderItem={renderItem}
renderItemData={renderItemData}
sectionIndex={sectionIndex}
focusedItemIndex={focusedSectionIndex === sectionIndex ? focusedItemIndex : null}
getItemId={this.getItemId}
theme={theme}
keyPrefix={keyPrefix}
ref={this.storeItemsListReference} />
</div>
);
/* eslint-enable react/jsx-key */
})
}
</div>
);
return items.map((section, sectionIndex) => {
if (!shouldRenderSection(section)) {
return null;
}

const keyPrefix = `react-autowhatever-${id}-`;
const sectionKeyPrefix = `${keyPrefix}section-${sectionIndex}-`;

// `key` is provided by theme()
/* eslint-disable react/jsx-key */
return (
<div {...theme(`${sectionKeyPrefix}container`, 'sectionContainer')}>
<SectionTitle
section={section}
renderSectionTitle={renderSectionTitle}
theme={theme}
sectionKeyPrefix={sectionKeyPrefix} />
<ItemsList
items={this.sectionsItems[sectionIndex]}
itemProps={itemProps}
renderItem={renderItem}
renderItemData={renderItemData}
sectionIndex={sectionIndex}
focusedItemIndex={focusedSectionIndex === sectionIndex ? focusedItemIndex : null}
onFocusedItemChange={this.onFocusedItemChange}
getItemId={this.getItemId}
theme={theme}
keyPrefix={keyPrefix}
ref={this.storeItemsListReference} />
</div>
);
/* eslint-enable react/jsx-key */
});
}

renderItems() {
Expand All @@ -203,16 +204,15 @@ export default class Autowhatever extends Component {

return (
<ItemsList
id={this.getItemsContainerId()}
items={items}
itemProps={itemProps}
renderItem={renderItem}
renderItemData={renderItemData}
focusedItemIndex={focusedSectionIndex === null ? focusedItemIndex : null}
onFocusedItemChange={this.onFocusedItemChange}
getItemId={this.getItemId}
theme={theme}
keyPrefix={`react-autowhatever-${id}-`}
ref={this.storeItemsListReference} />
keyPrefix={`react-autowhatever-${id}-`} />
);
}

Expand All @@ -235,31 +235,73 @@ export default class Autowhatever extends Component {
}
}

ensureFocusedItemIsVisible() {
const { focusedItem } = this;

if (!focusedItem) {
return;
}

const { itemsContainer } = this;
const itemOffsetRelativeToContainer =
focusedItem.offsetParent === itemsContainer
? focusedItem.offsetTop
: focusedItem.offsetTop - itemsContainer.offsetTop;

let { scrollTop } = itemsContainer; // Top of the visible area

if (itemOffsetRelativeToContainer < scrollTop) {
// Item is off the top of the visible area
scrollTop = itemOffsetRelativeToContainer;
} else if (itemOffsetRelativeToContainer + focusedItem.offsetHeight > scrollTop + itemsContainer.offsetHeight) {
// Item is off the bottom of the visible area
scrollTop = itemOffsetRelativeToContainer + focusedItem.offsetHeight - itemsContainer.offsetHeight;
}

if (scrollTop !== itemsContainer.scrollTop) {
itemsContainer.scrollTop = scrollTop;
}
}

render() {
const { theme } = this;
const { id, multiSection, focusedSectionIndex, focusedItemIndex } = this.props;
const renderedItems = multiSection ? this.renderSections() : this.renderItems();
const isOpen = (renderedItems !== null);
const ariaActivedescendant = this.getItemId(focusedSectionIndex, focusedItemIndex);
const containerProps = theme(
`react-autowhatever-${id}-container`,
'container',
isOpen && 'containerOpen'
);
const itemsContainerId = `react-autowhatever-${id}`;
const inputProps = {
type: 'text',
value: '',
autoComplete: 'off',
role: 'combobox',
'aria-autocomplete': 'list',
'aria-owns': this.getItemsContainerId(),
'aria-owns': itemsContainerId,
'aria-expanded': isOpen,
'aria-haspopup': isOpen,
'aria-activedescendant': ariaActivedescendant,
...theme(`react-autowhatever-${id}-input`, 'input'),
...this.props.inputProps,
onKeyDown: this.props.inputProps.onKeyDown && this.onKeyDown,
ref: this.storeInputReference
};
const itemsContainerProps = {
id: itemsContainerId,
...theme(`react-autowhatever-${id}-items-container`, 'itemsContainer'),
ref: this.storeItemsContainerReference
};

return (
<div {...theme(`react-autowhatever-${id}-container`, 'container', isOpen && 'containerOpen')}>
<div {...containerProps}>
<input {...inputProps} />
{renderedItems}
<div {...itemsContainerProps}>
{renderedItems}
</div>
</div>
);
}
Expand Down
Loading

0 comments on commit ecbb5a4

Please sign in to comment.