Accessible nested button inside button? - html

I'm trying to build a button that looks like Zoom's button.
Here there is a button to pick a device inside the camera button. I'd like to create something similar, where you have a button and another button that expands a picker inside it.
How can you create this in an accessible way?
If I nest buttons in React, it throws errors that you can't nest a button inside another. Zoom's equivalent would be:
<button>
Stop Video
<button>Pick Device</button>
</button>
which doesn't work. How would you create an interface like this so it stays accessible (and valid)?

Preword
Don't nest interactive elements
There is a reason that it isn't valid HTML to nest buttons or hyperlinks, it causes nightmares for knowing which action should be performed on a click (for a start) and for assistive technology this makes things even worse as it can confuse the accessibility tree as to what it should present to screen readers.
The answer
If you look carefully you will see they aren't actually nested, the "picker" button is placed on top of the other button.
Now there is an issue here in terms of accessibility, click / tap target size.
A button / interactive element should be no less than 44px by 44px
So the Zoom example you gave fails this criteria. Additionally the tooltip that says "stop video" looks wrong if you have the picker selected as that should be the tooltip for the button that is currently hovered.
So how could we create an accessible version of what you want?
I would recommend having a large button with a 44 by 44 button placed on top to the right.
This can easily be done with absolute positioning.
To ensure that it is evident visually that the buttons are related I inset the second button by 2px.
The below is not a complete example but I have given you a start.
I added aria-expanded to the button that opens the sub menu, this gets toggled when the menu is opened.
I also added the aria-haspopup attribute to let users know that this button opens a sub menu.
I also added aria-controls to let assistive technology know the relationship between the button and the menu it opens.
Finally you will see I added a <span> with some visually hidden text inside so that screen reader users know that the picker button opens the video controls.
The example maintains logical tab order and is pretty accessible, but there are still things such as being able navigate the menu buttons with the arrow keys, closing the menu with Esc key and returning focus to the button that opened the menu etc. that you need to implement yourself. Oh and styling obviously!
var mainButton = document.querySelector('.main-button');
var menuToggle = document.querySelector('.sub-button');
var menu = document.getElementById('controls');
mainButton.addEventListener('click', function(){
alert("clicked the main button");
});
menuToggle.addEventListener('click', function(){
if(menu.classList.contains('open')){
menu.classList.remove('open');
menuToggle.setAttribute('aria-expanded', false);
}else{
menu.classList.add('open');
menuToggle.setAttribute('aria-expanded', true);
}
});
.container{
position: relative;
width: 144px;
height: 48px;
}
.main-button{
width: 144px;
height: 48px;
padding-right: 50px;
}
.sub-button{
position: absolute;
width: 44px;
height: 44px;
top:2px;
right:2px;
}
.visually-hidden {
border: 0;
padding: 0;
margin: 0;
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
clip: rect(1px, 1px, 1px, 1px); /*maybe deprecated but we need to support legacy browsers */
clip-path: inset(50%); /*modern browsers, clip-path works inwards from each corner*/
white-space: nowrap; /* added line to stop words getting smushed together (as they go onto seperate lines and some screen readers do not understand line feeds as a space */
}
#controls{
display: none;
}
#controls.open{
display: block;
}
<div class="container">
<button class="main-button">Stop Video</button>
<button class="sub-button" aria-expanded="false" aria-haspopup="true" aria-controls="controls">⌄ <span class="visually-hidden">Pick Device</span></button>
<ul id="controls"
role="menu"
aria-labelledby="sub-button">
<li><button>Option 1</button></li>
<li><button>Option 2</button></li>
</ul>
</div>

The react gives a warning if you try to do so and the reason is simple. It has everything to do with semantic HTML and you should never put a button inside a button.
Alternatively to get the desired behaviour you can do something like this:
<div style={{ position: "relative", width: "200px", height: "40px" }}>
<button
onClick={() => console.log("Stop Video")}
style={{ width: "100%", height: "100%" }}
>
Stop Video
</button>
<button
onClick={() => console.log("Pick Device")}
style={{ position: "absolute", right: 0, top: 0 }}
>
Pick Device
</button>
</div>
This will do the same thing you need. Here is the codesandbox example for the same implementing the exact same thing.

Related

Clicking content div inside overlay div also closes the popup

I am trying to create a pop-up where the container is just a black background with some opacity, and then that div container contains the content of the popup.
Basically it looks something like:
<button (click)="showPopup = !showPopup">Open popup</button>
<div class="overlay-bg" (click)="showPopup = !showPopup" [ngClass]="showPopup ? 'is-active' : ''">
<div class="content">Some content</div>
</div>
The CSS looks something like:
.overlay {
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
display: grid;
position: fixed;
pointer-events: none;
opacity: 0;
&.is-active {
opacity: 1;
pointer-events: all;
}
.content {
width: 400px;
height: 400px;
place-self: center;
background: red;
}
}
So basically, when the state is not active, it's not showing, and when is-active is enabled by clicking the button, the overlay + content is displayed. Now, what I would also like is that when clicking the background, the pop-up should close - which is does. However, the problem is, that the pop-up also closes when I just click the content - which it shouldn't since it should be somewhat interactive.
So how do I prevent the pop-up from closing when interacting with the content div ?
Problem
The reason this happens is due to the event flow in javascript, which is the order in which events are handled. This is important when there are nested elements, like with your two divs ('overlay-bg' and 'content'). Modern browsers use event bubbling which means that the innermost child element handles the event first. The event then 'bubbles' and works its way outwards. In your case, the 'content' div must handle the event first. You do not want the pop-up to disappear when the 'content' div is clicked, but you do when the 'overlay-bg' div is clicked. Therefore you must find a way of stopping the event bubbling from occurring by capturing it with the onclick event on the 'content' div. This can be done by using the stopPropagation() method of the event object.
Solution
To stop the event from bubbling upwards when the user clicks on the 'content' div, add an onclick event handler in the HTML:
<div class="content" onclick="preventBubbling(event)">Some content</div>
Then, with JavaScript, you can use the stopPropagation() method of the event object which is passed as a parameter to the function. To achieve this, the function would look like this:
function preventBubbling(event) {
event.stopPropagation();
}
This stops the event from bubbling up to the parent div, so the click event is never triggered, and the pop-up does not hide (when the user clicks on the content div).

Click goes through element when there is an overlay - how does it work?

I have found the technique to customize file input element through putting a regular button in "front of it" and make the file input element with opacity: 0. Like this:
#wrapper {
position: relative;
overflow: hidden;
}
#button-on-top {
width: 200px;
padding: 10px;
}
#file-input-below {
position: absolute;
top: 0;
left: 0;
width: 200px;
padding: 10px;
}
<div id="wrapper">
<button id="button-on-top">Upload</button>
<input type="file" id="file-input-below">
</div>
But why does it actually work that when you click the button "above", the click goes to the real file input button and activates it? Normally, when there's an element "below" another, it doesn't register a click.
Such as with different kinds of overlays where buttons underneath cannot be clicked?
Thank you for an explanation in advance.
HTML files are rendered from top to bottom, so your input field is rendered later. That means if you put absolute to your button the input field slides under it.
But if you put your button below your button will slide under your input field.
If you still want to make it work put to your button an index-z of 1
#button-on-top {
z-index: 1;
}
and your input field should have an lower z-index then your button if you want to make your button clickable
#file-input-below {
z-index: -1;
}

Reset CSS hover dropdown with JQLite

I'm currently in a project developing an Angular SPA that has dropdown menus in its main navbar. To get this effect, we are using CSS: hover selectors. The issue is that when an action is performed within this dropdowns we would like to close them without hindering the ability to open them again. For example, if a user opens a link within one of this dropdowns (internal link with ui-sref) he is then taken to this particular state, but the dropdown would still be visible until he moves the mouse outside it (and partially obscuring the new content shown). We would like the dropdown to be closed when an action within is performed and if the user would like to open it again, he would be able to hover the mouse again over the trigger.
We tried removing and re-adding classes (even after a timeout) but the dropdown reappears again.
Link to a Plunker with a setup similar to what we are trying to accomplish: https://plnkr.co/edit/qzQk4r2WQFhwsgUWug39?p=preview
And the relevant portions (Angular controller omitted as it has no content):
HTML:
<div class="hoverable has-dropdown">
<button class="dropdown-trigger">Hover me!</button>
<div class="dropdown">
Dropdown content
<button ng-click="buttonAction()">Action</button>
</div>
</div>
CSS:
.dropdown {
display: none;
position: absolute;
top: 20px;
width: 100px;
height: 100px;
background: lightgrey;
padding: 1em;
}
.has-dropdown {
position: relative;
height: 20px;
}
.has-dropdown .dropdown-trigger:hover + .dropdown,
.has-dropdown .dropdown-trigger + .dropdown:hover {
display: block;
}
Thanks!
Finally solved it by using ng-mousenter and ng-mouseleave and dropping CSS :hover rules. As everything is based on JS I can just trigger mouseleave when I want to close them.

Can't click the button because of the overlay?

This is the HTML
<li id="nav1" class="navs"><a unselectable="on" draggable="false" class="Navigation" href="http://youtube.com">YouTube</a></li>
This is the CSS
.navs:after {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: #0d0d0d;
opacity: 0.5;
transform: scaleY(0);
transform-origin: 0 100%;
transition: all .2s ease-out;
}
.navs:hover:after{
transform: scaleY(1);
}
.navs:active:after{
background: #FFFFFF;
}
I think the reason why i can't click the button is because when i click the button, the overlay forms. I do not want to remove the overlay though. Is there any way to click through the overlay?
Option one
You can give your element a higher z-index. This will move your button above the overlay, so you will be able to click it
Option two
You can disable all mouse events on your overlay using pointer-events:none; so the click event will 'fall through' it and the button will register it
Edit: Use pointer-events when you can, let z-index be your backup plan. If you fall back to it, I suggest that you don't use it inline, but write a specific selector for it in your CSS.
use span instead of before and after,
something like
<a href="my link"><img class="" src="my_image" alt="">
<span class="rig-overlay"></span>
<span class="rig-text">
<span>name</span>
<span>function</span>
</span>
</a>
the span will not cover the clickable region
It could be many different things...
Definitely try to check if you've used any z-index properties for other elements that are the parents of the element.
I encountered the exact same problem and I fixed it by troubleshooting:
what I did was pull up a javascript file and console log the target className of where I was clicking (can be done by:
window.addEventListener('click' , (e) => {
const target = e.target.className;
console.log(target);
})
)
Once I did that, click on the button that doesn't seem to be working. Make sure to add a class to your button before this and check if the class is displayed properly. Sometimes, in my case, I had to move the console out of the window.
From this, I found my SVG Animation was actually taking up invisible space that covered the button. All I had to do to fix this problem was give the SVG a z-index of -1.
Hope this helped! I know I took a long time to find a solution so I hope my solution can help others too.
Note: Also check your pointer events (make sure it isn't set to none) for the button and other elements

Firefox does not show tooltips on disabled input fields

Firefox doesn't display tooltips on disabled fields.
The following displays tooltip in IE/Chrome/Safari except Firefox:
<input type="text" disabled="disabled" title="tooltip text."/>
Why doesn't Firefox display tooltip on disabled fields? Is there a work around this?
Seems to be a (very old, and very abandoned) bug. See Mozilla Bugs #274626 #436770
I guess this could also be explained as intended behaviour.
One horrible Workaround that comes to mind is to overlap the button with an invisible div with a title attribute using z-index; another to somehow re-activate the button 'onmouseover' but to cleverly intercept and trash any click event on that button.
I guess you are using some frameworks like bootstrap. If so, it add pointer-events: none to the 'disabled' element, so all the mouse events are ignored.
.btn[disabled] {
pointer-events: auto !important;
}
can fix the problem.
You can work around this with jQuery code like:
if ($.browser.mozilla) {
$(function() {
$('input[disabled][title]')
.removeAttr('disabled')
.addClass('disabled')
.click(function() {return false})
})
}
The z-indexing thing could be done like this:
.btnTip
{
position: absolute;
left: 0%;
right: 0%;
z-index: 100;
width: 50px;
/*background-color: red;*/
height: 17px;
}
(…)
<div style="background-color: gray; width: 400px;">
Set width of the tip-span to the same as the button width.
<div style="text-align: center;">
<span style="position:relative;">
<span class="btnTip" title="MyToolTip"> </span>
<input type="button" name="" disabled="disabled" value="Save" style="width: 50px;height:17px;" />
</span>
</div>
Left and right helps positioning the host on top of the disabled element.
The z-index defines what kind of layer you put an element in.
The higher number of a z-layer the more ‘on top’ it will be.
The z-index of the host and/or the disabled element should be set dynamically.
When the disabled element is disabled you want the tooltip host on top and vice versa - at least if you want to be able to click your element (button) when it is NOT disabled.
I have faced the similar issue and i could fix it using juery and small css class, you would require to create two new span elements and a css class which are as follows
Just add these in a general js and css file which is used in all over the application or web site
.DisabledButtonToolTipSpan
{
position :absolute;
z-index :1010101010;
display :block;
width :100%;
height :100%;
top :0;
}
To display tooltip for disabled button in firefox browser.
$(window).load(function() {
if ($.browser.mozilla) {
$("input").each(function() {
if ((this.type == "button" || this.type == "submit") && this.disabled) {
var wrapperSpan = $("<span>");
wrapperSpan.css({ position: "relative" });
$(this).wrap(wrapperSpan);
var span = $("<span>");
span.attr({
"title": this.title,
"class": "DisabledButtonToolTipSpan"
});
$(this).parent().append(span);
}
});
}
});
Hope this helps,
Preetham.
You could use javascript. Use the mouseover event.
(A lot of libraries like JQuery and Mootools have tooltip plugins available. Using these you can even style the tooltips).