Submenu closes improperly in React + Material UI - reactjs

I have a nested menu structure like follows:
menuItems = [
{
"key": "key1",
"caption": "Text1"
},
{
"key": "key2",
"caption": "Text2",
"subMenuItems": [
{
"key": "key3",
"caption": "Text3",
"subMenuItems": [
{
"key": "key4",
"caption": "Text4"
}
]
},
{
"key": "key5",
"caption": "Text5",
"subMenuItems": []
},
{
"key": "key6",
"caption": "Text6"
}
]
},
{
"key": "key7",
"caption": "Text7"
}
]
And I want to open submenu items in a submenu that opens to right side of its parent. Something similar to Angular Material Menu (Nested menu example).
I tried using Material UI Menu, and it opens the menu as needed (See here), but has the following 2 issues:
If you open the parent menu and then any of the child menu, you will need to click outside as many times as the menus. Instead, it should close all the parent + child menus on clicking outside.
If you want to switch the child menu by clicking another parent menu item, you will need to first click on the parent menu (or as per issue-1, click outside) to close the currently open child menu and then click the desired parent menu item to open corresponding child menu.
Issue 1 can be addressed using ClickAwayListener as implemented here, but then it closes all menus even when trying to switch to another child menu, like switching from "Button 3" submenu to "More Items".
Thanks.

Let's build a drawer/navbar which receives a list of items objects and render then in a recursive way. First thing first, define the structure like you already did in menuItems, i'll change it a little bit:
const items = [
{name: 'Shallow Item', path='/', type:'shallow'},
{
name: 'Nested',
type: 'nested',
items:[
{name: 'Shallow again', path='/nestedpath'}
]
}
]
Now we have two items in your list, one is a plain shallow item, which does not contain nested options, the second is a nested item which contains a list of shallow items inside it.
Now we have to build or two core components: <NestedItem> and <ShallowItem>. They should look something like this:
const ShallowItem = ({item, onClick}) =>(
<ListItem
component={Link}
to={item.path || #}
onClick={onClick ? onClick : ()=>{} }
>
<ListItemText primary={item.name}/>
{Boolean(nested) &&
<Icon>
{collapsed ? 'arrow_drop_up' : 'arrow_drop_down'}
</Icon>
}
</ListItem>
)
Notice that onClick and to(react-router link) properties had their values attributed conditionally, because we're going to use the same component to render the first layer and the list of the NestedItem, like this:
const NestedItem = ({ item }) => {
const [collapsed, toggler] = useState(false)
const classes = useStyles()
return (
<div>
<ShallowItem
item={{ icon: item.icon, title: item.title }}
nested
collapsed={collapsed}
onClick={() => toggler(!collapsed)}
/>
<Collapse in={collapsed}>
<List component='div'>
{item.items.map(item => (
<ShallowItem item={item} key={item.title}/>
))}
</List>
</Collapse>
</div>
)}
There we go, did you grabbed the ideia? The only component really getting rendered is ShallowItem cause NestedItem is just a collection of shallow items. So we first render a shallow item that opens a collapse container containing a sublist of more shallow items.The only thing that we need to do now is check if the item have subitems, case positive render a NestedItem otherwise render ShallowItem. That function is:
const renderItems = items =>{
for(let item of items){
switch(item.type){
case 'nested': return <NestedItem item={item} />
case 'shallow' : return <ShallowItem item={item} />
}
}
}
Now you have a recursive way to render nested items, you only need to stylize one component, and make the depth deeper is pretty easy now. We use this solution in our company and it works like charm.

Related

Making reusable component for array data in reactjs

I want to make a footer component in react with a heading and several links under it like in the image.
Also I want to pass only an array for the links like the following.
const FooterLinksData = [
{
link1: "Insolvers",
link2: "How it works?",
link3: "I'm looking for job",
link4: "I'm looking to hire",
},
{
link1: "Features",
link2: "Pricing",
link3: "Schedule a Demo",
},
];
My code:
return (
<>
<li>
<a href="/" className="footer_link">
{props.data[`link${props.num + 1}`]}
</a>
</li>
// I have written this li tag for times in my code
// props.data is the array I passed using the parent
// props.num is the number (0 in this case) to get link1 from
// the object
</>
);
My problem is that first obj under the array has 4 items while the second one only have 3. Is there any way to make a reusable component for it?
You can add the title inside the json and use it to bracke. Something like
const FooterLinksData = [
{
"title":"some title",
"links":[
"Insolvers",
"How it works"
]
},
{
"title":"some other title",
"links":[
"other Insolvers",
"other How it works"
}]
];
so then you will do something like:
FooterLinksData.map(({title, links}) => ({
<div>
<div>{title}</div>
<ul>{links.map(link => <li>{link}</li>)}</ul>
</div>
})
})
If you ask this question, I assume you use FooterLinksData[x].link1, FooterLinksData[x].link2, FooterLinksData[x].link3 ..... to display your links.
You could change your code to use the map() function instead, not having to deal with a fixed number of links.
Can't really help with code since you didn't posted how you render your links.

Fluent UI DetailsList - Is there a way to add filters to each column

I am using Fluent UI DetailsList. My table looks like below:
I need filters below every column (text or drop-down) as shown below:
Please let me know if this is possible? Or maybe a way to display custom header (using html) ?
This actually turned out to be easier than I thought it'd be...
If you're ok with clicking the column header to reveal the choices (vs having the dropdown directly under the title) then this can be achieved using the ContextualMenu component in conjunction with DetailsList. I got it working by tweaking from the variable row height example in the official docs: https://developer.microsoft.com/en-us/fluentui#/controls/web/detailslist/variablerowheights.
Add a ContextualMenu underneath your DetailsList:
<DetailsList
items={items}
columns={columns}
/>
{this.state.contextualMenuProps && <ContextualMenu {...this.state.contextualMenuProps} />}
Inside your column definition, set the hasDropdown action so the user gets a UI indicator that they can/should click the header, and call a contextMenu method (note I'm using onColumnContextMenu as well as onColumnClick so it doesn't matter if they left or right click the header:
{
key: 'dept',
name: 'Department',
fieldName: 'dept',
minWidth: 125,
maxWidth: 200,
onColumnContextMenu: (column, ev) => {
this.onColumnContextMenu(column, ev);
},
onColumnClick: (ev, column) => {
this.onColumnContextMenu(column, ev);
},
columnActionsMode: ColumnActionsMode.hasDropdown,
}
When the onColumnContextMenu method gets invoked, we need to build the context menu properties that will get consumed by the ContextualMenu component. Note the dismissal method as well, which clears out the state so the menu is hidden.
private onContextualMenuDismissed = (): void => {
this.setState({
contextualMenuProps: undefined,
});
}
private onColumnContextMenu = (column: IColumn, ev: React.MouseEvent<HTMLElement>): void => {
if (column.columnActionsMode !== ColumnActionsMode.disabled) {
this.setState({
contextualMenuProps: this.getContextualMenuProps(ev, column),
});
}
};
Finally, inside of getContextualMenuProps you need to determine what the options should be for the user to click. In this example, I'm simply giving sort options (you'll need to add an onClick handler to actually do something when the user clicks the item), but I'll use the column to determine what those items should actually be and paint the filters into the items collection so the user can select one to filter.
private getContextualMenuProps = (ev: React.MouseEvent<HTMLElement>, column: IColumn): IContextualMenuProps => {
const items: IContextualMenuItem[] = [
{
key: 'aToZ',
name: 'A to Z',
iconProps: { iconName: 'SortUp' },
canCheck: true,
checked: column.isSorted && !column.isSortedDescending,
},
{
key: 'zToA',
name: 'Z to A',
iconProps: { iconName: 'SortDown' },
canCheck: true,
checked: column.isSorted && column.isSortedDescending,
}
];
return {
items: items,
target: ev.currentTarget as HTMLElement,
directionalHint: DirectionalHint.bottomLeftEdge,
gapSpace: 10,
isBeakVisible: true,
onDismiss: this.onContextualMenuDismissed,
}
}
Note the target on the ContextualMenuProps object, which is what tells the ContextualMenu where to lock itself onto (in this case, the column header that you clicked to instantiate the menu.
Detail list filter for each column without context menu -
https://codesandbox.io/s/rajesh-patil74-jzuiy?file=/src/DetailsList.CustomColumns.Example.tsx
For instance - Providing filter in text field associated with each column will apply filter on color column.

Scroll to element when clicking table of contents

In React, how can I scroll to a heading on the page, when the corresponding heading is clicked in my table of contents?
I parse my markdown document to generate a TOC:
const toc = [
{ id: 'lorem', title: 'Lorem' },
{ id: 'ipsum', title: 'Ipsum' },
{ id: 'dolor', title: 'Dolor' }
];
I then render this TOC in render()::
{ toc.map((entry) => {
return (
<li key={entry.id}>
{entry.title}
</li>
);
})}
When I click on an entry in my TOC, I would like the page to scroll to the h2 element with that id. How?
The examples I have seen all use ref. But I am not sure how to use ref in my case, where the TOC is created dynamically at runtime and hence the refs would need to be created dynamically? I cannot add them to my document's elements, since they are generated by markdown-to-jsx.
You need to add IDs to your Markdown headers in your Markdown files and you can add IDs with this syntax:
### Lorem {#lorem}
Then in your React code you render the TOCs with link tag linking to header IDs:
{ toc.map((entry) => {
return (
<li key={entry.id}>
<a href={`#${entry.id}`}>{entry.title}</a>
</li>
);
})}
To read more about Markdown ID syntax: https://www.markdownguide.org/extended-syntax/#heading-ids

How do I stop Office Fabric Dropdown from selecting first element on focus?

I am using the Dropdown from Office Fabric in React. I have many dropdowns that I am happy to select the first element by default, but I have some that I do not want this behavior. When I put a breakpoint in the debugger onDropdownChange() is being called when I click onto the dropdown for the first time.
I was able to trace the event to Dropdown.base.js, where I see this code, which seems to force a focus on the first element if none are selected:
_this._onFocus = function (ev) {
var _a = _this.state, isOpen = _a.isOpen, selectedIndices = _a.selectedIndices;
var multiSelect = _this.props.multiSelect;
var disabled = _this._isDisabled();
if (!disabled) {
if (!isOpen && selectedIndices.length === 0 && !multiSelect) {
// Per aria
_this._moveIndex(ev, 1, 0, -1);
}
if (_this.props.onFocus) {
_this.props.onFocus(ev);
}
_this.setState({ hasFocus: true });
}
};
Here is my dropdown rendering code:
return <Dropdown
selectedKey={selected}
placeholder={placeholder}
ariaLabel={this.props.ariaLabel || this.props.label}
label={this.props.label}
onChange={this.onDropdownChange}
options={this.getDropdownOptions()}
/>
Is there a way that I can get around this? I see others use office fabric and not have this behavior, such as VSO with their Columns Options pane (which is what I am building for my own site), but I don't see what I need to do or if they somehow custom handled it. The only idea I have so far as to put a blank entry as an option and then pre-select it. It is not only that it selects the first entry, but when I click the dropdown of the combo box for the first time it just selects the first option and doesn't pop open the dropdown.
Indeed, this is the default behavior, on focus event the first available dropdown option is getting selected. One option to override this behavior would be:
a) to introduce a default option
<Dropdown componentRef={this.dropdownRef}
styles={dropDownStyles}
onFocus={this.handleDrownDownFocus}
options={[
{ key: "All", text: "", disabled: true },
{ key: "A", text: "Option A" },
{ key: "B", text: "Option B" }
]}
/>
b) and force it to get selected on focus event
private handleDrownDownFocus(ev: React.FormEvent<HTMLDivElement>) {
const dropDown = this.dropdownRef.current;
if(dropDown && !this.selectedDefault){
dropDown.setSelectedIndex(ev,0);
this.selectedDefault = true
}
}
Here is a demo

Accessing the value of a child component redux-form

I have a form created using redux-form and have a requirement for display and filtering based on selections within the child objects of the form (being mapped using a FieldArray). The form data json structure is something like:
{
name: "my form",
selection: "1"
date: "02/02/2020",
children:
[{
childName: "child2",
childSelection: "1",
grandchildren: [{
grandchild: "3"
}]
},
{
childName: "child2",
childSelection: "1",
grandchildren: [{
grandchild: "2"
}]
}
]
}
On the actual form I have a dropdown list for the field selection, childSelection and grandchild
const mapStateToProps = (state) => {
const selector = formValueSelector(FORM_NAME);
const selection= selector(state, "selection")
...
I want to be able to filter the grandchildren dropdown whenever childSelection changes, amongst other UI requirements.
I understand that redux-form fires an onChange event, and I have written a selector like the above to capture the change of selection for the top level which works well, but when it comes to the child/grandchild - how do I know which item has changed to effect the filter (ie pass the child selection into the selector for the dropdown that drives the grandchildren list?

Resources