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

Spinner button #471

Merged
merged 1 commit into from
Dec 21, 2016
Merged

Spinner button #471

merged 1 commit into from
Dec 21, 2016

Conversation

sonhanguyen
Copy link
Contributor

Changes
Add SpinnerButton component
Background context
In Alexandria we now have a spinner component that comes in three sizes.
In Adslot UI we have buttons.

We'd now like to export a new SpinnerButton, one that allows us to show progress.

Trello Cards
https://trello.com/c/FoelX6UK/5932-adslot-ui-spinner-button-component

What it currently looks like:
screen shot 2016-12-12 at 4 24 40 pm
What it is expected to look like after https://github.com/Adslot/alexandria/pull/356 is merged
screen shot 2016-12-12 at 4 25 59 pm

import 'styles/adslotUi/SpinnerButton.scss';
import Button from './ButtonComponent';

const isBtnProp = (val, key) => Button.propTypes[key];
Copy link
Contributor

@OndrejCholeva OndrejCholeva Dec 12, 2016

Choose a reason for hiding this comment

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

isButtonProp = (value, key) => Button.propTypes[key]

@@ -0,0 +1 @@
module.exports = require('react-bootstrap/lib/Button');
Copy link
Member

Choose a reason for hiding this comment

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

just do import Button from 'react-bootstrap/lib/Button';' below and remove this file

loadingElement,
loadingHoverElement = children,
//...btnProps
} = props;
Copy link
Contributor

@pjivers pjivers Dec 12, 2016

Choose a reason for hiding this comment

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

Destructure props as arguments...

const SpinnerButton = ({
  isLoading,
  children,
  ...etc...
}) =>

Also, set default value of className (className = '') as

SpinnerButton.defaultProps = {
  className: '',
};

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, nevermind. I didn't see you were using props elsewhere. Instead, just put props argument in parentheses... SpinnerButton = (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.

I couldn't find out how to destructure argument and get the argument at the same time

const SpinnerButton = ({
  isLoading,
  children,
  ...etc...
}) => {
   // I need the props object here
}

const size = t.oneOf(['lg', 'large'])(props.bsSize)? null: 'medium'
loadingElement = React.cloneElement(loadingElement, { size })
*/
const btnProps = _.pick(props, isBtnProp);
Copy link
Contributor

Choose a reason for hiding this comment

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

buttonProps

{...btnProps}
className={`spinner-button ${className}`}
>
<div className="spinner-button__wrapper" style={{ visibility: isLoading ? 'initial' : 'hidden' }}>
Copy link
Contributor

@pjivers pjivers Dec 12, 2016

Choose a reason for hiding this comment

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

Change class name to spinner-button-wrapper. And the next two.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, don't use style attribute. Add a class that changes the style.

<div className="spinner-button__loading">{loadingElement}</div>
<div className="spinner-button__reveal">{loadingHoverElement}</div>
</div>
<div style={{ visibility: isLoading ? 'hidden' : 'initial' }}>{children}</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here... use classes to change style.

<div className="spinner-button__loading">{loadingElement}</div>
<div className="spinner-button__reveal">{loadingHoverElement}</div>
</div>
<div style={{ visibility: isLoading ? 'hidden' : 'initial' }}>{children}</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

can we just not render elements rather than hiding them?

isLoading ?
<div className="spinner-button__wrapper">
  <div className="spinner-button__loading">{loadingElement}</div>
  <div className="spinner-button__reveal">{loadingHoverElement}</div>
</div> :
<div>{children}</div>

(pseudocode)

Copy link
Contributor

Choose a reason for hiding this comment

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

We need to preserve the original size of the element. If we hide/remove the text part of the button the size will shrink. The only alternatives are to measure the size and set it in style.

Copy link
Contributor

@OndrejCholeva OndrejCholeva Dec 12, 2016

Choose a reason for hiding this comment

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

umm, so let's suppose you're making a button that says [ O Exporting... ] when 'loading' and [ Export ] initially. Wouldn't it render like [ O Exporting... Export ] so effectively as [ _____________ Export ] ?

ExampleForm,
ExampleSelect,
ExampleSpinnerButton,
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to do this, you can just import the spinner button directly to Main.jsx. These examples were put here to isolate larger, more complicated examples.

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 need a stateful component to hold isLoading state but our linter does not allow having two components in Main.jsx. Also the file already has a lot of things.

Copy link
Contributor

Choose a reason for hiding this comment

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

nm, see what you're doing further down

@@ -0,0 +1,42 @@
.spinner-button {
&__layer {
Copy link
Contributor

Choose a reason for hiding this comment

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

These double underscores are not a naming convention we follow.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, layer, wrapper(maybe) and reveal aren't particularly helpful names. What are they trying to explain.

expect(onClick.calledOnce).to.equal(true);
onClick = sinon.spy();
element = shallow(
<SpinnerButton isLoading onCLick={onClick}>Test</SpinnerButton>
Copy link
Contributor

Choose a reason for hiding this comment

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

onClick rather than onCLick

}
}
// workaround for vertical centering
& .spinner-component {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think the & is needed here, the nesting should do this for us


return (
<Button
disabled={isLoading}
Copy link
Contributor

@omgaz omgaz Dec 12, 2016

Choose a reason for hiding this comment

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

isLoading || btnProps.disabled. This still needs to be disabled if it's passed as a prop to disable

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 think the next line covered that

{...btnProps}

loadingElement,
loadingHoverElement = children,
//...btnProps
} = props;
Copy link
Contributor

Choose a reason for hiding this comment

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

We generally do destructuring in the arguments of the fn

Copy link
Contributor

Choose a reason for hiding this comment

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

nm, i've seen yours and patricks comments

import { Spinner } from 'alexandria-adslot';
import _ from 'lodash';
import 'styles/adslotUi/SpinnerButton.scss';
import Button from './ButtonComponent';
Copy link
Contributor

Choose a reason for hiding this comment

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

Require direct from bootstrap rather than having the one line export file.

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 having SpinnerButton dependent on AdslotUI's Button rather than Bootstrap's, that way if we diverge from Bootstrap we can simply change that one file and have consistency.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think one day we will have wrappers for 3rd party components which would make it a single dependency (and provide way to inject/overwrite stuff globally), but we should do it consistently (and purposely).


SpinnerButton.defaultProps = {
isLoading: false,
loadingElement: <Spinner size="small" />,
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need to pass this as a prop; if we want this to do anything else it'll either be a different component or we update it then.

this.setState({ isLoading: true }, () =>
setTimeout(() => this.setState({ isLoading: false }), 5000)
)
,
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe change style to...

onClick: () => {
  this.setState({ isLoading: true }, () =>
    setTimeout(() => this.setState({ isLoading: false }), 5000)
  );
},

Just for consistency's sake.
@omgaz Thoughts on this?

@@ -0,0 +1,42 @@
.spinner-button {
&__layer {
Copy link
Contributor

@pjivers pjivers Dec 12, 2016

Choose a reason for hiding this comment

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

This is not our convention. Please change to &-layer. The same for the remaining styles.

import 'styles/_icheck-custom.scss';
import 'styles/_react-select-custom.scss';
import 'styles/_react-toggle-custom.scss';
import 'styles/_react-datepicker-custom.scss';
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe, SCSS files still need to be required so that webpack creates a separate css file.

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 tried dist in both cases and saw only one css ouput, had content of those files. Am I missing something?

Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome! Just trash your dist file and rebuild; double-check that it's still all good :)

import React from 'react';
import { SpinnerButton } from 'components/distributionEntry';

export default class extends React.Component {
Copy link
Contributor

Choose a reason for hiding this comment

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

Export at the end of the file, not here.

}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

New line at end of file. 😝

it('should render with defaults', () => {
const element = shallow(<SpinnerButton>Test</SpinnerButton>);
const hoverElement = element.find('.spinner-button__reveal');
expect(hoverElement.text()).to.equal('Test');
Copy link
Contributor

Choose a reason for hiding this comment

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

Not that it makes a functional difference, but you can write it as .to.be('Test');

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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

just a note that it is not a function, there is to.be.a() but not to.be()

);

const btnElement = element.find('Button');
expect(btnElement.prop('bsStyle')).is.equal('primary');
Copy link
Contributor

@pjivers pjivers Dec 12, 2016

Choose a reason for hiding this comment

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

Try to write the expect in a way that reads like an English sentence:
e.g. "Expect the button element prop style to be 'primary'" reads better than "Expect the button element prop style is equal 'primary'"

So change it to expect(btnElement.prop('bsStyle')).to.be('primary');

The same goes for the others.

const element = shallow(
<SpinnerButton loadingNode="loading..">Test</SpinnerButton>
);
expect(element.children().first().text()).to.contain('loading..');
Copy link
Contributor

Choose a reason for hiding this comment

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

For future reference, it's better to use the ellipsis character (…)

const size = t.oneOf(['lg', 'large'])(props.bsSize)? null: 'medium'
loadingElement = React.cloneElement(loadingElement, { size })
*/
const buttonProps = _.pick(props, isButtonProp);
Copy link
Contributor

@OndrejCholeva OndrejCholeva Dec 12, 2016

Choose a reason for hiding this comment

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

_.pick takes array as props filter, so you can use following instead extra function:
const buttonProps = _.pick(props, _.keys(Button.propTypes));

@codecov-io
Copy link

codecov-io commented Dec 13, 2016

Current coverage is 100% (diff: 100%)

No coverage report found for master at 6dceee0.

Powered by Codecov. Last update 6dceee0...61a9705

}

&-medium {
margin-top: -3px;
Copy link
Contributor

Choose a reason for hiding this comment

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

should be -5px, see (left -3, right -5):
screenshot 2016-12-13 15 02 20

Copy link
Contributor Author

@sonhanguyen sonhanguyen Dec 13, 2016

Choose a reason for hiding this comment

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

that spinner is 30x30
the -3px is for 24x24 spinner which is in http://github.comAdslot/alexandria#356
screen shot 2016-12-12 at 4 25 59 pm
I thought 30 was a bit too big as it leaves no padding for the button.
@omgaz what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see what you mean. It's ok for now, maybe we can re-visit it later.


return (
<Button
disabled={isLoading}
Copy link
Contributor

Choose a reason for hiding this comment

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

We still want the button to be disabled if it's disabled, not just loading.

props.disabled || props.isLoading

Copy link
Contributor Author

@sonhanguyen sonhanguyen Dec 13, 2016

Choose a reason for hiding this comment

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

the line right bellow covers that

{...propsIncludingDisabledPassed}

Main.jsx has that disabled example too.

Copy link
Contributor

Choose a reason for hiding this comment

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

@sonhanguyen yep, although I could not find the line you mention. @omgaz do we will need to pass it in like

disabled={disabled || isLoading}

in which case, you would still need to pull it out of props just above.

@pjivers
Copy link
Contributor

pjivers commented Dec 13, 2016

Same here... Amend commit instead of pushing new commits. 😉

@@ -0,0 +1,48 @@
.spinner-button {
@mixin layer-fillparent {
Copy link
Contributor

Choose a reason for hiding this comment

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

fillparent should be fill-parent. Also, I'm a bit confused about layer fill parent and what it means. I think just fill-area or fill-element or something may do the job just as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

by layer I mean position absolute, changed to "fill-area" anyway

@omgaz
Copy link
Contributor

omgaz commented Dec 13, 2016

aside from the layer-fillparent; LGTM

Copy link
Contributor

@lteacher lteacher left a comment

Choose a reason for hiding this comment

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

LGTM

expect(element.children().first().text()).to.contains('loading…');
});

it('should be disabled in loading mode', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

@omgaz with the design, is there a case where the button is disabled without showing the spinner (disabled prop) prop and disabled showing the spinner (isLoading prop)?

@sonhanguyen depending on Garry's answer:
should be disabled in loading mode when passed isLoading for this test and maybe another test should be disabled when passed isLoading for the disabled prop talked about above.

@sonhanguyen sonhanguyen force-pushed the spinner-button branch 2 times, most recently from 0507793 to f16ce44 Compare December 14, 2016 00:58
isLoading,
children,
loadingHoverElement = children,
} = props;
Copy link
Contributor

@OndrejCholeva OndrejCholeva Dec 14, 2016

Choose a reason for hiding this comment

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

oh, I am sorry to come with late request, but can you add support for passing data-test-selector property down to the button? (https://github.com/Adslot/adslot-ui/blob/8fc657aa6cefd760fecff5f5e063d6f0d1ecaa55/src/components/adslotUi/SplitPaneComponent.jsx as inspiration)

  loadingHoverElement = children,
  dts #yeah, I know it is a shortcut, but that's what we use...
} = props;

...
<Button
     {..._.pick(props, _.keys(Button.propTypes))}
     expandDts(dts)

...

   dts: PropTypes.string

pseudocode


it('should pass props to button', () => {
const element = shallow(
<SpinnerButton bsStyle="primary" bsSize="lg">Test</SpinnerButton>
Copy link
Contributor

@OndrejCholeva OndrejCholeva Dec 14, 2016

Choose a reason for hiding this comment

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

can we add the dts prop as well? expecting data-test-selector to be a buttonElement prop?

Copy link
Contributor

Choose a reason for hiding this comment

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

All react components accept data as do bootstrap components. We should be able to use data-test-selector directly on the component. @sonhanguyen do you just want to verify that data-test-selector gets passed through correctly and without duplication/errors.

@omgaz
Copy link
Contributor

omgaz commented Dec 18, 2016

Alexandria pr has merged and released as 4.0.1; you can update your npm package in this PR to pull in those changes :)

Also, squash your commits down to one and we should be good to merge

@omgaz omgaz force-pushed the spinner-button branch 2 times, most recently from a53bac3 to 2be5a1f Compare December 21, 2016 06:36
@omgaz omgaz merged commit a3a1005 into master Dec 21, 2016
@omgaz omgaz deleted the spinner-button branch December 21, 2016 22:24
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.

8 participants