Skip to content
This repository has been archived by the owner on Mar 4, 2020. It is now read-only.

feat(Input): adding variation for clearable input #37

Merged
merged 12 commits into from
Aug 21, 2018

Conversation

alinais
Copy link
Contributor

@alinais alinais commented Aug 1, 2018

Input clear icon variation

Adding variation for clearing the input on click on Cancel icon

TODO

  • Conformance test
  • Minimal doc site example
  • Stardust base theme
  • Teams Light theme
  • Teams Dark theme
  • Teams Contrast theme
  • Confirm RTL usage
  • W3 accessibility check
  • Stardust accessibility check
  • Update glossary props table
  • Update the CHANGELOG.md

API Proposal

default:
image

typing text:
image

click on remove text:
image

Copy link
Member

@levithomason levithomason left a comment

Choose a reason for hiding this comment

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

I've left some comments on cleaning up the code for the current direction.

I would also like to discuss changing the course a little bit. It would be ideal if the user could just add a clearable prop to the Input and get the behavior for free. In that case, it would achieved using the AutoControlledComponent for now. Feedback?

class InputExampleIconChangeShorthand extends React.Component<
{},
{ icon: string; inputValue: string }
> {
Copy link
Member

Choose a reason for hiding this comment

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

No typings in examples, please. The public facing code is not typescript but JS. We want to advertise the most minimal syntax possible to users in order to focus on our features. We also don't want to assume they are using typescript since this is a small percentage of the community.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

icon: 'search',
inputValue: '',
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Can be simplified to:

state = { value: '' }

For simplicity, I've also renamed the inputValue key above to value to reduce more boilerplate below.

I removed icon as there is no need to persist this on state since it can be computed inline in the render function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

icon: value ? 'close' : 'search',
inputValue: value,
})
}
Copy link
Member

Choose a reason for hiding this comment

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

Example refactor given the above comment:

handleChange = (e, { value }) => this.setState({ value })

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

placeholder="Search..."
value={inputValue}
onChange={this.handleChange}
onIconClick={e => this.onIconClick(e, icon)}
Copy link
Member

Choose a reason for hiding this comment

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

Avoid creating functions in the render method. This is an anti-pattern since it causes the prop to receive a new value on every render. This means you cannot compare the previous and current value of this prop for optimizations.

That said, I'm not sure we should have this prop since the user can pass an onClick prop to the icon shorthand prop:

<Input icon={{ name: 'search', onClick: this.handleIconClick }} />

Thoughts?

Copy link
Contributor Author

@alinais alinais Aug 2, 2018

Choose a reason for hiding this comment

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

Cannot think of a real example of the usage of the onIconClick handler right now. I believe that the callback can be added to the onClick property of an Icon and same thing should happen. I can remove the extra handler for now and add it further if needed.

public onIconClick = (e, value) => {
if (value === 'close') {
this.setState({
icon: 'search',
Copy link
Member

Choose a reason for hiding this comment

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

It is safe to not have icon here since setting value to empty will result in a re-render and the correct icon will be derived from state.

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 introduced the clearable prop that will take care of it


return (
<Input
icon={icon}
Copy link
Member

Choose a reason for hiding this comment

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

Per the above, you can this: icon={value ? 'close' : 'search'}.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

change the approach

...icon,
...(icon.onClick && { tabIndex: '0' }),
}),
}
Copy link
Member

Choose a reason for hiding this comment

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

You can avoid the logic required to create these props by adding the tabIndex in the overrideProps function:

computeTabIndex = props => {
  if (!_.isNil(props.tabIndex)) return props.tabIndex
  if (props.onClick || props.onIconClick) return 0
}

Icon.create(icon, {
  overrideProps: predefinedProps => ({
    tabIndex: this.computeTabIndex(predefinedProps)
  })
})

The predefinedProps handed to the overrideProps function are the final computed props of the factory function. The factory has already taken into account all factors necessary to compute the final props.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sounds like a good idea, thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

@alinais alinais force-pushed the feature/input-component-clearable-icon-variation branch from da9d255 to 47dbe7c Compare August 3, 2018 10:37
@codecov
Copy link

codecov bot commented Aug 3, 2018

Codecov Report

Merging #37 into master will decrease coverage by 0.01%.
The diff coverage is 72.72%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #37      +/-   ##
==========================================
- Coverage   88.39%   88.37%   -0.02%     
==========================================
  Files          45       45              
  Lines         741      757      +16     
  Branches       97      109      +12     
==========================================
+ Hits          655      669      +14     
- Misses         83       85       +2     
  Partials        3        3
Impacted Files Coverage Δ
src/components/Input/Input.tsx 78.68% <72.72%> (+3.13%) ⬆️

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 f5b35af...45fc37f. Read the comment docs.

@alinais
Copy link
Contributor Author

alinais commented Aug 6, 2018

@levithomason I addressed the requested changes. Please have a look.

value: props.value || '',
}

this.handleChange = this.handleChange.bind(this)
Copy link
Member

Choose a reason for hiding this comment

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

Looks like useless line because handleChange is an arrow function.

https://medium.com/@machnicki/handle-events-in-react-with-arrow-functions-ede88184bbb

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed


computeTabIndex = props => {
if (!_.isNil(props.tabIndex)) return props.tabIndex
if (props.onClick) return 0
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 that desctruction is preferred in this case.

const inputValue = _.get(e, 'target.value')
const { clearable } = this.props

_.invoke(this.props, 'onChange', e, { ...this.props, inputValue })
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 that inputValue should be just value, too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

public render() {
return <Input icon="search" clearable placeholder="Search..." />
}
}
Copy link
Member

Choose a reason for hiding this comment

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

This example can be a stateless component.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

@alinais alinais force-pushed the feature/input-component-clearable-icon-variation branch 3 times, most recently from 937af84 to 0e8584e Compare August 8, 2018 11:00
@@ -9,6 +9,11 @@ const Variations = () => (
description="An input can have an icon."
examplePath="components/Input/Variations/InputExampleIcon"
/>
<ComponentExample
title="Clearable icon"
Copy link
Member

Choose a reason for hiding this comment

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

"Clearable", titles should be the prop name when possible.

this.state = {
value: props.value || '',
}
}
Copy link
Member

@levithomason levithomason Aug 9, 2018

Choose a reason for hiding this comment

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

Looks like we have a default prop here. Also, no need for a constructor:

state = { value: this.props.value }

static defaultProps = { value: '' }

This will ensure the default value is advertised.


_.invoke(this.props, 'onChange', e, { ...this.props, value })

this.setState({ value })
Copy link
Member

Choose a reason for hiding this comment

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

If the user passes a value prop, their value will no longer be respected. Clear should only work if the user does not have a value prop set.

You can paste this in an example to try it:

import React from 'react'
import { Input } from '@stardust-ui/react'

const InputExampleIconClearableShorthand = () => (
  <Input
    clearable
    icon="search"
    placeholder="Search..."
    value='keep me'
  />
)

export default InputExampleIconClearableShorthand

The value of props should always be reflected in the DOM. This is idiomatic React. Just as when using an input and passing a value prop, the input does not update when typing in it unless you capture that event and update the prop also. User props always win.

The AutoControlledComponent will handle this logic for you if you want to use that for now. Otherwise, you will need to implement it yourself until we get the state manager for inputs. I would not advise doing that as there are many edge cases (see AutoControlledComponent).

@@ -9,6 +9,11 @@ const Variations = () => (
description="An input can have an icon."
examplePath="components/Input/Variations/InputExampleIcon"
/>
<ComponentExample
title="Clearable icon"
description="An input can have a search icon that can change into clear button on typing."
Copy link
Member

@levithomason levithomason Aug 9, 2018

Choose a reason for hiding this comment

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

This feature is only showing that an input can make its value clearable.

The use of the search icon is unrelated and should not be in the description. In fact, it may warrant a separate example showing that the clearable icon will override a normal icon when there is a value. The simplest example doesn't actually require an existing icon in the input, correct?

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'm trying to imagine the example where the input is clearable and doesn't have an icon. I might misunderstand the original ask...

Copy link
Member

Choose a reason for hiding this comment

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

For example, I would expect as a user that I could do this:

<Input clearable />

And it would result in an input that can clear its value. As a user, I do not have to also supply an icon='search' prop in order for the clearable feature work. I can supply one if I wish, and it will be replaced with an x when there is an input value, but it is not required for the clearable feature to work.

SUIR Loading Input

Here's a corresponding example from SUIR. An Input can be in the loading state. The input can be defined with or without an icon prop. Here is the same loading input, but shown with the icon defined:

<Input placeholder='With an icon' icon='search' loading />
<Input placeholder='Without an icon' loading />

Here I am toggling the loading prop:

http://g.recordit.co/TnO9WfAVBH.gif

The clearable prop would work similar. The use of the icon='search' is not required for the clearable icon to show up when there is a value.

Copy link
Member

@levithomason levithomason left a comment

Choose a reason for hiding this comment

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

Let's clean up docs a bit. Also, the user's value prop is not being respected. See comments.

@alinais alinais force-pushed the feature/input-component-clearable-icon-variation branch from 0e8584e to 92ad327 Compare August 15, 2018 15:01

_.invoke(this.props, 'onChange', e, { ...this.props, value })

!this.props.value && this.setState({ value })
Copy link
Member

Choose a reason for hiding this comment

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

AutoControlledComponent allows you to this.trySetState() which will automatically handle the proper checks against props of the same name as the state keys you are trying to set.

this.setState({ value: '' })
} else if (clearable && this.props.value && this.props.value.length !== 0) {
this.setState({ value: this.props.value })
}
Copy link
Member

@levithomason levithomason Aug 16, 2018

Choose a reason for hiding this comment

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

This can safely be replaced with:

if (clearable) this.trySetState({ value: '' })

This method handles these checks and more edge cases. Also, note that when using AutoControlledComponent you no longer access this.props for the value, but this.state only.


getInitialAutoControlledState() {
return { value: '' }
}
Copy link
Member

Choose a reason for hiding this comment

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

The state = {} on line 96 seems to conflict with the getInitialAutoControlledState here on 98. The result of getInitialAutoControlledState is applied after the state member.

Copy link
Member

@levithomason levithomason left a comment

Choose a reason for hiding this comment

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

When applying defaultValue, the value is not respected on first render. As an example, I had updated the examples to use the below code to make it more clear that the input can be cleared:

<Input clearable defaultValue="Clear me" placeholder="Search..." />

This does not render an input with a value of Clear me. The current PR seems to be overriding the AutoControlledComponent's handling of the default props.

@alinais alinais force-pushed the feature/input-component-clearable-icon-variation branch from e14840d to 97e22b1 Compare August 16, 2018 14:16
@levithomason
Copy link
Member

Heads up, I merged master here to get latest CI speed up benefits.

@@ -1,4 +1,4 @@
import React from 'react'
import * as React from 'react'
Copy link
Member

Choose a reason for hiding this comment

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

No TS in examples please.


describe('Input', () => {
isConformant(Input)
})
Copy link
Member

Choose a reason for hiding this comment

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

💡 Tip

You can use git mv <old-path> <new-path> to move files. This will ensure git can track changes between filenames instead of appearing as a file delete and and file create. It improves readability and also makes rebase/merge more robust.

Copy link
Member

@levithomason levithomason left a comment

Choose a reason for hiding this comment

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

Changes look good here. Left a couple comments for consideration but not blocking merge. Any further updates we can make it separate PRs.

@alinais alinais closed this Aug 21, 2018
@alinais alinais reopened this Aug 21, 2018
@alinais alinais closed this Aug 21, 2018
@alinais alinais reopened this Aug 21, 2018
@alinais alinais merged commit 62ac7b5 into master Aug 21, 2018
@levithomason levithomason deleted the feature/input-component-clearable-icon-variation branch October 11, 2018 21:23
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants