I'm using amp-sidebar in my website and when I open/close it occasionally throws this error:
Uncaught TypeError: Cannot read property 'hiddenElementInfos' of undefined
at oa (modal.js:228)
at amp-sidebar.js:385
at mutate (resources-impl.js:1093)
at Zm (vsync-impl.js:461)
at Vm.f.Qh (vsync-impl.js:417)
My website is server-side rendered and I'm using react-amphtml version 3.0.0 for amp components. Below is the my sidebar code:
import * as Amp from 'react-amphtml';
const AmpSidebarComponent = () => (
<AmpSidebar id="sidebar" layout="nodisplay" side="left">
{/* content */}
</AmpSidebar>
);
The error occurs in this amp function:
updateForClosing_() {
this.closeMask_();
this.mutateElement(() => {
setModalAsClosed(this.element); // error is here
});
this.element.removeAttribute('open');
this.element.setAttribute('aria-hidden', 'true');
this.setUpdateFn_(() => this.updateForClosed_(), ANIMATION_TIMEOUT);
}
while setModalAsOpen opens a sidebar by adding aria-hidden HTML attributes. It looks like an internal amp error to me not related to my code but maybe I'm wrong. Below is the code for setModalAsOpen
/**
* Sets an Element as an open modal, making all Elements outside of the page
* hidden from the tab order and screenreaders.
*
* This is done by making other subtrees 'aria-hidden', as well as giving a
* negative `tabindex` to all focusable elements outside the modal. When
* opening a modal, the ancestry has `aria-hidden` removed any any `tabindex`
* values within the modal restored.
*
* Note: this does not block click events on things outside of the modal. It is
* assumed that a backdrop Element blocking clicks is present.
* #param {!Element} element
*/
export function setModalAsOpen(element) {
devAssert(modalEntryStack.every(info => info.element !== element));
devAssert(isConnectedNode(element));
const elements = getElementsToAriaHide(element);
const ancestry = getAncestors(element).filter(
n => n.nodeType == Node.ELEMENT_NODE
);
const focusableElements = getPotentiallyFocusableElements(element);
// Get the elements that are internally focusable, and have been made
// non-focusable; we want to unhide these.
const focusableInternalElements = focusableElements.filter(e => {
return element.contains(e) && e[SAVED_TAB_INDEX] !== undefined;
});
// Get the elements that are externally focusable, and have not yet been made
// non-focusable; we want to hide these.
const focusableExternalElements = focusableElements.filter(e => {
return !element.contains(e) && e[SAVED_TAB_INDEX] === undefined;
});
const hiddenElementInfos = elements.concat(ancestry).map(element => {
return {
element,
prevValue: element.getAttribute('aria-hidden'),
};
});
// Unhide the ancestry, in case it was hidden from another modal.
ancestry.forEach(e => e.removeAttribute('aria-hidden'));
// Hide everything else.
elements.forEach(e => e.setAttribute('aria-hidden', 'true'));
// Make everything outside of the modal non-focusable via tab.
focusableExternalElements.forEach(e => {
e[SAVED_TAB_INDEX] = e.getAttribute('tabindex');
e.setAttribute('tabindex', '-1');
});
// Restore the focusability of everything inside of the modal that was made
// non-focusable.
focusableInternalElements.forEach(e => {
devAssert(e[SAVED_TAB_INDEX] !== undefined);
restoreAttributeValue(e, 'tabindex', e[SAVED_TAB_INDEX]);
});
modalEntryStack.push({
element,
hiddenElementInfos,
focusableExternalElements,
focusableInternalElements,
});
}
EDIT: I'm using styled-system version 5.0.16 and styled-components version 4.3.2.
Related
I am using ReactJS on an App and currently need to be able to print some elements from the page on user's request (click on a button).
I chose to use the CSS media-query type print (#media print) to be able to check if an element should be printed, based on a selector that could be from a class or attribute on an Element. The strategy would be to hide everything but those "printable" elements with a stylesheet looking like:
#media print {
*:not([data-print]) {
display: none;
}
}
However, for this to work I need to also add the chosen print selector (here the attribute data-print) on every parent element each printable element has.
To do that here's what I've tried so far:
export default function PrintButton() {
useEffect(() => {
const handleBeforePrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
let element = printableElement;
while (element.parentElement) {
element.setAttribute("data-print", "");
element = element.parentElement;
}
element.setAttribute("data-print", "");
}
});
};
const handleAfterPrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
let element = printableElement;
while (element.parentElement) {
element.removeAttribute("data-print");
element = element.parentElement;
}
element.removeAttribute("data-print");
}
});
};
window.addEventListener("beforeprint", handleBeforePrint);
window.addEventListener("afterprint", handleAfterPrint);
return () => {
window.removeEventListener("beforeprint", handleBeforePrint);
window.removeEventListener("afterprint", handleAfterPrint);
};
}, []);
return <button onClick={() => window.print()}>Print</button>;
}
With printNodeSelectors being a const Array of string selectors.
Unfortunately it seems that React ditch out all my dirty DOM modification right after I do them 😭
I'd like to find a way to achieve this without having to manually put everywhere in the app who should be printable, while working on a React App, would someone knows how to do that? 🙏🏼
Just CSS should be enough to hide all Elements which do not have the data-print attribute AND which do not have such Element in their descendants.
Use the :has CSS pseudo-class (in combination with :not one) to express that 2nd condition (selector on descendants):
#media print {
*:not([data-print]):not(:has([data-print])) {
display: none;
}
}
Caution: ancestors of Elements with data-print attribute would not match, hence their text nodes (not wrapped by a tag) would not be hidden when printing:
<div>
<span>should not print</span>
<span data-print>but this should</span>
Caution: text node without tag may be printed...
</div>
Demo: https://jsfiddle.net/6x34ad50/1/ (you can launch the print preview browser feature to see the effect, or rely on the coloring)
Similar but just coloring to directly see the effect:
*:not([data-print]):not(:has([data-print])) {
color: red;
}
<div>
<span>should not print (be colored in red)</span>
<span data-print>but this should</span>
Caution: text node without tag may be printed...
</div>
After some thoughts, tries and errors it appears that even though I managed to put the attribute selector on the parents I completely missed the children of the elements I wanted to print! (React wasn't at all removing the attributes from a mysterious render cycle in the end)
Here's a now functioning Component:
export default function PrintButton() {
useEffect(() => {
const handleBeforePrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
const elements: Element[] = [];
// we need to give all parents and children a data-print attribute for them to be displayed on print
const addParents = (element: Element) => {
if (element.parentElement) {
elements.push(element.parentElement);
addParents(element.parentElement);
}
};
addParents(printableElement);
const addChildrens = (element: Element) => {
elements.push(element);
Array.from(element.children).forEach(addChildrens);
};
addChildrens(printableElement);
elements.forEach((element) => element.setAttribute("data-print", ""));
}
});
};
const handleAfterPrint = () => {
document.querySelectorAll("[data-print]").forEach((element) => element.removeAttribute("data-print"));
};
window.addEventListener("beforeprint", handleBeforePrint);
window.addEventListener("afterprint", handleAfterPrint);
return () => {
window.removeEventListener("beforeprint", handleBeforePrint);
window.removeEventListener("afterprint", handleAfterPrint);
};
}, []);
return <button onClick={() => window.print()}>Print</button>;
}
I usually don't like messing with the DOM while using React but here it allows me to keep everything in the component without having to modify anything else around (though I'd agree that those printNodeSelectors need to be chosen from outside and aren't dynamic at the moment)
when we call element.remove(), element is removed from DOM since and element.isConnected will return false. is there a function that reverse remove method and put the element back into location?
let's say I have three elements: html_area, css_area and js_area. their display are managed individually by html_hider, js_hider, css_hider. when hider is clicked, I need to remove the area from DOM, also modifies container dimension. here's the thing, I want to keep these in order:
html
css
js
when they hide and show (appended or removed) form DOM, I want their sequence to be maintained. I manually coded it as:
html_area.connect = () => container.prepend(html_area);
css_area.connect = () => {
if(!html_area.isConnected){
container.prepend(css_area);
}else if(!js_area.isConnected){
container.append(css_area);
}else{
container.insertBefore(css_area, js_area);
}
}
js_area.connect = () => container.append(js_area);
html_hider.onclick = () => {
if(html_area.isConnected){
html_hider.classList.add('hide');
html_area.remove();
}else{
html_hider.classList.remove('hide');
html_area.connect();
}
aMode();
}
css_hider.onclick = () => {
if(css_area.isConnected){
css_hider.classList.add('hide');
css_area.remove();
}else{
css_hider.classList.remove('hide');
css_area.connect();
}
aMode();
}
js_hider.onclick = () => {
if(js_area.isConnected){
js_hider.classList.add('hide');
js_area.remove();
}else{
js_hider.classList.remove('hide');
js_area.connect();
}
aMode();
}
not robust and will be extremely complicated when elements and hiders pair more than 3. is there a way to reverse the remove method and put the element back in its original location?
I'm attempting to replicate the content in a particular IFrame element inside of a modal to avoid unnecessary DB calls. I am invoking a clientside callback via Python (see here) that returns the index of a particular IFrame element I would like to replicate in my modal.
Here is the snippet of Python code that toggles my modal and tracks the index of the most recently clicked figure to replicate:
#app.callback(
[Output('my-modal', 'is_open'),
Output('modal-clone', 'children')],
[Input(f'button{k}', 'n_clicks_timestamp') for k in range(20)] +
[State('my-modal', 'is_open')])
def toggle_modal(*data):
clicks, is_open = data[:20], data[20]
modal_display = not is_open if any(clicks) else is_open
clicked = clicks.index(max(clicks))
return [modal_display, clicked]
app.clientside_callback(
ClientsideFunction(namespace='clientside', function_name='clone_figure'),
Output('modal-test', 'children'),
[Input('modal-clone', 'children'), Input('modal-figure', 'id')]
)
And the following Javascript:
window.dash_clientside = Object.assign({}, window.dash_clientside, {
clientside: {
clone_figure: function(clone_from, clone_to) {
source = document.getElementById(clone_from);
console.log(document.getElementById(clone_to))
console.log(document.getElementById(clone_to).contentDocument);
clone = document.getElementById(clone_to);
// set attributes of clone here using attributes from source
return null
}
}
});
Now, from my console.log() statements, I noticed the following (note that modal-clone in the screenshot corresponds to modal-figure in my example):
How is contentDocument changing between these two log statements? Any insight would be greatly appreciated, I am stumped.
It appears that you need to addEventListener() to the IFrame element:
clone_spray: function(clone_from, clone_to) {
source = document.getElementById(clone_from);
clone = document.getElementById(clone_to);
if (!clone) {return null;}
clone.addEventListener("load", function() {
// set attributes of clone here using attributes from source
The question is as given in the title, ie, to access element whose parent is hidden. The problem is that, as per the cypress.io docs :
An element is considered hidden if:
Its width or height is 0.
Its CSS property (or ancestors) is visibility: hidden.
Its CSS property (or ancestors) is display: none.
Its CSS property is position: fixed and it’s offscreen or covered up.
But the code that I am working with requires me to click on an element whose parent is hidden, while the element itself is visible.
So each time I try to click on the element, it throws up an error reading :
CypressError: Timed out retrying: expected
'< mdc-select-item#mdc-select-item-4.mdc-list-item>' to be 'visible'
This element '< mdc-select-item#mdc-select-item-4.mdc-list-item>' is
not visible because its parent
'< mdc-select-menu.mdc-simple-menu.mdc-select__menu>' has CSS property:
'display: none'
The element I am working with is a dropdown item, which is written in pug. The element is a component defined in angular-mdc-web, which uses the mdc-select for the dropdown menu and mdc-select-item for its elements (items) which is what I have to access.
A sample code of similar structure :
//pug
mdc-select(placeholder="installation type"
'[closeOnScroll]'="true")
mdc-select-item(value="false") ITEM1
mdc-select-item(value="true") ITEM2
In the above, ITEM1 is the element I have to access. This I do in cypress.io as follows :
//cypress.io
// click on the dropdown menu to show the dropdown (items)
cy.get("mdc-select").contains("installation type").click();
// try to access ITEM1
cy.get('mdc-select-item').contains("ITEM1").should('be.visible').click();
Have tried with {force:true} to force the item click, but no luck. Have tried to select the items using {enter} keypress on the parent mdc-select, but again no luck as it throws :
CypressError: cy.type() can only be called on textarea or :text. Your
subject is a: < mdc-select-label
class="mdc-select__selected-text">Select ...< /mdc-select-label>
Also tried using the select command, but its not possible because the Cypress engine is not able to identify the element as a select element (because its not, inner workings are different). It throws :
CypressError: cy.select() can only be called on a . Your
subject is a: < mdc-select-label
class="mdc-select__selected-text">Select ...< /mdc-select-label>
The problem is that the mdc-select-menu that is the parent for the mdc-select-item has a property of display:none by some internal computations upon opening of the drop-down items.
This property is overwritten to display:flex, but this does not help.
All out of ideas. This works in Selenium, but does not with cypress.io. Any clue what might be a possible hack for the situation other than shifting to other frameworks, or changing the UI code?
After much nashing-of-teeth, I think I have an answer.
I think the root cause is that mdc-select-item has display:flex, which allows it to exceed the bounds of it's parents (strictly speaking, this feels like the wrong application of display flex, if I remember the tutorial correctly, however...).
Cypress does a lot of parent checking when determining visibilty, see visibility.coffee,
## WARNING:
## developer beware. visibility is a sink hole
## that leads to sheer madness. you should
## avoid this file before its too late.
...
when $parent = parentHasDisplayNone($el.parent())
parentNode = $elements.stringify($parent, "short")
"This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'display: none'"
...
when $parent = parentHasNoOffsetWidthOrHeightAndOverflowHidden($el.parent())
parentNode = $elements.stringify($parent, "short")
width = elOffsetWidth($parent)
height = elOffsetHeight($parent)
"This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'overflow: hidden' and an effective width and height of: '#{width} x #{height}' pixels."
But, when using .should('be.visible'), we are stuck with parent properties failing child visibility check, even though we can actually see the child.
We need an alternate test.
The work-around
Ref jquery.js, this is one definition for visibility of the element itself (ignoring parent properties).
jQuery.expr.pseudos.visible = function( elem ) {
return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
}
so we might use that as the basis for an alternative.
describe('Testing select options', function() {
// Change this function if other criteria are required.
const isVisible = (elem) => !!(
elem.offsetWidth ||
elem.offsetHeight ||
elem.getClientRects().length
)
it('checks select option is visible', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
//cy.get('mdc-select-item').contains("ITEM1").should('be.visible') //this will fail
cy.get('mdc-select-item').contains("ITEM1").then (item1 => {
expect(isVisible(item1[0])).to.be.true
});
});
it('checks select option is not visible', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
cy.document().then(function(document) {
const item1 = document.querySelectorAll('mdc-select-item')[0]
item1.style.display = 'none'
cy.get('mdc-select-item').contains("ITEM1").then (item => {
expect(isVisible(item[0])).to.be.false
})
})
});
it('checks select option is clickable', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
//cy.get('mdc-select-item').contains("ITEM1").click() // this will fail
cy.get('mdc-select-item').contains("ITEM1").then (item1 => {
cy.get('mdc-select-item').contains("ITEM2").then (item2 => {
expect(isVisible(item2[0])).to.be.true //visible when list is first dropped
});
item1.click();
cy.wait(500)
cy.get('mdc-select-item').contains("ITEM2").then (item2 => {
expect(isVisible(item2[0])).to.be.false // not visible after item1 selected
});
});
})
Footnote - Use of 'then' (or 'each')
The way you normally use assertion in cypress is via command chains, which basically wraps the elements being tested and handles things like retry and waiting for DOM changes.
However, in this case we have a contradiction between the standard visibility assertion .should('be.visible') and the framework used to build the page, so we use then(fn) (ref) to get access to the unwrapped DOM. We can then apply our own version of the visibility test using stand jasmine expect syntax.
It turns out you can also use a function with .should(fn), this works as well
it('checks select option is visible - 2', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
cy.get('mdc-select-item').contains("ITEM1").should(item1 => {
expect(isVisible(item1[0])).to.be.true
});
});
Using should instead of then makes no difference in the visibility test, but note the should version can retry the function multiple times, so it can't be used with click test (for example).
From the docs,
What’s the difference between .then() and .should()/.and()?
Using .then() simply allows you to use the yielded subject in a callback function and should be used when you need to manipulate some values or do some actions.
When using a callback function with .should() or .and(), on the other hand, there is special logic to rerun the callback function until no assertions throw within it. You should be careful of side affects in a .should() or .and() callback function that you would not want performed multiple times.
You can also solve the problem by extending chai assertions, but the documentation for this isn't extensive, so potentially it's more work.
For convenience and reusability I had to mix the answer of Richard Matsen and Josef Biehler.
Define the command
// Access element whose parent is hidden
Cypress.Commands.add('isVisible', {
prevSubject: true
}, (subject) => {
const isVisible = (elem) => !!(
elem.offsetWidth ||
elem.offsetHeight ||
elem.getClientRects().length
)
expect(isVisible(subject[0])).to.be.true
})
You can now chain it from contains
describe('Testing select options', function() {
it('checks select option is visible', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
//cy.get('mdc-select-item').contains("ITEM1").should('be.visible') // this will fail
cy.get('mdc-select-item').contains("ITEM1").isVisible()
});
});
I came across this topic but was not able to run your example. So I tried a bit and my final solution is this. maybe someone other also needs this. Please note that I use typescript.
First: Define a custom command
Cypress.Commands.add("isVisible", { prevSubject: true}, (p1: string) => {
cy.get(p1).should((jq: JQuery<HTMLElement>) => {
if (!jq || jq.length === 0) {
//assert.fail(); seems that we must not assetr.fail() otherwise cypress will exit immediately
return;
}
const elem: HTMLElement = jq[0];
const doc: HTMLElement = document.documentElement;
const pageLeft: number = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
const pageTop: number = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
let elementLeft: number;
let elementTop: number;
let elementHeight: number;
let elementWidth: number;
const length: number = elem.getClientRects().length;
if (length > 0) {
// TODO: select correct border box!!
elementLeft = elem.getClientRects()[length - 1].left;
elementTop = elem.getClientRects()[length - 1].top;
elementWidth = elem.getClientRects()[length - 1].width;
elementHeight = elem.getClientRects()[length - 1].height;
}
const val: boolean = !!(
elementHeight > 0 &&
elementWidth > 0 &&
elem.getClientRects().length > 0 &&
elementLeft >= pageLeft &&
elementLeft <= window.outerWidth &&
elementTop >= pageTop &&
elementTop <= window.outerHeight
);
assert.isTrue(val);
});
});
Please note the TODO. In my case I was targeting a button which has two border boxes. The first with height and width 0. So i must select the second one. Please adjust this to your needs.
Second: Use it
cy.wrap("#some_id_or_other_locator").isVisible();
I could solve it by calling scrollIntoView after getting an element. See this answer.
A related problem:
Cypress was unable to find a tab element because it had a style of display: none (even though it was visible on the page)
My workaround:
Cypress could target the tab by matching text and clicking
cy.get("[data-cy=parent-element]").contains("target text").click();
To expand a bit the answer of BTL, if anyone faced an error - Property 'isVisible' does not exist on type 'Chainable<JQuery<HTMLElement>> in Typescript, following is what I added at the top of commands.ts in cypress to get away with it -
declare global {
namespace Cypress {
interface Chainable {
isVisible;
}
}
}
And may be replacing expect(isVisible(subject[0])).to.be.true with assert.True(isVisible(subject[0])); if you see any chai assertion error with expect and don't want to import it - as in Josef Biehler answer..
I was facing the same error that parent is hidden so Cypress is unable to click the child element, I handled this by handling the visibility of parent from hidden to visible by this code
cy.get('div.MuiDrawer-root.MuiDrawer-docked').invoke('css', 'overflow-x', 'visible').should('have.css', 'overflow-x', 'visible')
Note: You can apply any css you want in the invoke function like I have
Remove the flex and try. If it is solved then use the flex standard way
I am using angular material.
When I create my own directive and add it to md-tab-label, like
<md-tab-label>
<custom-directive>
Label
</cutom-directive>
</md-tab-label>
Then the custom directive is applied to some "md-dummy-tab" also.
But if I give mdtooltop to the md-tab-label ,like
<md-tab-label>
Label
<md-tooltip>Label</md-tooltip>
</md-tab-label>
then there is no md-tooltip applied to "md-dummy-tab" class
I tried searching inside the mdtooltip code, but couldnt get any clue.
https://github.com/angular/material/blob/master/src/components/tooltip/tooltip.js
How can I do the same for my custom directive , ie custom directive should not apply to md dummy tab?
<md-tooltip> is not appended to the <md-dummy-tab> because it doesn't render any HTML code inside the <md-tab-label>. Its template is appended to the nearest parent <md-content> the moment you trigger the tooltip to show.
scope.$watch('visible', function (isVisible) {
if (isVisible) showTooltip();
else hideTooltip();
});
-
function showTooltip() {
// Insert the element before positioning it, so we can get the position
// and check if we should display it
tooltipParent.append(element);
// Check if we should display it or not.
// This handles hide-* and show-* along with any user defined css
if ( hasComputedStyleValue('display','none') ) {
scope.visible = false;
element.detach();
return;
}
positionTooltip();
angular.forEach([element, background, content], function (element) {
$animate.addClass(element, 'md-show');
});
}
-
current = getNearestContentElement(),
tooltipParent = angular.element(current || document.body)
-
function getNearestContentElement () {
var current = element.parent()[0];
// Look for the nearest parent md-content, stopping at the rootElement.
while (current && current !== $rootElement[0] && current !== document.body) {
current = current.parentNode;
}
return current;
}