Skip to content

Commit

Permalink
Use updated dropdowns (#753)
Browse files Browse the repository at this point in the history
* Moved skip link into it's own component. Updated the style to match the other nav buttons. Updated <NavButton> to accept both href (regular link) and to (react router link). Updated AppList/CurrentApp to use the 'to'-prop instead of the 'href'-prop.

* Added localized aria-label for <SkipLink>

* Structural updates for <MainNav> to get it ready for further updates. Removed <NavGroup> since it's no longer needed.

* Merged <List> and <AppList> into one component. Created <ResizeContainer> which will be used to determine which apps are visible in the nav bar and which apps will be hidden away in the dropdown. Moved sub-components for <AppList> into a components folder for better structure.

* Moved some code around and updated all methods to be arrow functions in <AppList>

* Updated <ResizeContainer> and <AppList> to hide/show nav items based on the available space in the nav part of the MainNav. (WIP).

* Added small transition for appearing/disappearing nav items. Increased the offset a bit.

* Updated <ResizeContainer> to work on right-to-left direction

* Updated <ResizeContainer> to hide all items from the nav bar on a specific width.

* Added a little horizontal padding to prevent interaction styles from being cropped on nav items.

* Updated AppListDropdown to show items in alphabetically order

* Fixed active app in app list dropdown

* Removed the first divider for now. It looked a bit buggy when items fades in/out.

* Fixed linting warning

* Fixed SkipLink bug

* Minor

* Updated NavItem IDs to fix integration tests

* Added test coverage for <ResizeContainer>

* Fixed linting

* Removed .only from test.

* Fixed <ResizeContainer> test by setting a scope for the interactor. Added 'isRTL'-prop that allows for forcing RTL.

* Added test scaffolding for AppList.

* Updated test index to look for tests inside the src folder and in all sub-folders.

* Added interactors and tests for AppList and AppListDropdown.

* Remove .only

* Fixed linting

* Updated MainNav skeleton colors and fixed linting

* Minor update: Now checking for the wrapper el before trying to use the getBoundingClientRect

* Added slight delay when determining visible items on mount. This should make the measurement more accurate because fonts etc. hopefully has loaded after the timeout.

* Removed delayed visible item determination on mount for now since it caused tests to fail.

* update profileDropdown

* remove 'data-role' attribute from profile dropdown

* Update AppList to use new DropdownAPI

* remove focusTrap

* update tests for applist

* add tests for profileDropdown

* use Portal for profile dropdown for now

* use relative positioning for ProfileDropdown

* remove only from test

* lint, move eslint config to top level

* set usePortal to false in ProfileDropdown

* improve Applist test coverage
  • Loading branch information
JohnC-80 authored Dec 3, 2019
1 parent f247ac5 commit 093be5f
Show file tree
Hide file tree
Showing 10 changed files with 245 additions and 187 deletions.
13 changes: 12 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
{
"extends": "@folio/eslint-config-stripes",
"parser": "babel-eslint"
"parser": "babel-eslint",
"overrides": [
{
"files": [ "src/**/tests/*", "test/**/*" ],
"rules": {
"func-names": "off",
"no-unused-expressions": "off",
"max-len": "off",
"one-var": "off"
}
}
]
}
3 changes: 1 addition & 2 deletions src/components/MainNav/AppList/AppList.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
.navItemsList {
list-style: none;
margin: 0;
padding: 0;
padding: 0 var(--gutter-static-two-thirds);
display: flex;
justify-content: flex-end;
}
Expand Down Expand Up @@ -63,7 +63,6 @@
.dropdownBody {
width: 100%;
outline: 0;
padding: var(--gutter-static-one-third) var(--gutter-static);
}

.dropdownList {
Expand Down
156 changes: 49 additions & 107 deletions src/components/MainNav/AppList/AppList.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ import React, { Component, Fragment } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import rtlDetect from 'rtl-detect';

import { Dropdown } from '@folio/stripes-components/lib/Dropdown';
import DropdownMenu from '@folio/stripes-components/lib/DropdownMenu';
import Icon from '@folio/stripes-components/lib/Icon';

import IntlConsumer from '../../IntlConsumer';
import { ResizeContainer, AppListDropdown } from './components';
import NavButton from '../NavButton';
import css from './AppList.css';
Expand Down Expand Up @@ -47,23 +45,37 @@ class AppList extends Component {
open: false,
};

this.focusHandlers = {
open: (trigger, menu, firstItem) => {
if (this.props.selectedApp) {
/* the selected app may not be in the list...
* if focusing the selected item fails, focus
* the first item... */
if (!this.focusSelectedItem()) {
firstItem.focus();
}
// If not; focus first item in the list
} else if (firstItem) firstItem.focus();
}
};

this.dropdownListRef = React.createRef();
this.dropdownToggleRef = React.createRef();
}

componentDidUpdate(prevProps, prevState) {
/**
* focus management
*/
focusSelectedItem = () => {
const selectedApp = this.props.selectedApp;

// Set focus on dropdown when it opens
if (this.state.open && !prevState.open) {
// If there's an active app
if (selectedApp) {
this.focusSelectedItem();
// If not; focus first item in the list
} else {
this.focusFirstItemInList();
if (selectedApp) {
const activeElement = document.getElementById(`app-list-dropdown-item-${selectedApp.id}`);
if (activeElement) {
activeElement.focus();
return true;
}
}
return false;
}

/**
Expand Down Expand Up @@ -106,23 +118,10 @@ class AppList extends Component {
);
}

/**
* When dropdown is toggled
*/
toggleDropdown = () => {
this.setState(state => ({ open: !state.open }), () => {
// Re-focus dropdown toggle on close
if (!this.state.open) {
this.focusDropdownToggleButton();
}
});
}

/**
* The button that toggles the dropdown
*/
renderDropdownToggleButton = () => {
const { open } = this.state;
renderDropdownToggleButton = ({ open, getTriggerProps }) => {
const { dropdownToggleId } = this.props;
const icon = (
<svg className={css.dropdownToggleIcon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
Expand All @@ -143,21 +142,17 @@ class AppList extends Component {
data-test-app-list-apps-toggle
label={label}
aria-label={ariaLabel}
aria-haspopup="true"
aria-expanded={open}
data-role="toggle"
className={css.navMobileToggle}
labelClassName={css.dropdownToggleLabel}
onClick={this.toggleDropdown}
selected={this.state.open}
icon={icon}
{...getTriggerProps()}
id={dropdownToggleId}
ref={this.dropdownToggleRef}
noSelectedBar
/>
)}
</FormattedMessage>
{open && this.focusTrap(this.focusFirstItemInList)}
</Fragment>
);
}
Expand All @@ -168,12 +163,7 @@ class AppList extends Component {
renderNavDropdown = (hiddenItemIds) => {
const {
renderDropdownToggleButton,
toggleDropdown,
focusTrap,
focusDropdownToggleButton,
focusFirstItemInList,
dropdownListRef,
state: { open },
} = this;

const { apps, dropdownId, dropdownToggleId, selectedApp } = this.props;
Expand All @@ -183,80 +173,32 @@ class AppList extends Component {
}

return (
<IntlConsumer>
{ intl => {
const tether = {
attachment: rtlDetect.isRtlLang(intl.locale) ? 'top left' : 'top right',
targetAttachment: rtlDetect.isRtlLang(intl.locale) ? 'bottom left' : 'bottom right',
constraints: [{
to: 'target',
}],
};

return (
<div className={css.navListDropdownWrap}>
<Dropdown
tether={tether}
dropdownClass={css.navListDropdown}
open={open}
id={dropdownId}
onToggle={toggleDropdown}
hasPadding={false}
>
{renderDropdownToggleButton()}
<DropdownMenu data-role="menu" onToggle={toggleDropdown}>
{focusTrap(focusDropdownToggleButton)}
<AppListDropdown
apps={apps.filter(item => hiddenItemIds.includes(item.id))}
dropdownToggleId={dropdownToggleId}
listRef={dropdownListRef}
selectedApp={selectedApp}
toggleDropdown={toggleDropdown}
/>
{focusTrap(focusFirstItemInList)}
</DropdownMenu>
</Dropdown>
</div>
);
}}
</IntlConsumer>
<div className={css.navListDropdownWrap}>
<Dropdown
placement="bottom-end"
dropdownClass={css.navListDropdown}
id={dropdownId}
renderTrigger={renderDropdownToggleButton}
usePortal={false}
focusHandlers={this.focusHandlers}
>
{ ({ onToggle }) => (
<DropdownMenu onToggle={onToggle}>
<AppListDropdown
apps={apps.filter(item => hiddenItemIds.includes(item.id))}
dropdownToggleId={dropdownToggleId}
listRef={dropdownListRef}
selectedApp={selectedApp}
toggleDropdown={onToggle}
/>
</DropdownMenu>
)
}
</Dropdown>
</div>
);
}


/**
* Focus management
*/
focusFirstItemInList = () => {
if (this.dropdownListRef && this.dropdownListRef.current) {
// Applies focus to the <a> inside the first <li> in the list
this.dropdownListRef.current.firstChild.firstChild.focus();
}
}

focusSelectedItem = () => {
const selectedApp = this.props.selectedApp;
if (selectedApp) {
const activeElement = document.getElementById(`app-list-item-${selectedApp.id}`);
if (activeElement) {
activeElement.focus();
}
}
}

focusDropdownToggleButton = () => {
if (this.dropdownToggleRef && this.dropdownToggleRef.current) {
this.dropdownToggleRef.current.focus();
}
}

/**
* Insert hidden input to help trap focus
*/
focusTrap(onFocus) {
return <input aria-hidden="true" className="sr-only" onFocus={onFocus} />;
}

render() {
const { apps } = this.props;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* AppListDropdown interactor
*/

import { interactor, scoped, is, collection } from '@bigtest/interactor';
import { interactor, scoped, is, collection, attribute } from '@bigtest/interactor';

export default interactor(class AppListDropdownInteractor {
static defaultScope = '[data-test-app-list-dropdown]';
Expand All @@ -13,5 +13,6 @@ export default interactor(class AppListDropdownInteractor {

items = collection('[data-test-app-list-dropdown-item]', {
isFocused: is(':focus'),
id: attribute('id'),
});
});
72 changes: 51 additions & 21 deletions src/components/MainNav/AppList/tests/AppList-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,50 @@ describe('AppList', () => {
expect(appList.itemsCount).to.equal(apps.length);
});

describe('opening the appList with a selected app', () => {
beforeEach(async () => {
await mountWithContext(
// Simulate very small screen
<div style={{ width: 150, background: 'yellow' }}>
<BrowserRouter>
<AppList
apps={apps}
selectedApp={selectedApp}
dropdownToggleId="xyz"
/>
</BrowserRouter>
</div>
);
await appList.dropdownToggle.click();
});

it('focuses the corresponding item for the selected app', () => {
expect(document.activeElement).to.not.equal(null);
});

describe('if the selected app is not present in the list', () => {
beforeEach(async () => {
await mountWithContext(
// Simulate very small screen
<div style={{ width: 150, background: 'yellow' }}>
<BrowserRouter>
<AppList
apps={apps}
selectedApp={{ id: 'test-fake-module', route: '/dummy' }}
dropdownToggleId="xyz"
/>
</BrowserRouter>
</div>
);
await appList.dropdownToggle.click();
});

it('focuses the first item in the list', () => {
expect(document.activeElement.id).to.equal(appList.dropdownMenu.items(0).id);
});
});
});

describe('If there is no apps to show', () => {
beforeEach(async () => {
await mountWithContext(
Expand Down Expand Up @@ -78,29 +122,15 @@ describe('AppList', () => {
it('Should focus the first item in the dropdown if there is no current app', () => {
expect(appList.dropdownMenu.items(0).isFocused).to.equal(true);
});
});

describe('Clicking an item inside the app list dropdown', () => {
beforeEach(async () => {
await appList.dropdownMenu.items(0).click();
});

describe('Clicking an item inside the app list dropdown', () => {
beforeEach(async () => {
await mountWithContext(
// Simulate very small screen
<div style={{ width: 150, background: 'yellow' }}>
<BrowserRouter>
<AppList
apps={apps}
selectedApp={selectedApp}
dropdownToggleId="xyz"
/>
</BrowserRouter>
</div>
);
await appList.dropdownToggle.click();
await appList.dropdownMenu.items(0).click();
});

it('Should close the app dropdown and focus the dropdown toggle', () => {
expect(appList.dropdownToggle.isFocused).to.equal(true);
it('Should close the app dropdown and focus the dropdown toggle', () => {
expect(appList.dropdownToggle.isFocused).to.equal(true);
});
});
});
});
8 changes: 2 additions & 6 deletions src/components/MainNav/NavDropdownMenu/NavDropdownMenu.css
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
@import '@folio/stripes-components/lib/variables.css';

.DropdownMenu {
position: absolute;
top: 105%;
right: 0;
z-index: 1002;
display: none;
float: left;
padding: 5px 8px;
padding: 5px 15px;
margin: 6px 0 0;
text-align: left;
list-style: none;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ccc;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
pointer-events: all;

& ul {
list-style: none;
Expand Down
Loading

0 comments on commit 093be5f

Please sign in to comment.