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

New Accessible Dropdown Nav Bar Component #478

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ac1c68a
create all components, update to use actual button trigger
danparnella Apr 7, 2021
60ffbf6
various updates to clean up code
danparnella Apr 12, 2021
7b49707
update id/class names, add aria-label for menu
danparnella Apr 13, 2021
1efbe42
code cleanup, allow for click and keyboard actions in mobile view
danparnella Apr 15, 2021
bbc67b1
add any missing aria attributes
danparnella Apr 29, 2021
f360128
modify naming, add a comment
danparnella Apr 29, 2021
d0a4703
default with buttons always shown, add option to hide before focus
danparnella Apr 29, 2021
4b4a912
grab the top most window to check for browser width
danparnella Apr 29, 2021
e048cd4
allow for no mobile breakpoint/styles, update naming
danparnella Apr 30, 2021
e9a3f52
move id, add aria controls
danparnella Apr 30, 2021
a69dd10
add tests
danparnella Apr 30, 2021
0f058ed
- update some docs
danparnella May 10, 2021
e176440
change menuLabel to menuAriaLabel, update docs
danparnella May 10, 2021
c0c5f0b
update version
danparnella May 10, 2021
f2af2b7
update docs
danparnella May 10, 2021
1d2222f
build updated docs file
danparnella May 10, 2021
b3e5825
pull mobile menu button out of component and add to stories
danparnella May 13, 2021
f9f79de
update naming
danparnella May 13, 2021
c2658ec
apply new naming to tests
danparnella May 13, 2021
aa1e826
start refactor with updated tests
danparnella Jun 1, 2021
3c77519
add more tests
danparnella Jun 2, 2021
a10e8cd
fix tests
danparnella Jun 2, 2021
054ebdb
update tests
danparnella Jan 31, 2022
cf7d3b4
reorganize test file
danparnella Jan 31, 2022
1c26012
update tests to pass where they should
danparnella Jan 31, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
allow for no mobile breakpoint/styles, update naming
  • Loading branch information
danparnella committed Apr 30, 2021
commit e048cd45be0ddd6ce0db24d0bda7becfa97c816c
2 changes: 1 addition & 1 deletion .storybook/styles/application.scss
Original file line number Diff line number Diff line change
@@ -64,7 +64,7 @@
@import "components/search-results-and-filtering";
@import "components/recruiter-search";
@import "components/tabs";
@import "components/dropdown-nav-bar";
@import "components/dropdown-nav-bar/dropdown-nav-bar";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this need to be ported over to the client template as well or will the styles automatically be applied when a user imports the component into their project?


// Page Styles
@import "pages/main";
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.dropdown-nav-bar {
& {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't seen this usage before! What does & signify when it's used at the top-level?

input[type='checkbox'] {
position: absolute;
top: -9999px;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$tablet: new-breakpoint(min-width 0);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.dropdown-nav-bar.no-mobile {
@import 'dropdown-nav-bar-no-mobile';
@import 'dropdown-nav-bar-common';
}

.dropdown-nav-bar:not(.no-mobile) {
@import 'dropdown-nav-bar-common';
}
Comment on lines +1 to +8
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allowed me to use the same styles for both implementations and just change the $tablet sass variable for the no-mobile version.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! I'll defer to @marchwiany on best practices in using dynamic variables as this is pretty foreign to me.

For my own edification, would the following not perform a similarly sequenced import? Maybe in the compilation step the second block is executed before the first?

.dropdown-nav-bar.no-mobile {
  @import 'dropdown-nav-bar-no-mobile';
}

.dropdown-nav-bar {
  @import 'dropdown-nav-bar-common';
}

54 changes: 32 additions & 22 deletions src/controls/dropdown-nav-bar.js
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import { isMobileView, toggleElementArray } from './helpers'
import { menuItemType } from './helpers/nav-prop-types'
import DropdownNavMenu from './dropdown-nav-menu'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the intent to make all of the subcomponents exportable so that users can build their own nav bars, or is this the only file that's getting exported? It looks like it's the latter (based on src/controls/index.js). If that's the case, then I think it would be appropriate to create a dropdown-nav-bar directory (similar to tab-bar)

import classnames from 'classnames'

// TODO: Finish documentation
/**
@@ -12,7 +13,7 @@ import DropdownNavMenu from './dropdown-nav-menu'
* @type Function
* @description A control component for navigating among multiple navigation menu items that can include dropdowns with sub-menu items
* @param {String} [baseUrl] -
* @param {Number} [mobileBreakpoint] -
* @param {Number|Boolean} [mobileBreakpoint] -
* @param {Array} [menuItems] -
* @example
*
@@ -35,25 +36,25 @@ import DropdownNavMenu from './dropdown-nav-menu'

const propTypes = {
menuItems: PropTypes.arrayOf(menuItemType).isRequired,
mobileBreakpoint: PropTypes.number,
mobileBreakpoint: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
baseUrl: PropTypes.string,
menuLabel: PropTypes.string,
hideDropdownButtonsBeforeFocus: PropTypes.bool,
hideMenuButtonsBeforeFocus: PropTypes.bool,
}

const defaultProps = {
mobileBreakpoint: 1024,
mobileBreakpoint: 720,
baseUrl: '',
menuLabel: 'Primary Menu',
hideDropdownButtonsBeforeFocus: false,
hideMenuButtonsBeforeFocus: false,
}

function DropdownNavBar({
menuItems,
mobileBreakpoint,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you explore other alternatives to using a pixel value? How does this compare to other component libraries like Semantic UI, Bootstrap, etc.?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this value be configured via css instead of within the React component? I'm sure there's good reasoning for including it here I just haven't connected the dots yet

baseUrl,
menuLabel,
hideDropdownButtonsBeforeFocus,
hideMenuButtonsBeforeFocus,
}) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
const [openMenuIds, setOpenMenuIds] = useState([])
@@ -71,29 +72,38 @@ function DropdownNavBar({
}

return (
<nav className="dropdown-nav-bar" aria-label={menuLabel}>
<input
type="checkbox"
id="mobile-nav-button"
checked={isMobileMenuOpen}
onChange={() => {
setIsMobileMenuOpen(!isMobileMenuOpen)
closeDesktopSubmenu()
}}
/>
<label htmlFor="mobile-nav-button" className="mobile-menu">
<span />
<span />
<span />
</label>
<nav
className={classnames('dropdown-nav-bar', {
'no-mobile': !mobileBreakpoint,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the underlying concept that this represents? I think "no-mobile" doesn't necessarily communicate intent directly.

Also, could you use the boolean that you calculated in line 70?

})}
aria-label={menuLabel}
>
{!!mobileBreakpoint && (
<React.Fragment>
<input
type="checkbox"
id="mobile-nav-button"
checked={isMobileMenuOpen}
onChange={() => {
setIsMobileMenuOpen(!isMobileMenuOpen)
closeDesktopSubmenu()
}}
/>
<label htmlFor="mobile-nav-button" className="mobile-menu">
<span />
<span />
<span />
</label>
danparnella marked this conversation as resolved.
Show resolved Hide resolved
</React.Fragment>
)}
<DropdownNavMenu
openMenuIds={openMenuIds}
toggleOpenMenuId={toggleOpenMenuId}
closeDesktopSubmenu={closeDesktopSubmenu}
baseUrl={baseUrl}
menuItems={menuItems}
menuLabel={menuLabel}
hideDropdownButtonsBeforeFocus={hideDropdownButtonsBeforeFocus}
hideMenuButtonsBeforeFocus={hideMenuButtonsBeforeFocus}
/>
</nav>
)
18 changes: 9 additions & 9 deletions src/controls/dropdown-nav-menu-item.js
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ const propTypes = {
toggleSubmenu: PropTypes.func.isRequired,
isFirstItem: PropTypes.bool.isRequired,
children: PropTypes.node,
hideDropdownButtonBeforeFocus: PropTypes.bool.isRequired,
hideMenuButtonBeforeFocus: PropTypes.bool.isRequired,
}

const defaultProps = {
@@ -32,11 +32,11 @@ function DropdownNavMenuItem({
toggleSubmenu,
isFirstItem,
children,
hideDropdownButtonBeforeFocus,
hideMenuButtonBeforeFocus,
}) {
// show dropdown button on desktop, will always be shown on mobile
const [showDropdownButton, setShowDropdownButton] = useState(
!hideDropdownButtonBeforeFocus
// show menu button on desktop, will always be shown on mobile
const [showMenuButton, setShowMenuButton] = useState(
!hideMenuButtonBeforeFocus
)

return (
@@ -51,13 +51,13 @@ function DropdownNavMenuItem({
<OutsideClickHandler onOutsideClick={closeDesktopSubmenu}>
<a
onFocus={() => {
if (hideDropdownButtonBeforeFocus) setShowDropdownButton(true)
if (hideMenuButtonBeforeFocus) setShowMenuButton(true)
closeDesktopSubmenu()
}}
onBlur={() => {
if (hideDropdownButtonBeforeFocus) {
if (hideMenuButtonBeforeFocus) {
// timeout needed to move from link to button without it disappearing
setTimeout(() => setShowDropdownButton(false), 0)
setTimeout(() => setShowMenuButton(false), 0)
}
}}
href={getNavLink(baseUrl, path)}
@@ -69,7 +69,7 @@ function DropdownNavMenuItem({
<button
type="button"
className={classnames('menu-item-button', {
'desktop-visible': isSubmenuOpen || showDropdownButton,
'desktop-visible': isSubmenuOpen || showMenuButton,
})}
onKeyDown={(e) => {
if (
6 changes: 3 additions & 3 deletions src/controls/dropdown-nav-menu.js
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ const propTypes = {
toggleOpenMenuId: PropTypes.func.isRequired,
closeDesktopSubmenu: PropTypes.func.isRequired,
menuLabel: PropTypes.string.isRequired,
hideDropdownButtonsBeforeFocus: PropTypes.bool.isRequired,
hideMenuButtonsBeforeFocus: PropTypes.bool.isRequired,
}

const defaultProps = {}
@@ -24,7 +24,7 @@ function DropdownNavMenu({
toggleOpenMenuId,
closeDesktopSubmenu,
menuLabel,
hideDropdownButtonsBeforeFocus,
hideMenuButtonsBeforeFocus,
}) {
return (
<ul className="dropdown-nav-menu" aria-label={menuLabel} role="menubar">
@@ -42,7 +42,7 @@ function DropdownNavMenu({
closeDesktopSubmenu={closeDesktopSubmenu}
toggleSubmenu={() => toggleOpenMenuId(id)}
isFirstItem={isFirstParentItem}
hideDropdownButtonBeforeFocus={hideDropdownButtonsBeforeFocus}
hideMenuButtonBeforeFocus={hideMenuButtonsBeforeFocus}
>
{childItems && !isEmpty(childItems) && (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need the additional childItems guard when using isEmpty?

<ul className="sub-menu" role="menu" aria-label={name}>
1 change: 1 addition & 0 deletions src/controls/helpers/is-mobile-view.js
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
*/

function isMobileView(mobileBreakpoint) {
if (!mobileBreakpoint) return false
// eslint-disable-next-line no-undef
return window.top.innerWidth < mobileBreakpoint
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if mobileBreakpoint is true, this expression will almost always return false (unless window.top.innerWidth === 0) since Number(true) === 1. Do you think that might be a bit misleading?

}
9 changes: 6 additions & 3 deletions stories/controls/dropdown-nav-bar.story.js
Original file line number Diff line number Diff line change
@@ -77,13 +77,16 @@ const menuItems = [
]

storiesOf('DropdownNavBar', module)
.add('with dropdown buttons always visible', () => (
.add('default', () => (
<DropdownNavBar menuItems={menuItems} mobileBreakpoint={940} />
))
.add('with dropdown buttons only visible on link focus', () => (
.add('without mobile breakpoint', () => (
<DropdownNavBar menuItems={menuItems} mobileBreakpoint={false} />
))
.add('with menu buttons only visible on link focus', () => (
<DropdownNavBar
menuItems={menuItems}
mobileBreakpoint={940}
hideDropdownButtonsBeforeFocus
hideMenuButtonsBeforeFocus
/>
))