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.
Related
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.
I am looking for something like this to implement in ReactJS/Material-UI. Any existing component or library for this?
You can do that with the Autocomplete component by overriding the ListboxProps of the Listbox which is the container of the dropdown and set the CSS grid to display 3 equal width columns using the fr unit:
<Autocomplete
ListboxProps={{
style: {
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
maxHeight: "initial",
}
}}
Now assuming the option has the following type:
type TagOption = {
tag: string;
views: number;
description: string;
};
In the Stackoverflow dropdown, there are only 6 options at max, so we need to restrict the number of options shown in the Listbox:
import Autocomplete, { createFilterOptions } from "#mui/material/Autocomplete";
const _filterOptions = createFilterOptions<TagOption>();
const filterOption = (props, state) => {
const results = _filterOptions(props, state);
return results.slice(0, 6);
};
<Autocomplete filterOptions={filterOption}
We also want to create a customized option component for each grid item. Here is a minimal example without any much styles to get you started:
function OptionItem({ option, ...other }) {
return (
<li
{...other}
style={{
display: "block"
}}
>
<div>
<Chip label={option.tag} />
{option.views}
</div>
<div>{option.description}</div>
</li>
);
}
renderOption={(props, option, { selected }) => (
<OptionItem {...props} option={option} />
)}
And finally set the isOptionEqualToValue and getOptionLabel to ensure that the option get filtered properly and the input display the correct tag when selected:
isOptionEqualToValue={(option, value) => option.tag === value.tag}
getOptionLabel={(option) => option.tag}
Live Demo (Source)
That's not a perfect fit, but you can use autocomplete (using multiple, customizable and asynchronous)
I have built a custom tree view in React, and each item contains a dropdown which is positioned using Popper. Since the child elements are not visible on render, Popper is not positioning the dropdown correctly, for example:
When the tree is open on mount (i.e the children are visible), the positioning is correct:
Each level in the tree is rendered via a CategoryNavItem component, which essentially looks like this:
<div className={ className.join(' ') }>
<div className={ `collection-nav_item-link depth${depth}` } style={{ paddingLeft: `${paddingLeft}px`}}>
<Link to={ linkTo } onClick={() => { setIsOpen(!isOpen) }}>
<i className="collection-nav_item-link_icon"></i>
<span className="collection-nav_item-link_text">{ category.name }</span>
</Link>
<Dropdown
toggleClassName="btn-icon-white btn-sm"
toggleContent={ <Icon name="ellipsis-h" />}
position="bottom-end"
size="small"
items={[
{ text: 'Edit category' },
{ text: 'Add subcategory', onClick: (() => { dispatch(openAddSubcategory(category)) }) }
]} />
</div>
{ children }
</div>
The Dropdown component is where we use Popper, and it works well everywhere else. The visibility of a CategoryNavItem is handled via the component's state in React.
Is there any way to trigger Popper's update() method programmatically in React? We should force update when toggling the item's visibility.
It turns out we just need to expose the update property from the usePopper hook, and then call it when setting the dropdown's visibility, for example:
const { styles, attributes, update } = usePopper(referenceElement, popperElement, {
placement: placement,
modifiers: [
{ name: 'arrow', options: { element: arrowElement } },
{ name: 'offset', options: { offset: [ 0, 3 ] } }
]
});
And similarly:
const toggleDropdown = (e) => {
e.preventDefault();
e.stopPropagation();
setVisible(!visible);
update();
};
According to the docs, you can manually update the propper instance so that it recomputes the tooltip position:
Manual update
You can ask Popper to recompute your tooltip's position by running instance.update().
This method will return a promise, that will be resolved with the updated State, from where you will optionally be able to read the updated positions.
const state = await popperInstance.update();
When clicking on your item visibility toggle, you could add your popper manual update, like the line of code above.
Here is the reference.
I have a list of 3 status that should be shown as the default value but I need to remove one of those options from the dropdown. I was able to disable it using the isOptionDisabled prop but my goal is to remove.
Right now I have an object with the option
export const userStatus = [
{ label: 'Active', value: 'ACTIVE' },
{ label: 'Blocked', value: 'BLOCKED' },
{ label: 'Pending', value: 'ACTIVATION_PENDING', isDisabled: true },
];
I want to remove the pending from the dropdown but show as default value if it is the default value.
My select component looks like this
<Select
name={name}
fullWidth={fullWidth}
components={{ DropdownIndicator }}
isSearchable={false}
value={selectValue}
options={options}
classNamePrefix="styled-select"
variant={variant}
isDisabled={disabled}
hasError={hasError}
onChange={onSelectChange}
isOptionDisabled={isOptionDisabled}
/>
Can you filter the options before you send it as a prop to React-Select?
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.