Skip to content
This repository was archived by the owner on Jul 3, 2023. It is now read-only.

Add select component#227

Merged
JosephusPaye merged 8 commits intomasterfrom
paye/select-component
Feb 7, 2019
Merged

Add select component#227
JosephusPaye merged 8 commits intomasterfrom
paye/select-component

Conversation

@JosephusPaye
Copy link
Member

@JosephusPaye JosephusPaye commented Feb 4, 2019

This PR adds a generic single select component. We might expand it to allow multi-select later.

Used in the robot selector for #208.

Screenshot of select component

Storybook

https://nusight-pr-227.herokuapp.com/storybook/?selectedKind=components.select&selectedStory=renders%20basic&full=0&addons=1&stories=1&panelRight=0&addonPanel=storybook%2Factions%2Factions-panel

@BrendanAnnable BrendanAnnable temporarily deployed to nusight-pr-227 February 4, 2019 05:28 Inactive
Copy link
Member

@BrendanAnnable BrendanAnnable left a comment

Choose a reason for hiding this comment

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

Nice!

Gave it a quick skim, I'll have another deeper look soon 🙂

)

return (
<OutsideClickHandler onOutsideClick={() => this.close()}>
Copy link
Member

@BrendanAnnable BrendanAnnable Feb 4, 2019

Choose a reason for hiding this comment

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

This render method currently creates functions every time it executes, this is generally to be avoided for performance reasons, ideally it should create none.

Aside: We should probably add the jsx-no-lambda tslint rule from https://github.com/palantir/tslint-react to make it easy to spot 🙂

Event handlers can be fixed by using the @action.bound annotation instead of just @action, then you can just refer to this.close here instead.

The child items (e.g. which use () => this.onSelect(option)) can be solved by splitting out each item into its own react component, such that each item has its own @action.bound onSelect() {} method that can access its own props. I can give an example if that makes no sense.

Copy link
Member Author

@JosephusPaye JosephusPaye Feb 4, 2019

Choose a reason for hiding this comment

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

That makes sense 👍. I'll update to use @action.bound, and extract a standalone select option component.

private removeListeners?: () => void

componentDidMount() {
const onKeydown = (event: KeyboardEvent) => this.onDocumentKeydown(event)
Copy link
Member

Choose a reason for hiding this comment

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

Just use property syntax on the method, then it doesn't need to be rebound here. e.g.

private readonly onDocumentKeydown = (event: KeyboardEvent) => {
  // ...
}

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

@observer
export class Select extends React.Component<SelectProps> {
@observable
private isOpen: boolean = false
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 think we will ever want to control the open-state of this component from outside?

Copy link
Member Author

Choose a reason for hiding this comment

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

Probably.

I couldn't think of a nice way to allow it to be controlled from outside (via a prop) while still being able to open/close it from inside, since changing the prop would violate the one-way data flow from parent to child.

Maybe there's a pattern I'm missing here.

Copy link
Member

Choose a reason for hiding this comment

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

Happy to leave it for now, but if we ever do, the pattern is generally to make 2 components:

  • The first is a pure/dumb component, no internal state, only reacts to its props. Basically just visual styling only.
  • The seconds wraps/controls the first, it manages all the state (it has all the observables and actions), and just delegates to the first.

https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

Copy link
Member Author

Choose a reason for hiding this comment

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

Noted.

onChange: action('onChange'),
}

const container = { maxWidth: '320px', fontFamily: 'Arial, sans-serif' }
Copy link
Member

Choose a reason for hiding this comment

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

Might as well just make this a component

const Container = ({ children }: { children: any }) => {
  <div style={{ maxWidth: '320px', fontFamily: 'Arial, sans-serif' }}>
    {children}
  </div>
}

Each story can then use

<Container>
  <Select props/>
</Container>

Copy link
Member Author

Choose a reason for hiding this comment

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

True 😄

@BrendanAnnable BrendanAnnable temporarily deployed to nusight-pr-227 February 5, 2019 05:25 Inactive
Also fix lint errors from the new rule
@BrendanAnnable BrendanAnnable temporarily deployed to nusight-pr-227 February 5, 2019 06:33 Inactive
@BrendanAnnable
Copy link
Member

Could we add an interactive story as well? Like the switch has 🙂

BrendanAnnable
BrendanAnnable previously approved these changes Feb 5, 2019
Copy link
Member

@BrendanAnnable BrendanAnnable left a comment

Choose a reason for hiding this comment

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

Some suggestions, but LGTM 🎉

@observer
export class Select extends React.Component<SelectProps> {
@observable
private isOpen: boolean = false
Copy link
Member

Choose a reason for hiding this comment

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

Happy to leave it for now, but if we ever do, the pattern is generally to make 2 components:

  • The first is a pure/dumb component, no internal state, only reacts to its props. Basically just visual styling only.
  • The seconds wraps/controls the first, it manages all the state (it has all the observables and actions), and just delegates to the first.

https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

}

private readonly onDocumentKeydown = (event: KeyboardEvent) => {
if (this.isOpen && event.keyCode === KeyCode.Escape) {
Copy link
Member

Choose a reason for hiding this comment

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

FYI you can now just use event.key === 'Escape'

http://keycode.info/ is handy.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

"jsx-use-translation-function": false,
"jsx-self-close": false,
"jsx-space-before-trailing-slash": false,
"jsx-wrap-multiline": false
Copy link
Member

Choose a reason for hiding this comment

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

I haven't really reviewed these settings, but can we revisit if they ever become a problem!

Copy link
Member Author

Choose a reason for hiding this comment

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

I disabled those rules since I only wanted the jsx-no-lambda, but they're all enabled by default when you extend the preset.

Enabling them might require some fixes across the codebase.


export type SelectProps = {
className?: string
dropdownMenuClassName?: string
Copy link
Member

@BrendanAnnable BrendanAnnable Feb 5, 2019

Choose a reason for hiding this comment

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

Passing classes directly into components is generally an anti-pattern. It breaks encapsulation.

It forces the consumer to provide a class which must understand the internal DOM structure of the component, which means you can't refactor the component without potentially breaking consumers who put weird CSS properties all over the place.

It's better for the component to remain responsible for styling itself, and allow consumers to set various "styles" via a constrained set of props.

I'm not sure what we plan to use this for, but for example instead of passing a className to increase the padding of an element, it would be better to instead pass in paddingStyle: 'compact' | 'wide' prop and let the component own the CSS styling.

Copy link
Member Author

Choose a reason for hiding this comment

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

We need it to style the dropdown (width, shadows, etc), since the basic dropdown component only sets the position.

Maybe we can move that there.

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed as it wasn't needed.

Also removed the dropdownMenuClassName prop on the Dropdown component, replaced with two new props:

  • isFullwidth: boolean: which adds a class for width: 100%`
  • dropdownPosition: 'left' | 'right': adds a class which styles right-aligned dropdowns, used by the current robot selector

Instead use other props which handle the styling internally.

Also remove KeyCode enum
@BrendanAnnable BrendanAnnable temporarily deployed to nusight-pr-227 February 7, 2019 03:35 Inactive
@JosephusPaye
Copy link
Member Author

@BrendanAnnable Added an interactive story and made other suggested changes. I think it's ready to merge.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants