I have a navbar component as shown below with a bunch of tabs that are displayed dynamically depending on how many components the developer passes in to Navbar.js. If the amount of tabs overflow, it is automatically hidden and cut off from the fixed width of the parent div component. Now I want to add an arrowLeft and arrowRight component that lets the user click to scroll to show the hidden tabs. How would I go about this? I'm really hoping for a solution without the need of jquery.
Navbar.js
import React, { useState } from 'react';
import Tab from './Tab';
import Filter from './Filter';
import { StyledTabs, NavbarOutline, arrowLeft, arrowRight } from '../styledComponents/StyledNavbar';
const Navbar = ({ value, tabFilter, contentFilter }) => {
const [activeTab, setActiveTab] = useState(value[0].title);
const onClickTabItem = tab => {
setActiveTab(tab);
}
return (
<React.Fragment>
<NavbarOutline>
<ol>
<arrowLeft />
</ol>
<ol>
{value.map(child => {
const { title } = child;
return <Tab activeTab={activeTab} key={title} title={title} handleClick={onClickTabItem} />;
})}
</ol>
<ol>
{tabFilter && !contentFilter && <Filter key="tabFilter" tab />}
{contentFilter && !tabFilter && <Filter key="contentFilter" content />}
<arrowRight />
</ol>
</NavbarOutline>
<div>
{value.map(child => {
if (child.title !== activeTab) return undefined;
return <StyledTabs className="content">{child.title}</StyledTabs>
})}
</div>
</React.Fragment>
);
}
export default Navbar;
StyledNavbar.js (styled-components)
import styled from 'styled-components';
export const NavbarOutline = styled.div`
margin-left: 35px;
margin-right: 35px;
overflow-x: auto;
white-space: nowrap;
top: 0px;
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
&::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
`;
export const StyledTabs = styled.button.attrs(props => ({
className: props.className,
}))`
&.not-active {
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 20px;
list-style: none;
padding: 16px 31px 16px 31px;
background: none;
border: none;
border-bottom: 2px solid #e3e3e3;
z-index: -1;
}
&.active {
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 20px;
list-style: none;
margin-bottom: -2px;
padding: 16px 31px 16px 31px;
background: none;
border: none;
color: #2b8000;
border-bottom: 3px solid #2b8000;
z-index: -1;
}
&.content {
list-style: none;
background: none;
border: none;
margin-left: 35px;
margin-right: 35px;
}
&.filter {
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 20px;
list-style: none;
position: relative;
padding: 16px 31px 16px 31px;
border: none;
border-bottom: 2px solid #e3e3e3;
display: block;
margin-top: -54px;
background: white;
right: 0px;
}
`;
What you are looking for is called an intersectionObserver. There's a native DOM API, but I recommend going with the react-intersection-observer package. It's well maintained and documented.
You'll want to use the useInView hook to create refs for every DOM node you want to keep track of, and use the parent component as root for them. Then you show the arrows conditionally depending on which refs' inView === true.
To scroll you can use the native scrollIntoView() API, or use a react package for that as well.
Related
My issue is that I have two selects on a page and when I click on anyone one of them, the second one will automatically close when ai touch it, but only when I do it the first click, afterwards it works fine.
Here is my react element
"use client";
import React from "react";
import styles from "./Dropdown.module.scss";
import { BsChevronDown } from "react-icons/bs";
export const Dropdown = ({
name,
value,
onChange,
required = false,
children,
}: {
name: string;
value: string;
onChange: (e: React.ChangeEvent<any>) => void;
required?: boolean;
children: JSX.Element[];
}) => {
return (
<div className={styles.container}>
<select
className={styles.dropdown}
value={value}
name={name}
onChange={onChange}
required={required}
>
{children}
</select>
<div className={styles.caret}>
<BsChevronDown />
</div>
</div>
);
};
Here is my CSS
.container {
position: relative;
display: flex;
align-items: center;
}
.dropdown {
width: 100%;
border-radius: 4px;
height: 45px;
outline: none;
border: 1px solid #e9ecef;
padding: 8px 16px;
background-color: transparent;
cursor: pointer;
font-family: Lato, sans-serif;
font-size: 15px;
letter-spacing: 0.5px;
-moz-appearance: none; /* Firefox */
-webkit-appearance: none; /* Safari and Chrome */
appearance: none;
color: black;
}
.caret {
position: absolute;
right: 16px;
pointer-events: none;
cursor: pointer;
& > svg {
color: #adb5bd;
}
}
I made sure it wasn't caused by CSS.
I was made code and slider by this code
import { mainModule } from 'process';
import React, { useState } from 'react';
import styled from 'styled-components';
const DragScaleBar = () => {
const [value, setValue] = useState(0);
const changeWidth = (event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
setValue(parseInt(event.target.value));
};
const MIN = 0;
const MAX = 10;
const DeadlineStyle = styled.h1`
position: absolute;
margin-left: 25em;
font-weight: bold;
font-size: 1em;
font-family: 'Poppins', sans-serif;
`;
const DragStyle = styled.input`
-webkit-appearance: none;
background: #f5f6fa;
position: absolute;
margin-top: 2em;
margin-left: 18em;
width: 20em;
height: 1em;
border-radius: 1em;
cursor: pointer;
box-shadow: 0px 0.5px 0.75px black;
&::-webkit-slider-thumb {
-webkit-appearance: none;
border: 2px solid white;
height: 25px;
width: 25px;
opacity: 0.8;
border-radius: 50%;
background: gray;
}
`;
return (
<>
<DeadlineStyle>마감일 + {value} Day</DeadlineStyle>
<div>
<DragStyle
type="range"
min={MIN}
max={MAX}
value={value}
step="1"
onChange={changeWidth}
/>
</div>
</>
);
};
export default DragScaleBar;
and here is picture
slider can change value to click in slider area.
but when I use onchange event, dragEvent is not working
I tried onMouseUp replace for onChange, but I don't know how can I use that
with typescript.
is anyone else solved problem like my code before??
Putting your styled components inside of your "real" component is causing them to re-render each time you update the value which causes the bug you are describing.
Extracting all the things which do not need to be re-rendered outside of your DragScaleBar will fix your issue.
Have a look at https://codesandbox.io/s/loving-joji-f9u0xw?file=/src/DragScaleBar.tsx
import React, { useState } from "react";
import styled from "styled-components";
const DeadlineStyle = styled.h1`
position: absolute;
margin-left: 0em;
font-weight: bold;
font-size: 1em;
font-family: "Poppins", sans-serif;
`;
const DragStyle = styled.input`
-webkit-appearance: none;
background: #f5f6fa;
position: absolute;
margin-top: 2em;
margin-left: 0em;
width: 20em;
height: 1em;
border-radius: 1em;
cursor: pointer;
box-shadow: 0px 0.5px 0.75px black;
&::-webkit-slider-thumb {
-webkit-appearance: none;
border: 2px solid white;
height: 25px;
width: 25px;
opacity: 0.8;
border-radius: 50%;
background: gray;
}
`;
const MIN = 0;
const MAX = 10;
const DragScaleBar = () => {
const [value, setValue] = useState(0);
const changeWidth = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(parseInt(event.target.value));
};
return (
<>
<DeadlineStyle>마감일 + {value} Day</DeadlineStyle>
<div>
<DragStyle
type="range"
min={MIN}
max={MAX}
value={value}
step="1"
onChange={changeWidth}
/>
</div>
</>
);
};
export default DragScaleBar;
ps, you do not need to do event.preventDefault() in your changeWidth method :)
The following code gives this result:
Is there a way I can get the dropdown to go over on top of the 'HELLO's so it doesn't show the h1 and p tag? I tried using z-index but to no success. I tried making parent element of overall component to be relative and dropdown (child) to be absolute, but has not worked. I specifically want the dropdown to be on top of ANYTHING, and don't want to make z-index of the h1 or p to be negative for example. Would there be a solution that makes the dropdown be on top of every element?
Again, I would really appreciate a solution that makes the DROPDOWN go on top of every element, not the h1 or p tag. Thanks!
I abstracted most of the methods away for the sake of simplicity, but here is the fully working version:
https://codesandbox.io/s/quirky-shadow-zsr21?file=/src/styled-dropdown-selector.js
App.js
const App = () => {
return (
<>
<DropdownSelector
lineColour="#000000"
arrowColour="#000000"
width="100%"
onSelect={(value, options) => console.log(value, options)}
options={[
{ label: '3777 Kingsway, Burnaby, BC V5H 3Z7', id: 1 },
{ label: '3888 Kingston, Ontario, ON V5H 3Z7', id: 2 },
{ label: '2999 Address, Vancouver, BC V5H 3Z7', id: 3 },
{ label: '4777 George, Richmond, BC V5H 3Z7', id: 4 },
{ label: '4222 Topaz, Coquitlam, BC V5H 3Z7', id: 5 },
{ label: '4333 Walnut, Langley, BC V5H 3Z7', id: 6 },
]}
/>
<p>HELLO HELLO HELLO</p>
<h1>HELLO HELLO HELLO</h1>
<h1>HELLO HELLO HELLO</h1>
</>
)
};
DropdownSelector.js
import React, { useState, useRef } from 'react';
import { SelectBox, SuggestionBox, SuggestionLabel, InvisibleButton } from './styled-dropdown-selector';
const DropdownSelector = ({ options, onSelect }) => {
const initialValue = options ? options[0].label : '';
const [val, setVal] = useState(initialValue);
const [suggestions, setSuggestions] = useState([]);
const [toggleEnabled, setToggleEnabled] = useState(false);
const suggestionValue = () => {
return suggestions.map(option => {
return (
<SuggestionLabel onMouseDown={() => setVal(option.label)}>
{option.label}
</SuggestionLabel>
);
});
};
return (
<div>
<SelectBox value={val} />
<InvisibleButton >
<img src={Arrow} alt="arrow" />
</InvisibleButton>
<div>
{toggleEnabled && (
<SuggestionBox>
{suggestionValue()}
</SuggestionBox>
)}
</div>
</div>
);
};
export default DropdownSelector;
styled-drpodown-selector.js
import styled from 'styled-components';
export const SelectBox = styled.input`
outline: none;
border: none;
background: none;
width: 100%;
height: auto;
margin: auto;
display: inline;
padding-bottom: 5px;
margin-bottom: 5px;
margin-top: 5px;
font-style: normal;
font-weight: normal;
font-size: 14px;
line-height: 19px;
`;
export const SuggestionBox = styled.div`
position: absolute;
width: ${props => props.width};
margin-left: 0px;
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.05);
padding-top: 5px;
padding-bottom: 5px;
`;
export const SuggestionLabel = styled.button`
outline: none;
background: none;
border: none;
width: 100%;
font-style: normal;
font-weight: normal;
font-size: 14px;
line-height: 19px;
text-align: left;
margin-left: 5px;
margin-right: 5px;
padding: 5px 0px 5px 0px;
transition: all 0.3s ease;
&:hover {
background: #e8e8e8;
}
`;
export const InvisibleButton = styled.button`
position: relative;
top: 5px;
float: right;
margin-right: 3px;
margin-top: -32px;
cursor: pointer;
background: none;
outline: none;
border: none;
width: auto;
height: auto;
display: inline;
`
Not sure if this is a lo-fi solve for what you need
export const SuggestionBox = styled.div`
position: absolute;
width: ${(props) => props.width};
margin-left: 0px;
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.05);
padding-top: 5px;
padding-bottom: 5px;
background-color: white;
height: 100%;
`;
Added the background-color and height to get you there.
https://codesandbox.io/s/great-mendeleev-2dl7n?file=/src/styled-dropdown-selector.js:353-609
tldr; How do I make my component hide the overflow and make it "toggle" views with a button. ex) user can initially see tab 1,2,3 and 4,5,6 is hidden, a button click will hide the 1,2,3 and show 4,5,6 now (without knowing that the screen will cut after 1,2,3)
I'm trying to make a navbar that can be reused in any screen size. My goal is to allow developers to dynamically add tabs (which is a child element) to the navbar without it overflowing. In the case where the navbar overflows, meaning it makes a new row because it reached its max width, a clickable arrow key goes to the next set of tabs. This is my code so far. How can I achieve this behavior?
Navbar.js
import React, { useState } from 'react';
import Tab from './Tab';
import { StyledTabs } from '../styledComponents/StyledNavbar';
import { NavbarOutline } from '../styledComponents/StyledNavbar';
const Navbar = ({ children }) => {
const [activeTab, setActiveTab] = useState(children[0].props.label);
const onClickTabItem = tab => {
setActiveTab(tab);
}
return (
<>
<NavbarOutline>
<ol>
{children.map(child => {
const { label } = child.props;
return <Tab activeTab={activeTab} key={label} label={label} handleClick={onClickTabItem} />;
})}
</ol>
</NavbarOutline>
<div>
{children.map(child => {
if (child.props.label !== activeTab) return undefined;
return <StyledTabs className="content">{child.props.children}</StyledTabs>
})}
</div>
</>
);
}
export default Navbar;
Tab.js
import React from 'react';
import { StyledTabs } from '../styledComponents/StyledNavbar';
const Tab = props => {
const { activeTab, label, handleClick } = props;
let className = 'not-active';
const onClick = () => {
handleClick(label);
};
if (activeTab === label) {
className = 'active';
}
return (
<StyledTabs className={className} onClick={onClick}>
{label}
</StyledTabs>
);
};
export default Tab;
EDIT: here is my css as well; I used styled components for styling
StyledNavbar.js
import styled from 'styled-components';
export const NavbarOutline = styled.div`
border-bottom: 2px solid #e3e3e3;
padding-left: 0;
display: inline-block;
margin-left: 35px;
margin-right: 35px;
`;
export const StyledTabs = styled.button.attrs(props => ({
className: props.className,
}))`
&.not-active {
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 20px;
display: inline-block;
list-style: none;
padding: 16px 31px 16px 31px;
background: none;
border: none;
}
&.active {
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 20px;
display: inline-block;
list-style: none;
margin-bottom: -2px;
padding: 16px 31px 16px 31px;
background: none;
border: none;
color: #2b8000;
border-bottom: 2px solid #2b8000;
}
&.content {
display: inline-block;
list-style: none;
background: none;
border: none;
margin-left: 35px;
margin-right: 35px;
`;
This seems like a CSS issue, it would be great if you could've attached the css as well. However, you can solve this using flexbox. Add the following css to the parent container to ensure the tabs create a new row instead of overflowing,
.flex-item {
padding: 5px;
width: 100px;
height: 100px;
margin: 10px;
line-height: 100px;
color: white;
font-weight: bold;
font-size: 2em;
text-align: center;
}
.wrap {
-webkit-flex-wrap: wrap;
flex-wrap: wrap;
}
.wrap li {
background: gold;
}
.flex-container {
padding: 0;
margin: 0;
list-style: none;
border: 1px solid silver;
-ms-box-orient: horizontal;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -moz-flex;
display: -webkit-flex;
display: flex;
}
<ol class="flex-container wrap">
<li class="flex-item">1</li>
<li class="flex-item">2</li>
<li class="flex-item">3</li>
<li class="flex-item">4</li>
<li class="flex-item">5</li>
<li class="flex-item">6</li>
<li class="flex-item">7</li>
<li class="flex-item">8</li>
<li class="flex-item">9</li>
<li class="flex-item">10</li>
</ol>
Using styled components, I want to build a React component that will mimic the following code from W3 Schools:
ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #333;
}
li {
float: left;
}
li a, .dropbtn {
display: inline-block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
li a:hover, .dropdown:hover .dropbtn {
background-color: red;
}
li.dropdown {
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f9f9f9;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
text-align: left;
}
.dropdown-content a:hover {background-color: #f1f1f1;}
.dropdown:hover .dropdown-content {
display: block;
}
<body>
<ul>
<li>Home</li>
<li>News</li>
<li class="dropdown">
Dropdown
<div class="dropdown-content">
Link 1
Link 2
Link 3
</div>
</li>
</ul>
<h3>Dropdown Menu inside a Navigation Bar</h3>
<p>Hover over the "Dropdown" link to see the dropdown menu.</p>
</body>
</html>
Here is my React Component:
import React, { Component } from "react";
import PropTypes from "prop-types";
import styled, { ThemeProvider } from "styled-components";
const StyledUl = styled.ul`
list-style-type: none;
margin: 0;
overflow: hidden;
background-color: #333;
padding: 0px;
background-color: lightblue;
color: black;
font-weight: bold;
`;
const StyledLi = styled.li`
float: left;
height: 100%;
padding: 10px;
&:hover {
background-color: red;
}
`;
const DropDownContent = styled.div`
display: none;
position: absolute;
min-width: 160px;
z-index: 1;
`;
const DropDownLi = styled(StyledLi)`
display: inline-block;
&:hover ${DropDownContent} {
display: block;
}
`;
const StyledA = styled.a`
display: inline-block;
text-align: center;
text-decoration: none;
`;
const SubA = styled(StyledA)`
text-decoration: none;
display: block;
text-align: left;
background-color: lightblue;
padding: 10px;
`;
class Menu extends Component {
handleClick = action => {
if (!action) return;
if (this.props.onClick) this.props.onClick(action);
};
render = () => {
return (
<StyledUl>
<StyledLi>
<StyledA onClick={() => this.handleClick("Home")}>
Home
</StyledA>
</StyledLi>
<StyledLi>
<StyledA onClick={() => this.handleClick("News")}>
Home
</StyledA>
</StyledLi>
<DropDownLi>
<StyledA onClick={() => this.handleClick("DropDown")}>
DropDown
</StyledA>
<DropDownContent>
{" "}
<SubA onClick={() => this.handleClick("Link1")}>
Link 1
</SubA>
<SubA onClick={() => this.handleClick("Link2")}>
Link 2
</SubA>
<SubA onClick={() => this.handleClick("Link3")}>
Link 3
</SubA>
</DropDownContent>
</DropDownLi>
</StyledUl>
);
};
}
export default Menu;
The result is not the same, basically because I'm not sure how to work with selectors inside styled-component.
How can I fix my ReactJs component Menu code to behave exactly like the W3 example?
You made some minor mistakes in translating the code, but the syntax and concepts were all correct. Your use of selectors is correct:
/* Hovering over current component */
&:hover {
background-color: red;
}
/* Selecting another styled component */
/* calls DropDownContent.toString(), which returns */
/* the class name of the styled component: .sc-bxivhb */
&:hover ${DropDownContent} {
display: block;
}
Here is a corrected version:
import React, { Component } from "react";
import styled from "styled-components";
const StyledUl = styled.ul`
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #333;
`;
const StyledLi = styled.li`
float: left;
`;
const Dropbtn = styled.div`
display: inline-block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
`;
const DropDownContent = styled.div`
display: none;
position: absolute;
background-color: #f9f9f9;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 1;
`;
const DropDownLi = styled(StyledLi)`
display: inline-block;
&:hover {
background-color: red;
}
&:hover ${DropDownContent} {
display: block;
}
`;
const StyledA = styled.a`
display: inline-block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
&:hover {
background-color: red;
}
`;
const SubA = styled.a`
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
text-align: left;
&:hover {
background-color: #f1f1f1;
}
`;
class Menu extends Component {
handleClick = action => {
if (!action) return;
if (this.props.onClick) this.props.onClick(action);
};
render = () => {
return (
<StyledUl>
<StyledLi>
<StyledA onClick={() => this.handleClick("Home")}>Home</StyledA>
</StyledLi>
<StyledLi>
<StyledA onClick={() => this.handleClick("News")}>Home</StyledA>
</StyledLi>
<DropDownLi>
<Dropbtn onClick={() => this.handleClick("DropDown")}>
DropDown
</Dropbtn>
<DropDownContent>
{" "}
<SubA onClick={() => this.handleClick("Link1")}>Link 1</SubA>
<SubA onClick={() => this.handleClick("Link2")}>Link 2</SubA>
<SubA onClick={() => this.handleClick("Link3")}>Link 3</SubA>
</DropDownContent>
</DropDownLi>
</StyledUl>
);
};
}
export default Menu;
You need to do it scss-like syntax. In order to use pseudo selectors like hover you need to do Nesting and use The ampersand (&) which will refer back to the main selector. Also you could use the regular class selector if you like.
Your code example:
export const Menu = styled.ul `
/* main UL component called: "Menu" */
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #333;
/* main LI */
& > li {
float: left;
& > a {
display: inline-block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
&:hover {
background-color: red;
}
}
}
/* dropdown LI */
& > .dropdown {
display: inline-block;
& > .dropdown-content {
display: none;
position: absolute;
background-color: #f9f9f9;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
& > a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
text-align: left;
&:hover {
background-color: #f1f1f1;
}
}
}
&:hover .dropdown-content {
display: block
}
}
`
And for the JSX part, Just call Menu component
<Menu>
<li>Home</li>
<li>News</li>
<li className="dropdown">
Dropdown
<div className="dropdown-content">
Link 1
Link 2
Link 3
</div>
</li>
</Menu>
That way, I've only created one component called Menu and all JSX elements are Nested just like normal HTML.
See full example at codesandbox: