React Admin Input-components always add some extra space for helperText - material-ui

Recently React Admin input components started to add extra space below in order to display helperText (if provided). But it seems that it is impossible to avoid this behaviour by added helperText={false} as it is recommended, e.g.:
<TextInput source="myField" helperText={false} />
It always displays some extra space no matter if I pass "false" as value to the heplerText prop or not.
I'm on React Admin version 3.2.3 now.
In RA's TextInput component we can see this code:
helperText={ // <-- goes to MUI TextField
<InputHelperText
touched={touched}
error={error}
helperText={helperText} // <-- goes to RA InputHelperText
/>
}
If the value of InputHelperText component's helperText prop is equal to false then InputHelperText renders nothing (returns null in its render function).
But the value of heplerText prop that goes to underlying MaterialUI TextField is never null nor undefined, even if I pass "false": it is always an InputHelperText component, which just may or may not render something.
MaterialUI TextField component, in turn, analyses its heplerText prop:
const helperTextId = helperText && id ? `${id}-helper-text` : undefined; // <-- helperText from TextInput
...
{helperText && (
<FormHelperText id={helperTextId} {...FormHelperTextProps}>
{helperText}
</FormHelperText>
)}
Since helperText is never null nor undefined, it will always render FormHelperText, perhaps with an empty helperText.
If I change the RA TextInput component's code like that:
helperText={helperText && touched && error ?
<InputHelperText
touched={touched}
error={error}
helperText={helperText}
/> : null
everything works fine: no extra space is added since the value of helperText (passed to the underlying MUI TextField) is indeed null when the value of TextInput component's helperText prop is equal to false.
Am I missing something or it is indeed a bug?

This is a feature:
https://github.com/marmelab/react-admin/pull/4364
A questionable feature.

As of react-admin v4.3(+), passing a props helperText={false} to <TextInput/> does remove the extra space.
Example:
<TextInput source="meta.og.description" helperText={false} />

Related

Quasar2 Vue3 Cypress q-popup-edit

I have the following vue template:
<template>
<q-item tag="label" v-ripple>
<q-popup-edit
v-model="model"
:cover="false"
fit
buttons
:validate="validate"
#before-show="modelProxy = model"
>
<template v-slot:title>
<div class="text-mono">
{{ name }}
</div>
</template>
<q-input
color="indigo"
v-model="modelProxy"
dense
autofocus
counter
:type="dataType ? dataType : 'text'"
:hint="hint"
:error="error"
:error-message="errorMessage"
/>
</q-popup-edit>
<q-item-section>
<q-item-label class="text-mono">{{ name }}</q-item-label>
<q-item-label v-if="offset && model && model.length > offset" caption
>...{{
model.substring(model.length - offset, model.length)
}}</q-item-label
>
<q-item-label v-else caption>{{ model }}</q-item-label>
</q-item-section>
</q-item>
</template>
I would like to perform E2E test using Cypress with the following code snippet:
it('Verify Edit Box from auto-generated page', () => {
cy.get('[data-test="popup-edit-setting-1"]').contains("Auto Generated Edit box");
cy.get('[data-test="popup-edit-setting-2"]').contains("Auto Generated Edit box (Number)");
cy.get('[data-test="popup-edit-setting-1"]').should("be.enabled"); // XXX
cy.get('[data-test="popup-edit-setting-1"]').focus().click().type("Hello");//.click("SET");
cy.get('[data-test="popup-edit-setting-1"]').find("label").should('have.value', 'Hello') // XXX
});
It stumbles on the XXX points.
#Fody's solution works but there is one minor issue. I have 2 popup edit box. One with normal string, another with only numeric. There are 2 test cases for the numeric popup editbox. One with invalid normal string entry and another with valid numbers. The problem is that at the end of the test, the numeric popup edit box does NOT return to display mode. It stays popup.
This is the way I would test q-popup-edit. I used a generic example, yours may differ in some details.
I aimed to test based on what a user sees rather than any internal class or internal properties.
The user story is:
the text to be edited has a "hand" pointer when hovered
click on it to change it from "display" mode to "edit" mode
the input is automatically focused, user can start typing
user enters some text
user clicks away and the input loses focus, goes back to "display" mode
// activate popup editor
const initialText = 'Click me'
cy.contains('div.cursor-pointer', initialText) // displayed initial text
.should('be.visible') // with hand cursor
.click()
// initial condition
cy.focused() // after click <input> should have focus
.as('input') // save a reference
.should('have.prop', 'tagName', 'INPUT') // verify it is the input
cy.get('#input')
.invoke('val')
.should('eq', initialText) // displayed text is also in the input
cy.contains('8').should('be.visible') // character count
// edit action
cy.get('#input')
.clear()
.type('test input')
cy.get('#input')
.invoke('val')
.should('eq', 'test input') // verify input
cy.contains('10').should('be.visible') // character count has changed
// back to display mode
cy.get('body').click() // go back to display mode
cy.contains('div.cursor-pointer', 'test input')
.should('be.visible')
.and('contain', 'test input') // verify display element
cy.contains('10').should('not.exist') // edit counter has gone
Notes
To start the edit, you need to identify the display-mode element. It's easiest if you have some unique text in the field, so try to arrange that in the page initial data.
If no unique text, look for a label or some other selectable element nearby then navigate to it.
If you add a data-cy attribute to the <q-popup-edit>, it will not exist in the DOM until the component enters edit-mode (so you can't use it in the initial click()).

How do I make the MUI Dialog title an H1 element so the modal is accessible

I'm working on making my app more accessible and am struggling with the MUI Dialog component. I'm using the DialogTitle component, which creates an H2 element and am getting an issue of "page doesn't contain a level-one heading". Should I be creating my modal in some other way, or are MUI Dialogs just not accessible?
import { Dialog, DialogTitle } from '#mui/material';
const MyModal = () => {
return (
<Dialog open={true}>
<DialogTitle>
My Title
</DialogTitle>
</Dialog>
);
};
export default MyModal;
Updated for MUI v5:
The Dialog component API includes a helper DialogTitle component which, by default, renders its contents within an h2 element. To change this functionality, you can pass the component property to the DialogTitle to have the DialogTitle rendered using whatever elementType that you wish. For example:
<DialogTitle component="h1">
My Dialog Title
</DialogTitle>
This is currently an undocumented feature of DialogTitle, but it can been seen in the source code that properties that are passed to DialogTitle are spread onto the underlying Typography component -- By passing component, you are essentially overwriting the hardcoded component="h2" prop with your own value.
Working example: https://codesandbox.io/s/simpledialog-material-demo-forked-kpq9k?file=/demo.js
Original answer for MUI v4:
The Dialog component API includes a helper DialogTitle component which, by default, renders its contents within an h2 element. To disable this functionality, you can use the DialogTitle component with the disableTypography prop (to disable the h2 wrapping behavior) and then include your own Typography component set to h1. For example:
<DialogTitle disableTypography>
<Typography variant="h1">My Dialog Title</Typography>
</DialogTitle>
Working example: https://codesandbox.io/s/material-demo-forked-7pso2?file=/demo.js
Extra Credit: You may then come across the problem that the h1 is styled "too large" for your design. If so, and you prefer the h2 look, you can use the Typography prop named component in combination with the variant prop to visually style it back to an h2, while maintaining the underlying h1 element. For example:
<DialogTitle disableTypography>
<Typography variant="h2" component="h1">My Dialog Title</Typography>
</DialogTitle>
In the case of TypeScript there's no typing provided so you need to do a hack:
<DialogTitle {...{ component: 'div' } as any}>
<Typography variant="h1">My Dialog Title</Typography>
</DialogTitle>
This is a temporary hack until they add the typing for all the props there.
As #Steve mentionned, in Mui v5, the job is harder. Even if you specify a component prop (which won't work in TS, moreover it's a bad practice), it won't change the style as it renders a h2 with a h6-style.
The cleanest workaround would be to mess with CSS (in a styled component for eg)
// Styling
import { styled } from "#mui/system";
// UI
import { DialogTitle as MuiDialogTitle } from "#mui/material";
const DialogTitle = styled(MuiDialogTitle)(({ theme }) => ({
"&.MuiDialogTitle-root.MuiTypography-root": {
fontSize: 25,
fontWeight: "bold",
},
}));

React-Bootstap-Typeahead: Manually set custom display value in onChange() upon menu selection

In the onChange of React-Bootstrap-Typeahead, I need to manually set a custom display value. My first thought was to use a ref and do something similar to the .clear() in this example.
But although .clear() works, inputNode.value = 'abc' does not work, and I'm left with the old selected value from the menu.
onChange={option => {
typeaheadRef.current.blur(); // This works
typeaheadRef.current.inputNode.value = 'abc'; // This does not work (old value is retained)
}}
I also tried directly accessing the DOM input element, whose ID I know, and doing
var inputElement = document.querySelector('input[id=myTypeahead]');
inputElement.value = 'abc';
But that didn't work either. For a brief second, right after my changed value = , I do see the new display label, but then it's quickly lost. I think the component saves or retains the menu-selected value.
Note: I cannot use selected, I use defaultSelected. I have some Formik-related behavior that I've introduced, and it didn't work with selected, so I'm stuck with defaultSelected.
The only workaround I found is to re-render the Typeahead component (hide and re-show, from a blank state) with a new defaultSelected="abc" which is a one-time Mount-time value specification for the control.
I couldn't get selected=.. to work, I have a wrapper around the component which makes it fit into Formik with custom onChange and onInputChange and selected wasn't working with that.
So the simple workaround that works is, if the visibility of the Typeahead depends on some condition (otherwise it won't be rendered), use that to momentarily hide and re-show the component (a brand new repaint) with a new defaultSelected, e.g.
/* Conditions controlling the visibility of the Typeahead */
!isEmptyObject(values) &&
(values.approverId === null || (values.approverId !== null && detailedApproverUserInfo)
)
&&
<AsyncTypehead defaultSelected={{...whatever is needed to build the string, or the literal string itself...}}
..
// Given the above visibility condition, we'll hide/re-show the component
// The below will first hide the control in React's renders
setFieldValue("approver", someId);
setDetailedUserInfo(null);
// The below will re-show the control in React's renders, after a small delay (a fetch)
setDetailedUserInfo(fetchDetailedUserInfo());

Material UI IconButton onClick doesn't let to handle event

I installed "#material-ui/core": "^4.9.2" and "#material-ui/icons": "^4.9.1".
In my form i have several rows, each row has an add button and a remove button. I want the remove button to remove the row from it was clicked. It works fine with regular Button with a "-" character in it. But i want it fancy, so i replaced my Button from an IconButton, and imported the icons to use
import {AddCircleOutline,RemoveCircleOutlineOutlined} from "#material-ui/icons";
And my IconButton looks like this:
<IconButton
onClick={props.onRemoveClick}
className="align-self-center"
color="info"
size="sm"
disabled={props.index > 0 ? false : true}
<RemoveCircleOutlineOutlined/>
</IconButton>
When the IconButton is hit, the onClick method is called (i know because of logs in my console) but i can't handle the event because it is now undefined.
The funny thing is that if i click on the button area that doesn't correspond to the icon, it works. But obviously i need it to work in the whole area of the button.
It is not a binding issue because i already tested it.
Any ideas?
Props that are not cited in the documentation are inherited to their internal <EnhancedButton />, so you need to use a wrapper.
<IconButton
onClick={(e) => props.onRemoveClick(e)}
className="align-self-center"
color="info"
size="sm"
disabled={props.index > 0 ? false : true}
<RemoveCircleOutlineOutlined/>
</IconButton>
Well you gave an idea. Since i needed an index to identify the row's button, i sended the index through a paramater on the onClick method, like this:
onClick={e => props.onRemoveClick(props.index)}
In this way i didn't need to handle the event. I also had to bind my method on the constructor:
constructor(props) {
super(props);
this.handleRemoveClick = this.handleRemoveClick.bind(this);
}
Now i got the behaviour wanted
You can see the github ussue here. There is some problem with typescript definition files but we can work around it.
Solution
I tried to solve it like in the github issue but didn't work. So this works for me.
const onClick = (e: any) => {
// e is of type any so the compiler won't yell at you
}
<IconButton onClick={(e) => onClick(e)}>
I don't know the reason but using e.currentTarget helped me to get the button that I wanted and not the material icon inside it.
onClick={(e) => {
return console.log(e.currentTarget)
}}

Best practice for testing for data-testid in a nested component with React Testing Library?

I'm trying to write a test to check if my app is rendering correctly. On the initial page Ive added a data-testid of "start". So my top level test checks that the initial component has been rendered.
import React from "react";
import { render } from "react-testing-library";
import App from "../App";
test("App - Check the choose form is rendered", () => {
const wrapper = render(<App />);
const start = wrapper.getByTestId("start");
// console.log(start)
// start.debug();
});
If I console.log(start) the I can see all the properties of the node. However if I try and debug() then it errors saying it's not a function.
My test above does seem to work. If I change the getByTestId from start to anything else then it does error. But I'm not using the expect function so am I violating best practices?
There are two parts to this question -
Why console.log(start) works and why not start.debug()?
getByTestId returns an HTMLElement. When you use console.log(start), the HTMLElement details are logged. But an HTMLElement does not have debug function. Instead, react-testing-library provides you with a debug function when you use render to render a component. So instead of using start.debug(), you should use wrapper.debug().
Because you don't have an expect function, is it a good practice to write such tests ?
I am not sure about what could be a great answer to this, but I will tell the way I use it. There are two variants for getting an element using data-testid - getByTestId and queryByTestId. The difference is that getByTestId throws error if an element with the test id is not found whereas queryByTestId returns null in such case. This means that getByTestId in itself is an assertion for presence of element. So having another expect which checks if the element was found or not will be redundant in case you are using getByTestId. I would rather use queryByTestId if I am to assert the presence/absence of an element. Example below -
test("App - Check the "Submit" button is rendered", () => {
const { queryByTestId } = render(<App />)
expect(queryByTestId('submit')).toBeTruthy()
});
I would use getByTestId in such tests where I know that the element is present and we have expects for the element's properties (not on the element's presence/absence). Example below -
test("App - Check the "Submit" button is disabled by default", () => {
const { getByTestId } = render(<App />)
expect(getByTestId('submit')).toHaveClass('disabled')
});
In the above test, if getByTestId is not able to find the submit button, it fails by throwing an error, and does not execute the toHaveClass. Here we don't need to test for presence/absence of the element, as this test is concerned only with the "disabled" state of the button.