-
Notifications
You must be signed in to change notification settings - Fork 843
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
Discrete unit input #353
Discrete unit input #353
Conversation
Fancy! 🎉 🎉 |
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 given a very cursory glance through this PR, but I'll have more time to look at it more closely tomorrow. From looking at the PR description, this looks really cool!
A couple things jumped out at me from my quick once-over. Also, can we add tests for these new components?
padding: 0; | ||
} | ||
|
||
.euiFieldDiscrete-input { |
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.
For child elements, can we use a double underscore to concatenate the parts of the name? e.g. euiFieldDiscrete__input
. We've been following the Kibana CSS style guide for our styles.
|
||
const fieldClasses = classNames('euiFieldDiscrete', className); | ||
|
||
const isInvalid = value !== `` && !isValid(value); |
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 check against an empty string here? Shouldn't isValid
be responsible for determining validity?
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 internal state, an empty value
means the input doesn't have anything, so we don't accept "nothing" as valid input, but marking the field as invalid because its empty would be weird? I can see how the consumer might be the one who gets to decide that :)
const { value } = this.state; | ||
const { isValid, parse, onInsert } = this.props; | ||
|
||
if (e.key === 'Enter') { |
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 we import the keyCodes service and use it here?
@@ -14,6 +14,7 @@ const colorToClassNameMap = { | |||
accent: 'euiBadge--accent', | |||
warning: 'euiBadge--warning', | |||
danger: 'euiBadge--danger', | |||
ghost: 'euiBadge--ghost', |
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 we update the Badge page of the doc site with an example of this?
inset 0 -2px 0 0 $euiColorPrimary; | ||
} | ||
|
||
@mixin euiFormControlStyleDisabled { |
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 love that you pulled these 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.
This is really cool Nico!! I love it. I have a few suggestions and some requests.
Tests
Would you mind adding Jest tests for the new components you've added?
Hover state
Currently, the hover state replaces the entire badge with the "x" icon. This makes for a tough UX for me because when I hover it I can't confirm what I'm deleting. I'm sure @snide and @cchaos can provide a strong suggestion. Off-hand, I imagine showing the "x" icon on the right side of the badge would work. Maybe keeping it permanently visible:
Long badge text
When entering long badge text, I was able to get the component to enter a broken visual state.
Maybe we should add a max-width to each badge and truncate the overflowing text? That's what they did for the example I provided in my previous point.
import React, { Component } from 'react'; | ||
import classNames from 'classnames'; | ||
|
||
export class EuiFocusEmulator extends Component { |
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 we add an example of this to the doc site? Perhaps in its own example page.
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 I would be out of character if I didn't suggest another name... 😄 "Emulator" confuses me... how about naming this EuiFieldProxy
? Because it surfaces the visual cues of a field, driven by the "true field" beneath the surface. So it proxies the underlying field for visually communicating state to the user.
this.setState({ hasFocus: false }); | ||
} | ||
|
||
onClick = (...rest) => { |
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 I understand the role this plays. Can you explain it for me?
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.
Sure. Rather than assuming an e
parameter and passing that to this.props.onClick
, I call this.props.onClick(...rest)
and I never have to worry about the API for onClick
changing or adding parameters I didn't originally account for. Or so the theory goes
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 I see that makes sense. Though it would be such a breaking change to the API that I am certain it would never happen, so maybe this is being overly defensive? Using ...rest
instead of e
makes this code look special so I can see other people being confused by it if they come across it. Can we either change the parameter to being the more conventional event, or add a comment to explain it the way you just did for me?
EuiFocusEmulator | ||
} from '../focus_emulator'; | ||
|
||
export class EuiFieldDiscrete extends Component { |
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.
Naming suggestion: how about EuiFieldBadges
or EuiFieldTags
? The dumber the better. This way people don't need to learn what "discrete" means in this context.
@include euiFormControlStyleDisabled; | ||
} | ||
|
||
.euiFocusEmulator--invalid { |
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 three of these classes are currently formatted as modifiers, but they're actually states. Can we rename them?
.euiFocusEmulator-isFocused
.euiFocusEmulator-isDisabled
.euiFocusEmulator-isInvalid
@@ -0,0 +1,150 @@ | |||
import { noop, omit } from 'lodash'; |
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're removing our dependency upon lodash. Could you inline these functions? CC @uboness
@@ -1,3 +1,5 @@ | |||
import { without } from 'lodash'; |
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're removing our dependency upon lodash. Could you inline these?
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 sounds great, what's the reasoning behind this? Maybe instead of making lodash a module non grato, we could switch to importing from the smaller bundles like lodash/without
and so on?
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 hit a problem where we were using a version of lodash incompatible with the one Kibana was using (addressed by #359). I think the best way to avoid problems like that in the future is by removing the dependency entirely... which seems viable/reasonable to me because we're mostly using some really simple lodash utilities, which should be simple to internalize and maintain. This way, lodash will no longer be a critical dependency for using EUI, which should simplify consumption.
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 of this approach? Are there modules in lodash you feel we really need, which we would have trouble maintaining?
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 this is a non-solution to that problem. We've hit so many problems due to the way we distribute EUI (build headaches, svg icons, now lodash, etc). As long as we refuse to bundle EUI into a drop-in thing that compiles whatever code it needs into a nicely self-contained bundle, we'll keep hitting these issues.
Sure, not using lodash wouldn't be such a huge deal, but it doesn't fix the underlying problem that just keeps spitting out issues for us to deal with.
In any case, there's enough stuff using lodash today that I'd defer the removal of lodash calls to a new PR unless it's already being actively worked on
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.
Are there modules in lodash you feel we really need, which we would have trouble maintaining?
Well there's a ton of tiny utilities in it, obviously. Some are trivial to reimplement, some aren't that trivial. None are a huge concern, obviously, this being a utility library, but why reinvent the wheel when there's this large set of finely tuned utility functions at our disposal
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.
In any case, there's enough stuff using lodash today that I'd defer the removal of lodash calls to a new PR unless it's already being actively worked on
SGTM
} | ||
}; | ||
|
||
const otherProps = omit(rest, 'parse', 'onInsert'); |
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 you explain this bit to me?
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 passing otherProps
to the input
(or your suggestion to pass them to the parent div, not sure which is better). parse
and onInsert
aren't using on render, so I need to exclude them from the rest bag even after destructuring props that we do use, otherwise they'd fall through to the child component
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 I see! Thanks, that makes sense. Could I suggest pulling them out in the original destructuring assignment on line 34 and using // eslint-disable-line no-unused-vars
? I believe this is how we've been doing this kind of thing elsewhere in the codebase.
onRemove, | ||
className, | ||
disabled, | ||
...rest |
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.
Typically we pass ...rest
through to some part of the rendered DOM, so that the consumer can provide arbitrary HTML attributes. How about on line 71?
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.
See previous comment, was already passing ...otherProps
to the <input />
disabled={disabled} | ||
invalid={isInvalid} | ||
> | ||
<div className={fieldClasses}> |
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.
My suggestion is:
<div className={fieldClasses} {...rest}>
color="ghost" | ||
className="euiFieldDiscrete-value" | ||
onClick={() => onRemove(value)} | ||
> |
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 you mind adding a data-test-subj="fieldProxyBadge"
attribute to this element so we can query it in our tests?
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.
Had a couple more thoughts about accessibility.
key={hash(value)} | ||
color="ghost" | ||
className="euiFieldDiscrete-value" | ||
onClick={() => onRemove(value)} |
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.
@timroes How do you think we should support screen reader accessibility for this component? I think it might make sense to pull aria-labelledby
off the props and apply it to each badge.
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.
@snide I think we need to solve the problem of clickable badges, e.g. by updating EuiBadge to support clicking on them or creating a new component for this role. Currently this component isn't keyboard-accessible because we didn't design for this use case.
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 @cchaos is coming up with something awesome for this FYI.
fullWidth: false, | ||
isLoading: false, | ||
isValid: () => true, | ||
renderValue: value => String(value), |
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 create an individual example page for this component. Then we can add examples of how to use this renderValue
, parse
, and isValid
.
Hey @bevacqua , I can help you a bit on the UI for this. I have a couple questions:
|
renderValue={value => value.toUpperCase()} | ||
onInsert={this.onDiscreteInsert} | ||
onRemove={this.onDiscreteRemove} | ||
placeholder="Type and press enter …" |
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 would remove the ellipses. We have some writing guidelines on the usage of punctuation, in particular ellipses: https://elastic.github.io/eui/#/writing (under Punctuation).
I also noticed that in your screenshots it stuck around once someone was typing. This seems a bit of odd behavior.
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 also noticed that in your screenshots it stuck around once someone was typing. This seems a bit of odd behavior.
I just typed some more … to drive you mad, but they don't stick around 😅
className={classes} | ||
disabled={disabled} | ||
invalid={invalid} | ||
onClick={this.onClick} |
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 it makes sense to look again at this in the next iteration, so this comment is more a note for myself.... this onClick
is currently inaccessible, which can be solved in a number of ways. But I think the appropriate solution will become apparent after we address the accessibility of the pills.
* Move react-color from devDependencies to dependencies. * Update CHANGELOG.
- created a `predicate` module that exports all predicate function (e.g. `isString`, `isFunction`,...) - created `isNil` predicate - `sortable_properties` no longer uses lodash `sortBy` and uses our comparators instaed this change should enable using tables in Kibana.
I would, but I wouldn't want the component to be that smart. If I need this, I'll do it as a consumer, imo.
They can be rendered however the consumer chooses, it's completely up to the consumer |
No, but it's not a problem anymore now that the delete action shows up top right instead of overlaying |
@bevacqua On the Kibana side, we've found that we're able to make faster and steadier progress using EUI if we bake some of our design rules into our components. This has meant that recently we've started approaching our component design expecting them to be more opinionated than we have in the past. What has the experience regarding opinionated components been like on your side? Have you had cases where greater flexibility has been advantageous? In terms of this component, I'd like to propose this solution, which I think will strike the right balance between defaulting to an opinionated visual state (and thereby a simpler interface) while still offering an escape hatch for consumers who want greater control:
Thoughts? |
This is getting out of hand, unless we can move those requests to a future iteration I'll need to pull this code into Cloud and come back to this PR in the future. |
Closing for now |
Sorry, Nico! Don't want to hold you up, but I also want to be a good shepherd to EUI. :) Feel free to put this on hold if you have to -- we'll probably need a little time to update EuiBadge to support our new design anyway. Not that it matters, but I think most of my comments mostly entailed moving the parsing code out of the component into the consumer, which seems like mostly cut-and-pasting...? Or not, I could be wrong. |
A new kind of input field that conforms to our design guidelines but allows the user to add or remove discrete values.
Usage example where
this.state.discreteValues
is['a', 'b', 'c', 'd']
, and insert/remove modify said state object via React'ssetState
:isValid
prop can be provided, and should returntrue
only when an input string would be validparse
prop you can provide, which should worry about taking the plain text user input and mapping it into whatever value we actually need for rendering and for our collection of discrete valuesrenderValue
and return whatever you want to display each itemThe input renders the list of values using the
renderValue
prop, or just casting them to strings.On hover, items show a cross that signals they can be removed.
When clicking on the input, the entire discrete input gets focus styles, and not just the actual input element
The focus behavior is enabled thanks to a second component introduced in this PR,
EuiFocusEmulator
, meant to be used internally by EUI, which makes sure to transfer focus/disabled/invalid state and styles to the parent when the input gets one of those states.Usage of
<EuiFocusEmulator>
is below. The<input />
doesn't need to be inside the emulator, but when the input is clicked, the<div>
emitted by the emulator will get the styles that were meant for the input. Note that it should be a raw input, not a<EuiFieldText>
or such, since the emulator is meant to be the target of our styling, so this component is meant mostly to be reused in cases like this where we have an actual input inside a component that's supposed to say "hey, this whole thing has focus", not just the input itself, which would look bad"/cc @andrew-moldovan @gjones