I’m writing a custom <select> element using native DOM (no Polymer).
I’m trying to use my element with a <label> element and correctly trigger click events to my element when the <label> is clicked, i.e.:
<label>
My Select:
<my-select placeholder="Please select one">...</my-select>
</label>
or
<label for='mySelect1'>My Select:</label>
<my-select id='mySelect1' placeholder="Please select one">...</my-select>
However, this behavior doesn’t seem to work out of the box, even if I add a tabindex to make it focusable.
Here’s a stripped down version of the code and a JSFiddle with some basic debugging:
var MySelectOptionProto = Object.create(HTMLElement.prototype);
document.registerElement('my-select-option', {
prototype: MySelectOptionProto
});
var MySelectProto = Object.create(HTMLElement.prototype);
MySelectProto.createdCallback = function() {
if (!this.getAttribute('tabindex')) {
this.setAttribute('tabindex', 0);
}
this.placeholder = document.createElement('span');
this.placeholder.className = 'my-select-placeholder';
this.appendChild(this.placeholder);
var selected = this.querySelector('my-select-option[selected]');
this.placeholder.textContent = selected
? selected.textContent
: (this.getAttribute('placeholder') || '');
};
document.registerElement('my-select', {
prototype: MySelectProto
});
Only the phrasing content elements can be targeted by <label>.
So you'll have to manage the focus action by yourself if you want to use a non-standard (autonomous custom) element.
Instead you can choose to define a Customized built-in element that will extend the <select> element, as in the following example:
https://jsfiddle.net/h56692ee/4/
var MySelectProto = Object.create( HTMLSelectElement.prototype )
//...
document.registerElement('my-select', { prototype: MySelectProto, extends: "select" } )
You'll need to use the is attribute notation for HTML:
<label>
My Select:
<select is="my-select" placeholder="Please select one">
<option>...</option>
</select>
</label>
Update More explanations in these 2 posts: here and there.
But the standard is not finished, and maybe in the future you’ll be able to use the built-in semantic with autonomous custom elements, too.
— Supersharp, Jul 15, 2016 at 16:05
We live in the future now.
The standard has significantly changed and we have more capable custom elements now.
Tl;dr:
Labels work iff the constructor associated with your custom element has a formAssociated property with the value true.
When looking at the documentation, at first sight, you might conclude that custom elements are not allowed as <label> targets.
The docs say:
Elements that can be associated with a <label> element include <button>, <input> (except for type="hidden"), <meter>, <output>, <progress>, <select> and <textarea>.
But custom elements are not mentioned.
You try your luck anyway and try to enter this HTML fragment in an HTML validator:
<label>Label: <my-custom-element></my-custom-element></label>
or:
<label for="my-id">Label: </label><my-custom-element id="my-id"></my-custom-element>
and both of these are valid HTML snippets!
However, trying this HTML fragment:
<label for="my-id">Label: </label><span id="my-id"></span>
shows this error message:
Error: The value of the for attribute of the label element must be the ID of a non-hidden form control.
But why is a random <my-custom-element> considered a “non-hidden form control”?
In the validator’s GitHub issues you then find the already fixed issue #963:
label for doesn’t allow custom elements as targets
The spec allows form-associated custom elements as for targets
https://html.spec.whatwg.org/multipage/forms.html#attr-label-for
This currently triggers an error:
<label for="mat-select-0">Select:</label>
<mat-select role="listbox" id="mat-select-0"></mat-select>
Easy to check if element name contains a dash.
Much harder to check that custom element definition includes a static formAssociated property returning true, since this is likely buried deep in an external framework .js file:
https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example
There is indeed one peculiar addendum in the WHATWG specification which enumerates the same “labelable elements” as the documentation plus form-associated custom elements.
There is an example in the specification, and researching this problem today yields this Q&A post in the results as well as blog articles such as “More capable form controls” by Arthur Evans (archived link), which show what the solution is.
The solution to enable <label> clicking targeting custom elements: formAssociated
All that needs to be done to enable the <label> action is to add a formAssociated property with the value true to the constructor of the custom element.
However, the code in the question needs to be ported to the current Web Components API.
The new formAssociated property is part of the new ElementInternals API (see attachInternals) which is used for richer form element control and accessibility.
Nowadays, custom elements are defined with classes and customElements.define, and ShadowRoots are used.
The static formAssociated = true; field can then be added to the class which you consider to belong to the “form-associated custom elements”.
Having this property makes your custom element dispatch click events when clicking the <label>; now we’re getting somewhere!
In order to turn this click into a focus action, there’s one more thing we need in the attachShadow call: the property delegatesFocus set to true.
// Rough translation of your original code to modern code, minus the `tabIndex` experimentation.
class MySelectOptionProto extends HTMLElement{}
customElements.define("my-select-option", MySelectOptionProto);
class MySelectProto extends HTMLElement{
#shadowRoot;
#internals = this.attachInternals();
static formAssociated = true;
constructor(){
super();
this.#shadowRoot = this.attachShadow({
mode: "open",
delegatesFocus: true
});
this.#shadowRoot.replaceChildren(Object.assign(document.createElement("span"), {
classList: "my-select-placeholder"
}));
this.#shadowRoot.firstElementChild.textContent = this.querySelector("my-select-option[selected]")?.textContent ?? (this.getAttribute("placeholder") || "");
}
}
customElements.define("my-select", MySelectProto);
<label>My label:
<my-select placeholder="Hello">
<my-select-option>Goodbye</my-select-option>
<my-select-option>world</my-select-option>
</my-select>
</label>
<label for="my-id">My label:</label>
<my-select id="my-id">
<my-select-option>Never gonna give you</my-select-option>
<my-select-option selected>up</my-select-option>
</my-select>
<label for="my-id">My other label</label>
delegatesFocus will focus the first focusable child of the shadow root, when some non-focusable part of the custom element has been clicked.
If you need more control, you can remove the property, and add an event listener to the custom element instance.
In order to be sure that the click came from the <label>, you can check if the event’s target is your custom element, i.e. this, and that the element at the x and y position on the screen was a <label> targeting your custom element.
Fortunately, this can be done quite reliably by passing x and y to document.elementFromPoint.
Then, simply calling focus on your desired element will do the job.
The list of labels that target a custom element is another thing that the ElementInternals API provides via the labels property.
class MySelectProto extends HTMLElement{
// …
constructor(){
// …
this.addEventListener("click", ({ target, x, y }) => {
const relatedTarget = document.elementFromPoint(x, y);
if(target === this && new Set(this.#internals.labels).has(relatedTarget)){
console.log("Label", relatedTarget, "has been clicked and", this, "can now become focused.");
// this.focus(); // Or, more likely, some target within `this.#shadowRoot`.
}
});
}
}
Related
When you dynamically add an element to an aria-live region, Chrome will read out all the items in that region which is great.
But when you remove an element, Chrome does not re-read out the list. This is an issue when you're using the region for errors and such, as when the user has fixed an error, the list is not re-read out.
Example here: https://codepen.io/mildrenben/pen/WNNzVzN?editors=1010
<div aria-live='assertive'>
</div>
<button id='add'>add</button>
<button id='remove'>remove</button>
const addBtn = document.querySelector('#add')
const removeBtn = document.querySelector('#remove')
const ariaLive = document.querySelector('div')
let tick = 0
addBtn.addEventListener('click', () => {
let newElem = document.createElement('span')
newElem.textContent = tick
tick++
console.log(ariaLive, newElem)
ariaLive.appendChild(newElem)
})
removeBtn.addEventListener('click', () => {
ariaLive.removeChild(ariaLive.lastChild)
})
The correct method should be to use the aria-relevant attribute, however the browser support is very poor, and as such it is not reliable.
I don't normally advocate for doing hacky things to make a browser behave a certain way, but if you really need to make the live region report removals, here's what I would suggest:
Set the aria-atomic attribute on your live region to true. This means that the screen reader will read the entire contents of the live region each time content is added (but not removed).
When you delete an element from the live region, add another invisible element, wait a few hundred milliseconds, and then delete that element.
The live region should announce all of the contents (minus the deletion) when the remove button is pressed.
Example fiddle here: https://jsfiddle.net/mug6vonf/3/
You should also use aria-relevant :
Values:
A space-delimited list of one or more of the following values:
additions are insertion of nodes into the live region; should be considered relevant.
removals are deletion of nodes; should be considered relevant.
text are changes to the textual content of existing nodes; should be considered relevant.
all is equivalent to additions removals text.
aria-relevant="additions text" is the default value on a live region.
The default value doesn't include removals, which you probably need.
I'm trying to use bootstraptoggle in one of my pages. The initial state is off / disabled.
The page loads several boolean values and stores them as hidden text. Then I have a script which looks them up via their IDs. Upon that hidden text it should toggle the slider.
I was able to get the hidden text and make the conditional check but I'm not able to toggle the slider for some reason.
Here is my code:
$(document).ready(function () {
var flags = [];
var userID = '',
toggleSlider = '';
flags = document.querySelectorAll('*[id^="activeFlag_"]');
flags.forEach(function (flag) {
userID = flag.id.split('_')[1];
// This is where i search for the hidden text
if (flag.firstChild.data == 'True') {
// Nothing works here.
$('#activeToggle_' + userID).bootstrapToggle('toggle');
}
});
});
And this is the html code that I need to work with:
<p id="activeFlag_#user1">#item.activeFlag</p>
<div class="checkbox">
<label>
<input id="activeToggle_user1" type="checkbox" data-toggle="toggle" data-on="Enabled" data-off="Disabled">
</label>
</div>
Your code is too opaque without any data example.
However one thing could be a cause of its problem:
if (flag.firstChild.data == 'True') {
Try to replace it with:
if (flag.firstElementChild.data == 'True') {
Here you could find explanation:
The firstChild property returns the first child node of the specified node, as a Node object.
The difference between this property and firstElementChild, is that firstChild returns the first child node as an element node, a text node or a comment node (depending on which one's first), while firstElementChild returns the first child node as an element node (ignores text and comment nodes).
Note: Whitespace inside elements is considered as text, and text is considered as nodes (See "More Examples").
Update after example code was added
For the example code you provided, you should change the split argument:
userID = flag.id.split('_')[1];
to:
userID = flag.id.split('_#')[1];
Thanks to twain for initial jsfiddle. I have updated it accordingly: jsfiddle
I guess the problem is, that the following part does not use the correct id for the toggle $('#activeToggle_' + userID).bootstrapToggle('toggle');
Your html ID is activeToggle_user1, but the js part above will probably resolve to #activeToggle_1. So the text user is missing here.
I created a working fiddle here: https://jsfiddle.net/pbcrh5d2/
Ok, for some reason asp.net and javascript have a problem with coping together. I used asp.net to provide javascript to build the strings.
So I switched to the raw id that is used in the table.
I am applying a binding like this in a restartless add-on:
var css = '.findbar-container { -moz-binding:url("' + self.path.chrome + 'findbar.xml#matchword") }';
var cssEnc = encodeURIComponent(css);
var newURIParam = {
aURL: 'data:text/css,' + cssEnc,
aOriginCharset: null,
aBaseURI: null
}
cssUri = Services.io.newURI(newURIParam.aURL, newURIParam.aOriginCharset, newURIParam.aBaseURI);
myServices.sss.loadAndRegisterSheet(cssUri, myServices.sss.USER_SHEET);
findbar.xml contents are:
<?xml version="1.0"?>
<bindings xmlns="http://www.mozilla.org/xbl" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="matchword">
<content>
<children/>
<xul:toolbarbutton anonid="matchwordbtn" accesskey="w" class="tabbable" label="Whole Word Only" tooltiptext="Match only whole words" oncommand="console.log('hi')" type="checkbox"/>
</content>
</binding>
</bindings>
This just adds a button to the FindBar labeled "Whole Word Only". But now to remove it, I am just unregistering the stylesheet with myServices.sss.unregisterSheet(cssUri, myServices.sss.USER_SHEET);, however this is not unbinding it.
An answer on ask.mozilla.org told me this is expected behavior, but offered no solution.
I was thinking maybe I should dynamically add the binding rather than via CSS, I didn't test this but it doesn't fit the 3 reasons for XBL updates:
A bound element matches a style rule that specifies a different binding
The element is removed from the bound document
The element is destroyed (e.g., by closing the document)
The answer told me it's expected yet funky behavior.
Well, I just remembered that I have some working code that does (re)bind different XBL bindings, essentially.
It goes like this:
There is a base binding, or not (in your case the original binding of .findbar-container).
Then I have multiple classes that define different -moz-bindings.
These classes are set and removed at runtime.
Since that works for me, it should in theory work for you:
In your style, do not have a rule for the element itself, but for a class, e.g.
.findbar-container.myaddonclass { moz-binding: ... }
In your code, on load add that new class, e.g.
Array.forEach(
document.querySelectorAll(".findbar-container"),
e => e.classList.add("myaddonclass")
);
In your code, on unload remove the class again:
Array.forEach(
document.querySelectorAll(".findbar-container"),
e => e.classList.remove("myaddonclass")
);
This should force a CSS-rule reevaluation, and bindings reevaluation with that and hence fits the "A bound element matches a style rule that specifies a different binding" rule.
Of course, this sucks when not all elements you want to rebind are already present on load of your add-on, but MutationObserver could help with that...
I have the following code,
HTML
<label for="fName">First Name<sup>*</sup></label>
<input type="text" autocomplete="off" name="fName" id="fName" value='' required/>
JavaScript
var fName = document.getElementById("fName");
fName.label.style.color="red";
Is this a valid way to change the color or the label or does it need it's own id?
Thanks for the help!
Clarification, the color needs to change if the field is empty on the form submit.
Your code is valid for changing attribute color. But I don't think your code will change colour of your label. If this style unique for that element you can use a id for label and make same kind script to change color for label too. I think it would be great if you define a class in css and add this class name using JavaScript,Code for that follows.
document.getElementById('id').classList.add('class');
document.getElementById('id').classList.remove('class');
If your can use jQuery framework. It will save lots of time.
Check out this very complete answer:
Javascript change color of text and background to input value
I believe that there is not any short and direct way to access the attached label corresponding to an input field using javascript. You can access the attached label via CSS (with some tweaks in layout) but in javascript, you have to set up a few lines of code. To use this code, the layout also has a requirement that all the attached label should go before the input field (spaces in between are allowed). This code just use the previousSibling property of a DOM element with some other DOM stuffs. Here is the detail:
function getLabelFromInputID(inputID){
var prevSib = document.getElementById(inputID).previousSibling;
//remove the spaces if any exists
while(prevSib.nodeName=='#text') prevSib=prevSib.previousSibling;
if(prevSib.getAttribute('for') != inputID) prevSib = null;
return prevSib;
}
Use the getLabelFromInputID function to access the attached label from the corresponding input field's ID. Note that the label should have for attribute set-up correctly (this is the standard and common practice).
Here is the Fiddle Demo. In this demo, you just try clicking on the page to see it in action.
This all depends on your use case. If you are simply trying to style the element red statically, you should define a css class (e.g. `red-label { color: red; }) and apply that class to the label.
If you are trying to dynamically set the color to red (e.g. upon a form validation error), you'll need to target the label using a query selector of some sort.
function makeLabelRedDirectly() {
document.getElementById('fNameLabel').style.color = 'red';
}
function makeLabelRedViaClass() {
// Note you can use the more robust `element.classList` with modern browsers
// document.getElementById('fNameLabel').classList.add('red-label');
document.getElementById('fNameLabel').className += ' red-label' ;
}
The examples above use document.getElementById to target the element. You could also opt to use document.querySelector or a library like jQuery to target the labels.
Working example: http://jsbin.com/calol/1
using css
form label{color:red};
using javascript
<label for="fName" class="lbl">First Name<sup>*</sup></label>
<input type="text" autocomplete="off" name="fName" id="fName" value='' required/>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js></script>
<script>
$(document).ready(function() {
$('.lbl').css('color','red');
});
</script>
or simple javascript
document.getElementsByClassName("lbl").style.color="red";
An input element does not have a label property or another way to directly refer to its label. You need to assign an id to the label element or otherwise find a way to access it. You could traverse all label elements on a page and use the one with a for property matching the input element, but it’s probably easier to just use id for it.
Is there a way in <select> list, for example, to make onClick activate a JavaScript function that will show the title of the element just as pattern in HTML 5 does?
I want to do that when you click on the <select>, it will activate a JavaScript function that under some condition (doesn’t matter—some if expression) will show a sentence (that I wrote) in a bubble like place (the place that the pattern shows the title when something isn’t according to the pattern (pattern in HTML5)).
You can set a custom validity error on a select element by calling the setCustomValidity method, which is part of the constraint validation API in HTML5 CR. This should cause an error to be reported, upon an attempt at submitting the form, in a manner similar to reporting pattern mismatches. Example:
<select onclick="this.setCustomValidity('Error in selection');
title="Select a good option">
(In practice, you would probably not want to use onclick but onchange. But the question specifically mentions onClick.)
There are problems, though. This only sets an error condition and makes the element match the :invalid selector, so some error indicator may happen, but the error message is displayed only when the form data is being validated due to clicking on a submit button or something similar. In theory, you could use the reportValidity method to have the error shown immediately, but browsers don’t support it yet.
On Firefox, the width of the “bubble” is limited by the width of the select element and may become badly truncated if the longest option text is short. There is a simple CSS cure to that (though with a possible impact on the select menu appearance of course).
select { min-width: 150px }
You might also consider the following alternative, which does not affect the select element appearance in the normal state but may cause it to become wider when you set the custom error:
select:invalid { min-width: 150px }
There is also the problem that Firefox does not include the title attribute value in the bubble. A possible workaround (which may or may not be feasible, depending on context) is to omit the title attribute and include all the text needed into the argument that you pass to setCustomValidity.
A possible use case that I can imagine is a form with a select menu such that some options there are not allowed depending on the user’s previous choices. Then you could have
<select onchange="if(notAllowed(this)) setCustomValidity('Selection not allowed')" ...>
where notAllowed() is a suitable testing function that you define. However, it is probably better usability to either remove or disable options in a select as soon as some user’s choices make them disallowed. Admittedly, it might mean more coding work (especially since you would need to undo that if the user changes the other data so that the options should become allowed again).
In my opinion Jukka's solution is superior however, its fairly trivial to do something approaching what you're asking for in JavaScript. I've created a rudimentary script and example jsFiddle which should be enough to get you going.
var SelectBoxTip = {
init : function(){
SelectBoxTip.createTip();
SelectBoxTip.addListeners();
},
addListeners : function(){
var selects = document.getElementsByTagName("select");
for (var i = 0; i < selects.length; i++){
var zis = selects[i];
if(zis.getAttribute('title')){//only if it has a title
zis.addEventListener("focus", SelectBoxTip.showTip, false);
zis.addEventListener("blur", SelectBoxTip.hideTip, false);
}
}
},
createTip : function(){
tip = document.createElement("div");
tip.id = "tip";
tip.style.position = "absolute";
tip.style.bottom = "100%";
tip.style.left = "0";
tip.style.backgroundColor = "yellow";
document.body.appendChild(tip);
},
showTip : function(e){
this.parentNode.appendChild(tip);
tip.innerHTML=this.title;
tip.style.display="block";
},
hideTip : function(e){
tip.style.display="none";
}
};
SelectBoxTip.init();