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

feat(switch): Add component #208

Merged
merged 44 commits into from
Aug 30, 2018
Merged

feat(switch): Add component #208

merged 44 commits into from
Aug 30, 2018

Conversation

bonniezhou
Copy link
Contributor

Fixes #152.

Ripple is modified because the element with the ripple surface (thumb underlay) is different from the ripple activating element (native input).

@bonniezhou bonniezhou requested a review from moog16 August 6, 2018 21:12

You may want to apply the visual treatment (CSS classes and styles) for a ripple surface on one element, but have its activation rely on a different element. For example, putting a ripple on a `<div>` which will be activated by focusing on a child `<input>` element. We call the visual element the "ripple surface" and the activating element the "ripple activator".

The `initRipple` callback prop will take in an extra `activator` argument for the case where the ripple activator differs from the ripple surface. If the `activator` argument is not provided, the ripple surface will also serve as the ripple activator.
Copy link
Contributor

Choose a reason for hiding this comment

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

prop will take in --> prop can take in?

this.props.initRipple(el /* surface */, this.rippleActivatorElement /* activator */);
}

setRippleActivator = (el) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should be telling devs to use React.createRef for these situations.


export default class NativeControl extends React.Component {
componentDidMount() {
this.props.handleChange(this.props.checked);
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 you need to call these here since they are initialized in state in index.js

handleChange = (e) => {
const {handleChange, onChange} = this.props;
const {checked} = e.target;
handleChange(checked);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can an onChange be passed in that calls handleChange in the parent function? Then you don't need 2 change functions being passed to this component.

id: PropTypes.string,
onChange: PropTypes.func,
handleDisabled: PropTypes.func,
handleChange: 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.

make sure to remove these if they're unnecessary

disabled={disabled}
handleChange={handleChange}
handleDisabled={handleDisabled}
setrippleActivatorEl={(el) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

use React.createRef() here. Also setRippleActivator**

}

get classes() {
return classnames('mdc-switch__thumb-underlay', this.props.className);
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 supposed to have the same this.props.className from the parent? It looks like classes will be duplicated here from index.js. I don't think that is the intention.

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 think so? The classes from index.js aren't passed down to ThumbUnderlay

Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha - you're just not passing any classes from index.js to the ThumbUnderlay component so it seemed like you didn't need this. But I guess it doesn't hurt to have this.

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 I see what you're saying. ThumbUnderlay is actually receiving props.className from withRipple, so it's necessary. On the other hand NativeControl isn't getting classes from anywhere, so I removed it there!

Copy link
Contributor

Choose a reason for hiding this comment

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

ah! i didn't even notice :)

<ThumbUnderlay
checked={this.state.checked}
disabled={disabled}
id={id}
Copy link
Contributor

Choose a reason for hiding this comment

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

is the ID for a aria-label relationship?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup! I renamed it nativeControlId to make it clearer

require('./select/screenshot-suite').default,
require('./switch/screenshot-suite').default,
Copy link
Contributor

Choose a reason for hiding this comment

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

double

@codecov-io
Copy link

codecov-io commented Aug 8, 2018

Codecov Report

Merging #208 into master will decrease coverage by 0.12%.
The diff coverage is 94.28%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #208      +/-   ##
==========================================
- Coverage   98.86%   98.73%   -0.13%     
==========================================
  Files          23       26       +3     
  Lines         969     1028      +59     
  Branches       98      102       +4     
==========================================
+ Hits          958     1015      +57     
- Misses         11       13       +2
Impacted Files Coverage Δ
packages/switch/index.js 100% <100%> (ø)
packages/switch/NativeControl.js 100% <100%> (ø)
packages/ripple/index.js 96.15% <81.81%> (ø) ⬆️
packages/switch/ThumbUnderlay.js 81.81% <81.81%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ac0f87f...f82c425. Read the comment docs.

Copy link
Contributor

@moog16 moog16 left a comment

Choose a reason for hiding this comment

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

lint is failing as well. Also i left a comment about this but there is no way for the user to get a hold of the checked value.

import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import withRipple from '../ripple';
Copy link
Contributor

Choose a reason for hiding this comment

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

for now we should be using @material/ripple until we move to a monopackage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But ripple is modified in this PR so it won't work if it uses @material/ripple :(

Copy link
Contributor

Choose a reason for hiding this comment

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

even if you do lerna add @material/ripple --scope=@material/react-switch?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it worked!

const {handleChange, onChange} = this.props;
const {checked} = e.target;
handleChange(checked);
onChange(e);
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 this should be here. You're not passing onChange in index.js to .

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok makes sense since the user wouldn't have access to the ThumbUnderlay anyways. Removed.

export default class Switch extends Component {
foundation_ = null;
state = {
checked: this.props.checked,
Copy link
Contributor

Choose a reason for hiding this comment

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

oh cool - i didn't know this worked.

render() {
return (
<Switch nativeControlId='my-switch' />
<label for='my-switch'>My Switch</label>
Copy link
Contributor

Choose a reason for hiding this comment

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

for -> htmlFor

import "@material/react-switch/dist/switch.css";
```

### Javascript Instantiation
Copy link
Contributor

Choose a reason for hiding this comment

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

how does one get a hold of the value? if its checked/not checked

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a note to the README, basically same way as in text field

const MATCHES = util.getMatchesProperty(HTMLElement.prototype);

return {
browserSupportsCssVars: () => util.supportsCssVariables(window),
isUnbounded: () => this.props.unbounded,
isSurfaceActive: () => instance[MATCHES](':active'),
isSurfaceActive: () => activator ? activator[MATCHES](':active') : surface[MATCHES](':active'),
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could probably modify createAdapter to take in an extra param as well

@@ -148,7 +145,13 @@ const withRipple = (WrappedComponent) => {
this.setState({style: updatedStyle});
}

getMergedStyles = () => {
get classes() {
const {className: wrappedCompClasses} = this.props;
Copy link
Contributor

Choose a reason for hiding this comment

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

wrappedComponentClasses. Don't shorten it

disabled: PropTypes.bool,
id: PropTypes.string,
onChange: PropTypes.func,
setRippleActivator: PropTypes.oneOfType([
Copy link
Contributor

Choose a reason for hiding this comment

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

can this just be rippleActivator, which I assume is an Element, since it gets passed to 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.

It's a ref which gets this PropType definition according to StackOverflow. Changed the name to rippleActivatorRef to make it clearer.


Prop Name | Type | Description
--- | --- | ---
className | String | Classes to be applied to the chip element
Copy link
Contributor

Choose a reason for hiding this comment

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

hmmm, not a chip

classList.delete(className);
this.setState({classList});
},
isNativeControlChecked: () => this.state.checked,
Copy link
Contributor

Choose a reason for hiding this comment

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

is it weird that we implement isNativeControlChecked by look at the root component's state? I guess we are basically guaranteeing that if the native control's state changes, then ThumbUnderlay will respond in an onChange callback. And then if ThumbUnderlay's state changes we guarantee that Switch's state will change also with and handleChange callback.

But for some reason that feels a bit fragile. Did we do the same design for the text field?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, but text field only has one layer of nesting (is nesting the right word?) TextField -> Input which makes it seem less fragile. And similarly Select->NativeControl. Switch is the only component for now with 3 layers because it needs one for the native input, one for the ripple surface, and one for the top-level component.

className: PropTypes.string,
disabled: PropTypes.bool,
nativeControlId: PropTypes.string,
handleChange: 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.

why not onChange? Why handleChange?

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 on* conventionally expects an event parameter. Confirm, @moog16?

Copy link
Contributor

Choose a reason for hiding this comment

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

I've been using on* as an event handler. I think we invented handle* so it wouldn't collide with an end developer. But in this case the end dev won't ever pass an onChange method so we can change it to onChange

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍 Updated!

Copy link
Contributor

@moog16 moog16 left a comment

Choose a reason for hiding this comment

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

I think add a test for the Thumbunderlay's onChange={(e) => onChange(e.target.checked)}. But the rest looks good to me! Nice work 🗡

@lynnmercier
Copy link
Contributor

I pushed some changes to this PR. But We it now relies on the MDCSwitchFoundation.handleChange method taking an event as an argument. That is implemented in material-components/material-components-web#3342, but it is not merged or released yet. So I think we might have to hold off merging this PR until that PR 3342 is merged.

Or we could do some work around for now? I'd rather wait than think about hacks, but maybe someone has a clever idea

@moog16
Copy link
Contributor

moog16 commented Aug 15, 2018

Ya I agree - no sense in making a work around and rush getting this PR in. We can wait until MDCW releases 0.39.0. Then we can update MDCR repo, and get this PR working. Then release 0.5.0

import React from 'react';
import PropTypes from 'prop-types';

export default class NativeControl extends React.Component {
Copy link
Contributor

@moog16 moog16 Aug 28, 2018

Choose a reason for hiding this comment

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

change this to stateless component:

const NativeControl = (props) => {
  return (...)
}

export default NativeControl;

<input
type='checkbox'
role='switch'
className='mdc-switch__native-control'
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you think people will use this component on its own? If so we should merge this.props.className and 'mdc-switch__native-control'

export default class Switch extends Component {
rippleActivator = React.createRef();
foundation_ = null;
state = {
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 we should use constructor to set initial state:

constructor(props) {
  super(props);
   rippleActivator = React.createRef();
    foundation_ = null;
    this.state = {
     checked: props.checked,
      classList: new Set(),
      disabled: props.disabled,
      nativeControlChecked: props.checked,
      nativeControlDisabled: props.disabled,
    };
}

"name": "@material/react-switch",
"version": "0.0.0",
"description": "Material Components React Switch",
"license": "Apache-2.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

MIT!

},
"dependencies": {
"@material/react-ripple": "^0.4.0",
"@material/ripple": "^0.38.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

ripple needs to be 39

Copy link
Contributor

Choose a reason for hiding this comment

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

actually remove this and only rely on react-ripply

Copy link
Contributor

@moog16 moog16 left a comment

Choose a reason for hiding this comment

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

This doesn't work:

class TestSwitch extends React.Component {
  state = {checked: true};

  render() {
    return (
      <div>
        <Switch checked={this.state.checked} />
        <button onClick={() => this.setState({checked: !this.state.checked})}>
          update
        </button>
      </div>
    )
  }
}

@moog16 moog16 merged commit 3eb8a4b into master Aug 30, 2018
@moog16 moog16 deleted the feat/switch branch August 30, 2018 21:27
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.

None yet

4 participants