I'm creating an accordion module in Elm with an open/close animation. Initially, the accordion body has an attribute style="height: 0". When opening the accordion I calculate the height of the hidden body and apply this by changing the attribute style="height: <contentheight>". This is how I get the CSS transition to work.
After the accordion body has been fully expanded I set style="height: auto". This is done using a setTimeout which matches the transition-duration time in the CSS. By doing this I allow the accordion body to resize automatically if its content changes after it has been opened.
In order to get the CSS transition working when I close the accordion, I first replace auto with a numeric value. I calculate the current height of the accordion body and put this in the height-style. After 1 millisecond (using setTimeout) I change to height: 0.
This works perfect in Chrome, but in IE 11 and Edge it fails about 80% of the time. If I increase the timeout to 50 ms it works every time, but of course there is a noticable input lag. If I use Browser.Events.onAnimationFrame instead of the 1ms timeout, it works in all three browsers, but since this requires a subscription in Elm it would be nice if there was a better way.
Are there other ways I can guarantee that IE and Edge will detect the attribute change if it's only there for 1ms?
Why not add a class to the accordion when you open it and make the changes that way?
In the example below, using a tranformation on the scaleY property allows you to go from zero to auto height.
document.getElementById('accordion-button').addEventListener('click', () => {
document.getElementById('accordion').classList.toggle('expanded');
});
#accordion {
transform: scaleY(0);
transform-origin: top;
transition: transform 300ms ease; // or however you want to handle this
}
#accordion.expanded {
transform: scaleY(1);
}
<button id="accordion-button">Click me</button>
<ul id="accordion">
<li>Some content</li>
<li>Some more</li>
</ul>
That way everything will be handled by the CSS transition.
Related
I am trying to add a pulsing ring that rapidly and repeatedly shrinks around a target element to draw a user's attention to that location. Not subtle, but it'll do the job. I have several places on my site I want to place it, so I want to be able to attach it to any element without significantly altering the element that I attach it to.
My current attempt uses a div with absolute positioning with a wide border and border radius of 50%:
HTML Declaration:
<a href="mysite/readme">
<div class="attention-ring"></div>
Click Me
<a>
CSS
.attention-ring {
border-radius: 50%;
border: 10px solid red;
position: absolute;
z-index: 100;
}
The animation is achieved with jQuery. It gives it a width of 100% and quickly shrinks it using setInterval(), resetting when width reaches 0:
function animateRing() {
if ($('.attention-ring') != null) {
var ringDiameter = 100;
setInterval(function () {
if (ringDiameter > 0) {
$('.attention-ring').css({ "padding":`${ringDiameter}%`, "margin":`${-1 * ringDiameter}%`})
ringDiameter -= 1
} else {
ringDiameter = 100;
}
}, 10)
}
}
What I have does work. But there are a few problems:
The ring element at maximum diameter extends past the edge of the page. This is particularly bad for elements already close to the edge of the page. It causes intermittent blank space to appear past the edge of the page and makes the scroll-bars go haywire. Ideally I want the page to ignore that this element is going out of bound.
I currently need to place the div with the ring class directly inside elements. This means for elements, the user will have difficulty clicking on other elements on pages where this feature is active. Not ideal because I eventually want to add a way to turn this off using a different button.
How would I solve the above problems?
This is a perfect use-case for pseudo-elements and CSS animations. In fact, you don't need any Javascript at all.
Problem #1 happens because the script resizes the actual .attention-ring div itself. With position: absolute this doesn't affect the size of its container, but it will still trigger overflow if it gets beyond the container's bounds. Hence the dancing scrollbars. (You'd have to set any container to position:relative; overflow:hidden to prevent that... which could get hairy if you want to apply this in a lot of locations. Fortunately that's not necessary!)
Instead of resizing the div as an element, you can use transform: scale(<some number>) to scale the rendered element visually. (See CSS transform property.) This takes place after the "boxes" for each element are laid out in the browser, so it doesn't cause anything to overflow.
You can then animate the transform property in CSS with a named #keyframes rule, which gets attached to the animated element with the animation property.
Problem #2 can be solved with two steps. First, set pointer-events: none on the ring element. This makes clicks go "through" it, as if it's not part of the a tag it belongs to. Then you can avoid adding a separate div by turning the ring into a ::before pseudoelement. This way you can turn it on or off simply by adding or removing a class from the element you want to highlight.
Here's a demo of the whole thing in action:
.attention-ring::before {
display: block;
content: "";
pointer-events: none;
border-radius: 50%;
border: 10px solid red;
position: absolute;
animation: attention 1s cubic-bezier(0,0,.2,1) infinite;
}
#keyframes attention {
50%,to {
transform: scale(5); /* multiple of the initial size */
opacity: 0;
}
}
<p><a href="mysite/readme" class="attention-ring">
Click Me
</a></p>
<p><a href="#">
Also Click Me
</a></p>
<p
Note: the display:block; content:"" on the before:: makes the ring display, otherwise the browser treats it as an empty node and won't render it.
Credit: this is partly inspired by the ping animation in Tailwind CSS, but my solution above is a bit more flexible.
I've been working on a web component that will hide/reveal content by hovering over a <div>. I've got the functionality working the way I want, but I just realized isn't accessible via tabbing.
I was able to include tabindex="0" role="button" aria-pressed="false" to each of the <div> boxes, which allows you to toggle between each box, but I have no way of revealing the hidden content.
You can find my code here, which demonstrates the issue:
https://codepen.io/ckatz/pen/XQaKdB
Is there a markup I'm missing to allow for someone to hit Enter to show the text?
I added this to your CSS and it worked when i press TAB and move from div to div:
.color:focus {
/* Change the flex-basis so that we know what
size to transition to on hover. Arbitrary,
based on our design/content.
*/
flex-basis: 20em;
}
.color:focus .details {
opacity: 1;
}
I have a component that, upon a hover, shows a button and a link that you can click on. This is not a menu... just a box in the middle of the page.
For accessibility, I would like a user to be able to tab into the container (happens now, and displays the content in the .HiddenUntilHover class) AND also continue to tab to the button and link that show up on the hover/focused state.
Right now you can focus on the container and see the hover state; however, when you tab it just goes to the next element and does not allow you to tab to the button or link WITHIN the hover state.
Pseudo code example:
/* My component .jsx */
<div tabIndex="0" className="MainContainer">
<div className="SomeOtherClass">
<div className="HiddenUntilHover">
/* I would like to be able to tab to these clickable things! */
<button>Click me!</button>
I am also clickable
</div>
</div>
</div>
And my SCSS:
.HiddenUntilHover {
display: none;
}
MainContainer:focus,
MainContainer:hover,
> .HiddenUntilHover {
display: block
}
I ran into this issue a few days ago and I solved it using css classes to make the hovered content accessible via keyboard navigation.
The way I got this working was to use css pseudo-classes to ensure that when the div element is active & focused that the buttons inside also display. Specifically the additional use of :focus-within & :focus-visible should ensure that when you tab over the list items, their contents are also displayed and keyboard accessible.
.MainContainer {
&:not(:hover, :focus, :active, :focus-visible, :focus-within) {
.HiddenUntilHover {
visibility: hidden;
}
}
}
<body>
<div tabIndex="0" className="MainContainer">
Content
<div className="SomeOtherClass">
<div className="HiddenUntilHover">
<button>Click me!</button>
I am also clickable
</div>
</div>
</div>
</body>
Here's a link to the Codesandbox demo of this working
When the box is in focus, tabbing further to the button will make the box blur, which will hide it, and its contents, so focus will move to the next accessible element. I think this is the behavior you are experiencing.
You might consider using inserting an aria-activedescendant or tabindex attribute when the box comes into focus. This requires a little javascript.
Strictly speaking, you don't need to rely on the hover state to make that control accessible. You could have an offscreen (or clipped) button/link that is not a DOM child of the hidden (display:none) box. If you take this approach, read up on the aria-owns attribute.
As long as it is marked up as a button or link (or has a tabindex="0" setting), and is not 'really' hidden, it ought to be possible to tab to it.
Try increasing the properties of the class MainContainer
for example.
.MainContainer {
width: 100%;
height: 100px;
}
.MainContainer .HiddenUntilHover {
display: none;
}
.MainContainer:hover .HiddenUntilHover, .MainContainer:focus .HiddenUntilHover {
display: block;
}
Elements appearing on hover are inherently inaccessible. You are experiencing one side of the problem with your code, where it is difficult to make it keyboard accessible.
But think about touch screens that have no real concept of hover: is there some way to reach your button on a smarphone or tablet?
For a more pragmatic answer, if you need to stay with hover, a less hacky solution than the two already posted ones could be the following:
use focusin and focusout events. See for example this question for explanations and differences with focus/blur, and this w3school doc for browser compatibility.
You will have to structure your HTML differently, such as:
<div id="outer">
<div id="hover">
...
</div><!--hover-->
<button>Your button which only appears on hover</utton>
</div><!--outer-->
As well as use a bit of js:
$('#outer').on('focusin', __=>$('#hover').classNames.add('keep-visible'));
$('#outer').on('focusout', __=>$('#hover').classNames.remove('keep-visible'));
With a corresponding .keep-visible class which will leave the element display:block (I'm not a CSS expert, I let you write the code).
The overal functionning is the following: when some element within #outer takes the focus, the focusin element is fired due to bubbling. In the event, you put your class .keep-visible which makes the element to stay visible.
The focusout event is fired when the focus leaves the last element within #outer. At that point you remove the .keep-visible class, which makes the element to disappear.
According to the link above, onfocusin/out aren't standard, but are supported by all major browsers including IE. Firefox is the last one to implement it in 52.0, so it's a kind of defacto standard; we can reasonably expect that it won't disappear soon.
My goal is to create a page with 3 absolutely positioned, overlapping divs. Only 1 div should be visible at a time. 3 buttons at the bottom of the page are used to "select" each div with a fade-in/out using CSS transitions.
I'm close but I have a small issue.
Currently, upon initialization the page is blank with 3 buttons.
My question is, how do I have the first div (with id="#1") appear by default upon initialization without the need to press a button. I still want div #1 to follow the normal fade-in/fade-out rules, with the exception of upon initialization.
HTML:
<div id="1" class="inner">One</div>
<div id="2" class="inner">Two</div>
<div id="3" class="inner">Three</div>
Toggle One
Toggle Two
Toggle Three
SOME CSS:
.inner{
position:absolute;
visibility:hidden;
opacity:0;
transition:visibility 0s linear .5s, opacity .5s linear;
}
.inner:target{
visibility:visible;
opacity:1;
transition-delay:0s;
}
I would prefer a solution that doesn't use javascript/jQuery but I'm not against it if necessary.
Most likely you will need to use JavaScript/jQuery at a minimum to set the visibility of the #1 div on load. You can do this with CSS via:
.inner:first-of-type {
visibility: visible;
opacity: 1;
}
http://jsfiddle.net/74qYY/
The restriction there is that a background is required to obscure the other divs. If you can't live with that, you can always use JS: http://jsfiddle.net/74qYY/1/
For those interested, I found a round-about way to preserve the CSS transitions without requiring a background. Specifically I used the url fragments set by the buttons.
In the question, I wanted div "#1" to show as default. Another way to look at this is that I wanted a GET request of "/" to redirect to "/#1".
I used some jQuery to check for the absence of a URL fragment and then redirect to www.url.com/#1, which in turn triggers the CSS transition.
Here's a fiddle showing you a demo of the dropdown menu I've written.
The problem:
Part of the text (the site title link in this case) that is below the dropdown menu ("Channels") is unselectable / unclickable, while the other part below the normal "Home" link is rendered just fine. (You can try that in the demo.)
Why I think this is happening: I use JavaScript to dynamically change the height between 0 and auto values when the menu ("Channels") is clicked; NOT something like display: none;, and hence the menu-item element is only hidden, rendering the text that falls beneath it un-selectable/clickable.
The question is, how do I fix this, without breaking the menu's current functionality and style (i.e. transition for dropdown). Everything I've tried, including display: none | block;, visibility: hidden | visible;, and opacity: 0 | 1; have failed me.
EDIT: As seen in the latest versions of Google Chrome and Chromium web browsers.
It work in FF
For Chrome where for some reason the child element (of #channels-menu-item-wrapper) does not respect the overflow:hidden of the parent use (it respects the hidden in a visual manner only..)
You can use a delayed transition and move the sub-element out of the way ..
.collapse > div{
position:relative;
}
.collapse:not(.in) > div {
left:-10000px;
-webkit-transition:left 0s ease;
-webkit-transition-delay:0.35s; /*same delay as the time it takes to open/close so it does not show*/
}
(i have only added the -webkit- vendor specific rule.. apply for all)
Demo at http://jsfiddle.net/gaby/cfH33/5/
Set .collapse div's height and width to 0 using script when clicking on the menu.
Update: Form the #Gaby answer got this hint ".collapse:not(.in)".
.collapse:not(.in){
width:0;
}
This also will work. http://jsfiddle.net/8Mde7/3/
This is what I meant by setting width:0.