-
Notifications
You must be signed in to change notification settings - Fork 65
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
Menu widget #79
Menu widget #79
Conversation
Codecov Report
@@ Coverage Diff @@
## master #79 +/- ##
==========================================
- Coverage 100% 99.89% -0.11%
==========================================
Files 16 18 +2
Lines 726 987 +261
Branches 202 297 +95
==========================================
+ Hits 726 986 +260
- Partials 0 1 +1
Continue to review full report at Codecov.
|
src/menu/Menu.ts
Outdated
|
||
@theme(css) | ||
export class Menu extends ThemeableMixin(WidgetBase)<MenuProperties> { | ||
protected _wasOpen = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think protected
properties should be prefixed with _
, also is there any reason why this is protected
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah; didn't realize about the prefix. There is a lot of shared functionality between Menu
and a future DropDown
widget that will extend it, in which case using private
is not really an option.
src/menu/Menu.ts
Outdated
} | ||
|
||
@theme(css) | ||
export class Menu extends ThemeableMixin(WidgetBase)<MenuProperties> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to declare a const
with the result of ThemableMixin(WidgetBase)
. Usually with the naming convention const MenuBase = Themeable(WidgetBase)
and used to extend like class Menu extends MenuBase<MenuProperties> {
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You also need to export that const. So export const MenuBase = Themeable(WidgetBase)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup sorry, I should have put that (basically need to export all the things that are used publicly)
src/menu/Menu.ts
Outdated
private _hideTimer: Handle; | ||
private _initialRender = true; | ||
|
||
onLabelClick() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of the class functions should be private
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These types of functions aren't private in the other widgets?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They should be if they are not an API intended to be overridden to modify behaviour in widgets that extend it. I think @bitpshr did get round to updating Combo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The visibility throughout was updated as part of the ComboBox PR. Most things are private
as @agubler said.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. I'll have to find some way to test those methods that doesn't require using <any>
hacks that will break once we start emitting directly to ES6. I should be able to access the event listeners directly of vnode.properties
, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of our existing tests use the hacky (<any> menu)._onFoo()
method for testing. I'd just use that for now; we'll have to do a full sweep of the components once @kitsonk's harness lands anyway.
src/menu/Menu.ts
Outdated
this._animate(element, hidden); | ||
} | ||
|
||
protected _animate(element: HTMLElement, hidden: boolean) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this not possible with css?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the hidden
value be taken from this.properties
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Animating to and from auto
is not possible with CSS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The animation is being done using CSS. It just needs an explicit value to animate to besides auto
. @mwistrand see comment above re: marginTop
and offsetHeight
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay. Do you think that we should expose a property that can provide an overriding function for the animation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@agubler We already do. properties.animate
can be a boolean or a function. If it's a boolean, then it enables/disables the default animation. If it's a function, then the built-in _animate
function is bypassed in favor of the property function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess my question was, do we think it is valid to accept an overriding function for the animate?
(Sorry if that wasn't clear, I'm happy with the boolean switch)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha. animate
as a function was a last minute add. Given that as long as the height is set in the build-in function, most animations can be handled correctly in CSS, I'll remove this option.
src/menu/Menu.ts
Outdated
id, | ||
role, | ||
classes: this.classes.apply(this, this._getMenuClasses(hidden, Boolean(label))), | ||
afterCreate: this._afterCreate, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can use the onElementCreated
and onElementUpdated
functions provided by WidgetBase
now (dojo/widget-core#382)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great. I'll update those.
src/menu/Menu.ts
Outdated
} = this.properties; | ||
|
||
if (typeof isHidden === 'undefined') { | ||
const { hidden = this._getDefaultHidden() } = this.properties; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be done in the original destructuring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also what is the difference between isHidden
and this.properties.hidden
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isHidden
is a way to force which state is requested. I'll update the parameter name to make that obvious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about using argument defaults?
class Test {
properties: any = {};
private _getDefaultHidden(): boolean {
return true;
}
private _toggleDisplay(isHidden = this.properties.hidden || this._getDefaultHidden()) {
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be cleaner if this_getDefaultHidden
returns the property hidden
if it existed
src/menu/MenuItem.ts
Outdated
}), label ? [ label ] : undefined); | ||
|
||
if (this.children.length) { | ||
const children = this.children as HNode[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need the cast?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might not anymore. I'll double check, but I did need it at one point.
src/menu/MenuItem.ts
Outdated
/** | ||
* Returns an object of aria properties to apply to the widget's DOM element. | ||
*/ | ||
getAriaProperties?: () => { [key: string]: string; }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't feel like an external property?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't, but this will be used in a few different ways requiring different ARIA properties. For example, use in Menu
requires one set of properties, whereas use with a drop down would require other properties. This seemed like the easiest way to provide any flexibility needed with hard-coding a bunch of scenarios directly within MenuItem
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@smhigley may have specifics, but you can check https://www.w3.org/WAI/tutorials/menus/. @smhigley and I have been adding a camel-cased property without the "aria" for each ARIA attribute that needs to be set externally. Example. We should stay consistent.
src/menu/MenuItem.ts
Outdated
} | ||
|
||
@theme(css) | ||
export class MenuItem extends ThemeableMixin(WidgetBase)<MenuItemProperties> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment about creating a base const
for the expression result.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And exporting!
src/menu/example/index.html
Outdated
@@ -0,0 +1,26 @@ | |||
<!DOCTYPE html> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't need the specific widget example index.html anymore, this is centralised in common
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I just remembered I hadn't moved this yet last night.
src/menu/Menu.ts
Outdated
export type Role = 'menu' | 'menubar'; | ||
|
||
export interface MenuProperties extends ThemeableProperties { | ||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These comments should be formatted using TypeDoc.
src/menu/example/index.html
Outdated
<body> | ||
<script src="../../../../node_modules/@dojo/loader/loader.min.js"></script> | ||
<link rel="stylesheet" href="../../common/styles/widgets.css"> | ||
<link rel="stylesheet" href="./styles/app.css"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like you put structural styling in the component CSS and theme styling in this example CSS. For now, I'd just put everything in the component CSS. This stuff will be properly separated as part of #51.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I knew that was being separated out eventually, so I just did that here. I'll move it back into the main menu.css
, perhaps with notes disambiguating any styles that could go either way.
src/menu/Menu.ts
Outdated
} | ||
|
||
@theme(css) | ||
export class Menu extends ThemeableMixin(WidgetBase)<MenuProperties> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You also need to export that const. So export const MenuBase = Themeable(WidgetBase)
.
src/menu/Menu.ts
Outdated
/** | ||
* Indicates whether the menu is disabled. If true, then the widget will ignore events. | ||
*/ | ||
disabled?: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would an entire menu really be disabled? Wouldn't each item within it be disabled instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If label
is specified, then a MenuItem
is created and disabled
is passed to it, which keeps it from being expanded.
src/menu/Menu.ts
Outdated
* Indicates whether the menu is nested within another menu. Useful for styling, this does not affect | ||
* functionality. Defaults to false. | ||
*/ | ||
nested?: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this? If it's just for styling, can we use child selectors to detect if something is nested?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strictly speaking, we could use child selectors to manage the same thing. But since composes
only works with single selectors, I added the extra class to be safe.
src/menu/MenuItem.ts
Outdated
/** | ||
* Applies only when a URL is provided. If `true`, the URL will be opened in a new window. | ||
*/ | ||
external?: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't a user just put an anchor as the child to this MenuItem? I don't think the MenuItem should handle link behavior.
src/menu/MenuItem.ts
Outdated
/** | ||
* Returns an object of aria properties to apply to the widget's DOM element. | ||
*/ | ||
getAriaProperties?: () => { [key: string]: string; }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@smhigley may have specifics, but you can check https://www.w3.org/WAI/tutorials/menus/. @smhigley and I have been adding a camel-cased property without the "aria" for each ARIA attribute that needs to be set externally. Example. We should stay consistent.
src/menu/MenuItem.ts
Outdated
} | ||
|
||
@theme(css) | ||
export class MenuItem extends ThemeableMixin(WidgetBase)<MenuItemProperties> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And exporting!
src/menu/MenuItem.ts
Outdated
selected ? css.selected : null | ||
); | ||
|
||
const labelNode = v('a', assign(ariaProperties, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could spread ariaProperties
instead of assigning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll update to explicitly list all possible ARIA attributes, which will also make destructuring possible/assign
ing unnecessary.
src/menu/MenuItem.ts
Outdated
role: 'menuitem', | ||
tabIndex : disabled ? -1 : tabIndex, | ||
target: url && external ? '_blank' : undefined | ||
}), label ? [ label ] : undefined); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is odd to me. Menus don't have to contain links so I don't think we should be always creating an anchor. I also don't think the MenuItem should handle hrefs or targets or anything links-specific. It should just put its children in an element so it can still capture events, and that's it. Let the user make the child an anchor if they want.
f7a8679
to
52f22b9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some accessibility changes. On a high level, each menu should only have one element in the tab index at any given time. Once focus moves into the menu, arrow keys should be the main mode of navigating through it. So lots of keyboard event handling, yay!
src/menu/MenuItem.ts
Outdated
onkeypress: this.onKeypress, | ||
role: 'menuitem', | ||
tabIndex : disabled ? -1 : tabIndex | ||
}, this.children); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think widget consumers should be able to mess with the tabIndex. It should be fully controlled by the menu widget, and there should only be one focusable menu item per menu (i.e. all but the active menu item should have tabIndex: -1
, and the active item should have tabIndex: 0
. disabled
shouldn't affect the tabindex at all, actually.
src/menu/MenuItem.ts
Outdated
return v('span', { | ||
'aria-controls': controls, | ||
'aria-expanded': expanded, | ||
'aria-hasdropdown': hasDropDown, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is aria-haspopup
, and needs to be converted to a string. I would do 'aria-haspopup': hasDropDown ? 'true' : null
, since aria-haspopup="false"
is unnecessary, and I think it looks cleaner to leave it off entirely. Also, since we already use hasPopup
, it might be better to use that property name for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, both aria-expanded
and aria-disabled
need to be cast to strings
src/menu/Menu.ts
Outdated
} = this.properties; | ||
|
||
const label = this.renderLabel(id); | ||
const menu = v('nav', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This results in multiple nested nav
elements if the menu has sub-menus. Additionally, some of the use cases for even non-nested menus probably shouldn't use a nav
(e.g. the browser actions menu in the example). I'd be a little inclined to just make it a div
and let users wrap it in a v('nav' ...)
themselves, but I suppose we could also add a property to control it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's also add an aria-labelledby
that points back to the label, just for funsies. (only if it's a dropdown)
src/menu/Menu.ts
Outdated
const { | ||
id = uuid(), | ||
nested, | ||
role = 'menu' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The top-level menu should have a role of menubar
, at least if it's visually persistent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, this depends on the default use case. If we are assuming that Menu
will be used predominantly for main app navigation, then, yes, menubar
should be the default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. It should probably be tied to whatever determines whether or not we use nav
, if we decide to control that.
src/menu/Menu.ts
Outdated
this._hideTimer && this._hideTimer.destroy(); | ||
this.toggleDisplay(true); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the dropdown can be opened on hover, we should probably consider having it open on focus as well
src/menu/Menu.ts
Outdated
if (!disabled && (key === 'Enter' || key === 13)) { | ||
this.toggleDisplay(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The menu should show when you hit the down, enter, or space keys. All of them should place focus on the first item in the sub menu when menu opens, and focus should return to the menu trigger before it closes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, and all the keypress
events should probably be keydown
so they catch arrow keys
src/menu/MenuItem.ts
Outdated
const { disabled, onKeypress } = this.properties; | ||
if (!disabled && typeof onKeypress === 'function') { | ||
onKeypress(event); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should manage key events in a little more depth:
- up/down arrow keys should move focus to the previous or next menu item, and wrap beginning <--> end.
- left/right should do the same on horizontal menus
- space/enter: activates the menu item and closes the dropdown menu (if applicable)
- space on a checkbox: toggles the checkbox but doesn't close the menu
- escape: closes the menu
src/menu/Menu.ts
Outdated
nested?: boolean; | ||
onRequestHide?: () => void; | ||
onRequestShow?: () => void; | ||
role?: Role; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably also have an orientation
attribute that defaults to horizontal
, at least for top-level menus.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An orientation
attribute makes sense, but given the generic use case for the Menu
widget, do we really want to make it the default? @bitpshr?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I wasn't all that sure about horizontal
either. I don't have strong opinions either way, I just suggested horizontal
because I think that's the default browser assumption for menus.
src/menu/Menu.ts
Outdated
overrideClasses: overrideClasses || css, | ||
onClick: this.onLabelClick, | ||
onKeypress: this.onLabelKeypress | ||
}, [ label ]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably have an id
, so the dropdown can refer back to it
393cdbe
to
5294a25
Compare
src/menu/MenuItem.ts
Outdated
selected ? css.selected : null | ||
); | ||
|
||
return v(tag, assign(Object.create(null), properties, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is necessary to pass the tag
through here, I don't believe we are doing this anywhere else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any need for the assign
call here over simple passing properties
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be fine with internally determining the tag name based on the specified type
property. As for assign
, it cleanly separates the properties that are specific to and required by MenuItem
from those needed only by the underlying DOM node and have nothing to do with Menu
functionality. Also, since other widgets (e.g., Button
) use generic property names for the aria-
properties, I tried to do the same thing here.
src/menu/MenuItem.ts
Outdated
'aria-haspopup': hasPopup ? 'true' : undefined, | ||
'aria-disabled': String(disabled), | ||
classes, | ||
key: 'menu-item', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the key
should be set by the parent or should at least use the menu item id
or there will be no difference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. Both ComboBox
and SlidePane
use a static key for controlling how onElementCreated
behaves.
src/menu/MenuItem.ts
Outdated
}), this.children); | ||
} | ||
|
||
protected onClick(event: MouseEvent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these are usually private
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct; initially I included all the functionality for dropdowns within Menu
, but after the widget logic became too complicated decided that it might be better to have a dedicated DropDown
widget that extends Menu
, in which case using private
limits what can be done.
src/menu/MenuItem.ts
Outdated
protected onKeydown(event: KeyboardEvent) { | ||
const { disabled, onKeydown } = this.properties; | ||
if (!disabled) { | ||
if (event.keyCode === 32) { // space |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you think it would be better to define a const SPACE_KEY = 32
and use that here instead of needing a comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for catching that. I thought I'd updated all of those.
src/menu/MenuItem.ts
Outdated
if (event.keyCode === 32) { // space | ||
(<HTMLElement> event.target).click(); | ||
} | ||
typeof onKeydown === 'function' && onKeydown(event); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pattern elsewhere for consistency is onKeyDown && onKeyDown(event)
.
Can onKeydown
take any form other than undefined or a function? Also do you think down
should be changed to have a capital D
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll update to onKeyDown
since that's the pattern used everywhere else.
src/menu/styles/menu.m.css
Outdated
@@ -0,0 +1,66 @@ | |||
@import '../../common/styles/variables.css'; | |||
|
|||
.container { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you change this to root
please?
src/menu/styles/menu.m.css
Outdated
transition: height 0.5s ease-in-out; | ||
} | ||
|
||
.hidden { height: 0; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that these classes should be using the base.css
classes. We should have a consistent approach to show
/ hidden
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately .hidden
in base.css
uses display: none
, which makes animation difficult; hence the need for the local classes. One option would be to change the class name to collapsed
, but when applied to drop downs hidden
feels more natural.
src/menu/styles/menu.m.css
Outdated
box-sizing: border-box; | ||
overflow: hidden; | ||
padding: 0 15px; | ||
transition: height 0.5s ease-in-out; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please use the short animation time
variable here
src/menu/MenuItem.ts
Outdated
protected onClick(event: MouseEvent) { | ||
const { disabled, onClick } = this.properties; | ||
if (!disabled && typeof onClick === 'function') { | ||
onClick(event); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be more advantageous to return the item id / key here instead of the click event? There's likely no need to expose the dom click event to the parent Menu
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Menu
widget does not need access to the event, but there may be instances in which an application needs to call event.preventDefault
on a MenuItem
click event.
- Use `properties.hidden` directly instead of passing it around. - Disambiguate `isHidden` to `requestShow` in `_toggleDisplay`. - Create an export base classes from Menu and MenuItem. - Remove underscore prefix from protected class members. - Fix doc comments for Menu/MenuItem properties. - Fix MenuItem child type.
- Remove the ability to specify `animate` as a function. - Remove `getAriaProperties` in favor of individual properties. - Simplify the MenuItem widget.
- Remove unused import - Remove menu/example.html - Fix example page - Use `KeyboardEvent#keyCode` where `key` is unavailable - Consolidate menu CSS into a single module - Ensure menu is scrolled to top when animating open
- Use `onElementCreated` and `onElementUpdated` - Correct use of `KeyboardEvent#keyCode` - Privacy updates
`MenuItem`: - Allow tag and vnode properties to be set. - Fix ARIA properties. - Allow different `role`s to be applied. `Menu`: - Update keyboard handling to more accurately match the WAI-ARIA spec. - Add handling for right/left arrows, space key. - Internalize active index management. - Add an `orientation` property.
- Rename event listener names for consistency with other widgets. - Change `container`/`nestedMenuContainer` CSS classes to `root`/`nestedMenuRoot`, respectively. - Use CSS variable for menu animation duration. - Simplify function property checks. - Only move focus from the label into the menu when navigating via the keyboard. - Add `hideOnActivate` options. - Always expand submenus when the designated "descend" key is pressed.
6d5884c
to
d291953
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like enough for round 2 :)
src/menu/Menu.ts
Outdated
onfocusout: this.onMenuFocusOut, | ||
onkeydown: this.onMenuKeyDown, | ||
role, | ||
tabIndex: this.state.active || label ? -1 : 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually don't think the container needs to have a tabIndex
at all. Semantically it doesn't make much sense, it creates extra work managing initial focus, and it should be more straightforward to just have a single menu item with tabIndex: 0
in menus without a label.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are also some weird things happening with my ability to focus on the second menu after I've opened and closed its sub-menu. I think everything would be simplified by always having exactly one top-level menu item with tabIndex: 0
, and directly moving focus as little as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on our private conversation, I can update MenuItem
to store a reference to its DOM node so that focus can be moved out of onElementUpdated
and changed in response to keyboard events. That way, this hack workaround can be removed.
src/menu/Menu.ts
Outdated
} = this.properties; | ||
|
||
if (id) { | ||
this._id = id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could use destructuring instead, i.e.
const {
id = this._id,
...
} = this.properties
src/menu/MenuItem.ts
Outdated
|
||
protected onElementUpdated(element: HTMLElement, key: string) { | ||
if (key === 'root') { | ||
this.properties.active && element.focus(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think one problem is properties.active
is controlling both current focus and tabIndex
. There are times when a menu item should have tabIndex: 0
without being the currently focused page element.
This is also messing with normal focus behavior. For example, clicking on something else should move focus, but it doesn't b/c this sets focus back on the menu.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MenuItem
s now have both an active
property for determining whether it should be focused and a focusable
property for determining the tabIndex
.
src/menu/MenuItem.ts
Outdated
'aria-controls': controls, | ||
'aria-expanded': String(expanded), | ||
'aria-haspopup': hasPopup ? 'true' : undefined, | ||
'aria-disabled': String(disabled), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aria-expanded
and aria-disabled
should only show up if true: 'aria-expanded': expanded ? 'true' : null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. The WAI-ARIA example always shows aria-expanded
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mwistrand yeah, sorry, it should be aria-expanded: hasPopup ? String(expanded) : null
return v(tag, assign(Object.create(null), properties, itemProperties), this.children); | ||
} | ||
|
||
protected onClick(event: MouseEvent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clicking on a menu item also needs to update it to active, same as navigating to it with the keyboard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/menu/MenuItem.ts
Outdated
const { disabled, onKeyDown } = this.properties; | ||
if (!disabled) { | ||
if (event.keyCode === SPACE_KEY) { | ||
(<HTMLElement> event.target).click(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to trigger a fake click on event.target
? Why not just do onClick && onClick(event)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If clicking a MenuItem
just opens a link in a new window, for example, then calling onClick
1) requires the MenuItem
to manually navigate to the underlying URL within its onClick
handler and 2) potentially causes problems with popup blockers that are circumvented by just calling click()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By "popup blocker", I mean built-in browser functionality.
src/menu/MenuItem.ts
Outdated
if (event.keyCode === SPACE_KEY) { | ||
(<HTMLElement> event.target).click(); | ||
} | ||
onKeyDown && onKeyDown(event); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be the first thing called, since onkeydown
would be called before onclick
with the enter key.
src/menu/Menu.ts
Outdated
return true; | ||
} | ||
|
||
return label ? true : false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why you need the if (label && disabled)
block, since it still boils down to label ? true : false
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol. You're right.
src/menu/Menu.ts
Outdated
this.toggleDisplay(); | ||
} | ||
else if (key === keys.descend) { | ||
this.toggleDisplay(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also need to preventDefault
if the key is up/down to prevent page scrolling, same for onkeydown
on menu items
src/menu/example/index.ts
Outdated
} | ||
}, | ||
onClick: toggleSelected, | ||
selected: <boolean> this.state[`${key}Selected`] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These seem like they're functioning as checkboxes with the selected
property. Should they have role: checkbox
even though the text seems to imply otherwise?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. Since they are using selected
, they should have role: checkbox
.
- general cleanup - remove tabIndex from menu container - reset active index on hide - move focus on click
- Prevent the default action when the down arrow key is pressed while the menu trigger is focused. - Add `parentOrientation` property to ensure that vertical submenus of horizontal menus are opened with down arrow key instead of the right arrow key.
Closing in favor of #104. |
Type: feature
The following has been addressed in the PR:
Description:
Provides the base
Menu
widget for #52. Once dojo/widget-core#415 lands, theDropDown
widget that extendsMenu
, and theNavBar
widget that relies on both can also be included to completely fulfill #52.