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

Listbox #312

Merged
merged 26 commits into from
Nov 6, 2017
Merged

Listbox #312

merged 26 commits into from
Nov 6, 2017

Conversation

smhigley
Copy link
Contributor

@smhigley smhigley commented Oct 17, 2017

Type: feature

The following has been addressed in the PR:

  • There is a related issue
  • All code matches the style guide
  • Unit or Functional tests are included in the PR

Description:

Refactors Select and ComboBox to use a common Listbox widget. Resolves #117 and #102. This PR also introduces some of the discussed changes to widget patterns, namely splitting the render() function into more granular, overridable functions.

@@ -0,0 +1,264 @@
import { WidgetBase } from '@dojo/widget-core/WidgetBase';
Copy link
Contributor

Choose a reason for hiding this comment

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

Imports should be alphabetical

active?: boolean;
classes?: (string | null)[];
disabled?: boolean;
label: DNode;
Copy link
Contributor

Choose a reason for hiding this comment

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

Alphabetize

import * as css from './styles/listbox.m.css';

/* Listbox Option sub-widget */
export interface ListboxOptionProperties extends ThemeableProperties {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can/should this be moved to its own file?

Copy link
Contributor Author

@smhigley smhigley Oct 31, 2017

Choose a reason for hiding this comment

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

Good question 😂 . It used to be, then after talking with Ant/Matt G, the conclusion was that sub-widgets shouldn't really exist because they don't stand on their own, except that they make handling certain events much easier. I moved it out of its own file so it would seem less like a standalone widget, but I agree that it seemed better organized when separate

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved back to its own file. We'll see what happens 😄

* Properties that can be set on a Listbox component
*
* @property activeIndex Index of the currently active listbox option
* @property customOption Custom widget constructor for options. Should extend ListboxOption
Copy link
Contributor

Choose a reason for hiding this comment

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

customOption isn't in this interface

describedBy?: string;
visualFocus?: boolean;
id?: string;
multiselect?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

multiselect not in TypeDoc

active: activeIndex === index,
classes: this.getOptionClasses(activeIndex === index, disabled, selected),
disabled,
label: this.renderOptionLabel(option, index),
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider moving this above (alongside the getOptionDisabled and getOptionSelected to standardize.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should I break out id as well? I originally only pulled out disabled and selected because they're also used to get the state classes whereas label/id are only used once.

activeIndex = 0,
key = '',
optionData = [],
getOptionDisabled = (option: any, index: number) => false,
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider pulling this out into a reusable function at the top of the file

import { ThemeableMixin, ThemeableProperties, theme } from '@dojo/widget-core/mixins/Themeable';
import { WidgetBase } from '@dojo/widget-core/WidgetBase';
import { diffProperty } from '@dojo/widget-core/decorators/diffProperty';
import { reference } from '@dojo/widget-core/diff';
import ResultItem from './ResultItem';
import ResultMenu from './ResultMenu';
import Listbox from '../listbox/Listbox';
Copy link
Contributor

Choose a reason for hiding this comment

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

Alphebetize

private _getResultLabel(result: any) {
const { getResultLabel } = this.properties;

return getResultLabel ? getResultLabel(result) : result;
return getResultLabel ? getResultLabel(result) : `${result}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this just be String(result)?

Copy link
Member

Choose a reason for hiding this comment

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

Template strings are the most optimal way to do string conversion now.

* @property disabled Prevents the user from interacting with the form field
* @property invalid Indicates the value entered in the form field is invalid
* @property label Label settings for form label text, position, and visibility
* @property multiple Whether the widget supports multiple selection
Copy link
Contributor

Choose a reason for hiding this comment

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

How is this handled now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Listbox on its own is effectively a custom multiselect widget, and the native multiselect is so universally awful that I don't think we need to go out of our way to support it.


@diffProperty('activeIndex', auto)
protected calculateScroll(previousProperties: ListboxProperties, { activeIndex = 0 }: ListboxProperties) {
const scrollOffset = this.meta(Dimensions).get('root').scroll.top;
Copy link
Member

Choose a reason for hiding this comment

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

This should be a single call to this.meta(Dimensions) not multiple

Copy link
Member

@bitpshr bitpshr left a comment

Choose a reason for hiding this comment

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

Partial review. Things look good. I'm mostly confused by the apparent removal of certain pieces of functionality, like multiple on the Select, and the ability to pass custom options and menus to components. If this is supported, it's not immediately obvious based on the properties interfaces for each component that uses ListBox. I also had some various questions.

@@ -61,68 +59,14 @@ const data = [
{ value: 'West Virginia' }
];

class CustomResultItem extends ResultItem {
Copy link
Member

Choose a reason for hiding this comment

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

Why was the example of a custom result item removed?

}
}

class CustomResultMenu extends ResultMenu {
Copy link
Member

Choose a reason for hiding this comment

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

Why was the example of a custom result menuremoved?

}

@diffProperty('activeIndex', auto)
protected calculateScroll(previousProperties: ListboxProperties, { activeIndex = 0 }: ListboxProperties) {
Copy link
Member

Choose a reason for hiding this comment

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

What is the purpose of this function? Is this what _scrollIntoView used to do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

More or less. I separated out calculateScroll and animateScroll so that the latter would be easy to override (e.g. to animate the scroll)

}

protected onElementUpdated(element: HTMLElement, key: string) {
if (key === 'root' && typeof this._scroll === 'number') {
Copy link
Member

Choose a reason for hiding this comment

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

Can you refactor this with the login in onElementCreated?

onOptionSelect
} = this.properties;

const disabled = getOptionDisabled(option, index);
Copy link
Member

Choose a reason for hiding this comment

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

It looks like you call this function a few different places, and also define it in each of those places. Could you pull this out into a static function somewhere? Or consider using a default property.

Copy link
Member

Choose a reason for hiding this comment

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

Never mind, default properties haven't landed.

import dojoTheme from '../../themes/dojo/theme';

interface CustomOption {
value: string;
Copy link
Member

Choose a reason for hiding this comment

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

Nit, but alphabetize

getOptionLabel: (option: CustomOption) => option.label,
getOptionDisabled: (option: CustomOption) => !!option.disabled,
getOptionSelected: (option: CustomOption) => !!option.selected,
onActiveIndexChange: (index: number) => {
Copy link
Member

Choose a reason for hiding this comment

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

I thought the types of the arguments for these callbacks could be inferred now, but I could be insane.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I get implicit any errors without the type ¯_(ツ)_/¯

@@ -18,382 +17,332 @@ import * as iconCss from '../common/styles/icons.m.css';
*
* Properties that can be set on a Select component
*
* @property CustomOption Custom widget constructor for options. Should use SelectOption as a base
Copy link
Member

Choose a reason for hiding this comment

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

Is this no longer supported?

this.invalidate();
}

protected onElementCreated(element: HTMLElement, key: string) {
protected onElementUpdated(element: HTMLElement, key: string) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we remove reliance on onElement* methods? Technically they're sort of kind of deprecated, though the message was temporarily removed. I'm wondering if we can leverage meta in a way that negates the need for these altogether.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would the animation meta be able to handle scroll? I suppose we could write our own within Listbox, but I'd rather use something that exists, if we can.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved to meta 👍

}

export const SelectBase = ThemeableMixin(WidgetBase);

@theme(css)
@theme(iconCss)
@diffProperty('options', reference)
Copy link
Member

Choose a reason for hiding this comment

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

Was this working incorrectly before explicitly diffing by reference?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, it doesn't call a render if you update the options property otherwise.

}

protected animateScroll(element: HTMLElement, scrollValue: number) {
element.scrollTop = scrollValue;
Copy link
Member

Choose a reason for hiding this comment

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

I think the correct way to do this would be to create a local scrollTop meta within this file that takes a key and a scrollValue. That would avoid the need to use onElementCreated / onElementUpdated

@kitsonk kitsonk removed this from the 2017.10 milestone Oct 30, 2017
@codecov
Copy link

codecov bot commented Oct 31, 2017

Codecov Report

Merging #312 into master will decrease coverage by 0.2%.
The diff coverage is 98.15%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master    #312      +/-   ##
=========================================
- Coverage   99.01%   98.8%   -0.21%     
=========================================
  Files          24      22       -2     
  Lines        1718    1753      +35     
  Branches      447     469      +22     
=========================================
+ Hits         1701    1732      +31     
+ Misses          1       0       -1     
- Partials       16      21       +5
Impacted Files Coverage Δ
src/timepicker/TimePicker.ts 100% <ø> (ø) ⬆️
src/main.ts 100% <100%> (ø) ⬆️
src/select/Select.ts 97.57% <97.14%> (-1.87%) ⬇️
src/combobox/ComboBox.ts 97.36% <97.95%> (-0.89%) ⬇️
src/listbox/Listbox.ts 99.16% <99.16%> (ø)
src/slidepane/SlidePane.ts 98.82% <0%> (+2.35%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a5765e1...3652b59. Read the comment docs.

@codecov
Copy link

codecov bot commented Oct 31, 2017

Codecov Report

Merging #312 into master will decrease coverage by 0.19%.
The diff coverage is 98.5%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master     #312     +/-   ##
=========================================
- Coverage   99.03%   98.84%   -0.2%     
=========================================
  Files          24       23      -1     
  Lines        1866     1901     +35     
  Branches      480      488      +8     
=========================================
+ Hits         1848     1879     +31     
- Partials       18       22      +4
Impacted Files Coverage Δ
src/timepicker/TimePicker.ts 100% <ø> (ø) ⬆️
src/listbox/ListboxOption.ts 100% <100%> (ø)
src/select/Select.ts 97.57% <97.14%> (-1.87%) ⬇️
src/listbox/Listbox.ts 99.03% <99.03%> (ø)
src/combobox/ComboBox.ts 98.4% <99.04%> (+0.14%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update bae1322...96ec0af. Read the comment docs.

id?: string;
multiselect?: boolean;
optionData?: any[];
ScrollMeta?: WidgetMetaConstructor<DefaultScroll>;
Copy link
Member

Choose a reason for hiding this comment

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

do you really think this might be passed in as a property? I think it's more likely that we end up moving it out into widget-core/meta entirely if it's used in multiple places.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just want to make sure it's easy to customize, e.g. to animate the scroll or something. Do you think there's anything wrong with passing it in as a property? I suppose just having protected animateScroll could be enough, since someone could override it.

Copy link
Member

Choose a reason for hiding this comment

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

To me it feels somewhat overkill to provide both an extension point and an injection point. Should we not simply choose one pattern and stick to it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Works for me. I'm fine removing the prop and leaving protected animateScroll as the point of extension 👍

const menuDimensions = this.meta(Dimensions).get('root');
const scrollOffset = menuDimensions.scroll.top;
const menuHeight = menuDimensions.offset.height;
const optionOffset = this.meta(Dimensions).get(this._getOptionId(activeIndex)).offset;
Copy link
Member

Choose a reason for hiding this comment

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

does _getOptionId return an ID or a KEY?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It returns a unique string that's used in two places, once for an id and once for a key

Copy link
Member

@bitpshr bitpshr left a comment

Choose a reason for hiding this comment

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

@smhigley this is looking really good now. One question: was logic in onElement* methods actually moved to meta as commented? I still see the same focus functionality in those deprecated methods using direct DOM access. Other than that, I think we're at a point where merging this and fixing any nits after the fact is a good thing to do. Once the random CI failure about suite error is fixed :)

@smhigley smhigley merged commit b8a27c4 into dojo:master Nov 6, 2017
@dylans dylans added this to the beta.4 milestone Jan 10, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants