hide imported child component on hover using styled-components - reactjs

Like the title says, this works with styled components sitting within the same js file (provided they are procedural-ordered above). But with imported child components I can't get it to work.
import React from "react";
import styled from "styled-components";
// Components
import Bars from "../path/Bars.js";
import BarsHover from "../path/BarsHover.js";
// Variables
import { colors } from "../../path/index.js";
//============================================ styles =============================================
const DivBars = styled(Bars)``;
const DivBarsHover = styled(BarsHover)``;
const DivWrapper = styled.div`
display: flex;
width: 20rem;
margin-bottom: 3rem;
&:hover {
cursor: pointer;
}
&:hover ${DivBars} {
display: none;
}
&:hover ${DivBarsHover} {
display: block;
}
`;
//=========================================== component ===========================================
const ParentComponent = props => {
return (
<DivContainer>
<DivBars fullBarWidth={"100%"} fractionBarWidth={"70%"} barColor={colors.niagara} />
<DivBarsHover fullBarWidth={"100%"} fractionBarWidth={"70%"} barColor={colors.gallery2} />
</DivContainer>
);
};
export default ParentComponent;

I think this caveat is the cause:
...wrapping A in a styled() factory makes it eligible for
interpolation -- just make sure the wrapped component passes along
className.
class A extends React.Component {
render() {
return <div className={this.props.className} />
}
}
const StyledA = styled(A)``
const B = styled.div`
${StyledA} {
}
`
NOTE: Ensure the className prop is propagated all the way to the component being referenced in the case that it isn't a direct descendent.

Related

Component content not imported when importing styled components

I am building a site using React and have decided to use Styled Components. I have a component that is styled with Styled Components and is receiving a prop from my homepage where I am using an instance of this component. I have the styling being pulled into the homepage, but it is not pulling in the content of the component. When if I just pull in the whole component it pulls in the content fine, but it doesn't allow me to pass the prop, but if I import the component styling it doesn't pull in the content of the component, but the prop passes properly. So I need some help with how to pull in the content and styling and get the prop to pass.
I am pretty new to Gatsby and Styled Components so thanks for any help.
My Component Code
import React from 'react'
import styled from "styled-components"
import { useStaticQuery, graphql } from 'gatsby'
export const WhyChooseSection = styled.section`
border-bottom: 1px solid var(--charcoal);
`
export const WhyChooseH2 = styled.h2`
display: ${(props) => props.displayH2};
text-align: center;
`
export const WhyChooseContent = styled.div`
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
width: 100%;
max-width: 1160px;
margin: 0 auto;
padding-top: 40px;
padding-bottom: 40px;
column-gap: 100px;
row-gap: 30px;
#media(min-width: 992px) {
justify-content: space-between;
column-gap: 0;
}
.why-icon {
width: 180px;
height: 145px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
p {
margin-bottom: 0;
font-size: 19px;
font-weight: var(--font-bold);
}
}
`
export function WhyChooseIcons() {
const data = useStaticQuery(graphql`
{
whyChooseIcons: allWp {
nodes {
acfOptionsOfficeSettings {
whyChooseInteriorDetails {
whyChooseIcons {
whyChooseIconGraphic {
altText
id
sourceUrl
}
whyChooseIconTitle
}
}
}
}
}
}
`)
return (
<>
{data.whyChooseIcons.nodes.map((node, index) => (
<WhyChooseSection key={index}>
<WhyChooseH2>Why Choose Interior Details?</WhyChooseH2>
<WhyChooseContent>
{node.acfOptionsOfficeSettings.whyChooseInteriorDetails.whyChooseIcons && node.acfOptionsOfficeSettings.whyChooseInteriorDetails.whyChooseIcons.map((item) => (
<div className='why-icon' key={item.whyChooseIconGraphic.id}>
<img src={item.whyChooseIconGraphic.sourceUrl} alt={item.whyChooseIconGraphic.altText} />
<p>{item.whyChooseIconTitle}</p>
</div>
))}
</WhyChooseContent>
</WhyChooseSection>
))}
</>
)
}
export default WhyChooseIcons
My Homepage Code
import * as React from "react"
// import styled from "styled-components"
import { graphql } from 'gatsby'
import Seo from "../components/seo"
import Hero from '../components/heroComponent'
import { WhyChooseSection } from '../components/whyChooseIcons'
import { WhyChooseH2 } from '../components/whyChooseIcons'
import { WhyChooseContent } from '../components/whyChooseIcons'
import HomeProducts from '../components/homeProducts'
import HomeTestimonials from '../components/testimonials'
import HomeNewHomes from '../components/homeNewHomes'
import HomeFamilyTreatment from '../components/homeFamilyTreatment'
import HomeSolutions from '../components/homeSolutions'
import HomeMotorization from '../components/motorizationComps'
import HomeBrands from '../components/homeBrands'
import CTAOne from '../components/ctaOne'
const IndexPage = (hero) => {
const heroData = hero.data.homeHero
return (
<>
{heroData.edges.map(({ node }, index) => (
<Hero key={index} heroTitle={node.template.homePage.heroH1} heroSubTitle={node.template.homePage.heroParagraph} heroBg={node.template.homePage.heroBackground.gatsbyImage} heroBtnOneText={node.template.homePage.heroButton1Text} heroBtnOneURL={node.template.homePage.heroButton1Url} heroBtnTwoText={node.template.homePage.heroButton2Text} heroBtnTwoURL={node.template.homePage.heroButton2Url} />
))}
<WhyChooseSection>
<WhyChooseH2 displayH2="none"></WhyChooseH2>
<WhyChooseContent></WhyChooseContent>
</WhyChooseSection>
<HomeProducts />
<HomeTestimonials />
<HomeNewHomes />
<HomeFamilyTreatment />
<HomeSolutions />
<HomeMotorization />
<HomeBrands />
<CTAOne />
</>
)
}
export default IndexPage
export const Head = () => <Seo title="Home" />
export const HeroContent = graphql`
{
homeHero: allWpPage(filter: {template: {templateName: {eq: "Home Template"}}}) {
edges {
node {
template {
... on WpHomeTemplate {
homePage {
heroH1
heroParagraph
heroButton1Text
heroButton1Url
heroButton2Text
heroButton2Url
heroBackground {
gatsbyImage(placeholder: BLURRED, width: 1920)
altText
}
}
}
}
}
}
}
}
`
Since you only want to pass a prop into your <WhyChooseIcons> component and that the component itself renders its own content, there is no need to provide any children node to it in the consuming parent.
Simply update your <WhyChooseIcons> component to accept a prop that it will pass to its inner child. Let's name it displayH2 and we can simply pass it into the <WhyChooseH2> component by doing this: <WhyChooseH2 displayH2={displayH2}>. So your component definition now looks like this:
export function WhyChooseIcons({ displayH2 }) {
// Rest of the component logic here
return (
<>
{data.whyChooseIcons.nodes.map((node, index) => (
<WhyChooseSection key={index}>
<WhyChooseH2 displayH2={displayH2}>Why Choose Interior Details?</WhyChooseH2>
{/* Other content here */}
</WhyChooseSection>
))}
</>
)l
};
Then in the file where you consume this component, it is just a matter of passing "none" to the prop:
<WhyChooseSection displayH2="none">
HOWEVER this feel like a XY problem to me, because all you want is to control the conditional rendering of this component. What you can do is to create a prop on your component that determines whether <WhyChosseH2> should be rendered or not.
export const WhyChooseH2 = styled.h2`
text-align: center;
`
export function WhyChooseIcons({ shouldHideH2 }) {
return (
<>
{data.whyChooseIcons.nodes.map((node, index) => (
<WhyChooseSection key={index}>
{!shouldHideH2 && <WhyChooseH2>Why Choose Interior Details?</WhyChooseH2>}
{/* Other content here */}
</WhyChooseSection>
))}
</>
)
}
Then you can just control H2 rendering with a true/false boolean:
{/* To show it, just don't set the prop */}
<WhyChooseIcons />
{/* To hide it, both ways are valid */}
<WhyChooseIcons shouldHideH2 />
<WhyChooseIcons shouldHideH2={true} />

Conditional styled components effect all instances when nested

If i use a prop condition on the main component it will work per instance.
So for example if i have:
const Div = styled.div<Props>`
${(props) => {
return css`
${props.test && "border: 5px solid red;"};
`;
}}
`;
only components which have the test prop will have this ugly border :)
But if i use this same condition on a nested css rule like that:
const Div = styled.div<Props>`
${(props) => {
return css`
.tabletScreen & {
${props.test && "border: 5px solid red;"};
}
`;
}}
`;
All instances of this component will have this ugly border if one of the components has this test prop.
When inspect it i see that all instances of the component gets the same class generated, so the this:
.tabletScreen .sc-jcFjpl {
border: 5px solid red;
}
Is implemented to all instances.
But on the first case (when the condition is not nested) the component with the test prop will get another class so it won't override the others.
How can i fix this?
Use && instead of & and it'll be scoped to that stylistic instance of the component. Single ampersand refers to the "static component class".
More info here: https://styled-components.com/docs/basics#pseudoelements-pseudoselectors-and-nesting
import React from "react";
import { render } from "react-dom";
import styled, { css } from "styled-components";
import Wrapper from "./Wrapper";
import Title from "./Title";
// Render these styled components like normal react components.
// They will pass on all props and work
// like normal react components – except they're styled!
const Div = styled.div`
${(props) => {
return css`
.tabletScreen {
border: ${props.test && '5px solid red'};
}
`;
}}
`;
const App = () => (
<Wrapper>
<Div test>
Without Test
<div className="tabletScreen">TabletScreen</div>
</Div>
</Wrapper>
);
render(<App />, document.getElementById("root"));

What is the best way to typing React Components when they styling with styled-components

I am migrating a project to Typescript. I have a component styled with styled-components and I need to override it with styled-components on some other component. My component looks like this.
This component aims to render an image and if image not exist, returns a Fallback element styled with styled-components.
import React, { useState } from 'react'
import styled from 'styled-components'
export const Fallback = styled.div`
padding-bottom: 56%; /* 16/9 ratio */
background-color: #e5e5e5;
`
export const Image = styled.img`
image-rendering: pixelated;
image-rendering: crisp-edges;
`
interface ImageWithFallbackProps {
src: string
alt: string
fallback: typeof Fallback
}
const ImageWithFallback = ({
src,
alt,
fallback = Fallback,
...props
}: ImageWithFallbackProps): React.ReactNode => {
const [error, setError] = useState(false)
const FallbackElement: React.FC = fallback
if (error) return <FallbackElement />
return <Image src={src} alt={alt} {...props} onError={() => setError(true)} />
}
export default ImageWithFallback
And, I use it on another component.
const Content: AnyStyledComponent = styled.div`
/* styles */
`
Content.Text = styled.div`
font-weight: bold;
flex: 1;
text-align: left;
`
Content.Image = styled(ImageWithFallback)`
flex: 1;
max-width: 80px;
align-self: baseline;
`
Content.Fallback = styled(Fallback)`
width: 80px;
padding-bottom: 54px;
`
But I get an error where I define Content.Image like this Content.Image = styled(ImageWithFallback). Error code is ts(2769). How can I solve this issue?

How to add properties to styled component with Typescript?

I have components like this
import React from 'react';
import styled from '../../styled-components';
const StyledInput = styled.input`
display: block;
padding: 5px 10px;
width: 50%;
border: none;
border-radius: 10px;
outline: none;
background: ${(props) => props.theme.color.background};
font-family: 'Roboto', sans-serif;
`
;
export const Input = () => {
return <StyledInput placeholder="All notes"></StyledInput>
}
I want to place them as property on "index" component like this
import styled from 'styled-components';
import { Input } from './Input';
const NoteTags:any = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
width: 20%;
height: 5%;
border-bottom: 2px solid ${(props) => props.theme.color.background}; `;
NoteTags.Input = Input;
export default NoteTags;
So then I can use them like this
import React from 'react';
import NoteTags from '../../blocks/note-tags';
const ComponentNoteTags = () => {
return (
<React.Fragment>
<NoteTags.Input></NoteTags.Input>
</React.Fragment>
)
}
export default ComponentNoteTags;
The problem with this is that property Input doesn't exist on NoteTags so typescript gives me an error. I can solve it by setting type any to NoteTags but I'm not sure that it is the best way.
Is there a better way to fix it?
Update
I found it much better to use a type helper to add extra properties into a component's variable:
// type-helper.ts
export function withProperties<A, B>(component: A, properties: B): A & B {
Object.keys(properties).forEach(key => {
component[key] = properties[key]
});
return component as A & B;
}
Note: #Allan points out in the comment, type assertion might be necessary like so:
(component as any)[key] = (properties as any)[key]
to avoid typescript complaining about Element implicitly has an 'any' type because type '{}' has no index signature.
We're just passing each property in properties to component and give it a joined type of both. Then we can 'glue' sub components into a main component as follow:
// component/index.ts
import MainComponent from './MainComponent';
import SubComponent1 from './SubComponent1';
import SubComponent2 from './SubComponent2';
import { withProperties } from '../type-helper.ts';
export withProperties(MainComponent, { SubComponent1, SubComponent2 })
and use it as usual
import MainComponent from './Component';
export default () => (
<MainComponent>
<SubComponent1 />
<SubComponent2 />
</MainComponent>
)
This approach works with normal React component as well & not just styled components one, so I think it's better.
Here's a codesandbox demo with both approaches I've shared, one of each use your code.
Previous answer
Assuming you're using the latest packages:
"styled-components": "^4.1.3",
"#types/styled-components": "^4.1.10",
You can create an interection type that include both the type of the parent & child components:
import styled, { StyledComponent } from 'styled-components'
import ChildComponent from '...'
type Component = StyledComponent<'div', any> & {
ChildComponent: /* your component type */
}
interface IProps {
...
}
// create styled component per usual
const MyComponent = styled.div<IProps>`
...
` as Component
MyComponent.ChildComponent = ChildComponent;
Here's the StyledComponent definition:
export type StyledComponent<
C extends keyof JSX.IntrinsicElements | React.ComponentType<any>,
T extends object, // theme interface
O extends object = {}, // props interface
A extends keyof any = never
> = string & StyledComponentBase<C, T, O, A>;
You can use it as StyledComponent<C, T, O, A> or StyledComponent<C, T>
I've managed to get it working by defining the call signatures of the styled component and the additional properties you want to add. Typescript will complain that Item doesn't exist on List, so I had to use Object Assign to create the styled component and assign the property simultaneously.
import styled from 'styled-components'
import { PropsWithChildren, ReactElement } from 'react'
interface ListOverload {
({ children }: PropsWithChildren<{}>): ReactElement
Item({ children }: PropsWithChildren<{}>): ReactElement
}
export const Item = styled.li(
({ theme }) => css`
padding: ${theme.space.md};
border-bottom: 1px solid ${theme.colors.greyLight};
`
)
export const List: ListOverload = Object.assign(
styled.ul`
list-style: none;
padding: 0;
margin: 0;
`,
{ Item }
)
Then you can use like this in your JSX:
<List>
<List.Item>Todo 1</List.Item>
<List.Item>Todo 2</List.Item>
<List.Item>Todo 3</List.Item>
</List>

How to import React object: elements via module.exports and render inside component?

I am trying to import and render an object:property of type React 'element' from external file and import it using module.exports into another file containing a Component and render it inside the Component. The component has previously been called 3 times, to create 3 columns and fill them with text.
This works when importing 'text' however I cannot get it to work importing a React 'element'.
What do I need to do to render the imported React 'element'? I am also using css-modules. Below is the code. Thanks:-
(File: column.css)
.default {
box-sizing: border-box;
border: 1px solid black;
font-size: 20px;
width: 32%;
height: auto;
}
.red {
composes: default;
float: left;
margin-right: 2%;
background-color: red;
}
.green {
composes: default;
float: left;
background-color: green;
}
.blue {
composes: default;
float: right;
margin-left: 2%;
background-color: white;
}
(File: Home.js)
import React from 'react'
import Column from '../components/Column'
import styles from './home.css'
export default class Home extends React.Component {
render() {
return (
<div className={styles.container}>
<h1>Home</h1>
<Column style = {'red'} content={'firstColumn'}/>
<Column style = {'green'} content={'secondColumn'}/>
<Column style = {'blue'} content={'thirdColumn'}/>
</div>
)
}
}
(File: Column.js)
import React from 'react'
import style from './column.css'
const contents = require( '../components/content')
export default class Column extends React.Component {
render() {
return (
<div className={style[this.props.style]}>
{contents[this.props.content]}
</div>
)
}
}
(File: content.js)
import React from "react"
module.exports = {
firstColumn: text,
secondColumn: "This text is rendered",
thirdColumn: "This text is rendered",
}
const text = <p>This element text is NOT rendered</p>;
Hm ... shouldn't your content.js looks like:
const text = <p>This element text is NOT rendered</p>;
module.exports = {
firstColumn: text,
secondColumn: "This text is rendered",
thirdColumn: "This text is rendered",
}
the text variable is not hoisted so you're exporting undefined as firstColumn.

Resources