This is my old implementation of the Tabs component in Ant Design.
const tabList = [
{
key: "tab1",
label: "Tab1",
children: <Tab1 />,
},
{
key: "tab2",
label: "Tab2",
children: <Tab2 />,
},
];
<Tabs onChange={onTabChange} activeKey={selectedTab}>
{tabList.map((tab) => {
const { key, label, children } = tab;
return (
<Tabs.TabPane
key={key}
tab={label}
style={{ margin: "1.5rem auto 1.5rem" }}
>
{children}
</Tabs.TabPane>
);
})}
</Tabs>;
In the new version ( > 4.23.0 ) the boilerplate got reduced.
I can simply pass my tabList to my Tabs as a prop items.
The new code looks something like this.
<Tabs items={tabList} />
But I had an issue with styling.
I am adding top and bottom margins to all of my TabPane components.
To get that margin in the new implementation. I had to do something like this.
{
key: "tab1",
label: "Tab1",
children: <Tab1 style={{margin: "1.5rem 0 1.5rem"}} />,
},
Here I am facing two issues.
I need to add this for all my tabs in the tabList
I need to have a div in every component spreading the props that are passed above.
function Tab1(props) {
return <div {...props}>JSX for original Tab1</div>;
}
Is there a better way to do this?
Higher order component can solve this issue
Use a higher Order component like <TabPaneWrapper> or <TabChildrenWrapper>. This component does nothing but to wrap your children (<TabPane>) with a div and give the styles you require.
Component:
export function TabPaneWrapper({
children,
...props
}){
return (
<div style={{ margin: "1.5rem auto 1.5rem" }} {...props}>
{children}
</div>
);
}
Usage:
const tabList = [
{
key: "tab1",
label: "Tab1",
children: <TabPaneWrapper> <Tab1 /> </TabPaneWrapper>,
},
{
key: "tab2",
label: "Tab2",
children: <TabPaneWrapper> <Tab2 /> </TabPaneWrapper>,
},
];
If you have more tabs or use this tabs component in multiple places. You will find TabPaneWrapper to be repetitive. In such case,
You can create a custom Tabs component like <CustomTabs/> which takes the tabList mentioned in the question.
Instead of wrapping every tab in the tabList with <TabPaneWrapper/>. You can loop this list inside the <CustomTabs/> component to generate the tabList mentioned above and then pass it to the Ant Desing <Tabs/> component.
Related
I have a requirement to add tooltip on hover to disabled options in a dropdown in React fluent UI.
I am able to add tooltip to singular component using https://www.npmjs.com/package/#fluentui/react-tooltip
<Tooltipcontent="Example tooltip">
<Button/>
</Tooltip>
but how to add similar behaviour to dropdown options and only for disabled options
like: "Disabled cause of non avilability"
sample dropdown fluent ui code
const options: IDropdownOption[] = [
{ key: 'fruitsHeader', text: 'Fruits', itemType: DropdownMenuItemType.Header },
{ key: 'apple', text: 'Apple' },
{ key: 'banana', text: 'Banana' },
{ key: 'orange', text: 'Orange', disabled: true },
];
export const DropdownBasicExample: React.FunctionComponent = () => {
return (
<Stack tokens={stackTokens}>
<Dropdown
placeholder="Select an option"
label="Basic uncontrolled example"
options={options}
styles={dropdownStyles}
/>
</Stack>
);
};
Thanks
Fluent UI renders every disabled option as a button element with the disabled attribute, which makes it non-interactive by default.
Here's a method to solve this that I believe is also fairly accessible:
First, define your array of IDropdownOption items so that they conditionally set the disabled and title properties:
const options: IDropdownOption[] = [
{ key: 'apple', text: 'Apple' },
{ key: 'orange',
text: 'Orange',
disabled: isOrangeDisabled,
title: isOrangeDisabled ? "This is a disabled tooltip" : ""
},
];
You're also going to need to define a custom rendering function for the items in order to apply a nice tooltip:
<Dropdown onRenderOption={onRenderOption} />
and then define a function like this in your component:
const onRenderOption = (option: IDropdownOption): JSX.Element => {
return (
<>
{option?.disabled && (
<div className="interactive">
<TooltipHost content={option.title}>
<span>{option.text}</span>
</TooltipHost>
</div>
)}
{!option?.disabled && (
<span>{option.text}</span>
)}
</>
);
};
Finally, that interactive CSS class needs to be defined in your CSS file. This will override the browser's default behaviour of making disabled elements non-interactive:
.interactive {
pointer-events: auto;
}
Some things to note:
The reason the title is set to an empty string when the option is not disabled is so that it doesn't have a string value when the interactive item is rendered. Without this, the browser will render the tooltip when you hover on a selectable item, which looks ugly.
Using the title attribute should make the component pretty usable for screen readers and other assistive technology (though I am far from an expert)
The template only renders the TooltipHost and interactive class when the object is disabled, so that the tooltip and that behaviour only kick in when the option is disabled. Because the underlying option is still disabled, you still won't be able to select it.
I'm looking at the TransitionGroup documentation example, and I'm trying to update it to run the CSSTransition whenever the text value is updated for an existing item.
I have set up a codesandbox to show my current attempt.
const [items, setItems] = useState([
{ id: 1, text: 'Hello world!' },
{ id: 2, text: 'How are you?' },
]);
return (
<Container style={{ marginTop: '2rem' }}>
<ListGroup style={{ marginBottom: '1rem' }}>
<TransitionGroup className="todo-list">
{items.map(({ id, text }) => (
<CSSTransition
key={id}
timeout={500}
classNames="item">
<ListGroup.Item>{text}</ListGroup.Item>
</CSSTransition>
))}
</TransitionGroup>
</ListGroup>
<Button
onClick={() => {
setItems((items) => [
{ id: 1, text: 'Hello to you!' },
{ id: 2, text: 'How do you do?' },
]);
}}>
Update Items
</Button>
</Container>
);
When I click the button, the text content updates without the CSSTransition. I know this is because of the id not changing, and this is what key is set to, but how do I have TransitionGroup take into account the text value?
How do I keep the same id but update the text value with a transition?
Since key={id}, the key won't change when the text is updated, and the CSSTransition won't trigger because it doesn't get rerendered.
We can change the key to include the id and the text, so the component will always get rerendered when the text changes. We also want to add exit={false} to prevent us from seeing extra components while the new components are transitioning in. Here's what that should look like:
<CSSTransition
key={`${id}${text}`}
timeout={500}
classNames="item"
exit={false}
>
Here's a codesandbox example of this solution.
I'm converting from MUI 4 to 5. Working on converting from makeStyles() to styled components. Is there an example somewhere using the styled() method with a component that has child component props? For example, <ListItemText /> has primaryTypographyProps and secondaryTypographyProps, both of which I'm setting the inner className property for custom styles.
How does something like this...
<ListItemText
{...props}
primaryTypographyProps={{
variant: 'body2',
className: classes.primary,
}}
secondaryTypographyProps={{
variant: 'body1',
className: classes.secondary,
}}
/>
...convert to something like this?
const StyledListItemText = styled(ListItemText)(({ theme }) => ({
...???...
}));
[Edit] This is the closest I've been able to find, but it's not quite there. What I'm trying to do is pass it through a props object, rather than whole components.
I haven't had a chance to verify if this works, but I'm assuming this is the direction I need to go with this:
const StyledListItemText = styled(ListItemText)(() => ({
'MuiListItemText-primary': { ... },
'MuiListItemText-secondary': { ... },
}));
You are very close!
import listItemTextClasses from '#mui/material';
...
const StyledListItemText = styled(ListItemText)(() => ({
[`& .${listItemTextClasses.primary}`]: {...}
[`& .${listItemTextClasses.secondary}`]: {...}
}));
https://mui.com/base/getting-started/customization/#applying-custom-css-rules
react-rte is a Rich Text Editor based on draft-js. My goal is to customize the toolbar components with, e.g., material ui react components. Reading through the react-rte docs, I think that there are two styling hooks:
toolbarConfig for CSS (link); and
customControls for completely overriding components (as seen in demo).
I believe that my use case calls for customControls, but from the provided demo (see below) I am not able to understand how to hook the custom components back into rte's functionality. For example, if I render a custom button component for BOLD, how does this button get the default functionality that would have gone to the default button provided by toolbarConfig?
editor demo with customControls:
<RichTextEditor
value={value}
onChange={this._onChange}
className="react-rte-demo"
placeholder="Tell a story"
toolbarClassName="demo-toolbar"
editorClassName="demo-editor"
readOnly={this.state.readOnly}
customControls={[
// eslint-disable-next-line no-unused-vars
(setValue, getValue, editorState) => {
let choices = new Map([
['1', {label: '1'}],
['2', {label: '2'}],
['3', {label: '3'}],
]);
return (
<ButtonGroup key={1}>
<Dropdown
choices={choices}
selectedKey={getValue('my-control-name')}
onChange={(value) => setValue('my-control-name', value)}
/>
</ButtonGroup>
);
},
<ButtonGroup key={2}>
<IconButton
label="Remove Link"
iconName="remove-link"
focusOnClick={false}
onClick={() => console.log('You pressed a button')}
/>
</ButtonGroup>,
]}
/>
my currently invalid implementation:
<RichTextEditor
value={this.state.value}
onChange={this.onChange}
customControls={rteCustomControls}
/>
...
const inlineStyleButtonControls = [
{ label: "format_bold", style: "BOLD", component: FormatBoldIcon },
{ label: "format_italic", style: "ITALIC", component: FormatItalicIcon },
{
label: "format_underlined",
style: "UNDERLINE",
component: FormatUnderlinedIcon,
},
];
const rteCustomControls = [
(setValue, getValue, editorState) => {
return inlineStyleButtonControls.map((button, i) => (
<IconButton
key={i}
color="inherit"
aria-label={button.label}
selectedKey={getValue(button.style)}
onClick={value => setValue(button.style, value)}
>
<button.component />
</IconButton>
));
},
];
If your goal is purely to modify the display, you should be able to target the buttons via CSS to change their displays, just like you would target any other DOM element.
Change element display via CSS.
RichTextEditor button:nth-child(1){
background-image: url('/icon1.svg');
}
RichTextEditor button:nth-child(2){
background-image: url('/icon2.svg');
}
Or, change element display via javascript
Array.from(document.querySelectorsAll('RichTextEditor button')).forEach((el)=>{
// Modify element here
})
If you also want to modify the button's functionality, you can look at the code where they are defined, and potentially add them in as custom controls.
I have the following component, Layout:
const Layout = ({ children, data, ...otherProps }) => (
<ErrorBoundary>
<App render={({ isSidebarOpen, scrollTop, toggleSidebar }) => (
<React.Fragment>
<Helmet
title={get(data, 'site.siteMetadata.title')}
meta={[
{ name: 'description', content: get(data, 'site.siteMetadata.description') },
{ name: 'pinterest', content: 'nopin' },
{ name: 'og:title', content: 'Daniel Spajic' },
{ name: 'og:description', content: get(data, 'site.siteMetadata.description') },
{ name: 'og:type', content: 'website' },
{ name: 'og:url', content: get(data, 'site.siteMetadata.siteUrl') },
{ name: 'og:image', content: ogImage },
{ name: 'og:locale', content: 'en_AU' },
]}
>
<link rel="shortcut icon" type="image/png" href={favicon} />
<link href="https://afeld.github.io/emoji-css/emoji.css" rel="stylesheet" />
</Helmet>
<div id={PAGE_CONTENT_CONTAINER_ID}>
<Sidebar isOpen={isSidebarOpen} toggle={toggleSidebar} />
<div id={PAGE_CONTENT_ID}>
{children({ scrollTop, toggleSidebar, ...otherProps })}
</div>
</div>
</React.Fragment>
)}
/>
</ErrorBoundary>
);
As shown it renders an App with a render prop. The isSidebarOpen and scrollTop arguments for the prop are both from App's state. toggleSidebar is one of App's methods.
I want to test a few things:
The rendered Sidebar sets its toggle prop to toggleSidebar, and isOpen prop to isSidebarOpen
The children function is called with an object containing scrollTop, toggleSidebar, and otherProps as its argument
These involve retrieving App's state and methods for comparison. I've tried accessing its state with Enzyme, but it's not possible because state() can only be accessed on the root node:
ShallowWrapper::state() can only be called on the root
Therefore how can I access App's state and methods so I can test these things?
ShallowWrapper::state() can only be called on the root may not be a problem because tested values should be preferably hard-coded in unit tests. It's better to make a test unintentionally fail where it should pass than to make it unintentionally pass where it should fail, the former is much easier to debug and fix.
Though it may be beneficial to get component state, at least for assertions.
const layoutWrapper = mount(<Layout/>);
const appWrapper = layoutWrapper.find(App).dive();
expect(appWrapper.state('isSidebarOpen')).toBe(false);
expect(appWrapper.first(Sidebar).props('isOpen').toBe(false);
appWrapper.setState({ isSidebarOpen: true });
expect(appWrapper.state('isSidebarOpen')).toBe(true);
expect(appWrapper.first(Sidebar).props('isOpen').toBe(true);
There's a lot of moving parts in this component, this is also suggested by that it should be tested with mount and not shallow. it may be beneficial to provide fine-grained isolated tests, i.e. test render prop separately:
const layoutWrapper = mount(<Layout/>);
const appWrapper = layoutWrapper.first(App);
const Child = appWrapper.prop('render');
const childWrapper = shallow(<Child isSidebarOpen={false} ... />);
expect(childWrapper.find(Sidebar).props('isOpen').toBe(false);
...
And how App state interacts with render prop should be tested in dedicated App component test.