Fabric UI: How do I position a ContextualMenu? - javascript

I want to be able to open a ContextMenu at a specified point.
I can see from the API docs (https://dev.office.com/fabric#/components/contextualmenu) that I have to set targetPoint to an IPoint, but there is no explanation of how to actually do this.
So- how do you use IPoint to position components in Fabric UI?

(EDIT MARCH 2019: some users are reporting that target should be used instead of targetPoint in the example below. That said the example below worked for me.)
OK- this API is as far as I can see completely undocumented, but the following will generate a contextual menu around a mouse click
class Preview extends React.Component {
constructor (props) {
super(props)
this.showContextMenu = this.showContextMenu.bind(this)
this.state = { showContextMenu: false }
}
showContextMenu (e, text) {
this.setState({
showContextMenu: ((text.length > 0) ? true : false),
selectionText: text,
targetPoint: {
x: e.clientX,
y: e.clientY
}
})
}
render () {
return (
<div className='ms-font-m ms-bgColor-themeLighter' onMouseUp={(e) => this.showContextMenu(e, window.getSelection().toString())}>
<div>
{ this.state.showContextMenu && <ContextualMenu useTargetPoint="true" target={this.state.target} items={[ { key: '1', name: "boo" }, { key: '2', name: "yah" }]} /> }
</div>
<p>
This is a really interesting paragraph, etc...
</p>
</div>
)
}
}

For now (office-ui-fabric-react 6.92.0), there is no useTargetPoint and targetPoint props anymore.
We should use target which type is
Element | string | MouseEvent | IPoint | null
<ContextualMenu
target={{x: 200, y: 200}}
items={[ { key: '1', name: "boo" }, { key: '2', name: "yah" }]} />

Related

Getting content of currently active Text component wrapped inside popover of antd

I am using antd components for my react app. I have a Text component wrapped inside of Popover component. Now in my case this Popover is applied to one particular column of table, i.e. every row-element in that column has a Popover component rendered for it upon mouse hovering.
title: "Name",
dataIndex: "name",
key: "name-key",
sortType: "string",
sortDirections: ["descend", "ascend"],
sorter: (a, b) => a.name.length - b.name.length,
render: (text, record) => (
<Popover>
<Text onMouseOver={handleOnMouseOverCommitId}> {name} </Text>
</Popover>
)
I want to get hold of the row-element's value, the one contained by the above Text component whenever I hover over it. In this case the value denoted by {name} above.
I tried getting it with e.target.value via onMouseOver event, but it returned undefined.
I think I get the reason behind it, because the event.target returns an html node of type <span>.
With a normal div element e.target.value has worked in the past for me. But doing the same thing with a predefined component like antd's Text seems a bit trickier.
Just to elaborate, the Popover has two buttons and based on which button user clicks, I need to render some other components, something like an overlay component.
But in order to do that I would also need to get hold of the text value which originally triggered the Popover.
Below is the code(most of the things removed for preciseness).
record.name is what I ultimately need to capture.
<Popover
content={
<>
<Space>
<Button onClick={showSomeOverlayPaneForName}>
{"View Details for record.name"}
</Button>
<Button href={"https://abc.xyz.com/" + record.role}>
{"View Role Details"}
</Button>
</Space>
</>
}
trigger={"hover"}
>
<Text style={{"color": blue.primary}} copyable={true} onMouseOver={handleOnMouseOverName}>{record.name}</Text>
</Popover>
The handleOnMouseOverName function(which doesn't work anyway) :
const handleOnMouseOverName = (e) => {
//console.log("e.target.value :--- ", e.target.value);
setCurrentActiveName(e.target.value)
}
And once my currentActiveName variable is set(via useState), I use that value inside my function showSomeOverlayPaneForName
const showSomeOverlayPaneForName = (e) => {
axios
.get(
`some-url`,
{
params: {name: currentActiveName}
}
)
.then((response) => {
setData(response.data);
}).catch(reason => {
//xyz
});
}
You need to pass on the record of the enclosing render function to the handleOnMouseOverName function.
Check the following example
import React from 'react';
import 'antd/dist/antd.css';
import './index.css';
import { Space, Table, Button, Popover } from 'antd';
const App = () => {
const data = [
{
key: '1',
name: 'John Brown',
address: 'New York No. 1 Lake Park',
role: 'admin',
},
{
key: '2',
name: 'Jim Green',
address: 'London No. 1 Lake Park',
role: 'user',
},
{
key: '3',
name: 'Joe Black',
address: 'Sidney No. 1 Lake Park',
role: 'manager',
},
];
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (name, record) => {
const content = (
<>
<Space>
<Button
onClick={() => {
viewDetail(record);
}}
>
{'View Details for ' + record.name}
</Button>
<Button href={'https://abc.xyz.com/' + record.role}>
{'View Role Details'}
</Button>
</Space>
</>
);
return (
<>
<Popover content={content} title="Details">
<div
onMouseOver={() => {
handleOnMouseOverName(record);
}}
>
{name}
</div>
</Popover>
</>
);
},
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
},
];
const handleOnMouseOverName = (record) => {
console.log(record);
};
const viewDetail = (record) => {
console.log(record);
};
return <Table columns={columns} dataSource={data} />;
};
export default App;
Output:
I hope this helps.
From antd docs: https://ant.design/components/popover/#header
Apparently you're supposed to render the <Popover/> with a content={content}-prop
For example
const content = <div>Content to render under title</div>
const App = () => {
const value = "Text to hover";
return (
<Popover content={content} title="Title">
<Text>{value}</Text>
</Popover>
)
}

useRef([]) with dynamically created elements

I created a component with several div elements.
By adding a ?goto= parameter to the url I want to scroll the the relevant element. I now solved that with const itemsRef = useRef([]);.
My main concern now is if that's the right and performance efficient approach with itemsRef.current[element.id] = el. element.id will be unique for each element.
I also found packages such as: https://github.com/Macil/react-multi-ref
But I don't see the disadvantage of my approach yet.
Here you can find my current solution in action: https://codesandbox.io/s/scrolltoref-w5i7m?file=/src/Element.js
import React, { useRef, useEffect, useState } from "react";
import clsx from "clsx";
const blueprint = [
{
id: "3mD59WO",
name: "AUDITORIUM",
position: 0,
rooms: [
{
id: "zR8Qgpj",
name: "Audimax",
subtitle: null,
details: null,
position: 0,
elements: [
{
id: "1jLv04W",
position: 0,
type: "daily",
element: "listing_large",
properties: {
meetingId: null,
capacity: 6
}
},
{
id: "1jLv12W",
position: 1,
type: "daily",
element: "listing_large",
properties: {
meetingId: null,
capacity: 6
}
}
]
}
]
},
{
id: "4mDd9WO",
name: "FOYER",
position: 1,
rooms: [
{
id: "4R8Qgpj",
name: "Speakers Table",
subtitle: null,
details: null,
position: 0,
elements: [
{
id: "2jLv04W",
position: 0,
type: "daily",
element: "listing_large",
properties: {
meetingId: null,
capacity: 6
}
},
{
id: "2jLv12W",
position: 1,
type: "daily",
element: "listing_large",
properties: {
meetingId: null,
capacity: 6
}
}
]
}
]
}
];
export default function Query() {
const itemsRef = useRef([]);
const [currentRef, setCurrentRef] = useState();
useEffect(() => {
const scrollToRef = ref => {
window.scrollTo(0, ref.offsetTop);
};
const goto = "1jLv12W"; // This will become an URL parameter ?goto=:someID in the final version
const ref = itemsRef.current[goto];
setCurrentRef(ref); // This is needed to change the className to highlight
scrollToRef(ref); // Here I assign the ref and the component should scroll to that ref
}, []);
return (
<div key="element">
{blueprint.map(floor => (
<div key={floor.id} style={{ marginTop: 50 }}>
Floor: {floor.name} <br />
<br />
{floor.rooms.map(room => (
<div key={room.id}>
Room Name: {room.name}
<br />
{room.elements.map(element => (
<div
ref={el => (itemsRef.current[element.id] = el)}
className={clsx({
highlight:
currentRef && currentRef === itemsRef.current[element.id]
})}
key={element.id}
style={{ backgroundColor: "green", marginTop: 100 }}
>
ElementID: {element.id}
<br />
</div>
))}
</div>
))}
</div>
))}
</div>
);
}
That's the right approach, usually, you will see useRef([]) when handling multiple animations in a page, and that's exactly how it's done itemsRef.current[element.id] = el.
My main concern now is if that's the right and performance efficient approach
That's directly related to "Why Premature Optimization Is the Root of All Evil".
Premature optimization is spending a lot of time on something that you may not actually need.
You trying to optimize before you have any performance issues. Focus on delivering the product and clean code, find a time for optimization when you actually measured it.
We also don’t want to waste an enormous amount of time doing performance optimization on things that don’t matter. Many development teams get caught up in focusing on optimizing for performance and scale before they have validated their new product functionality.
useRef is basically the same as doing useState({ current: <value you pass in> });
Given your use case, what you have done is sufficient however I would change use ref to initialise with an object since that is what you are actually using as oppose to an array:
const itemsRef = useRef({});
Your code still works but may potentially give you some unexpected behaviour since assigning properties to an array can be a bit weird and is definitely not what you intend to do anyway.
For example with an array you are actually doing this:
[]["<some id>"] = el; // very weird!
vs an object:
{}["<some id>"] = el

Change node colour onclick react-d3-graph

I have created a very basic network graph with nodes and links between the nodes using the react d3-graph module. How could I possibly allow my users to change the colour of a node by double clicking on it?
Here are the docs I am following for the library I am using:
https://goodguydaniel.com/react-d3-graph/docs/
Note: I also have an empty on click function in my code which receives the id of the node the user clicks on.
Here is my code:
class App extends Component {
constructor(props) {
super(props)
this.state = {
data: {
nodes: [
{id: 'Harry'},
{id: 'Saly'},
{id: 'Aly'}
],
links: [
{source: 'Harry', target: 'Aly'},
{source: 'Harry', target: 'Saly'},
]
},
myConfig: {
nodeHighlightBehavior: true,
node: {
color: 'lightgreen',
size: 120,
highlightStrokeColor: 'blue'
},
link: {
highlightColor: 'lightblue'
}
}
}
}
render() {
return (
<div className="App">
<Graph
id='graph-id' // id is mandatory, if no id is defined rd3g will throw an error
data={this.state.data}
config={this.state.myConfig}
onClickGraph={onClickGraph}
onClickNode={onClickNode}
onDoubleClickNode={onDoubleClickNode}
onRightClickNode={onRightClickNode}
onClickLink={onClickLink}
onRightClickLink={onRightClickLink}
onMouseOverNode={onMouseOverNode}
onMouseOutNode={onMouseOutNode}
onMouseOverLink={onMouseOverLink}
onMouseOutLink={onMouseOutLink}
/>
</div>
);
}
}
According to the documentation, we can pass color as an attribute. All we need to do now is utilize this inside double click handler.
I believe this example will be useful.

React - changing the background of a single span class not working

I am new to React so my apologies if the question, or the thing I am trying to achieve is just weird (and please do tell if there is a better / more logic way to do this).
I am using the List Fabric React component in my React application, which is based on the ListGridExample component which is found here:
https://developer.microsoft.com/en-us/fabric#/components/list
I have set it up but I can't seem to accomplish the following:
When a span class (which is actually an item) in the List component is clicked, I want to change it's background color, to do this I have followed the instructions in the following post:
https://forum.freecodecamp.org/t/react-js-i-need-a-button-color-to-change-onclick-but-cannot-determine-how-to-properly-set-and-change-state-for-that-component/45168
This is a fairly simple example but this changes all my grid cells / span classes to the color blue instead of only the clicked one. Is there a way I can make just the clicked span class change it's background?
The Initial state:
The state after clicking one span class (which is wrong):
Implementation code (ommitted some unecesary code):
class UrenBoekenGrid extends React.Component {
constructor(props) {
super(props);
this.state = {
bgColor: 'red'
}
}
render() {
return (
<FocusZone>
<List
items={[
{
key: '#test1',
name: 'test1',
},
{
name: 'test2',
key: '#test2',
},
{
name: 'test3',
key: '#test3',
},
{
name: 'test4',
key: '#test4',
},
..... up to 32 items
]}
onRenderCell={this._onRenderCell}
/>
</FocusZone>
);
}
changeColor(item){
this.setState({bgColor: 'blue'});
console.log('clicked item == ' + item.name)
}
_onRenderCell = (item, index) => {
return (
<div
className="ms-ListGridExample-tile"
data-is-focusable={true}
style={{
width: 100 / this._columnCount + '%',
height: this._rowHeight * 1.5,
float: 'left'
}}
>
<div className="ms-ListGridExample-sizer">
<div className="msListGridExample-padder">
{/* The span class with the click event: */}
<span className="ms-ListGridExample-label" onClick={this.changeColor.bind(this, item)} style={{backgroundColor:this.state.bgColor}}>{`item ${index}`}</span>
<span className="urenboeken-bottom"></span>
</div>
</div>
</div>
);
};
}
I now have attached the click event to the span class itself but I would think it is way more logic to have the click event on the item(s) (array) itself, however I could not find a way to achieve this either.
----UPDATE----
#peetya answer seems the way to go since #Mario Santini answer just updates a single cell, if another cell is clicked then the previous one returns back to normal and loses it's color.
So what I did is adding the items array to the state and adding the bgColor property to them:
this.state = {
items: [
{
key: '#test1',
name: 'test1',
bgColor: 'blue',
},
{
name: 'test2',
key: '#test2',
bgColor: 'blue',
},
{
name: 'test3',
key: '#test3',
bgColor: 'blue',
},
{
name: 'test4',
key: '#test4',
bgColor: 'blue',
},
],
}
Now in my List rendering I have set the items to the state items array and added the onClick event in the _onRenderCell function:
render() {
return (
<FocusZone>
<List
items={this.state.items}
getItemCountForPage={this._getItemCountForPage}
getPageHeight={this._getPageHeight}
renderedWindowsAhead={4}
onRenderCell={this._onRenderCell}
/>
</FocusZone>
);
}
_onRenderCell = (item, index) => {
return (
<div
className="ms-ListGridExample-tile"
data-is-focusable={true}
style={{
width: 100 / this._columnCount + '%',
height: this._rowHeight * 1.5,
float: 'left'
}}
>
<div className="ms-ListGridExample-sizer">
<div className="msListGridExample-padder">
<span className="ms-ListGridExample-label"
onClick={this.onClick(item.name)}
style={{backgroundColor: item.bgColor}}
>
{`item ${index}`}
</span>
<span className="urenboeken-bottom"></span>
</div>
</div>
</div>
);
};
The problem is that I can't add the onClick event in the _onRenderCell function as this will give the following error:
I want to keep the Fabric List component as it also has functions for rendering / adjusting to screen size, removing the list component entirely and just replacing it with what #peetya suggested works:
render() {
<div>
{this.state.items.map(item => (
<div onClick={() => this.onClick(item.name)} style={{backgroundColor: item.bgColor}}>
{item.name}
</div>
))}
</div>
}
But this will also remove the List component functionality with it's responsive functions.
So my last idea was to just replace the items of the List with the entire onClick div and removing the _onRenderCell function itself, but this makes the page blank (can't see the cells at all anymore..):
render() {
return (
<FocusZone>
<List
items={this.state.items.map(item => (
<div onClick={() => this.onClick(item.name)} style={{backgroundColor: item.bgColor}}>
{item.name}
</div>
))}
getItemCountForPage={this._getItemCountForPage}
getPageHeight={this._getPageHeight}
renderedWindowsAhead={4}
// onRenderCell={this._onRenderCell}
/>
</FocusZone>
);
}
I thought that perhaps the css ms-classes / div's should be in there as well because these have the height/width properties but adding them (exactly as in the _onRenderCell function) does not make any difference, the page is still blank.
The problem is that you are storing the background color in the state of the Grid and assign this state to every element of the grid, so if you update the state, it will affect every element. The best would be if you create a separate component for the Grid elements and store their own state inside there or if you want to use only one state then store the items array inside the state and add a new bgColor attribute for them so if you want to change the background color only for one item, you need to call the setEstate for the specific object of the items array.
Here is a small example (I did not tested it):
class UrenBoekenGrid extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [
{
key: '#test1',
name: 'test1',
bgColor: 'blue',
},
],
};
}
onClick(name) {
this.setState(prevState => ({
items: prevState.items.map(item => {
if (item.name === name) {
item.bgColor = 'red';
}
return item;
})
}))
}
render() {
<div>
{this.state.items.map(item => (
<div onClick={() => this.onClick(item.name)} style={{backgroundColor: item.bgColor}}>
{item.name}
</div>
))}
</div>
}
}
Actually you are changing the color of all the span elements, as you set for each span the style to the state variable bgColor.
Insteas, you should save the clicked item, and decide the color based on that:
this.state = {
bgColor: 'red',
clickedColor: 'blue
}
In the constructor.
Then in the click handler:
changeColor(item){
this.setState({selected: item.name});
console.log('clicked item == ' + item.name)
}
So in the renderer (I just put the relevant part):
<span ... style={{backgroundColor: (item.name === this.state.selected ? this.state.clickedColor : this.state.bgColor)}}>{`item ${index}`}</span>

React-table events

I'm currently using React-Table and it's working great.
I want to have a dynamic pageSize according to the data filtered in the table.
<ReactTable
...
filtered={[{
"id": "stage",
"value": 1
}]}
getTdProps={(state, rowInfo, column, instance) => {
return {
onClick: () =>
this.setState({
preliminary:state.sortedData.length
})
};
}}
pageSize={this.state.preliminary}
/>
And my state in the constructor
this.state = {
preliminary: 10
};
This works great when clicking because of the onClick() event. I want it to fire onLoad() of the page.
Changing onClick() to onLoad() doesn't do anything.
Any help appreciated!
Adding an onLoad event to the getTdProps() method doesn't make much sense because td elements don't have an onload event.
Sounds like you'll want to make use of onFilteredChange to update this.state.preliminary to a new value, which will update the pageSize prop of the <ReactTable /> component.
Here's a simplified example. The table starts with a pageSize of 5. Inserting a filter of any kind into either column will change the pageSize to 10. This uses the onFilteredChanged prop. I imagine you would want to include some logic in the handleFilterChange function to set the pageSize to an appropriate value however.
class MyTable extends React.Component {
constructor(props){
super(props)
this.state = {
pageSize: 5,
filter: false
}
}
handleFilterChange = (column, value) => {
this.setState({
pageSize: 10
})
}
render() {
const data = [{
name: 'Tanner Linsley',
age: 26
},
{
name: 'Brett DeWoody',
age: 38
},
{
name: 'Santa Clause',
age: 564
}]
const columns = [{
Header: 'Name',
accessor: 'name' // String-based value accessors!
}, {
Header: 'Age',
accessor: 'age',
Cell: props => <span className = 'number'> {
props.value
} </span>
}]
return (
<div>
<ReactTable.default
data={data}
columns={columns}
filterable={true}
onFilteredChange={this.handleFilterChange}
pageSize={this.state.pageSize} />
</div>
)
}
}
ReactDOM.render(<MyTable /> , document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-table/6.7.5/react-table.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/react-table/6.7.5/react-table.css" rel="stylesheet" />
<div id="app"></div>
Following #Brett DeWoody advice, the solution was to find the length of the filtered data
For that, I used lodash
pageSize={_.filter(data.items, { 'stage': 1, 'status': 1 }).length}
Thanks!

Categories

Resources