I have a custom Polymer element that is meant to represent a complex input type. It contains an actual input tag inside its Shadow DOM, like this:
<polymer-element name="my-input">
<template>
<input type="text" on-blur="{{onBlur}}" on-focus="{{onFocus}}"/>
</template>
<script>
Polymer('my-input', {
onBlur: ...,
onFocus: ...
});
</script>
</polymer-element>
Currently, focus and blur events are leaking to outside listeners when the user clicks between the internal input and other areas within the custom element. If you open the Dev Tools Console in this CodePen, you'll see internal and external focus and blur events happening even when clicking between the input and the surrounding green area (which is all inside the custom element).
Is there no way to capture the focus and blur events inside my custom element so I can only fire them when actually focusing and blurring the entire custom element?
Turns out I needed a tabindex on my polymer-element tag:
<polymer-element name="my-input" tabindex="0">
Here's a working CodePen where the external focus and blur events only register when the entire element is focused and blurred.
Related
There is a bug I've opened up for this (https://github.com/angular/components/issues/20209), I'm asking this question to see if there is a workaround or fix anyone knows.
The issue is visible on this demo page https://stackblitz.com/edit/angular-rczreu
The issue is due to the CDK panel (autocomplete's panel) being rendered as a separate layer in the demo, distinct from the autocomplete element. So when the autocomplete element moves out of the visible area of the scrollable element, the autocomplete does become hidden, but the panel does not due to this separate layer rendering.
In pseudo code, the angular html is as follows,
<html>
<body>
<my-app>
<my-component cdkScrollable> // this is a scrollable element, it's children should become hidden when we scroll
<autocomplete></autocomplete>
<some-other-child></some-other-child>
</my-component>
</my-app>
<div class="cdk-overlay-container">
// ... other stuff
<div>
// autocomplete's panel
</div>
</div>
Okay, I've made a fork of your StackBlitz with the solution here:
https://stackblitz.com/edit/angular-rczreu-yqyphm
Basically here are the highlights:
On your component html, I added a component ID to the formField:
<mat-form-field #formField class="example-full-width">
Then on your component ts, I added the references to the ViewChild for the Autocomplete elements (the form field and the Autocomplete panel itself):
#ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger;
#ViewChild("formField") autoCompleteFormField: MatFormField;
Then I added an implementation of AfterViewInit to your component to make sure the view elements are already loaded so that it is not undefined.
ngAfterViewInit() {
var observer = new IntersectionObserver((entries) => {
if(!entries[0].isIntersecting)
console.log('Element is is not in screen');
this.autocomplete.closePanel();
}, { threshold: [1] });
observer.observe(this.autoCompleteFormField._elementRef.nativeElement);
}
So what this code snippet do is using the efficient Intersection Observer API which detects if an element is on screen or not. Then just simply close the autocomplete panel if the element is off-screen.
https://usefulangle.com/post/113/javascript-detecting-element-visible-during-scroll#:~:text=To%20know%20whether%20the%20element%20is%20fully%20visible%20in%20viewport,height%20and%20bottom%20%3E%3D%200.
jsfiddle : https://jsfiddle.net/leiming/5e6rtgwd/
class Sample extends React.Component {
onInputFocus(event) {
console.log('react input focus')
}
onSpanFocus(event) {
console.log('react span focus')
// event.stopPropagation()
}
render() {
return ( <span onFocus = {this.onSpanFocus}>
react input: <input type="text"
onFocus = {this.onInputFocus} />
</span> )
}
}
ReactDOM.render( < Sample / > ,
document.getElementById('container')
);
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>
<div>
<span onfocus="(function(){console.log('normal span')})()">
normal input:<input type="text" onfocus="(function(){console.log('normal input focus')})()">
</span>
</div>
jsfiddle : https://jsfiddle.net/leiming/5e6rtgwd/
Using React, onFocus in <input/> will bubble which is not same as usual HTML5.
Could anyone give me the refer doc why focus bubbles with React?
focus events do not bubble, so you're correct that the behavior in React differs from that of the DOM. The DOM has a focusin event that does bubble; here's a demonstration:
<div>
<span onfocus="(function(){console.log('span focus')})()">
onfocus: <input type="text"
onfocus="(function(){console.log('input focus')})()">
</span>
</div>
<div>
<span onfocusin="(function(){console.log('span focusin')})()">
onfocusin: <input type="text"
onfocusin="(function(){console.log('input focusin')})()">
</span>
</div>
Looking through the React source code, it seems this was intentional; the code checks for whether or not the browser supports the focus event with capturing, and implements it via the focus event with ReactEventListener.trapCapturedEvent instead of ReactEventListener.trapBubbledEvent. This is necessary because React implements its synthetic event system using event delegation, and so needs to use either capturing or bubbling for all its event handling. The article linked to in the comment explains how this works:
The problem is that these events do not bubble up. A focus or blur event on a link fires only on the link itself, and not on any ancestor element of the link.
This is an ancient rule. A few events, most notably focus, blur, and change, do not bubble up the document tree. The exact reasons for this have been lost in the mist of history, but part of the cause is that these events just don't make sense on some elements. The user cannot focus on or change a random paragraph in any way, and therefore these events are just not available on these HTML elements. In addition, they do not bubble up.
...
Except when you use event capturing.
...
One of the most curious conclusions of my event research is that when you define event handlers in the capturing phase the browser executes any and all event handlers set on ancestors of the event target whether the given event makes sense on these elements or not.
It seems pretty likely that the React team decided to simply make the event always bubble (which, to be honest, is what I expected from the DOM spec as well until I read your question). The browser implementations don't seem to be consistent; one issue comment mentions that focus events bubble in Firefox, but I was not able to reproduce that on a recent version. However, using an onfocusin attribute or using addEventListener("focusin", ...) also didn't work in FF. So it's possible that this was simply an attempt at normalizing the events across browsers.
All that said, it does seem there is perhaps a bug where the .bubbles property on a SyntheticFocusEvent is false instead of true.
I've stated creating a Polymer-based webpage and I'd like to add a link to the paper-button I created. I've figured out how to do it, but I'd like to add a delay to let users see the ripple animation after clicking it.
To link it to a webpage I used this code:
<paper-button raised> Polymer's Website </paper-button>
However, I don't know how to add a delay. Could someone help me? And, is there a better way to link the button to a webpage?
You can create an element that extends native HTML a tag and use paper-ripple element if you want to customize the ripple effect. Another solution is to add an event to you paper-button and use a setTimeout to delay your action
goto:function(){
setTimeout(function(){
window.location.href = 'http:\\www.google.com';
},1000);
}
<paper-button raised on-click="goto"> Polymer's Website </paper-button>
I am trying to implement my own custom tooltip element, which displays the tooltip when the on-mouseover event is fired. However, this does not work:
<div class="message>
<custom-tooltip text="{{time}}">
<div>{{message}}</div>
</custom-tooltip>
<paper-ripple fit></paper-ripple>
</div>
because the paper-ripple is "above" the custom-tooltip and therefore not passing on any of its events. I'm not sure if this is a polymer specific issue, or if it just my inexperience with DOM event propagation, but any help would be appreciated.
I'm trying to understand what events that originate in light DOM look like when received in shadow DOM via a <content> element. I'm reading the Shadow DOM W3C Draft, and I don't entirely understand it but it sounds like events are to be "retargeted" from the point of view of the EventListener attachment.
In the cases where event path is across multiple node trees, the
event's information about the target of the event is adjusted in order
to maintain encapsulation. Event retargeting is a process of computing
relative targets for each ancestor of the node at which the event is
dispatched. A relative target is a node that most accurately
represents the target of a dispatched event at a given ancestor while
maintaining the encapsulation.
and
At the time of event dispatch:
The Event target and currentTarget attributes must return the relative
target for the node on which event listeners are invoked
So here's a simple Polymer custom element that just puts its children into a container, and adds a click EventListener to the container (in the shadow DOM). In this case the child is a button.
<!DOCTYPE html>
<html>
<head>
<script src="bower_components/platform/platform.js"></script>
<link rel="import" href="bower_components/polymer/polymer.html">
</head>
<body unresolved>
<polymer-element name="foo-bar">
<template>
<div id="internal-container" style="background-color:red; width:100%;">
<content></content>
</div>
</template>
<script>
Polymer("foo-bar", {
clickHandler: function(event) {
console.log(event);
var element = event.target;
while (element) {
console.log(element.tagName, element.id);
element = element.parentElement;
}
},
ready: function() {
this.shadowRoot.querySelector('#internal-container').addEventListener('click', this.clickHandler);
}
});
</script>
</polymer-element>
<foo-bar id="custom-element">
<button>Click me</button>
</foo-bar>
</body>
</html>
When I run this on Chrome 38.0.2075.0 canary, when I click on the button I get:
MouseEvent {dataTransfer: null, toElement: button, fromElement: null, y: 19, x: 53…}altKey: falsebubbles: truebutton: 0cancelBubble: falsecancelable: truecharCode: 0clientX: 53clientY: 19clipboardData: undefinedctrlKey: falsecurrentTarget: nulldataTransfer: nulldefaultPrevented: falsedetail: 1eventPhase: 0fromElement: nullkeyCode: 0layerX: 53layerY: 19metaKey: falsemovementX: 0movementY: 0offsetX: 45offsetY: 10pageX: 53pageY: 19path: NodeList[0]relatedTarget: nullreturnValue: truescreenX: 472screenY: 113shiftKey: falsesrcElement: buttontarget: buttontimeStamp: 1404078533176toElement: buttontype: "click"view: WindowwebkitMovementX: 0webkitMovementY: 0which: 1x: 53y: 19__proto__: MouseEvent test.html:17
BUTTON test.html:20
FOO-BAR custom-element test.html:20
BODY test.html:20
HTML test.html:20
and when I click on the container I get:
MouseEvent {dataTransfer: null, toElement: div#internal-container, fromElement: null, y: 15, x: 82…} test.html:17
DIV internal-container test.html:20
So I get an event target in either the light or shadow DOM, depending on which DOM the source element was in. I was expecting to get a target from the shadow DOM in both cases because that's where the EventListener is attached. My questions are:
Is this the way it is supposed to work, and
If so, is there an alternative way to get events that bubble up from the light DOM retargeted to the shadow DOM?
In case someone wants to ask, "What are you trying to do?", I'm not trying to do anything specifically other than understand the behavior.
Events with shadow dom are tricky. I try to capture a braindump below.
Is this the way it is supposed to work
Yep. If you're testing in Chrome, you get native shadow dom.
I wrote a section on event retargeting in the HTML5Rocks - Shadow DOM 301 article. Basically, retargeting means that events that originate in the shadow dom look like they come from the element itself.
In your example, you're logging the event internal to the shadow dom, so it's still seen there. If you also add a 'click' listener outside of the element, the target will look as if it came from the element:
<script>
var el = document.querySelector('#custom-element');
el.addEventListener('click', function(e) {
console.log(e.target.tagName); // logs FOO-Bar
});
</script>
http://jsbin.com/womususe/1/edit
The 'click' event bubbles. This is why you see BUTTON in your top example. Why do you see it at all? You see it because the button is not part of your element's shadow dom. It's in the light dom and the target of the element. It's important to remember that light DOM nodes are still logically in the main document. They're not moved into the shadow dom, merely rendered at <content> insertion points.
BTW, there are a couple of Polymerized fixes to your examples:
this.shadowRoot.querySelector('#internalcontainer') -> this.$.internalcontainer. this.$.ID is Polymer's "automatic node finding" feature.
You don't need to use addEventListener() at all. Instead , use <div id="internalcontainer" on-click="{{clickHandler}}">. This is a declarative event handler.