I have a header with lots of navItems, for the last navItem, I have another expander div attached which also captures the hover effect of the parent div. How can I disable the pointer-events to none for this child absolute expander?
If I set pointer event of the absolute expander to none then the elements inside of expander also lose hover effect. (Note that they're also of same hover class).
Here's my JSX (HTML like template):
import React from 'react';
import classes from './NavItems.module.css';
import navItems from '#/constants/navItems.json';
import { BiChevronDown } from 'react-icons/bi';
import { useAppDispatch, useAppSelector } from '#/redux/hooks';
import { updateActiveSlice } from '#/slices/navigation.slice';
import { motion } from 'framer-motion';
const getActiveItem = (activeItem: string) => {
const result = navItems.find((element) => element.value === activeItem);
return result;
};
const moreItems = {
value: 'more',
label: 'More',
index: -1
};
const getItems = (activeItem: string) => {
const activeElement = getActiveItem(activeItem);
if (navItems.length > 5 && activeElement) {
if (activeElement.index > 3) return [...navItems.slice(0, 4), activeElement];
return [...navItems.slice(0, 4), moreItems];
}
return navItems;
};
export default function NavItems() {
const { activeSection } = useAppSelector((state) => state.navigation);
const dispatch = useAppDispatch();
const items = getItems(activeSection);
const activeItem = getActiveItem(activeSection);
return (
<div className={classes.NavItemContainer}>
{items.map((item, index) => {
const isLastItem = index === items.length - 1;
const isActive =
(activeItem ? activeItem.value : activeSection) === item.value;
return (
<h4
key={item.value}
className={[
classes.NavItem,
isLastItem ? classes.LastItem : null,
isActive ? classes.ActiveItem : null
].join(' ')}
onClick={() => {
// TODO: verify more click
dispatch(updateActiveSlice(item.value));
}}
>
{item.label}
{isLastItem && (
<React.Fragment>
<BiChevronDown className={classes.ShowMoreIcon} />
<motion.div
initial={{ y: 50 }}
animate={{ y: [-50, 20, 0] }}
className={classes.Expander}
>
<h4 className={classes.NavItem}>Rest</h4>
<h4 className={classes.NavItem}>Next Next Next</h4>
<h4 className={classes.NavItem}>Trust</h4>
<h4 className={classes.NavItem}>Rust</h4>
</motion.div>
</React.Fragment>
)}
</h4>
);
})}
</div>
);
}
and here's my CSS:
.NavItemContainer {
display: flex;
align-items: center;
color: var(--inactive-silver);
gap: 2rem;
}
.NavItem {
letter-spacing: 0.05em;
font-family: var(--raleway);
font-style: normal;
font-weight: 600;
font-size: 1.2rem;
line-height: var(--lineheight);
cursor: pointer;
transition: all 0.1s ease-in;
}
.NavItem:hover {
scale: 0.96;
}
.LastItem {
display: flex;
align-items: center;
position: relative;
}
.ActiveItem {
color: var(--active-blue);
}
.ShowMoreIcon {
font-size: 2rem;
color: var(--inactive-silver);
}
.Expander {
position: absolute;
top: 130%;
right: 5%;
width: max-content;
padding: 0 1rem;
background-color: var(--light-background-color);
box-shadow: var(--box-shadow-thick);
text-align: end;
border-radius: 0.4rem;
}
.Expander:hover {
scale: 1;
}
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 have a dropdown menu with an arrow in the after pseudo element that i want to rotate when clicked.
here is the styled component:
export const Header = styled.div`
padding: 0.2rem 0.5rem;
&:after {
content: url(${arrowUp});
height: 100%;
width: 15px;
text-align: right;
float: right;
transform: rotate(180deg);
}
&:hover {
cursor: pointer;
}
`;
This is the React.ts component
export const AttributeValuesChecklist: React.FC<IProps> = ({
title,
name,
values,
valuesShowLimit = false,
valuesShowLimitNumber = 5,
onValueClick,
}: IProps) => {
const [viewAllOptions, setViewAllOptions] = React.useState(!valuesShowLimit);
const showAttributeList = e => {
const actualDisplay = e.target.parentNode.children[1].style.display;
if (actualDisplay === "block") {
e.target.parentNode.children[1].style.display = "none";
} else {
e.target.parentNode.children[1].style.display = "block";
}
};
return (
<S.Wrapper>
{title && (
<S.Header onClick={e => showAttributeList(e)}>{title}</S.Header>
)}
<DropMenu>
{values &&
values.map((value, index) => {
if (!viewAllOptions && index > valuesShowLimitNumber - 1) {
return <></>;
}
return (
<Checkbox
name={name}
checked={!!value.selected}
onChange={() => onValueClick(value)}
>
{value && value.name}
</Checkbox>
);
})}
</DropMenu>
<S.BottomBorder />
</S.Wrapper>
);
};
I've tried the "data-attribute" trick but it doesn't work. And beacuse i'm using styled-components i don't know how can I use premade classes.
//styled-component
export const Header = styled.div<{isClicked: boolean}>`
padding: 0.2rem 0.5rem;
&:after {
content: url(${arrowUp});
height: 100%;
width: 15px;
text-align: right;
float: right;
transform: ${props => props.isClicked ? rotate(180deg) : rotate(0deg)};
}
`;
//component
//you can make a state to store the value or pass from props
<S.Header isClicked={//boolean value}>{title}</S.Header>
I have a custom table made in React with PostCSS which I not made (therefore is kind of confusing. I need to remove the left and right borders.
I want to remove both borders from the left and the right (in the image I managed to remove one (red <- ->) and just keep the inside ones.
Here is my postCSS code: (it's kind of a custom table)
.datatable-wrapper {
background : #ffffff;
min-height: 41rem;
.filter-column{
border-right: 1px solid var(--clear-grey);
}
.btn-filter_view{
cursor: pointer;
display: flex;
align-items: center;
}
>.container {
padding: .3rem .7rem .3rem .8rem;
}
.badge {
border: 1px solid;
border-radius: 4px;
display: inline-flex;
font-size: 0.75rem;
line-height: 1.125rem;
padding: 0.1875rem 0.6875rem 0.1875rem 0.6875rem;
width: -moz-fit-content;
width: fit-content;
}
.selected-items {
padding-left: 1.0625rem;
padding-right: 2.125rem;
height : 3.5rem;
border-radius: .25rem;
background-color: #0f4aa1;
font-size: 0.875rem;
font-weight: 500;
color: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
.actions{
>span:nth-child(2){
margin-left: 3.6875rem;
}
}
}
.hidden-selected-items {
display: none;
overflow: hidden;
transition: max-height 0.6s ease;
}
.search {
min-height : 2.75rem;
color : #9ea0a5;
font-size: .75rem;
vertical-align: middle;
/* border: 1px solid var(--clear-grey); */
margin-right: 5px;
.form-group {
min-width : 100%;
}
.form-control {
color : #9ea0a5;
font-size: .75rem;
min-width : 100%;
height : 100%;
}
.group-search{
width:100%;
padding-left: 1.6875rem;
>.form-group {
background:transparent;
>.input-group{
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
align-content: center;
padding: .2rem;
width: 10.875rem;
}
}
}
>.search-info {
padding: .5rem;
.dropdown-toggle {
min-width : 3.5rem;
min-height : 1.8rem;
font-size: .75rem;
padding-right: 1.5rem;
}
.dropdown-item {
font-size: .75rem;
height : 1.8rem;
}
}
> [class^="col-xs-"] {
/* padding: 0.5625rem 1.6875rem 0.5625rem 1.6875rem; */
font-size: 0.875rem;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
align-content: stretch;
.input-group-append {
display: initial;
margin-left: -1rem;
}
>.uikon {
padding : .250rem;
}
}
}
.hidden-search {
display: none;
overflow: hidden;
transition: max-height 0.6s ease;
}
.filter {
color : #9ea0a5;
font-size: .75rem;
border-top: 1px solid var(--clear-grey);
border-radius: 4px;
padding: .5rem;
> [class^="col-xs-12"] {
>.filter-elements {
display: flex;
align-items: center;
>.form-group {
width: inherit;
margin: 0 .5rem 0 0;
.form-control {
color : #9ea0a5;
font-size: .75rem;
/* min-width : 100%; */
height : 100%;
width: 9.375rem;
}
>.btn {
position: relative;
float: right;
color : #9ea0a5;
font-size: .75rem;
}
}
.dropdown-toggle {
min-height : 1.8rem;
width: 9.375rem;
/* width: 100%; */
font-size: .75rem;
padding-right: 1.5rem;
}
.dropdown-item {
font-size: 0.875rem;
height: auto;
padding-bottom: 0.625rem;
padding-top: 0.625rem;
}
}
}
}
.hidden-filter {
display: none;
overflow: hidden;
transition: max-height 0.6s ease;
}
.head {
cursor: pointer;
border-top: 1px solid var(--clear-grey);
border-bottom: 1px solid var(--clear-grey);
min-height : 2.75rem;
color : #9ea0a5;
font-size: 0.75rem;
vertical-align: middle;
width: 100%;
> [class^="col-xs-"] {
font-size: 0.75rem;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: center;
align-content: stretch;
/* border-left: 1px solid var(--clear-grey); */
border-right: 1px solid var(--clear-grey);
padding-left: 0.93125rem;
}
}
.body {
/* min-height : 4.375rem; */
color : #3e3f42;
font-size: .75rem;
vertical-align: middle;
width: 100%;
> [class^="col-xs-"] {
border-right: 1px solid var(--clear-grey);
border-bottom: 1px solid var(--clear-grey);
padding-top: 1.5rem;
font-size: 0.875rem;
padding-bottom: 1.5rem;
/* width: 7.99375rem; */
display: flex;
flex-direction: row;
flex-wrap: nowrap;
/* justify-content: flex-start; */
padding-left: 0.93125rem;
align-items: center;
align-content: flex-start;
}
.check {
min-height : 2.75rem;
/* border: 1px solid var(--clear-grey); */
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
align-content: flex-start;
}
}
.datatable {
min-width: 100%;
padding-right: 2.25rem;
}
}
Here is my .jsx code (only where the table is implemented):
// #flow
import * as React from 'react';
import classnames from 'classnames';
import { t as typy } from 'typy';
import { Button, Checkbox, TextField, Select } from '#duik/it'
import Icon from '#duik/icon'
import arrowdown from '__/images/arrowdown.svg';
import { Grid, Row, Col, } from 'react-flexbox-grid';
import { Formik, Field } from 'formik';
import { DUIKSelectDate, DUIKSelect, DUIKTextField } from 'produceUI';
import { Pagination } from 'produceUI/atoms';
type Props = {
/**
* A simple definition of each column, so we know what will be displayed and how.
* This requires an 'elementKey', which is they key to be used on each element of
* the 'data' list to get and display a value. In case an 'elementKey' is not given,
* or the 'elementKey' points to something different than a number or a string
* 'renderCell' should be present. */
columns: {
className?: string,
elementKey?: string,
title?: string,
invisible?: boolean,
filterable?: boolean,
sortable?: boolean,
width?: number,
selectable?: boolean,
cols?: number,
capitalLetters: Boolean,
renderCell?: (element: Object, rowIndex: number, key: string) => React.Node,
showDetail?: (element: Object, rowIndex: number, key: string) => React.Node,
}[],
/** Filter the data elements with this field */
filter?: string | number,
/** An array of JSON Objects. Nesting objects is allowed. */
placeholderSearch?: string,
filteredLabelForMany?: string,
filteredLabelForOne?: string,
data: { [string]: any },
/** Every row needs an unique identifier. What is the key for it? */
dataID?: string,
/** Should it have checkboxes? */
selectable?: boolean,
/** Functional component to be rendered on the bottom of the table when
* "selectable" is true AND when there is at least one selected row. The selected
* elements are passed as a parameter so this logic is managed from the parent component.
*/
actionsForSelected?: (elements: { [string]: any }) => React.Node,
/** A React element to be rendered in case the data is empty */
renderEmpty?: React.Node,
totalPages?: number,
currentPage?: number,
pagesLimit?: number,
onChangePage?: (page: number) => void,
idToShow?: number,
onClickDelete?: (ids: []) => void,
handleSort?: (elementKey: string) => void,
debounceOnSearch?: (textToFilter: string) => void,
onFilter?: (values: object) => void,
onClickFilter?: () => void,
shouldFilter?: boolean,
shouldSearch?: boolean,
};
function getID(element, key): string | number {
const value = typy(element, key);
return value.isString
? value.safeString
: value.isNumber
? value.safeNumber
: 0;
}
const Table = (props: Props) => {
const {
columns = [], data = [],
placeholderSearch, filter, selectable,
filteredLabelForMany, filteredLabelForOne,
renderEmpty, actionsForSelected,
dataID, idToShow,
totalPages, currentPage, pagesLimit,
onChangePage, onClickDelete, debounceOnSearch,
handleSort, onFilter, onClickFilter,
shouldFilter, shouldSearch
} = props;
const [selected, setSelected] = React.useState([]);
const [dataToShow, setDataToShow] = React.useState(data);
const [selectedId, setSelectedId] = React.useState(0);
const [selectAll, setSelectAll] = React.useState(false);
const [activeOption, setActiveOption] = React.useState()
const convertPagesToArrayPages = () => {
let idx = 1;
let tmpArray = [];
while (idx <= totalPages) {
tmpArray.push({ label: `${idx}`, value: idx++ });
}
return tmpArray;
};
const arrayPages = convertPagesToArrayPages();
React.useEffect(() => {
const ids = data.map(d => getID(d, dataID));
const newSelected = selected.filter(s => ids.indexOf(getID(s, dataID)));
setDataToShow(data);
setSelected(newSelected);
}, [JSON.stringify(data), dataID]); // This is because the data variable is always a new one.
React.useEffect(() => {
setSelectedId(idToShow);
}, [idToShow]); // This is because the data variable is always a new one.
const filterKeys = columns.filter(col => (col.filterable)).map(col => (col.elementKey));
const initialFilterFields = () => {
let values = {};
columns.map((item, index) => {
if (item.filterable) {
if (item.type === 'select') {
values[item.elementKey] = item.activeElinSelect;
} else
values[item.elementKey] = '';
}
});
return values;
};
const renderHeaders = () => {
return (
<React.Fragment>
<Row className={selected.length > 0 ? "selected-items" : "hidden-selected-items"}>
<Col xs={10}>
{`${selected.length} ${selected.length > 1 ? filteredLabelForOne : filteredLabelForMany} Seleccionada(s) `}
</Col>
<Col xs={1}>
{`Descargar `}
</Col>
<Col xs={1}>
<a onClick={(ev) => onClickDelete(selected)} href="#"><span>Rechazar</span></a>
</Col>
</Row>
<Row className={shouldFilter ? "filter" : "hidden-filter"}>
<Col xs={12} >
<Formik
initialValues={initialFilterFields()}
render={({ values }) => {
return (
<div className='filter-elements'>
{columns.map((item, index) => {
if (item.filterable) {
return (
item.type === 'select' ?
<div className="form-group" key={`selecteFilterField_${item.elementKey}`}>
<Field
activeOption={item.activeElinSelect}
options={item.data}
id={item.elementKey}
name={item.elementKey}
placeholder={item.title}
component={DUIKSelect}
/>
</div>
: item.type === 'date'
?
<div className="form-group" key={`datedFilterField_${item.elementKey}`}>
<Field
placeholder={item.title}
format="dd/mm/yyyy"
id={item.elementKey}
name={item.elementKey}
type='text'
component={DUIKSelectDate}
/>
</div>
:
<Field
key={`textFilterField_${index}`}
placeholder={item.title}
id={item.elementKey}
name={item.elementKey}
type='text'
component={DUIKTextField}
/>
);
}
})
}
<div className="form-group">
<Button onClick={() => {
/* onFilter(values); */
/*
if(status !== null || PF !== null){}
*/
}}>
Filtrar
</Button>
</div>
</div>
)
}}
>
</Formik>
</Col>
</Row>
{/*Fila de filtro, búsqueda, etc.*/}
<Row className={shouldSearch ? "search" : "hidden-search"}>
{/* Filtro */}
<Col className='filter-column' xs={1} onClick={() => { onClickFilter() }}>
<span className='btn-filter_view'><Icon mr>view_list</Icon>Filtrar</span>
</Col>
{/* Búsqueda */}
<Col xs={3}>
<div className='group-search'>
<TextField
placeholder={placeholderSearch}
rightEl={<Icon mr>search_left</Icon>}
onChange={e => {
debounceOnSearch(e.target.value);
}}
/>
</div>
</Col>
<Col xs={5} xsOffset={3} className='search-info'>
<span>
{`${dataToShow.length <= 0 ? 'No existen datos ' : dataToShow.length} ${dataToShow.length === 1 ? filteredLabelForOne : filteredLabelForMany}`}
</span>
<Pagination
pages={totalPages}
currentPage={currentPage}
limit={pagesLimit}
onChange={onChangePage}
/>
<b>Ir a página</b>
<Select
activeOption={arrayPages.length > 0 ? arrayPages[0] : { label: 'Seleccione opcion', value: 0 }}
defaultOption={arrayPages.length > 0 ? arrayPages[0] : null}
options={arrayPages}
/>
</Col>
</Row>
<Row className='head'>
{/* Checkbox maestro */}
<Col xs={1}>
{selectable ?
<Checkbox
value={selectAll}
onChange={(ev) => {
const checked = typy(ev, 'target.checked').safeBoolean;
if (!checked) setSelected([]);
else {
let tmp = [];
dataToShow.map((item, index) => {
tmp.push(item.id);
})
setSelected(tmp);
}
setSelectAll(checked);
}}
/> :
null
}
</Col>
{columns.map((item, index) => {
return (
<Col xs={item.cols} key={`col_${index}`}
onClick={
data.length > 2 ? () => handleSort(item.elementKey) : null
}>
{item.title}
</Col>
)
})
}
</Row>
</React.Fragment>
);
};
const shouldShowRow = (element) => {
if (filter) {
return filterKeys.some((key) => {
const field = typy(element, key);
if (field.isNumber) return `${field.safeNumber}`.includes(`${filter}`);
if (field.isString) return field.safeString.includes(filter);
return false;
});
}
return true;
};
const renderBody = () => dataToShow.reduce((accum: any[] = [], rowData, rindex) => {
if (!shouldShowRow(rowData)) return accum;
const cols = columns.map((col, cindex) => {
const renderer = col.renderCell
? col.renderCell
: (element, rowIndex, key) => {
const field = typy(element, key);
if (field.isNumber) return field.safeNumber;
if (field.isString) return col.capitalLetters ? field.safeString.toUpperCase() : field.safeString;
return undefined;
};
const content = (
<Col xs={col.cols}
style={{ justifyContent: col.position }}
key={cindex}
>
{renderer(rowData, rindex, typy(col, 'elementKey').safeString)
}
</Col>
);
return content;
});
const eid = getID(rowData, dataID);
const checkboxCell = selectable
? (
<Col xs={1} className='check'>
<div className="icon-arrow">
<img
onClick={() => {
const checked = true;
setSelectedId(eid === selectedId ? 0 : eid);
}}
src={arrowdown}
className={eid === selectedId ? 'arrow_up' : 'arrow_down'}
/>
</div>
<Checkbox
id={`check-${rindex}`}
name={`check-${rindex}`}
value={selected.indexOf(eid) > -1}
checked={selected.indexOf(eid) > -1}
onChange={(ev) => {
const checked = typy(ev, 'target.checked').safeBoolean;
if (!checked) setSelected(selected.filter(s => s !== eid));
else if (selected.indexOf(eid) === -1) setSelected([...selected, eid]);
}}
/>
</Col>
)
: undefined;
return [
...accum,
(<Row className='body' key={rindex}>{checkboxCell}{cols}
<div className='panel'>
<div className={eid === selectedId ? 'showed-panel' : 'hidden-panel'}>
{
rowData.detailComponent
? (<rowData.detailComponent />)
: ''
}
</div>
</div>
</Row>),
];
}, []);
let content = null;
let pagination = null;
if (data && data.length > 0) {
content = (
<Grid fluid>
{renderHeaders()}
{renderBody()}
</Grid>
);
} else {
content = (
<Grid fluid>
{renderHeaders()}
{renderEmpty}
</Grid>
);
}
return (
<div className='datatable-wrapper'>
{content}
{pagination}
<div className={classnames('datatable-actions')}>
{actionsForSelected && selected.some(t => t)
? actionsForSelected(data.filter(d => selected.indexOf(getID(d, dataID)) > -1))
: null
}
</div>
</div>
);
};
Table.defaultProps = {
filter: undefined,
selectable: false,
renderEmpty: undefined,
actionsForSelected: undefined,
dataID: 'id',
placeholderSearch: 'Buscar',
filteredLabelForMany: 'Elementos',
filteredLabelForOne: 'Elemento',
idToShow: 0,
};
export default Table;
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>