Implement shouldForwardProp for React Native/Primitives (#3107)

* implement shouldForwardProp for React Native and primitives #3093

* return true in native default prop validator function

* Update CHANGELOG.md
This commit is contained in:
macintoshhelper
2020-04-24 17:08:46 +01:00
committed by GitHub
parent 01e5747aa4
commit 4add697ac7
3 changed files with 124 additions and 8 deletions

View File

@@ -6,6 +6,8 @@ _The format is based on [Keep a Changelog](http://keepachangelog.com/) and this
## Unreleased
- Implement `shouldForwardProp` API for native and primitive platforms (see [#3093](https://github.com/styled-components/styled-components/pull/3107))
### Bugfixes
- Added `useTheme` hook to named exports for react-primitives entrypoint (see [#2982](https://github.com/styled-components/styled-components/pull/2982)) thanks @jladuval!

View File

@@ -16,6 +16,9 @@ import type { Attrs, RuleSet, Target } from '../types';
// NOTE: no hooks available for react-native yet;
// if the user makes use of ThemeProvider or StyleSheetManager things will break.
// Validator defaults to true if not in HTML/DOM env
const validAttr = () => true;
class StyledNativeComponent extends Component<*, *> {
root: ?Object;
@@ -35,7 +38,7 @@ class StyledNativeComponent extends Component<*, *> {
...props
} = this.props;
const { defaultProps, target } = forwardedComponent;
const { defaultProps, target, shouldForwardProp } = forwardedComponent;
const elementToBeRendered =
this.attrs.$as || this.attrs.as || transientAsProp || renderAs || target;
@@ -44,15 +47,20 @@ class StyledNativeComponent extends Component<*, *> {
this.props
);
const isTargetTag = isTag(elementToBeRendered);
const computedProps = this.attrs !== props ? { ...props, ...this.attrs } : props;
const propFilterFn = shouldForwardProp || (isTargetTag && validAttr);
const propsForElement = {};
let key;
for (key in props) {
if (key[0] !== '$') propsForElement[key] = props[key];
}
for (key in this.attrs) {
if (key[0] !== '$') propsForElement[key] = this.attrs[key];
for (key in computedProps) {
if (key[0] === '$' || key === 'as') continue;
else if (key === 'forwardedAs') {
propsForElement.as = props[key];
} else if (!propFilterFn || propFilterFn(key, validAttr)) {
// Don't pass through filtered tags through to native elements
propsForElement[key] = computedProps[key];
}
}
propsForElement.style = [generatedStyles].concat(style);
@@ -145,6 +153,22 @@ export default (InlineStyle: Function) => {
? Array.prototype.concat(target.attrs, attrs).filter(Boolean)
: attrs;
// eslint-disable-next-line prefer-destructuring
let shouldForwardProp = options.shouldForwardProp;
// $FlowFixMe
if (isTargetStyledComp && target.shouldForwardProp) {
if (shouldForwardProp) {
// compose nested shouldForwardProp calls
shouldForwardProp = (prop, filterFn) =>
// $FlowFixMe
target.shouldForwardProp(prop, filterFn) && options.shouldForwardProp(prop, filterFn);
} else {
// eslint-disable-next-line prefer-destructuring
shouldForwardProp = target.shouldForwardProp;
}
}
/**
* forwardRef creates a new interim component, which we'll take advantage of
* instead of extending ParentComponent to create _another_ interim class
@@ -155,6 +179,9 @@ export default (InlineStyle: Function) => {
WrappedStyledNativeComponent.displayName = displayName;
// $FlowFixMe
WrappedStyledNativeComponent.shouldForwardProp = shouldForwardProp;
// $FlowFixMe
WrappedStyledNativeComponent.inlineStyle = new InlineStyle(
// $FlowFixMe
@@ -197,6 +224,7 @@ export default (InlineStyle: Function) => {
// all SC-specific things should not be hoisted
attrs: true,
displayName: true,
shouldForwardProp: true,
inlineStyle: true,
styledComponentId: true,
target: true,

View File

@@ -183,7 +183,7 @@ Object {
});
it('passes simple props on', () => {
const Comp = styled.View.attrs(p => ({
const Comp = styled.View.attrs(() => ({
test: true,
}))``;
@@ -432,6 +432,92 @@ Object {
expect(TestRenderer.create(<Comp $color="red" />).toJSON()).toMatchSnapshot();
});
it('allows for custom prop filtering for elements', () => {
const Comp = styled('View').withConfig({
shouldForwardProp: prop => !['filterThis'].includes(prop)
})`
color: red;
`;
const wrapper = TestRenderer.create(<Comp filterThis="abc" passThru="def" />);
const { props } = wrapper.root.findByType('View');
expect(props.style).toEqual([{ color: 'red' }]);
expect(props.passThru).toBe('def');
expect(props.filterThis).toBeUndefined();
});
it('allows custom prop filtering for components', () => {
const InnerComp = props => <View {...props} />
const Comp = styled(InnerComp).withConfig({
shouldForwardProp: prop => !['filterThis'].includes(prop)
})`
color: red;
`;
const wrapper = TestRenderer.create(<Comp filterThis="abc" passThru="def" />);
const { props } = wrapper.root.findByType('View');
expect(props.style).toEqual([{ color: 'red' }]);
expect(props.passThru).toBe('def');
expect(props.filterThis).toBeUndefined();
});
it('composes shouldForwardProp on composed styled components', () => {
const StyledView = styled.View.withConfig({
shouldForwardProp: prop => prop === 'passThru'
})`
color: red;
`;
const ComposedView = styled(StyledView).withConfig({
shouldForwardProp: () => true
})``;
const wrapper = TestRenderer.create(<ComposedView filterThis passThru />);
const { props } = wrapper.root.findByType('View');
expect(props.passThru).toBeDefined();
expect(props.filterThis).toBeUndefined();
});
it('should filter out props when using "as" to a custom component', () => {
const AsComp = props => <View {...props} />
const Comp = styled.View.withConfig({
shouldForwardProp: prop => !['filterThis'].includes(prop)
})`
color: red;
`;
const wrapper = TestRenderer.create(<Comp as={AsComp} filterThis="abc" passThru="def" />);
const { props } = wrapper.root.findByType(AsComp);
expect(props.style).toEqual([{ color: 'red' }]);
expect(props.passThru).toBe('def');
expect(props.filterThis).toBeUndefined();
});
it('can set computed styles based on props that are being filtered out', () => {
const AsComp = props => <View {...props} />
const Comp = styled.View.withConfig({
shouldForwardProp: prop => !['filterThis'].includes(prop)
})`
color: ${props => props.filterThis === 'abc' ? 'red' : undefined};
`;
const wrapper = TestRenderer.create(<Comp as={AsComp} filterThis="abc" passThru="def" />);
const { props } = wrapper.root.findByType(AsComp);
expect(props.style).toEqual([{ color: 'red' }]);
expect(props.passThru).toBe('def');
expect(props.filterThis).toBeUndefined();
});
it('should filter our props when using "as" to a different element', () => {
const Comp = styled.View.withConfig({
shouldForwardProp: prop => !['filterThis'].includes(prop)
})`
color: red;
`;
const wrapper = TestRenderer.create(<Comp as="a" filterThis="abc" passThru="def" />);
const { props } = wrapper.root.findByType("a");
expect(props.style).toEqual([{ color: 'red' }]);
expect(props.passThru).toBe('def');
expect(props.filterThis).toBeUndefined();
});
it('should prefer transient $as over as', () => {
const OtherText = props => <Text {...props} foo />;