-
Notifications
You must be signed in to change notification settings - Fork 26
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
Adds Tooltip
component
#501
Conversation
8475d49
to
e27f89f
Compare
Tooltip
component
@@ -28,6 +28,7 @@ const AccessibleButton = React.forwardRef((props, ref) => ( | |||
props.isDisabled && | |||
css` | |||
cursor: not-allowed; | |||
pointer-events: none; |
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.
otherwise it swallows up events
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 leave a comment to explain the reasons
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.
Maybe instead more examples of uses with different elements would be useful>?
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, but also I mean for future references when someone looks at this code and it might not be clear what the reason for disabling pointer events is.
Percy is not playing at all nicely with the positioning library I'm using. It might not be possible to make useful VRTs for these tooltips. |
1785255
to
d8dba51
Compare
@@ -37,9 +38,10 @@ const getTextColor = (tone, isHover = false) => { | |||
|
|||
export const FlatButton = props => { | |||
const dataProps = { | |||
'data-track-component': 'LinkButton', | |||
'data-track-component': 'FlatButton', |
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.
🤕
import Spacings from '../../spacings'; | ||
import AccessibleButton from '../accessible-button'; | ||
import { | ||
getButtonLayoutStyles, | ||
getButtonStyles, | ||
} from './primary-button.styles'; | ||
|
||
const PrimaryButton = props => { | ||
const PrimaryButton = React.forwardRef((props, ref) => { |
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 every component wanting to render a tooltip forward a ref?
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.
Warning: Function components cannot be given refs. Attempts to access this ref will fail.
Yeah, good call. Should add this to the docs.
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.
Reworked it to not need this. All it requires is that the child spreads the event listener props
src/components/tooltip/tooltip.js
Outdated
horizontalConstraint: PropTypes.oneOf(['xs', 's', 'm', 'l', 'xl', 'scale']) | ||
.isRequired, | ||
leaveDelay: PropTypes.number.isRequired, | ||
open: PropTypes.bool, |
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.
Nitpick: how is UIKit's naming convention. Is it isOpen
in this case maybe?
}; | ||
} | ||
} | ||
); |
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.
Maybe put into a ui-kit/mocks/applyPopperMocks.js
cause then it could be reused.
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.
Yeah good call 👍
src/components/tooltip/tooltip.js
Outdated
} | ||
}; | ||
|
||
onRootRef = ref => { |
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.
Is this more a setRootRef
? I am just trying to follow along.
src/components/tooltip/tooltip.js
Outdated
...this.props.children.props, | ||
}; | ||
|
||
childrenProps.onMouseOver = this.handleEnter; |
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 do we do this here and not in L117? Just onMouseLeave: this.handleLeave
etc?
...filterAriaAttributes(props), | ||
...filterDataAttributes(props), | ||
...filterEventAttributes(props), |
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.
Is this function exported from UI-Kit then for app level components to spread event props? I still wonder if we should rather export a
import { Tooltip } from 'ui-kit';
const Component = Tooltip.forwardProps(props => <div {...props} />
which would internally can use the filterEeventAttributes
but I like the api more as the origin of the props is clear and the Tooltip gets the change to potentially spead any other props or even refs (2nd arg).
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 really see the point of having this functionality when most people will just follow the readme and do something like
const Component = props => (
<div {...props}>Foo</div>
)
This forwarding props only making sense because we are so against prop forwarding, but that's a technical decision we don't need to force on others...
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.
Ok.
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 some comments, I'll finish reviewing this later.
@@ -28,6 +28,7 @@ const AccessibleButton = React.forwardRef((props, ref) => ( | |||
props.isDisabled && | |||
css` | |||
cursor: not-allowed; | |||
pointer-events: none; |
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 leave a comment to explain the reasons
} | ||
} | ||
|
||
RootRef.propTypes = { |
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.
Nitpick: make it a static
prop as well within the class?
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 understand 🤔 Can you explain?
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.
class RootRef extends React.Component {
static propTypes = {}
}
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 yeah 🤦♂️ . Thanks
@@ -0,0 +1,48 @@ | |||
// inspired from https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/RootRef/RootRef.js |
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 also copy over the comments, just in case they get deleted from the source? Then we have a bit more context about the scope of this component. 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.
// inspired from https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/RootRef/RootRef.js | |
// inspired from https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/RootRef/RootRef.js | |
// inspired from https://github.com/mui-org/material-ui/blob/9ecc8db8abbfb829111d3b5c0678267827984024/packages/material-ui/src/RootRef/RootRef.js#L7-L36 |
src/components/tooltip/README.md
Outdated
|
||
#### Working with custom child elements | ||
|
||
The tooltip needs to apply DOM event listeners to its child element. If the child is a custom React element, you need to make sure that it spreads its properties to the underlying DOM element. |
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.
It would be helpful to list the DOM event listeners that we are referring to (onMouseOver
, etc)
src/components/tooltip/README.md
Outdated
disabled | ||
onClick={deleteDb()} | ||
style={{ | ||
pointerEvents: 'none', |
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 we make it clear that this needs to be set?
src/components/tooltip/README.md
Outdated
position="left" | ||
label="You do not have permission to delete the database" | ||
> | ||
<div style={{ cursor: 'not-allowed' }}> |
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.
How about we let the tooltip do the wrapping and the consumer can pass custom styles for this wrapper div, in case it's necessary. For example:
<Tooltip
disabledWrapperStyles={{...}}
disabledWrapperClassName=""
>
<button />
</Tooltip>
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.
Not sure. I'd be as restrictive with UIKit as one can be. Passing classNames
or styles
we should maybe avoid for as long as we ever can? Maybe if needed than rather a renderWrapper
or something to give full control.
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.
Either way it's a trade off. I tried with creating a wrapper div (like we do in core
Tooltip), and I found it's a lot easier for styling purposes if we don't create a wrapper 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.
The styles/classname prop names are on purpose very specific to the "wrapped element", so it's not that we spread them everywhere.
I just find it a bit weird to have to wrap a div, only when in disabled state. It's going to be a "mess" from a consumer perspective
<Tooltip
>
{isDisabled ? (
<div style={{ cursor: 'not-allowed' }}>
<MyComponent isDisabled={isDisabled} />
</div>
) : (
<MyComponent />
)}
</Tooltip>
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.
@emmenko but then we are just complaining about how the dom / html works. It's not unique to tooltips that disabled elements don't fire events 🤔
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 we wrapped in a div, we could have like a "display: block" vs "display: inline-block" option.
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 works, as long as the API is simple to use.
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.
Just leaving my humble opinion here after @montezume explained me both approaches.
I think I'm more with the mindset of Tobi, of not allowing the consumer "insert" custom styles in the component (eventhough this custom style for the wrapping div are not intended for customizing but for consistency I would say).
The reason is because allowing the user to add custom styles is not how the rest of components of UIKit works (maybe just collapsible panel which is quite old). So, for the sake of consistency (API wise) I would vote for just rely on the user to add proper children elements to the Tooltip
.
If it's well documented, explaining the use case and what you should do to make it consistent in a specific scenario, for me it's more than enough as a consumer 🤷♂️ .
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 like to keep the API like it is for now, and if we run into problems with the migration, I'm 100% open to reworking it.
4578b52
to
40604f2
Compare
src/components/tooltip/tooltip.js
Outdated
.isRequired, | ||
leaveDelay: PropTypes.number.isRequired, | ||
isOpen: PropTypes.bool, | ||
onClose: PropTypes.func, |
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 a onClose: requiredIf(PropTypes.func, props => !props.isOpen)
make sense?
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.
Just to understand, by !props.isOpen
, do you mean that the component is controlled by it's internal state, or it's controlled by isOpen
as a prop?
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.
Maybe it doesn't make sense true. I've just seen this elsewhere. If you pass e.g. isOpen
you mean to control the component. You however can't if don't use the event handlers (which is not true in this case).
I think it makes the most sense to do a call tomorrow afternoon about this because there is some controversy surrounding the API |
const childrenProps = { | ||
'aria-describedby': open ? this.state.id : null, | ||
// for seo and accessibility, we add the tooltip's title | ||
// as a native title when the title is hidden |
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.
💯
src/components/tooltip/tooltip.js
Outdated
invariant( | ||
!(childrenProps.disabled || childrenProps.isDisabled), | ||
[ | ||
'ui-kit: you are providing a disabled `button` child to the Tooltip 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.
button
should not be hardcoded, right?
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.
true
src/components/tooltip/tooltip.js
Outdated
WrapperComponent: (props, propName) => { | ||
if (props[propName] && !isValidElementType(props[propName])) { | ||
return new Error( | ||
`Invalid prop 'component' supplied to 'Route': the prop is not a valid React 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.
Route
🤔
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 😆
src/components/tooltip/wrapper.js
Outdated
children: PropTypes.node.isRequired, | ||
}; | ||
|
||
export default Wrapper; |
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 all thing can be
export default styled.div`display: inline-block;`
788d5a7
to
4601255
Compare
Okay I updated the Tooltip to now wrap the passed in component with a div. This way, if your element is disabled, the consumer doesn't need to wrap it. By default our wrapper uses display: block, but a consumer can pass in their own custom Wrapping component like this. import { Tooltip } from 'ui-kit';
const Button = styled.button`
width: 100%;
display: block;
`;
const Wrapper = styled.div`
display: block;
`;
<Tooltip title="Hi there" components={{WrapperComponent: Wrapper}}>
<Button>Submit</Button>
</Tooltip> I also removed all the changes to buttons etc that added event passing |
|
||
class TestComponent extends React.Component { | ||
state = { | ||
show: this.props.show, |
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.
Nitpick: even though internal we should follow our own conventions maybe and name is isOpen
or shouldShow
. I don't mind too much though.
'left-end', | ||
]), | ||
title: PropTypes.string.isRequired, | ||
components: PropTypes.shape({ |
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.
Nice and extensible!
// should add the title again | ||
expect(button).toHaveProperty('title', 'What kind of bear is best?'); | ||
}); | ||
}); |
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.
Really nice 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.
Nice, I like how "simpler" it got now. If we can improve the VRT, then this is good to go.
@@ -0,0 +1,48 @@ | |||
// inspired from https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/RootRef/RootRef.js |
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.
// inspired from https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/RootRef/RootRef.js | |
// inspired from https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/RootRef/RootRef.js | |
// inspired from https://github.com/mui-org/material-ui/blob/9ecc8db8abbfb829111d3b5c0678267827984024/packages/material-ui/src/RootRef/RootRef.js#L7-L36 |
src/components/tooltip/README.md
Outdated
|
||
#### Working with disabled child elements | ||
|
||
When you use a tooltip with a disabled element, you should the style `pointer-events: none` to the disabled element to stop it from capturing events. |
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.
When you use a tooltip with a disabled element, you should the style `pointer-events: none` to the disabled element to stop it from capturing events. | |
When you use a tooltip with a disabled element, you should define the style `pointer-events: none` to the disabled element to stop it from capturing events. |
src/components/tooltip/README.md
Outdated
|
||
#### Customizing the wrapper | ||
|
||
The tooltip applies event listeners (`onMouseOver`, `onMouseLeave`, `onFocus`, and `onBlur`) to a wrapping component around the passed in component. By default, this wrapper is displayed with style `inline-block`. If you want to customize this behaviour, then you can pass in a custom wrapping element. |
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 tooltip applies event listeners (`onMouseOver`, `onMouseLeave`, `onFocus`, and `onBlur`) to a wrapping component around the passed in component. By default, this wrapper is displayed with style `inline-block`. If you want to customize this behaviour, then you can pass in a custom wrapping element. | |
The tooltip applies event listeners (`onMouseOver`, `onMouseLeave`, `onFocus`, and `onBlur`) to a wrapping `div` component around the children element. By default, this wrapper is displayed with style `inline-block`. If you want to customize this behaviour, then you can pass in a custom element. |
src/components/tooltip/README.md
Outdated
| Props | Type | Required | Values | Default | Description | | ||
| ---------------------- | -------- | :------: | -------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------- | | ||
| `isOpen` | `bool` | - | - | - | If passed, the tooltip's open and closed states are controlled by this prop | | ||
| `leaveDelay` | `number` | - | - | 0 | Delay (in milliseconds) between the end of the user interaction, and the closing of the tooltip | |
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.
Nitpick: should we maybe name this closeAfter
or something similar? I feel that it's less confusing. I don't mind much though...
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 about this one? 👆
{open && ( | ||
<Popper placement={this.props.placement}> | ||
{({ ref, style, placement }) => ( | ||
<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.
We wanted to extract this into a <TooltipContent>
or something, in order to run VRT on it, right?
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 thought about that but it seems a bit weird since then we would need to export that, since the VRT uses the bundled ui-kit...
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.
Hmm right...that's unfortunate 😑
}; | ||
|
||
static defaultProps = { | ||
title: 'What kind of bear is best?', |
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.
Is this some kind of Canadian thing to say? 😅
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.
export const component = () => ( | ||
<Suite> | ||
<Spec label="Closed" omitPropsList> | ||
<Tooltip title={title} eventsEnabled={false}> |
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.
Where is this prop eventsEnabled
defined? 🤔
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 yeah that's a leftover from trying to get poppers to work with percy. I'll remove it.
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 you want to remove it?
src/components/tooltip/wrapper.js
Outdated
@@ -0,0 +1,5 @@ | |||
import styled from '@emotion/styled'; | |||
|
|||
export default styled.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.
Nitpick: we can define this component in the tooltip.js
file directly I suppose, right?
29097e7
to
b7da10d
Compare
pointerEvents: 'none', | ||
}} | ||
> | ||
Delete production database |
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.
😅
src/components/tooltip/tooltip.js
Outdated
@@ -0,0 +1,202 @@ | |||
// inspired from https://github.com/mui-org/material-ui/blob/9ecc8db8abbfb829111d3b5c0678267827984024/packages/material-ui/src/RootRef/RootRef.js#L7-L36 |
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.
Wrong file?
src/components/tooltip/README.md
Outdated
| Props | Type | Required | Values | Default | Description | | ||
| ---------------------- | -------- | :------: | -------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------- | | ||
| `isOpen` | `bool` | - | - | - | If passed, the tooltip's open and closed states are controlled by this prop | | ||
| `leaveDelay` | `number` | - | - | 0 | Delay (in milliseconds) between the end of the user interaction, and the closing of the tooltip | |
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 about this one? 👆
export const component = () => ( | ||
<Suite> | ||
<Spec label="Closed" omitPropsList> | ||
<Tooltip title={title} eventsEnabled={false}> |
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 you want to remove it?
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.
💯
Tooltip
Description
Tooltips display informative text when users hover over or focus on an element.
Usage
Working with disabled child elements
When you use a tooltip with a disabled element, you should the style
pointer-events: none
to the disabled element to stop it from capturing events.Customizing the wrapper
The tooltip applies event listeners (
onMouseOver
,onMouseLeave
,onFocus
, andonBlur
) to a wrapping component around the passed in component. By default, this wrapper is displayed with styleinline-block
. If you want to customize this behaviour, then you can pass in a custom wrapping element.Properties
isOpen
bool
leaveDelay
number
onOpen
func
onClose
func
position
object
top
,top-start
,top-end
,right
,right-start
,right-end
,bottom
,bottom-end
,bottom-start
,left
,left-start
,left-end
top
horizontalConstraint
object
xs
,s
,m
,l
,xl
,scale
scale
children
node
components
object
WrapperComponent