Skip to content

Commit

Permalink
Merge branch 'next' into menulist-tabindex
Browse files Browse the repository at this point in the history
  • Loading branch information
ryancogswell committed Apr 18, 2019
2 parents 7fa46a4 + 3044cb4 commit 11b6652
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 88 deletions.
24 changes: 24 additions & 0 deletions docs/src/pages/demos/buttons/ButtonRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { MemoryRouter as Router } from 'react-router';
import { Link } from 'react-router-dom';
import Button from '@material-ui/core/Button';

// required for react-router-dom < 6.0.0
// see https://github.com/ReactTraining/react-router/issues/6056#issuecomment-435524678
const AdapterLink = React.forwardRef((props, ref) => <Link innerRef={ref} {...props} />);

const HomeLink = React.forwardRef((props, ref) => <Link innerRef={ref} to="/home" {...props} />);

function App() {
return (
<Router>
<Button color="primary" component={AdapterLink} to="/">
Root
</Button>
{/* Avoids property collision */}
<Button component={HomeLink}>Home</Button>
</Router>
);
}

export default App;
29 changes: 29 additions & 0 deletions docs/src/pages/demos/buttons/ButtonRouter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { MemoryRouter as Router } from 'react-router';
import { Link, LinkProps } from 'react-router-dom';
import Button from '@material-ui/core/Button';

// required for react-router-dom < 6.0.0
// see https://github.com/ReactTraining/react-router/issues/6056#issuecomment-435524678
const AdapterLink = React.forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => (
<Link innerRef={ref as any} {...props} />
));

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
const HomeLink = React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'innerRef' | 'to'>>(
(props, ref) => <Link innerRef={ref as any} to="/home" {...props} />,
);

function App() {
return (
<Router>
<Button color="primary" component={AdapterLink} to="/">
Root
</Button>
{/* Avoids property collision */}
<Button component={HomeLink}>Home</Button>
</Router>
);
}

export default App;
31 changes: 3 additions & 28 deletions docs/src/pages/demos/buttons/buttons.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,32 +123,7 @@ component forwards this ref to the underlying DOM node.
Given that a lot of our interactive components rely on `ButtonBase`, you should be
able to take advantage of it everywhere:

```jsx
import React from 'react';
import { Link as RouterLink } from 'react-router-dom'
import Button from '@material-ui/core/Button';
{{"demo": "pages/demos/buttons/ButtonRouter.js", "defaultCodeOpen": true}}

// required for react-router-dom < 5.0.0
// see https://github.com/ReactTraining/react-router/issues/6056#issuecomment-435524678
const Link = React.forwardRef((props, ref) => <RouterLink {...props} innerRef={ref} />)

<Button component={Link} to="/open-collective">
Link
</Button>
```

or if you want to avoid properties collision:

```jsx
import { Link } from 'react-router-dom'
import Button from '@material-ui/core/Button';

// use `ref` instead of `innerRef` with react-router-dom@^5.0.0
const MyLink = React.forwardRef((props, ref) => <Link to="/open-collective" {...props} innerRef={ref} />);

<Button component={MyLink}>
Link
</Button>
```

*Note: Creating `MyLink` is necessary to prevent unexpected unmounting. You can read more about it in our [component property guide](/guides/composition/#component-property).*
_Note: Creating the Button components is necessary to prevent unexpected unmounting.
You can read more about it in our [component property guide](/guides/composition/#component-property)._
31 changes: 31 additions & 0 deletions docs/src/pages/demos/selection-controls/Switches.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import Switch from '@material-ui/core/Switch';

function Switches() {
const [state, setState] = React.useState({
checkedA: true,
checkedB: true,
});

const handleChange = (name: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
setState({ ...state, [name]: event.target.checked });
};

return (
<div>
<Switch checked={state.checkedA} onChange={handleChange('checkedA')} value="checkedA" />
<Switch
checked={state.checkedB}
onChange={handleChange('checkedB')}
value="checkedB"
color="primary"
/>
<Switch value="checkedC" />
<Switch disabled value="checkedD" />
<Switch disabled checked value="checkedE" />
<Switch defaultChecked value="checkedF" color="default" />
</div>
);
}

export default Switches;
63 changes: 5 additions & 58 deletions docs/src/pages/guides/typescript/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,63 +249,10 @@ const theme = createMyTheme({ appDrawer: { breakpoint: 'md' }});
```

## Usage of `component` property

Material-UI allows you to replace a component's root node via a `component` property.
For example, a `Button`'s root node can be replaced with a React Router `Link`, and any additional props that are passed to `Button`, such as `to`, will be spread to the `Link` component, meaning you can do this:
```jsx
import { Link } from 'react-router-dom';

<Button component={Link} to="/">Go Home</Button>
```

However, TypeScript will complain about it, because `to` is not part of the `ButtonProps` interface, and with the current type declarations it has no way of inferring what props can be passed to `component`.

The current workaround is to cast Link to `any`:

```tsx
import { Link } from 'react-router-dom';
import Button, { ButtonProps } from '@material-ui/core/Button';

interface LinkButtonProps extends ButtonProps {
to: string;
replace?: boolean;
}

const LinkButton = (props: LinkButtonProps) => (
<Button {...props} component={Link as any} />
)

// usage:
<LinkButton color="primary" to="/">Go Home</LinkButton>
```

Material-UI components pass some basic event handler props (`onClick`, `onDoubleClick`, etc.) to their root nodes.
These handlers have a signature of:
```ts
(event: MouseEvent<HTMLElement, MouseEvent>) => void
```

which is incompatible with the event handler signatures that `Link` expects, which are:
```ts
(event: MouseEvent<AnchorElement>) => void
```

Any element or component that you pass into `component` will have this problem if the signatures of their event handler props don't match.

There is an ongoing effort to fix this by making component props generic.
For example, a `Button`'s root node can be replaced with a React Router `Link`, and any additional props that are passed to `Button`, such as `to`, will be spread to the `Link` component. For a code
example concerning `Button` and `react-router-dom` checkout [this Button demo](/demos/buttons/#third-party-routing-library)

### Avoiding properties collision

The previous strategy suffers from a little limitation: properties collision.
The component providing the `component` property might not forward all its properties to the root element.
To workaround this issue, you can create a custom component:

```tsx
import { Link } from 'react-router-dom';
import Button from '@material-ui/core/Button';

const MyLink = (props: any) => <Link to="/" {...props} />;

// usage:
<Button color="primary" component={MyLink}>Go Home</Button>
```
Not every component fully supports any component type you pass in. If you encounter a
component that rejects its `component` props in TypeScript please open an issue.
There is an ongoing effort to fix this by making component props generic.
34 changes: 34 additions & 0 deletions packages/material-ui-styles/test/styles.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,37 @@ withStyles(theme =>
color: number;
}
}

function forwardRefTest() {
const styles = createStyles({
root: { color: 'red' },
});

function Anchor(props: WithStyles<typeof styles>) {
const { classes } = props;
return <a className={classes.root} />;
}
const StyledAnchor = withStyles(styles)(Anchor);

const anchorRef = React.useRef<HTMLAnchorElement>(null);
// forwarded to function components which can't hold refs
// property 'ref' does not exists
<StyledAnchor ref={anchorRef} />; // $ExpectError
<StyledAnchor innerRef={anchorRef} />;

const RefableAnchor = React.forwardRef<HTMLAnchorElement, WithStyles<typeof styles>>(
(props, ref) => {
const { classes } = props;
return <a className={classes.root} />;
},
);
const StyledRefableAnchor = withStyles(styles)(RefableAnchor);

<StyledRefableAnchor ref={anchorRef} />;
const buttonRef = React.createRef<HTMLButtonElement>();
// HTMLButtonElement is missing properties
<StyledRefableAnchor ref={buttonRef} />; // $ExpectError
// undesired: `innerRef` is currently typed as any but for backwards compat we're keeping it
// especially since `innerRef` will be removed in v5 and is equivalent to `ref`
<StyledRefableAnchor innerRef={buttonRef} />;
}
1 change: 0 additions & 1 deletion packages/material-ui/src/Button/Button.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';
import Button, { ButtonProps } from '@material-ui/core/Button';
import { Link as ReactRouterLink, LinkProps } from 'react-router-dom';
import { type } from 'os';

const log = console.log;

Expand Down
2 changes: 1 addition & 1 deletion packages/material-ui/src/TextField/TextField.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ const TextField = React.forwardRef(function TextField(props, ref) {
const labelNode = ReactDOM.findDOMNode(labelRef.current);
setLabelWidth(labelNode != null ? labelNode.offsetWidth : 0);
}
}, [variant]);
}, [variant, required]);

warning(
!select || Boolean(children),
Expand Down

0 comments on commit 11b6652

Please sign in to comment.