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

Adds Tooltip component #501

Merged
merged 47 commits into from
Feb 8, 2019
Merged

Adds Tooltip component #501

merged 47 commits into from
Feb 8, 2019

Conversation

montezume
Copy link
Contributor

@montezume montezume commented Feb 1, 2019

Tooltip

Description

Tooltips display informative text when users hover over or focus on an element.

Usage

<Tooltip
  position="left"
  label="If you buy a pizza, you will also get a free ice cream :)"
>
  <button onClick={orderPizza({ freeIceCream: 'yes please' })}>Submit</button>
</Tooltip>

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.

<Tooltip
  position="left"
  label="You do not have permission to delete the database"
>
  <button
    disabled
    onClick={deleteDb()}
    style={{
      pointerEvents: 'none',
    }}
  >
    Delete production database
  </button>
</Tooltip>

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.

const Wrapper = styled.div`
  display: block;
`;

const FullWidthButton = styled.button`
  display: block;
  width: 100%;
`;

<Tooltip title="Delete" components={{ WrapperComponent: Wrapper }}>
  <FullWidthButton>Submit</FullWidthButton>
</Tooltip>;

Properties

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
onOpen func - - - Called when the tooltip is opened
onClose func - - - Called after the tooltip is closed
position object - top, top-start, top-end, right, right-start, right-end, bottom, bottom-end, bottom-start, left, left-start, left-end top How the tooltip is positioned relative to the child element
horizontalConstraint object - xs, s, m, l, xl, scale scale Horizontal size limit of the tooltip
children node - - Content rendered within the tooltip
components object - WrapperComponent - If passed, the tooltip will wrap your component with this element

@montezume montezume force-pushed the ml-add-tooltip branch 2 times, most recently from 8475d49 to e27f89f Compare February 4, 2019 16:41
@montezume montezume changed the title [WIP] - For deploy link - Don't review Adds Tooltip component Feb 5, 2019
@montezume montezume self-assigned this Feb 5, 2019
@montezume montezume requested review from emmenko and tdeekens February 5, 2019 12:38
@@ -28,6 +28,7 @@ const AccessibleButton = React.forwardRef((props, ref) => (
props.isDisabled &&
css`
cursor: not-allowed;
pointer-events: none;
Copy link
Contributor Author

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

Copy link
Member

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

Copy link
Contributor Author

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>?

Copy link
Member

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.

@montezume
Copy link
Contributor Author

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.

@@ -37,9 +38,10 @@ const getTextColor = (tone, isHover = false) => {

export const FlatButton = props => {
const dataProps = {
'data-track-component': 'LinkButton',
'data-track-component': 'FlatButton',
Copy link
Contributor

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) => {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

horizontalConstraint: PropTypes.oneOf(['xs', 's', 'm', 'l', 'xl', 'scale'])
.isRequired,
leaveDelay: PropTypes.number.isRequired,
open: PropTypes.bool,
Copy link
Contributor

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?

};
}
}
);
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah good call 👍

}
};

onRootRef = ref => {
Copy link
Contributor

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.

...this.props.children.props,
};

childrenProps.onMouseOver = this.handleEnter;
Copy link
Contributor

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),
Copy link
Contributor

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).

Copy link
Contributor Author

@montezume montezume Feb 5, 2019

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...

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok.

Copy link
Member

@emmenko emmenko left a 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;
Copy link
Member

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 = {
Copy link
Member

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?

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 don't understand 🤔 Can you explain?

Copy link
Member

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 = {}
}

Copy link
Contributor Author

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
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 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

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// 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


#### 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.
Copy link
Member

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)

disabled
onClick={deleteDb()}
style={{
pointerEvents: 'none',
Copy link
Member

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?

position="left"
label="You do not have permission to delete the database"
>
<div style={{ cursor: 'not-allowed' }}>
Copy link
Member

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>

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Member

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>

Copy link
Contributor Author

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 🤔

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor

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 🤷‍♂️ .

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 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.

@montezume montezume force-pushed the ml-add-tooltip branch 2 times, most recently from 4578b52 to 40604f2 Compare February 6, 2019 14:23
.isRequired,
leaveDelay: PropTypes.number.isRequired,
isOpen: PropTypes.bool,
onClose: PropTypes.func,
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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).

@montezume
Copy link
Contributor Author

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
Copy link
Member

Choose a reason for hiding this comment

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

💯

invariant(
!(childrenProps.disabled || childrenProps.isDisabled),
[
'ui-kit: you are providing a disabled `button` child to the Tooltip component.',
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

true

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`
Copy link
Member

Choose a reason for hiding this comment

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

Route 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh 😆

children: PropTypes.node.isRequired,
};

export default Wrapper;
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 this all thing can be

export default styled.div`display: inline-block;`

@montezume
Copy link
Contributor Author

montezume commented Feb 7, 2019

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

@montezume
Copy link
Contributor Author

Could use some more reviews now that @emmenko's concerns have been addressed by adding the Wrapping component / cc @tdeekens


class TestComponent extends React.Component {
state = {
show: this.props.show,
Copy link
Contributor

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({
Copy link
Contributor

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?');
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Really nice tests 💯

Copy link
Member

@emmenko emmenko left a 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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// 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


#### 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.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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.


#### 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.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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.

| 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 |
Copy link
Member

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...

Copy link
Member

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
Copy link
Member

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?

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 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...

Copy link
Member

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?',
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 some kind of Canadian thing to say? 😅

Copy link
Contributor Author

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}>
Copy link
Member

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? 🤔

Copy link
Contributor Author

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.

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 want to remove it?

@@ -0,0 +1,5 @@
import styled from '@emotion/styled';

export default styled.div`
Copy link
Member

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?

pointerEvents: 'none',
}}
>
Delete production database
Copy link
Member

Choose a reason for hiding this comment

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

😅

@@ -0,0 +1,202 @@
// inspired from https://github.com/mui-org/material-ui/blob/9ecc8db8abbfb829111d3b5c0678267827984024/packages/material-ui/src/RootRef/RootRef.js#L7-L36
Copy link
Member

Choose a reason for hiding this comment

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

Wrong file?

| 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 |
Copy link
Member

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}>
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 want to remove it?

Copy link
Member

@emmenko emmenko left a comment

Choose a reason for hiding this comment

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

💯

@montezume montezume merged commit a60b6ab into master Feb 8, 2019
@montezume montezume deleted the ml-add-tooltip branch February 8, 2019 12:48
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.

4 participants