-
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
TitlePane #26
TitlePane #26
Conversation
Codecov Report
@@ Coverage Diff @@
## master #26 +/- ##
==========================================
- Coverage 99.24% 99.17% -0.07%
==========================================
Files 18 19 +1
Lines 928 972 +44
Branches 279 295 +16
==========================================
+ Hits 921 964 +43
Misses 1 1
- Partials 6 7 +1
Continue to review full report at Codecov.
|
}; | ||
|
||
export type TitlePane = Widget<TitlePaneProperties> & ThemeableMixin & { | ||
onClickTitle?(): void; |
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.
Event handlers shouldn't be optional since you're the widget author and you know they'll always be present.
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, fixed!
collapsible?: boolean; | ||
enterAnimation?: string; | ||
exitAnimation?: string; | ||
title: 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.
title
should probably be an optional property like it is on the Dialog. It looks like you're already correctly defaulting it to an empty string in getChildrenNodes
.
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.
Should it? I feel like if you haven't specified title, you've chosen the wrong widget.
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.
Fair. In that case, there's no need to default it when you destructure in getChildrenNodes
.
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 tried just now but v()
doesn't accept undefined
as a child, so we either have to cast to string when passing it as the child to v('div', ..., [ String(title) ])
or default it... I'm a little preferring "default it".
mixin: { | ||
baseClasses: css, | ||
|
||
onClickTitle: function (this: TitlePane) { |
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.
Should this be named onTitleClick
instead? Just preference, but it would be good to stay consistent with other components. Also, all of these functions can be written without the function
keyword:
...
onTitleClick(this: TitlePane) {
// ...
}
...
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 gave this some thought as I was writing it and I think "on<Action><Qualifier>" makes the most sense. I feel like I also found existing usage of this form, but I can't recall where.
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.
Whatever you prefer, just try to make sure the existing components are updated as well to stay consistent.
properties: { | ||
collapsed = false | ||
} | ||
} = this; |
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 following may be cleaner, negating the need for this.properties
four other times in this function:
const {
collapsed = false,
onRequestExpand,
onRequestCollapse
} = 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.
I'm not a fan of local variables without good justification:
- local default:
collapsed
gets a local default offalse
, the others are used as-is - frequent repeated access: name lookup time is reduced with a local variable
- excessive verbosity: more subjective, but for me
this.properties.someThing
does not cross the threshold of being too verbose
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 argue that having to use this.properties.someThing
four additional times, well as a lengthier destructuring, is excessively verbose...
} | ||
}, | ||
|
||
getChildrenNodes: function (this: TitlePane): DNode[] { |
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 you rebase against master, widgets will be using the most recent release of widget-core. Now, getChildrenNodes
is replaced by a render
function. This function should return a single top-level DNode
, the children of which should be what you're currently returning here. See the Dialog or SlidePanel for reference.
v('div', titleProperties, [ title ]) | ||
]; | ||
|
||
if (!collapsed) { |
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 if (collapsible && !collapsed)
so a non-collapsible TitlePane can't be collapsed programmatically.
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, good catch!
} | ||
}); | ||
|
||
export default createTitlePane; |
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 care what suffix we use ("Panel" vs. "Pane" vs. "BestDamnLayoutWidget"), but we should stay consistent with other components.
background-color: var(--title-bar-bg); | ||
border-color: var(--title-bar-bg); | ||
border-style: solid; | ||
border-width: 1px; |
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 be a CSS variable, e.g. var(--title-pane-border-width)
. Don't worry about getting specific.
border-width: 1px; | ||
color: var(--title-bar-color); | ||
font-size: var(--title-font-size); | ||
padding: 5px; |
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 be a variable.
.content { | ||
border-color: var(--title-bar-bg); | ||
border-style: solid; | ||
border-width: 1px; |
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 be the same variable used above for the .title
border width.
Something that still needs to be figured out is how to animate collapse/expand.
|
baseClasses: css, | ||
|
||
onClickTitle(this: TitlePanel) { | ||
const { |
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 tempted to drop the nested destructure in favour of const { collapsed } = this.properties
.
Could also destructure out onRequestExpand
and onRequestCollapse
. You could even consider defaulting them both to a noop
such that they can both just be called without the checks.
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.
+1
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.
Allocating extra function objects just to avoid null checks doesn't strike me as a good approach.
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.
Mangala, you don't have to use noop defaults if you don't want to (I didn't in SlidePane), but I'd still destructure fully to avoid the four extra uses of 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.
I prefer the clarity of knowing precisely where the information is coming from at the time it's being used, and don't find this case to be excessively verbose, but I can change it.
this.properties.onRequestClose
- at a glance I know exactly where onRequestClose
came from
onRequestClose
- at a glance I don't know where this came from, I have to look at other code
mixin: { | ||
baseClasses: css, | ||
|
||
onClickTitle(this: TitlePanel) { |
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 the naming convention for this should be onTitleClick
, but I could be wrong.
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 figure out and document a convention - I think on<Action><Qualifier>
is preferable myself.
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.
Consensus in the Dojo 2 room was for on<Qualifier><Action>
, so onTitleClick
.
import * as animations from '../../styles/animations.css'; | ||
|
||
export interface TitlePanelProperties extends WidgetProperties { | ||
collapsed?: 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.
I'd recommend swapping collapsed
for open
as we've used open
elsewhere and it seems that you're needing to negate collapsed
for most use cases.
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.
+1, and maybe closeable
instead of collapsible
.
@import '../../../styles/variables.css'; | ||
|
||
:root { | ||
--title-panel-border-width: 1px; |
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 these are likely standard component variables / styling and thus should go to variables.css
(with different names)
import * as animations from '../../styles/animations.css'; | ||
|
||
export interface TitlePanelProperties extends WidgetProperties { | ||
collapsed?: 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.
I'd recommend swapping collapsed
for open
as we've used open
elsewhere and it seems that you're needing to negate collapsed
for most use cases.
src/styles/animations.css
Outdated
} | ||
} | ||
|
||
.slideInDown { |
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 need to change this name, in animate-js
this is the class / naming for sliding something in and down from off the page. I think this should be slideDown
or expandDown
We likely need to be cleverer than this though as we want to make sure the text doesn't move about as we expand and it may be better to fade that in at the same time? Worth playing with a little. Also, where did 300px
come from for a max height? Seems restrictive, what are other frameworks / solutions doing?
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 they're all doing $(node).collapsible
, but I don't think we want to go that way? 😉
I know this solution is not good and would like some more discussion on what we want to do. Are we OK with programmatic animations? Not sure there's a good way to do this in CSS alone.
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 were originally sliding animations, which personally I think is a nice effect, but collapse/expand seems more common for accordions, and in any case, what we currently have is collapse/expand. Renamed to expandDown
and collapseUp
.
import * as animations from '../../styles/animations.css'; | ||
|
||
export interface TitlePanelProperties extends WidgetProperties { | ||
collapsed?: 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.
I'd recommend swapping collapsed
for open
as we've used open
elsewhere and it seems that you're needing to negate collapsed
for most use cases.
v('div', titleProperties, [ title ]) | ||
]; | ||
|
||
if (collapsible && !collapsed) { |
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 you need to have collapsible
here, would be better if !collapsed
was replaced with open
and defaulted to 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.
collapsible
is necessary so a non-collapsible TitlePane can't be closed programmatically.
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 change from collapsible
to closeable
destroyed this logic, which is fine with me - as long as we enforce UI consistency, I feel OK with allowing programmatic actions that are disabled in the UI.
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 logic should still work:
if (!closeable || (closeable && open)) {
...
}
}, this.children)); | ||
} | ||
|
||
return v('div', { |
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.
No need for the children
array you created, you can do this:
return v('div', {
classes: this.classes(css.main).get()
}, [
v('div', titleProperties, [ title ]),
!collapsed ? v('div', {
classes: this.classes(css.content).get(),
enterAnimation,
exitAnimation
}, this.children): 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.
I don't like ternary usage except for very simple and short (in character count) cases. Perhaps it's a matter of preference between a functional/declarative approach versus procedural. I'd rather read more lines of code that are each simple, grokkable at a glance, and sequentially and clearly build up to a solution than fewer lines of code that have to be stared at for quite some time to decipher what all is being accomplished.
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 being so, the example given above is consistent with how widgets and examples of v
are shown. The null
is filtered out by v
.
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.
So how about we improve widget and v
examples?
@@ -0,0 +1,90 @@ | |||
import { VNodeProperties } from '@dojo/interfaces/vdom'; |
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.
Before we land this, can we also add a11y
support please?
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 will have good suggestions for what's needed. You can omit focus / tabindex stuff for now until we get a focus manager.
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 add specific comments, but I'm working off the a11y specs in https://www.w3.org/TR/wai-aria-practices-1.1/#accordion
@msssk, looks like we decided to go with Pane, not Panel. Just a heads up. master will be updated shortly. |
titleProperties = { | ||
classes: this.classes(css.title, css.collapsible).get(), | ||
onclick: this.onClickTitle | ||
}; |
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 title should have a role="heading"
and ideally an aria-level="#"
. The heading level may need to be added to properties, if we use it. The title div should also wrap a button
element with the following properties:
aria-controls="id of content div"
aria-expanded: boolean
set tocollapsed
aria-disabled: boolean
set to!collapsible
id
attribute for the content div to reference
classes: this.classes(css.content).get(), | ||
enterAnimation, | ||
exitAnimation | ||
}, 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.
The content div needs an id
for the title to reference, as well as aria-labelledby="id of title button"
|
||
if (open) { | ||
// FIXME - id of content node | ||
titleButtonProperties['aria-controls'] = 'content'; |
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 button should actually have aria-controls="id string"
set all the time, both open and closed. (Incidentally, I believe Matt G recommended uuid
to generate unique id's, if it helps)
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 seems very awkward to me since with vdom, in the closed state there is no content pane existing in the DOM. So we set aria-controls="<id of non-existent element>"
?
const children: DNode[] = [ | ||
v('div', titleProperties, [ title ]) | ||
v('div', titleProperties, [ | ||
v('div', titleButtonProperties, [ title ]) |
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.
Nitpicking, sorry! I personally think a button
element is preferable to a div
, but if you prefer the div, it'll also need tabindex="0"
set (a focus manager wouldn't set initial tabindex
values). You'd also need to make sure keyboard events (enter and space) trigger the content opening -- I'm not completely sure they work by default on a fake button.
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 left it as a div since the w3 example doesn't use a button, and I wanted to avoid having to override button styles. But now I realize their example uses <a>
, which gets much of the behavior that button has. I'd rather override button styles than reimplement button behavior, so I'll use <button>
here.
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.
Looking good Mangala.
let titleButtonProperties: any = { | ||
'aria-disabled': String(!closeable), | ||
'aria-expanded': String(open), | ||
// FIXME - set unique 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.
You can use @dojo/core/uuid
for this, see the Dialog for an example.
baseClasses: css, | ||
|
||
onClickTitle(this: TitlePanel) { | ||
const { |
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.
+1
]) | ||
]; | ||
|
||
if (open) { |
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.
Thinking about it more, a non-closeable Dialog can be closed programmatically, so this is fine.
mixin: { | ||
baseClasses: css, | ||
|
||
onClickTitle(this: TitlePanel) { |
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.
Consensus in the Dojo 2 room was for on<Qualifier><Action>
, so onTitleClick
.
Can you please drop the camelCase naming on the folder name. ie. |
}; | ||
} | ||
else { | ||
titleProperties = { |
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.
does this not break the second rule of maquette? (and elsewhere for these properties?) http://maquettejs.org/docs/rules.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.
Thanks!
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 a good reason in itself not to procedurally build up properties btw 😄
src/examples/titlePanel/index.html
Outdated
<body> | ||
<script src="../../../../node_modules/@dojo/loader/loader.min.js"></script> | ||
<!-- TODO: should come from project dependency --> | ||
<script src="https://cdnjs.cloudflare.com/ajax/libs/maquette/2.4.2/css-transitions.min.js"></script> |
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've seen this elsewhere in the codebase but i'll ask anyway, is there any reason this cannot be loaded from node_modules/maquette/dist/css-transitions.min.js
?
}; | ||
|
||
@theme(css) | ||
export default class TitlePane extends ThemeableMixin(WidgetBase)<TitlePaneProperties> { |
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 need to set the result of ThemeableMixin(WidgetBase)
to a const
and use the result to extend from.
const TitlePaneBase = ThemeableMixin(WidgetBase);
export default class TitlePane extends TitlePaneBase<TitlePaneProperties> {
|
||
@theme(css) | ||
export default class TitlePane extends ThemeableMixin(WidgetBase)<TitlePaneProperties> { | ||
onClickTitle(event: MouseEvent & TouchEvent) { |
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 the event
param?
Test coverage is up to 100%. Outstanding issues to resolve:
|
Either the content or an element containing the content needs to be always present in the DOM, and its ID should be set in
I agree, I've never liked the max-height trick, but all the other options involve calculating height. What I've done in the past is clone the content node and position it offscreen with the correct width and box styles, and use it to calculate height. (@msssk ^) |
Thanks @smhigley that makes sense - DOM structure is now consistent, it's only the content nodes that are omitted when collapsed. Now the remaining issues are:
|
src/titlepane/TitlePane.ts
Outdated
onclick: closeable ? this.onClickTitle : undefined, | ||
role: 'heading' | ||
}, [ | ||
v('div', { |
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 the changes look good! One very, very last thing and then I'm satisfied a11y-wise. The button
just needs to either be an element that takes focus by default (<button>
or <a>
), or have tabindex
set to 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.
Thanks, done! (tabIndex
is 0, but if closeable
is false then tabIndex
is -1)
@msssk the changes look great! I think the For animations, maybe |
- Move titlePane.css to titlePane.m.css - Fix typo - Lowercase component attribute name(s)
- Destructure state variables in the example - Change "titlePanel" to "titlePane" in example `key`s - Store IDs as private variables - Fix typo in doc comments - Use `Keys` from `common/util`
Type: bug / feature
The following has been addressed in the PR:
Description:
This PR introduces a TitlePane component very similar to Dijit's TitlePane.
A title pane should be a suitable child for an Accordion widget.