fit text inside an element with fixed width - html

I have a list of buttons that uses roman numerals as label, something like this:
{list.map((item, index) => {
return (
<li key={`${item.id}`}>
<button
onClick={() => handleChange(index)}
>
{transformNumberToRomanNumeral(index + 1)}
</button>
</li>
)
})}
because of that, the numerals will have different sizes, like this: "I", "II", "III".
I need to make all the buttons the same size, both width and height, button if I set the width to a fixed value, some text will still overflow the button width. Is there a way to make the text scale to fit the max width, but never overflow it?
I tried using using svg with text tag, but the scale looks really bad, it usually don't get all the height so the text end ups really small.

In the example below, the buttons have a consistent width of 50px.
Where n is the number of characters in the roman numeral, both the:
font-size
line-height
are: (14 - n)
except where (14 - n) < 6, in which case that value remains 6.
Working Example:
let buttons = document.querySelectorAll('button');
for (let button of buttons) {
const numeralCount = button.textContent.length;
let textSize = (14 - numeralCount);
textSize = (textSize < 6) ? 6 : textSize;
button.style.setProperty('--text-size', textSize + 'px');
}
:root {
--text-size: 6px;
}
button {
display: block;
width: 50px;
height: 18px;
margin: 6px 0 0;
padding: 0 2px;
line-height: var(--text-size);
font-size: var(--text-size);
overflow-x: hidden;
}
<ol>
<li><button>M</button></li>
<li><button>MD</button></li>
<li><button>MDC</button></li>
<li><button>MDCC</button></li>
<li><button>MDCCC</button></li>
<li><button>MDCCCL</button></li>
<li><button>MDCCCLX</button></li>
<li><button>MDCCCLXX</button></li>
<li><button>MDCCCLXXX</button></li>
<li><button>MDCCCLXXXV</button></li>
<li><button>MDCCCLXXXVI</button></li>
<li><button>MDCCCLXXXVII</button></li>
<li><button>MDCCCLXXXVIII</button></li>
</ol>

I managed to do something that seems to look like what you want. Here is the Stackblitz and here is the code :
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const App = () => {
return (
<div className="container">
<button>
<SvgRenderer text="VII" />
</button>
<button>
<SvgRenderer text="LXXIII" />
</button>
<button>
<SvgRenderer text="LXIII" />
</button>
<button>
<SvgRenderer text="LXXIII" />
</button>
<button>
<SvgRenderer text="LXXXIII" />
</button>
</div>
);
};
const SvgRenderer = ({ text }) => (
<svg
width="100%"
height="100%"
viewBox="0 0 100 50"
preserveAspectRatio="xMinYMid meet"
// style={{ backgroundColor: 'green' }}
>
<text
x="0"
y="45"
fill="black"
lengthAdjust="spacingAndGlyphs"
textLength="100"
>
{text}
</text>
</svg>
);
render(<App />, document.getElementById('root'));
I based this on this post. Since I couldn't manage to make the checked answer to work properly, I used one of the answer bellow, but note that I'm absolutely not a pro svg so there might be a way better answer than mine using SVG (or other answers).
[previous answer] You coud do this by using flexbox. This way you don't need to set with and height. Here is the Stackblitz and here is the code :
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const App = () => {
return (
<div className="container">
<button>some text</button>
<button>Some more text</button>
<button>Some more text longer</button>
<button>Some more text longer again and again</button>
<button>Some more text longer again and again, and again ?</button>
</div>
);
};
render(<App />, document.getElementById('root'));
And some css :
.container {
display: flex;
flex-wrap: wrap
}
.container > button {
flex: 1
}
I used long string in the buttons for the use case, but with roman numerals it should be ok I guess.

You can iterate through the buttons, using a dummy button to notice when the text just fits within the button width.
This snippet starts with a largeish font size assuming the button width is 50px and gradually brings the font size down until the text fits.
<style>
.container {
widht: 100vw;
display: flex;
}
button {
width: 50px;
height: 50px;
padding: 0px;
text-align: center;
clear: both;
}
</style>
<body>
<div class="container">
<button>I</button>
<button>II</button>
<button>III</button>
<button>VIII</button>
<button>MMCXXII</button>
</div>
</body>
<script>
const buttonW = 50;
const buttons = document.querySelectorAll('button');
const dummy = document.createElement('button');
let w, fs ;
document.body.appendChild(dummy);
dummy.style.padding = 0;
buttons.forEach( button => {
fs = 40;
dummy.style.width = 'auto';
dummy.style.fontSize = fs + 'px';
dummy.innerHTML = button.innerHTML;
w = window.getComputedStyle(dummy).width.replace('px', '');
while (w > buttonW) {
w = window.getComputedStyle(dummy).width.replace('px', '');
fs--;
dummy.style.fontSize = fs + 'px';
}
button.style.fontSize = fs + 'px';
});
dummy.remove();
</script>

Related

CSS attribute selectors in JSX with TypeScript

I can do the following in pure HTML/CSS
<div color="red">
red
</div>
<div color="yellow">
yellow
</div>
div[color="red"] {
color: red;
}
div[color="yellow"] {
color: yellow;
}
However, using React and TypeScript, I want to do something like this:
const Colored = ({ color }: { color: "yellow" | "red" ) => (
<div color={color}>
{ color }
</div>
);
However, I can't add abritrary props to div!!
so we could do exactly the same as the above, but instead
<Colored color="red" />
<Colored color="yellow" />
How can I do something like this? Additionally, should I even be doing this? Is there a better way of doing this in React?
In this case using the type React.HTMLAttributes<HTMLDivElement> you can pass additional properties to the DIV mantaining your component Colored.
type IColoredProps = { color: "yellow" | "red" } & React.HTMLAttributes<HTMLDivElement>
const Colored: FC<IColoredProp> = (props) => (
<div {...props}>
{props.color}
</div>
);
Now the color is mandatory and can only be "red" or "yellow", and you can pass the rest of properties a normal div can have.
<Colored style={{ fontSize: "25px" }} color="red"/>
I created a codeSandBox example for you :
instead of <Colored text="red" /> you should use <Colored color="red" />

unable to remove css of span class due to $(...).css is not a function

I am trying to change the css of a html given a span class.
I have an html as follows:
<form id=thing>
<div id=a>
<span class=x>
</span>
</div>
<div id=b>
<span class=x>
</span>
</div>
</form>
When I inspect the webpage I see the following style being applied:
#thing .x {
display: none !important;
}
Now my goal is to get rid of this display css property completely.
I tried $("#thing .x").css("display", ""); but i keep getting "$(...).css is not a function - unable to remove css "
Any tips on how to solve this?
Using inline style with setAttribute()
I assume you are trying to remove the styling for display from none. '' => nothing would not be a valid style to use, use a valid property value recognized by the display property, like inline or block for example.
You can override the !important rule set in CSS with the same CSS hack used in inline style as inline style takes presidence over styles set in CSS sheet. The only way I was able to do it was by setting the style attribute using el.setAttribute('style', 'display: inline !important;'). See snippit and a few examples of failure and finally success below...
let dontUseImportant = document.querySelectorAll('#thing .x')
dontUseImportant.forEach(el => {
// style display inline !important is ignored
// this is likely a rule in JS that will disallow
// spaces in values passed into properties for style
// more research on this may be fruitful
// however note the outcome in console.log
el.style.display = "inline !important";
el.textContent = 'this will not parse to DOM'
console.log(el)
})
dontUseImportant.forEach(el => {
// style display is added but is over written by !important used in CSS
el.style.display = "inline";
el.textContent = 'this will not parse to DOM'
console.log(el)
})
// this also will not work
dontUseImportant.forEach(el => {
el.setAttribute('style', 'display: null !important;');
el.textContent = 'this will not parse to DOM'
console.log(el)
})
// by adding !important using setAttribute behind our
// property, this affects the elements style
// however, we are putting a bandaid on a bandaid
dontUseImportant.forEach(el => {
el.setAttribute('style', 'display: inline !important;');
el.textContent = 'content x is finally shown!'
console.log(el)
})
#thing .x {
display: none !important;
}
.y {
display: inline;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<form id="thing">
<div id="a">content a
<span class="x">content x
</span>
</div>
<div id="b">content b
<span class="x">content x
</span>
</div>
</form>
Swapping classes on element
Because the CSS file is using !important this unfortunately will override any styles you place on the element using el.style.property = '' regardless of using JS or a JS library like JQuery.
You could add a new class using JS that has a defining display: inline and then remove the old class. Keep in mind that you must add a valid property value when using styles. display = '' is not valid.
Your particular issue is a great example as to why !important is a BAD IDEA for styling as a crutch!
let $dontUseImportant = $('#thing .x')
// iterate over elements and swap out the classes
// make sure to add the new class 'y' first
// then remove initial class 'x'
$.each($dontUseImportant, function(i){
$(this).addClass('y')
$(this).removeClass('x')
console.log($dontUseImportant[i])
})
#thing .x {
display: none !important;
}
.y {
display: inline;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<form id="thing">
<div id="a">content a
<span class="x" >content x
</span>
</div>
<div id="b">content b
<span class="x">content x
</span>
</div>
</form>
Why not using the hidden attribute? It is supported in all modern browsers. So in your case, you don't even need CSS.
jQuery:
$('button').on('click', function() {
$('.x').each(function() {
$(this).show();
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<form id="thing">
<div id="a">
<span class="x" hidden>
1
</span>
</div>
<div id="b">
<span class="x" hidden>
2
</span>
</div>
</form>
<button>Show</button>
Vanilla JS
const x = document.querySelectorAll('.x');
document.querySelector('button').addEventListener('click', (e) => {
for (let item of x) {
item.removeAttribute('hidden');
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<form id="thing">
<div id="a">
<span class="x" hidden>
1
</span>
</div>
<div id="b">
<span class="x" hidden>
2
</span>
</div>
</form>
<button>Show</button>

Design of a custom accordion

I am trying to design a custom accordion with these fancy red lines connecting parent and children (see photo).
I am using Grommet components here but in summary, the layout it's just a bunch of divs (the Box tag) and a collapsible panel component for the children(the Collapsible tag). Children panels can be opened and show more content.
After a couple of tries, what I did to connect parent and children is to wrap the outer box with a left-border and then remove the extra border on the bottom using a white box on top of it (the Stack tag of the second example code). The horizontal line connecting the card and the left-border is just a styled div placed next to the Child tab ( inside the SubMenuElement component ).
I think this is quite an intricate solution ( also because I need to make the white box responsive ) but I couldn't think about a simpler one.
Do you have any suggestion on how can I improve or re-do the red connections?
Thanks in advance for the help!
Please note that I am aware all the panels are using the same variables on click
<MenuButton
open={openMenu}
label="PARENT TAB-XYZ"
onClick={() => {
const newOpenMenu = !openMenu;
setOpenMenu(newOpenMenu);
setOpenSubmenu1(!newOpenMenu ? false : openSubmenu1);
}}
/>
<Collapsible open={openMenu}>
<Box background="antiquewhite" margin={{ left: 'small' }} border={{ side: 'left', size: '2px', color: 'red' }}>
{Tabs.map(el => {
return (
<SubMenuElement
key={el.title}
open={openSubmenu1}
label={el.title}
onClick={() => setOpenSubmenu1(!openSubmenu1)}
/>
);
})}
</Box>
</Collapsible>
<MenuButton
open={openMenu}
label="PARENT TAB-POU"
onClick={() => {
const newOpenMenu = !openMenu;
setOpenMenu(newOpenMenu);
setOpenSubmenu1(!newOpenMenu ? false : openSubmenu1);
}}
/>
<Collapsible open={openMenu}>
<Stack anchor="bottom-left">
<Box
background="antiquewhite"
margin={{ left: 'small' }}
border={{ side: 'left', size: '2px', color: 'red' }}
>
{Tabs.map(el => {
return (
<SubMenuElement
key={el.title}
open={openSubmenu1}
label={el.title}
onClick={() => setOpenSubmenu1(!openSubmenu1)}
/>
);
})}
</Box>
<Box background="white" height="39px" width="35px"></Box>
</Stack>
</Collapsible>
</Box>
);
Based on one of my answer for someone who needed a divider, I can propose you something like that: repro on Stackblitz
You will find the original divider code on the first link. for your needs, i modified it a little so it just add the link on the left of the content. There is still a border-left on the content wrapper tho, it seems the easiest solution for me.
Your main file :
import React, { Component } from "react";
import { render } from "react-dom";
import Divider from "./divider";
import "./style.css";
const App = () => {
const toggleAccordion = e => {
e.target.classList.toggle("hidden");
};
return (
<>
<div className="accordion hidden" onClick={toggleAccordion}>
accordion header
<div className="accordion-content-wrapper">
<Divider>
<div>Content</div>
</Divider>
<Divider>
<div>Content</div>
</Divider>
<Divider>
<div>Content</div>
</Divider>
</div>
</div>
</>
);
};
render(<App />, document.getElementById("root"));
My accordion css (your component already have this feature i guess, i just made a minimal reproduction):
.accordion.hidden {
height: 18px;
overflow: hidden;
}
.accordion-content-wrapper{
margin-left: 10px;
border-left: 1px solid black;
}
And for the divider, there not a lot of change from my original answer, here is the code:
import React from 'react';
const Divider = ({ children }) => {
return (
<div className="divider-component">
<div className="container">
<div className="border" />
<span className="content">{children}</span>
</div>
</div>
);
};
export default Divider;
css:
.divider-component .container{
display: flex;
align-items: center;
}
.divider-component .border{
border-bottom: 1px solid black;
width: 15px;
}
.divider-component .content {
width: 100%;
}
Even if you'll have to edit it to fit your needs, the idea is to add a flex container so you can add the little link on the left of your content, correctly aligned with your content.
For whoever will encounter the same issue, I ended up using two 50% height flexboxes inside a div that replace the horizontal line. This allows managing the responsive resize automatically while giving flexibility on the last item border.
const SubMenuElement = ({ last, label, open, onClick }: { last?: boolean; label: string; open: any; onClick: any }) => {
return (
<Box direction="row">
<Line last={last} />
<Box width="100%" margin={{ vertical: 'small' }}>
<Card background="white" onClick={onClick}>
....
</Card>
</Box>
</Box>
);
};
Where the Line is
const Line = ({ last }: { last?: boolean }) => (
<Box direction="column" width="20px" flex="grow">
<Box height="50%" style={{ borderLeft: '1px solid red', borderBottom: '1px solid red' }} />
<Box height="50%" style={last ? {} : { borderLeft: '1px solid red' }} />
</Box>
);

Show image next to input rather than underneath it

So, I wanted to display a loading gif when an input box was on focus.
I've managed to do it but the loading gif it's getting below the input box, and not after
On CSS, if I change the display:none to display:yes the loading gif appears after the input box as I want, but only until the JS function is triggered.
var input = document.getElementById('showloading');
var message = document.getElementsByClassName('loadingGif')[0];
input.addEventListener('focus', function() {
message.style.display = 'block';
});
input.addEventListener('focusout', function() {
message.style.display = 'none';
});
.loadingGif {
display: none;
}
<input id="showloading" type="text" class="auto">
<img class="loadingGif" src="loading.gif">
Screenshoots:
What it looks like:
What I want it to look like:
#T.J. Crowder aswer is the one you should follow if you want to use Javascript although you don't need Javascript to do what you want.
You can use only CSS by using :focus selector and the sibling selector + to write a style rule. When showloading is focused all the adjacent siblings with the class loadingGif will have the display: inline-block
Such as:
.loadingGif {
display: none;
}
#showloading:focus + .loadingGif {
display: inline-block;
}
<input id="showloading" type="text" class="auto">
<img class="loadingGif" src="loading.gif">
The problem is that block is displayed as a block, so it starts a new visual line.
I wouldn't use style at all, I'd use a class to show/hide the image:
var input = document.getElementById('showloading');
var message = document.getElementsByClassName('loadingGif')[0];
input.addEventListener('focus', function() {
message.classList.remove("hide"); // <===
});
input.addEventListener('focusout', function() {
message.classList.add("hide"); // <===
});
.loadingGif.hide {
/* --------^^^^^ */
display: none; /* <=== */
}
<input id="showloading" type="text" class="auto">
<img class="loadingGif hide" src="loading.gif">
<!-- ------------------^^^^ -->
...although as Pepper says (and Diogo also now says), you can do this with just CSS and without JavaScript or a class.

CSS transition of height for injected content into a DIV

I'm having a following template:
<div class='content'>
{{content}}
</div>
And following style:
.content {
margin-top: 30px;
background-color: skyblue;
width: 300px;
transition: all linear 0.5s;
}
Please note that {{content}} will grow or shrink (technically to extend a card to show more information or hide it). I've already set the css transition and it does work when manually setting a different height to the element. However when more content is injected into content no transition is made, just a plain old resizing. Any help of getting the transision right?
See following plunkr
Thanks,
Amit.
I believe that's quite normal, transitions apply only to changes to the CSS, not for computed changes.
One option might be to have a nested div, set overflow: hidden on the outer one, then get the computed height of the inner one and set it on the outer one to get the transition.
CSS:
#outer {
margin-top: 30px;
background-color: skyblue;
width: 300px;
transition: all linear 0.5s;
overflow: hidden;
}
HTML:
<button id="more" onclick="increase();">More</button>
<button id="more" onclick="decrease();">Less</button>
<div id="outer">
<div id="inner">
lorem ipsum
</div>
</div>
JS:
function increase()
{
var o = document.getElementById('inner');
var t = o.innerHTML;
for (var i=0 ; i<20;i++)
t += " lorem ipsum";
o.innerHTML = t;
adjustHeight();
}
function decrease()
{
var o = document.getElementById('inner');
o.innerHTML = "lorem ipsum";
adjustHeight();
}
function adjustHeight()
{
var i = document.getElementById('inner');
var o = document.getElementById('outer');
o.style.height = window.getComputedStyle(i).height;
}
Working fiddle: http://jsfiddle.net/Lnts7w1f/
With the help of #jcaron answer I managed to sort this in a react component. So when the 'activeSubsection' button is clicked the useEffect hook is run.
const [topicsHeight, setTopicsHeight] = React.useState("0px")
const topicsDiv = React.useRef();
React.useEffect(() => {
setTopicsHeight(topicsDiv?.current?.offsetHeight + "px")
}, [activeSubsection])
{!hideTopics &&
<div style={{ overflow: "hidden", height: topicsHeight, transition: "0.2s" }}>
<Topics ref={topicsDiv} className="largeScreen">
{!!subsectionTopics && subsectionTopics.sort((a, b) => a.node.slug.localeCompare(b.node.slug)).map((topic, index) =>
<Link key={index} href={'/'+slug+'/'+topic.node.slug} scroll={false}>
<Topic
active={activeTopic == topic.node.slug} transition={true} bold={activeTopic == topic.node.slug}
>
{topic.node.title}
</Topic>
</Link>
)}
</Topics>
</div>
}
Ovbiously that's not the full component but hopefully enough to give you an idea.
Well... There's a trick you can do for that... I don't know if it will fit your needs.
The css transition effect is applied on css properties that have a previous value, and then change. Though you are indeed changing the content's height of the div, the actual css property height is not explicitly changing. That's why you don't get the animation.
A workaround is to find the inner height of the div and then set it to the element, causing it to animate. I've created a function for that, and added a span to control the inner size of the div.
Just call the function on every change:
<div class='content'>
<span id="height-control">
{{ctrl.content}}
</span>
</div>
JS function:
var spn = document.getElementById("height-control");
var UpdateHeight = function () {
var h = spn.offsetHeight;
spn.parentNode.style.height = h + "px";
};
http://plnkr.co/edit/p6QRAR5j4C8d0d0XRJRp?p=preview