I would like to make a dropdown for choosing from a list of items that always have a width of it's widest element. The selected item is the item that's always shown and other options are shown on hover.
It's a simple CSS dropdown, and ideally, I would like to see pure CSS solution. Since I'm using React and this dropdown is a component, a js solution would be acceptable (without using jQuery or other libraries if possible).
const Dropdown = (props) => (
<div className="dropdown">
<div className="dropdown-item">{props.active}</div>
<div className="dropdown-body">
{props.items
.filter(x => x !== props.active)
.map(x => <div className="dropdown-item">{x}</div>)}
</div>
</div>
)
var items = [
"abc", "abcdcdssd", "a"
]
ReactDOM.render(
<div>Hello <Dropdown items={items} active={"abc"} /> world.</div>,
document.querySelector("#app")
)
.dropdown {
display: inline-block;
background-color: blue;
text-align: center;
}
.dropdown .dropdown-item {
background-color: red;
width: auto;
padding: 0 6px;
}
.dropdown .dropdown-body {
display: none;
position: absolute;
z-index: 1000;
}
.dropdown:hover .dropdown-body {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
In the example above, I would like for the "abc" element to have a width of the largest element "abcdcdssd".
Pure CSS Solution
A pure css solution would be to give your container a width: auto and use the visibility style instead of display on your items to hide them:
const Dropdown = (props) => (
<div className="dropdown">
<div className="dropdown-item">{props.active}</div>
<div className="dropdown-body">
{props.items
.filter(x => x !== props.active)
.map(x => <div className="dropdown-item">{x}</div>)}
</div>
</div>
)
var items = [
"abc", "abcdcdssd", "a"
]
ReactDOM.render(
<div>Hello <Dropdown items={items} active={"abc"} /> world.</div>,
document.querySelector("#app")
)
var items = [
"abc", "abcdcdssd", "a"
]
ReactDOM.render(
<div>Hello <Dropdown items={items} active={"abc"} /> world.</div>,
document.querySelector("#app")
)
.dropdown {
display: inline-block;
text-align: center;
vertical-align: top;
width: auto;
}
.dropdown .dropdown-item {
background-color: red;
padding: 0 6px;
}
.dropdown .dropdown-body {
visibility: hidden;
z-index: 1000;
}
.dropdown:hover .dropdown-body {
visibility: visible;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="app"></div>
The caveat is that because the items are only hidden if you select the text your hidden items will be selected too.
JS Solution
You can measure the width of the dropdown body after the initial render and apply it to the active dropdown item. To make this work you initially need to use visibility: hidden instead of display: none because only the width of hidden elements can be measured. You could hide them as soon as they have been measured initially.
The example below uses react hooks which requires a version >16.8 but it can also be achieved with a class based component using componentDidMount.
const {useRef, useState, useLayoutEffect} = React;
const Dropdown = props => {
const bodyRef = useRef();
const [bodyWidth, setWidth] = useState(null);
const [itemsHidden, setItemsHidden] = useState(false);
useLayoutEffect(() => {
setWidth(bodyRef.current.clientWidth);
setItemsHidden(true);
}, []);
return (
<div style={{width: bodyWidth || 'auto'}} className="dropdown">
<div className="dropdown-item">{props.active}</div>
<div ref={bodyRef} className={`dropdown-body${itemsHidden ? ' hidden' : ''}`}>
{props.items
.filter(x => x !== props.active)
.map((x, idx) => (
<div key={idx} className="dropdown-item">{x}</div>
))}
</div>
</div>
);
};
var items = [
"abc", "abcdcdssd", "a"
]
ReactDOM.render(
<div>Hello <Dropdown items={items} active={"abc"} /> world.</div>,
document.querySelector("#app")
)
.dropdown {
display: inline-block;
background-color: blue;
text-align: center;
}
.dropdown .dropdown-item {
background-color: red;
width: auto;
padding: 0 6px;
}
.dropdown .dropdown-body {
position: absolute;
z-index: 1000;
}
.dropdown .dropdown-body.hidden {
display: none;
position: absolute;
z-index: 1000;
}
.dropdown:hover .dropdown-body {
display: block;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="app"></div>
Related
I'm hitting an API to get these data. I mapped the data to my select menu. I'm not able to change the height of the dropdown box . any idea on how it can be achieved?
react.js
inside render
<div className={styles.nationality}>
<label htmlFor="nationality">Nationality<span className={styles.star}>*</span></label><br/>
{/* <img className={styles.nationalitydrop} src={drop} alt='drop' /> */}
{['nationality'].map(key => (
<select
key={key}
className={styles.nationalitybox}
type="text"
placeholder="select"
menuPlacement="bottom"
onChange={(event) => this.handleUserInput(event)}
value={this.state.nationality}
name='nationality'
> {this.props.nationalityData.map(({[key]:id}) => <option className={styles.data}key={id}>{id}</option>)}
</select>
))}
</div>
stylesheet
.nationalitybox
{
width: 408px;
height: 56px;
background: #30333F 0% 0% no-repeat padding-box;
border-radius: 3px;
opacity: 1;
color: #B4B6C4;
font-size: 20px;
border-style:none none solid;
outline: none;
}
.nationalitybox:placeholder-shown{
font-size: 14px;
color: #B4B6C4;
}
.data{
background-color: #B4B6C4;
color: black;
width: 220px;
}
[![dropdown][1]][1]
I think you are using wrong to style your select menu, you cannot use 'style.nationalitybox' because that not a class.
I suggest you use these options.
1st Option:
Change className={styles.nationalitybox} To className = "nationalitybox".
2nd Option:
If you are using Material UI then you have to use useStyles above the component.
For example:
const useStyles = makeStyles((theme) => ({
nationalitybox: {
height: "56px"
}
}));
const ComponentName = () => {
const styles= useStyles();
}
here is a code example where i use Ref's to change style
import React, {useRef, useState, useEffect} from 'react'
import S from "./collapsible.module.css"
import PropTypes from 'prop-types'
import { faMinus, faPlus } from '#fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
function Collapsible(props) {
let myRef = useRef(null);
let buttonRef = useRef(null);
let [ button, setButton] = useState(true)
let Show = () => {
if(button) {
setButton(false)
buttonRef.current.style.backgroundColor = "#555"
myRef.current.style.maxHeight = myRef.current.scrollHeight + "px";
} else {
setButton(true)
buttonRef.current.style.backgroundColor = "hsl(181, 100%, 11%)"
myRef.current.style.maxHeight = "0px";
}
}
return (
<div
className={S.body}
>
<button
className={S.collapsible}
onClick={Show}
ref={buttonRef}
> {props.label}
<div className={S.icon}>
{button? <FontAwesomeIcon icon={faPlus} />:
<FontAwesomeIcon icon={faMinus} />}
</div>
</button>
<div
className={S.content}
ref={myRef}
>
<h3>{props.body}</h3>
</div>
</div>
)
}
Collapsible.propTypes = {
label: PropTypes.string,
body: PropTypes.string,
}
Collapsible.defaultProps = {
label: '',
body: "",
}
export default Collapsible
css:
.collapsible {
display: flex;
background-color: hsl(181, 100%, 11%);
color: white;
cursor: pointer;
padding: 18px;
width: 100%;
border: none;
outline: none;
font-size: 15px;
border-radius: 3px;
/* margin-bottom: 3px; */
box-shadow: 0px 1px 5px 1px black;
margin-top:13px;
}
.icon{
color:white;
position:absolute;
right:50px;
text-align:right;
justify-content: flex-end;
}
.active, .collapsible:hover {
background-color: #555;
}
.content {
padding: 0 18px;
max-height: 0px;
overflow: hidden;
transition: max-height 0.2s ease-out;
background-color: #f1f1f1;
}
This is just replicating this in React:
https://www.w3schools.com/howto/tryit.asp?filename=tryhow_js_collapsible_animate
I have read that using Refs is bad, especially when using it to change the DOM, but if I didn't change the style with the exact amount shown in "scrollHeight" then the transition would be a messed up speed.
If there is another method Is this still bad practice?
It's more common practice to use a state value to determine the style like this:
<button
className={S.collapsible}
onClick={Show}
style={{backgroundColor: button ? "#555" : "hsl(181, 100%, 11%)"}
>
{props.label}
<div className={S.icon}>
{button ? (
<FontAwesomeIcon icon={faPlus} />
) : (
<FontAwesomeIcon icon={faMinus} />
)}
</div>
</button>
Or have a conditional className for your styles:
<button
className={`${S.collapsible} ${
button ? S.buttonColor : S.anotherButtonColor
}`}
onClick={Show}
>
{props.label}
<div className={S.icon}>
{button ? (
<FontAwesomeIcon icon={faPlus} />
) : (
<FontAwesomeIcon icon={faMinus} />
)}
</div>
</button>
and add .buttonColor and .anotherButtonColor to your CSS Module file (collapsible.module.css).
.buttonColor {
background-color: #555;
}
.anotherButtonColor {
background-color: hsl(181, 100%, 11%);
}
For the maxHeight on myRef, I'd do something like:
<div className={S.content} ref={myRef}>
<div style={{ maxHeight: myRef.current.scrollHeight }}>
<h3>{props.body}</h3>
</div>
</div>
I need to make the vertical slider animation ( dots and line ) as in this pic
i managed to do the Accordion and the dots but i don't know how i will going to implement it ( i'm using pseudo )
**my accordion component Where i define the logic of my nested accordions as in images based on array of data **
function MultiLevelAccordion({
data,
bodyClass,
headerClass,
wrapperClass,
renderHeader,
renderContent,
}) {
const RootAccordionId = 'parent-0';
const [accordionsStates, setActiveCardsIndex] = useMergeState({});
const onAccordionToggled = (id, activeEventKey) => {
console.log(activeEventKey);
setActiveCardsIndex({
[id]: activeEventKey ? Number(activeEventKey) : activeEventKey
});
};
console.log('data', data);
const accordionGenerator = (data, parentId) => {
return map(data, (item, index) => {
const active = accordionsStates[parentId] === index;
const hasChildren = item.hasOwnProperty('children') && isArray(item.children) && !isEmpty(item.children);
const isRootAccordion = RootAccordionId === parentId;
const isLastNestedAccordion = !isRootAccordion && !hasChildren;
const accordion = (
<Card className={classNames(wrapperClass, {
'nested-root-accordion': !isRootAccordion,
'last-nested-root-accordion': isLastNestedAccordion,
'multi-level-accordion': !isLastNestedAccordion
})}
>
<Accordion.Toggle
{...{ ...item.id && { id: item.id } }}
onClick={() => this}
as={Card.Header}
eventKey={`${index}`}
className={'cursor-pointer d-flex flex-column justify-content-center'}
>
<div className="d-flex justify-content-between align-items-center">
{renderHeader(item, hasChildren)}
<img
style={{
transition: 'all .5s ease-in-out',
transform: `rotate(${active ? 180 : 0}deg)`
}}
src={setIcon('arrow-down')}
className="ml-2"
alt="collapse"
/>
</div>
</Accordion.Toggle>
<Accordion.Collapse eventKey={`${index}`}>
<Card.Body
className={`accordion-content-wrapper ${!hasChildren ? 'accordion-children-body' : ''} ${bodyClass}`}
>
{!hasChildren ? renderContent(item, hasChildren) : (
<Accordion onSelect={activeEventKey => onAccordionToggled(`${parentId}-${index}`, activeEventKey)}>
<Fade cascade top when={active}>
{accordionGenerator(item.children, `${parentId}-${index}`)}
</Fade>
</Accordion>
)}
</Card.Body>
</Accordion.Collapse>
</Card>
);
return isRootAccordion ? accordion : (
<div className={'d-flex align-items-center'}>
{accordion}
<div className="accordion-indicator-wrapper">
<div className="accordion-indicator" id={`indicator-${parentId}-${index}`} />
</div>
</div>
);
});
};
if (!isArray(data)) {
return;
}
return (
<Accordion onSelect={activeEventKey => onAccordionToggled(RootAccordionId, activeEventKey)}>
{accordionGenerator(data, RootAccordionId)}
</Accordion>
);
}
export default MultiLevelAccordion;
the styles used in scss
.faqs-questions-wrapper {
padding: 20px 10px
}
.faqs-q-count {
color: $black-color;
font-size: calc(1rem - 1rem/8)
}
.faqs-q-a-wrapper {
flex-basis: 95%;
}
.faq-child-title {
color: $black-color
}
.nested-root-accordion {
flex-basis: 90%;
}
.accordion-indicator-wrapper {
flex-basis: 10%;
width: 100%;
display: flex;
justify-content: center;
.accordion-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: $theme-color;
position: relative;
}
}
Any clue?
Thanks in Advance.
React JS is gonna make this easy
The lines expansion will need to be coded based on the height of the box window
For the dropdown container keep the vertical button lines in a separate div than the Accordian
Check out this pen for creating lines between buttons
https://codepen.io/cataldie/pen/ExVGjya
css part:
.status-container{
padding:10px;
margin:10px;
position:relative;
display: inline-block;
}
.bullet{
padding:0px;
margin:0px;
display: inline-block;
z-index: 10;
}
.bullet:before {
content: ' \25CF';
font-size: 5em;
}
.bullet-before{
/*position:relative;
right:-12px;*/
}
.bullet-after{
/*position:relative;
left:-30px;*/
}
.line{
stroke:blue;
stroke-width:0.3em;
padding:0px;
margin:0px;
display: inline-block;
}
.line-on{
stroke:red;
}
.line-off{
stroke:gray;
}
.color-on{
color: red;
}
.color-off{
color: gray;
}
https://codepen.io/emwing/pen/LgzJOx
I think you can use some inspiration here
I have spent countless hours trying to figure out why my dropdown that is opened/closed by a burger menu icon click is sitting in front of the navbar even though I have specified z-indexes, overflows and positions. This issue is only happening on the MobileNav component below. MobileNav consists of a burger icon and the actual dropdown. Once the burger icon is clicked, the dropdown will either close or open. Currently It is displaying above the nav component and I am having a very hard time figuring out why. Any help will be much appreciated.
Vid to see the dropdown's behavior: https://www.youtube.com/watch?v=zOBnb6r_RN4&ab_channel=TylerOreskey
The dropdown is supposed to come out from the bottom of the navbar and close up into the bottom of the navbar.
Navbar Component: Renders MobileNav component
const Navbar = (props) => {
const [showDropdown, setShowDropdown] = useState(false);
const dropdownToggleHandler = () => setShowDropdown(!showDropdown);
const dropdownClosedHandler = () => setShowDropdown(false);
return (
<header
className={classes.Navbar}
style={{
position: props.passedNavbar ? "fixed" : "relative",
}}
>
<nav className={classes.MobileNav}>
<MobileNav
allNavigationRefs={props.allNavigationRefs}
scrollToDiv={props.scrollToDiv}
open={showDropdown}
closed={dropdownClosedHandler}
dropdownToggleHandler={dropdownToggleHandler}
/>
</nav>
</header>
);
};
export default memo(Navbar);
CSS file for Navbar component: z-index is not working in here.
.Navbar {
top: 0;
height: 50px;
background-color: hsl(213, 27%, 15%);
border-bottom: #00bfff 3px solid;
width: 100%;
z-index: 500;
}
#media (max-width: 500px) {
.DesktopNav {
display: none;
}
}
#media (min-width: 500px) {
.MobileNav {
display: none;
}
}
MobileNav component
const MobileNav = (props) => {
return (
<div className={classes.MobileNav}>
<DropdownToggle clicked={props.dropdownToggleHandler} />
<Dropdown open={props.open} allNavigationRefs={props.allNavigationRefs} />
</div>
);
};
export default MobileNav;
CSS file for MobileNav component
.MobileNav {
overflow: hidden;
}
Dropdown component: (This is displayed above the Navbar component and I cannot get it to be behind the navbar component).
const Dropdown = (props) => {
let attachedClasses = [classes.Dropdown, classes.Close];
if (props.open) {
attachedClasses = [classes.Dropdown, classes.Open];
}
return (
<div className={attachedClasses.join(" ")}>
<NavigationItems allNavigationRefs={props.allNavigationRefs} />
</div>
);
};
export default Dropdown;
CSS file for Dropdown component: z-index is not working in here.
.Dropdown {
background: hsl(212, 87%, 3%);
height: 200px;
transition: transform 0.3s ease-out;
z-index: 400;
display: block;
}
.Open {
transform: translate(0, 25%);
}
.Close {
transform: translate(0, -75%);
}
You are confused on how z-index works.
Consider each level in your tree as a layer.
lets say that Navbar is layer 0, MobileNav is then layer 1, and its children are on layer 2.
By default z-index is calculated among children of the same layer. This is true when the position attribute is on default static. When you alter this to relative you can instruct which layers are going to interuct with each other in a more immediate way.
Having 500 z-index on Navbar will make no sense to MobileNav. It is not his sibling, it's his child.
Here is a possible solution if you can alter the DOM tree
<header
className={classes.Navbar}
style={{
position: props.passedNavbar ? "fixed" : "relative"
}}
>
<nav className={classes.Navbar}>
<DropdownToggle clicked={props.dropdownToggleHandler} />
</nav>
<MobileNav
className={classes.MobileNav}
allNavigationRefs={props.allNavigationRefs}
scrollToDiv={props.scrollToDiv}
open={showDropdown}
closed={dropdownClosedHandler}
/>
</header>
and here is an answer if you can alter the CSS
.Navbar {
top: 0;
height: 50px;
background-color: hsl(213, 27%, 15%);
border-bottom: #00bfff 3px solid;
width: 100%;
position: relative;
}
...
.Dropdown {
background: hsl(212, 87%, 3%);
height: 200px;
transition: transform 0.3s ease-out;
display: block;
position: relative;
z-index: -1;
}
i want to display a list of items in the side panel. If the height of content in list item exceeds 36px i want to hide the contents fitting more than 36px and show expand button for that list item (whose content exceeded 36px) and clicking expand button should show the whole content of list item.
What i have tried so far?
I have added a ref to the span element containing the text that overflows. and expand button appears if height of span element exceeds 36px and clicking expand button shows the content for the list item.
The problem now?
It works fine. except that expand buttons are added at the end of all list items....i want them to be shown at the bottom of that particular list item whose content exceeds 36px.
It works like in image below.
Below is the code,
switch (notification.type) {
case 'new_model_uploaded':
return (
<Notification
icon={<SvgProject width="26" height="26"/>}
text={<Text
base_height={36}
name={name}
text=' created'
item_name={itemname}/>}
timestamp={notification.timestamp}>
<div className="additional_details">
<Image
width={70}
height={70}
item_id={filtered_item.id}
/>
</div>
</Notification>
);
case 'deleted':
return (
<List
icon={<Svg width="20" height="22"/>}
text={<Text
base_height={36}
name={list.name}
text=' deleted item '
item_name={itemname}/>}
timestamp={item.timestamp}/>
);
default:
return null;
}
function List(props) {
return (
<li className="list">
<div className="details_container">
<div className="details">
{props.icon}
{props.text}
<Time>{props.timestamp}</Time>
</div>
{props.children}
</div>
</li>
);
}
class Text extends React.PureComponent {
constructor(props) {
super(props);
this.span_ref = React.createRef();
this.state = {
expanded: false,
overflow: false,
};
}
componentDidMount () {
if (this.span_ref.current.offsetHeight <
this.span_ref.current.scrollHeight) {
this.setState({overflow: true});
}
}
set_expanded = () => {
this.setState({expanded: !this.state.expanded});
};
render () {
return (
<span ref={this.span_ref} className={this.props.classname}
style={{overflow: 'hidden', height: (this.state.expanded ?
null : this.props.base_height)}}>
<span className="red">{name}</span> {this.props.text}
<span className="red">{this.props.name}
{this.props.item_name}</span>
{this.props.additional_text}
{this.state.overflow && <button onClick={this.set_expanded}
style={{position: 'absolute', bottom:
0}}>expand</button>}
</span>
);
}
}
.list {
display: flex;
flex-direction: row;
font-size: 12px;
padding: 8px;
min-height: 49px;
li {
list-style: none;
}
.details_container {
display: flex;
flex-direction: column;
flex-grow: 1;
margin-right: 8px;
.details {
display: flex;
color: #333;
align-items: center;
justify-content: center;
flex-grow: 1;
svg {
margin-right: 8px;
margin-left: 7px;
flex: 0 0 auto;
align-self: center;
flex-shrink: 0;
}
span {
flex-grow: 1;
}
time {
flex: 0 0 auto;
margin-left: 8px;
padding-top: 2px;
color: #CCCCCC;
}
}
span {
word-break: break-all;
}
}
}