How to show only one topbar at a time? - reactjs

I'm using a package called suneditor as a rich text editor. I'm only showing the toolbar on focus and hide in on blur. This works (more or less), but the problem is that I have several editors, so when I click from one editor to the other, the topbar doesn't disappear properly. I need to click in each one of them then click outside again to hide it. What am I doing wrong and how can I prevent this behaviour?
Here's how my code looks like
const [hideToolbar, setHideToolbar] = React.useState<boolean>(true)
const handleOnBlur = () => {
setHideToolbar(true)
}
return (
<StyleOverrides transparent={transparent} disable={disable}>
<SunEditor
autoFocus = {autoFocus}
height = '100%'
width = '100%'
resizeEnable = 'false'
onChange = {onChange}
onSave = {onSave}
onBlur = {handleOnBlur}
readOnly = {readOnly}
disable = {disable}
setContents = {value}
defaultValue = {defaultValue}
hideToolbar = {hideToolbar}
onImageUploadBefore = {onImageUploadBefore}
onImageUpload = {onImageUpload}
onFocus = {(event) => {
setHideToolbar(false)
}}
{...props}
/>
</StyleOverrides>
)
}

Related

useState not behaving as expected

I have a problem with useState. editorDataOpen is updating correctly when set from openEditorData , but not when set from closeEditorData. The line console.log('Entering Profile > closeEditorData()') is reached without a problem.
The output I see in the console log is:
Render
Render
false (editorDataOpen set for the first time)
Render
Render
Render
Render
true (editorDataOpen set to true by a click, which opens Editor)
Entering Profile > closeEditorData() (triggered by a different click to close Editor, should set editorDataOpen to false, but doesn't)
Render
Render
and that's it, it never prints a last false, which would mean editorDataOpen is never set?
I've spent too much time on this today and I just cannot see where the bug is. Can someone help please? This is the code in question:
import React from 'react'
import {withTheme} from 'styled-components'
import {withContext} from '../../context/ContextHOC'
import withLoadingScreen from '../hocs/LoadingScreenHOC'
import {getEditorWidth} from '../../functions/funcEditor'
import {saveImagePlus} from '../../functions/funcDataSaver'
import Submodule from '../ui/Submodule'
import ImageCapsule from '../ui/ImageCapsule'
import EditorImage from '../editors/EditorImage'
import EditorProfileData from '../editors/EditorProfileData'
import Spacer from '../ui/Spacer'
import Table from '../ui/Table'
import ContainerGrid from '../ui/ContainerGrid'
import * as ops from '../../functions/funcStringMath'
import * as parse from '../../functions/funcDataParser'
const Profile = (props) => {
const s = props.theme.sizes
const c = props.theme.cards
const {setLoadingOn, setLoadingOff} = props
const [image, setImage] = React.useState(props.context.current.modules.me.profile.image)
const [editorImageOpen, setEditorImageOpen] = React.useState(false)
const [editorDataOpen, setEditorDataOpen] = React.useState(false)
const openEditorImage = () => setEditorImageOpen(true)
const openEditorData = () => setEditorDataOpen(true)
const closeEditorImage = () => {
setEditorImageOpen(false)
setLoadingOff()
}
const closeEditorData = () => {
console.log('Entering Profile > closeEditorData()')
setEditorDataOpen(false)
setLoadingOff()
}
React.useEffect(() => console.log(editorDataOpen), [editorDataOpen])
const updateAfterSavingImage = (img) => {
setImage({
url: img.url,
scale: img.scale,
position: img.position
})
closeEditorImage()
}
const handleImageChanged = (img) => {
if (img != undefined){
setLoadingOn()
const data = {
companyId: props.context.current.company.id,
userId: props.context.current.user.id,
routeFile: props.context.routes.meProfileImage,
routeData: props.context.routes.meProfileImageData,
}
saveImagePlus(img, data, updateAfterSavingImage)
}
else {
console.log('Error: Image received is undefined, cannot save.')
closeEditorImage()
}
}
const spacer =
<Spacer
width = '100%'
height = {s.spacing.default}
/>
const unparsedData = props.context.current.modules.me.profile.data
const parsedData = parse.profileData(props.context.current.modules.me.profile.data)
console.log('Render')
return(
<Submodule
isMobile = {c.cardsPerRow == 1 ? true : false}
header = {{
text: 'Profile',
}}
{...props}
>
<ImageCapsule
onClick = {openEditorImage}
id = {'container_imageprofile'}
width = '100%'
image = {image}
$nodrag
/>
{editorImageOpen &&
<EditorImage
open = {editorImageOpen}
closeSaving = {handleImageChanged}
closeWithoutSaving = {closeEditorImage}
image = {image}
width = {getEditorWidth(1, c.cardsPerRow, c.card.width, s.spacing.default)}
header = {{
text: 'Edit Profile Image',
}}
/>
}
{spacer}
{spacer}
<ContainerGrid
// bgcolor = '#C43939'
width = '100%'
justify = {s.justify.center}
onClick = {openEditorData}
$clickable
>
<Table
$nomouse
width = '100%'
data = {parsedData}
settings = {{
cell: {
padleft: s.spacing.default,
padright: s.spacing.default,
padtop: ops.round((ops.divide([s.spacing.default, 4]))),
padbottom: ops.round((ops.divide([s.spacing.default, 4]))),
},
columns: [
{type: 'defaultRight', width: '30%'},
{type: 'default', width: '70%'},
]
}}
/>
{editorDataOpen &&
<EditorProfileData
open = {editorDataOpen}
close = {closeEditorData}
width = {getEditorWidth(1, c.cardsPerRow, c.card.width, s.spacing.default)}
header = {{
text: 'Edit Profile Data',
}}
data = {unparsedData}
/>
}
</ContainerGrid>
{spacer}
{spacer}
</Submodule>
)
}
export default (
withTheme(
withContext(
withLoadingScreen(
Profile
)
)
)
)
EDIT: This one has been solved by Dehan de Croos, many thanks!
So as Dehan mentions below, the event was bubbling up and triggering openEditorData in ContainerGrid. The whole thing was mistifying because editorImageOpen was working correctly while editorDataOpen was not, and they both do the same thing: open an Editor window.
Once Dehan solved the mistery, I realized that the differene between the 2 is that inside ImageCapsule there is a ClickLayer component, which is there just to catch the click and the callback.
I did not use a ClickLayer with ContainerGrid, and that is why the event was able to bubble up.
Following Dehan's advice, I solved just by adding a ClickLayer inside ContainerGrid, like this:
<ContainerGrid
// bgcolor = '#C43939'
width = '100%'
justify = {s.justify.center}
// onClick = {openEditorData}
$clickable
>
<ClickLayer
onClick = {openEditorData}
/>
<Table
$nomouse
width = '100%'
data = {parsedData}
settings = {{
cell: {
padleft: s.spacing.default,
padright: s.spacing.default,
padtop: ops.round((ops.divide([s.spacing.default, 4]))),
padbottom: ops.round((ops.divide([s.spacing.default, 4]))),
},
columns: [
{type: 'defaultRight', width: '30%'},
{type: 'default', width: '70%'},
]
}}
/>
{editorDataOpen &&
<EditorProfileData
open = {editorDataOpen}
close = {closeEditorData}
width = {getEditorWidth(1, c.cardsPerRow, c.card.width, s.spacing.default)}
header = {{
text: 'Edit Profile Data',
}}
data = {unparsedData}
/>
}
</ContainerGrid>
It would be better to have a working code snippet to diagnose this, but there is a good chance that the event might be bubbling up the component tree and triggering the openEditorData event here.
<ContainerGrid
// bgcolor = '#C43939'
width = '100%'
justify = {s.justify.center}
onClick = {openEditorData}
$clickable
>
To check this quickly move the component shown below outside the <ContainerGrid ... /> component. And check whether this fixes things.
To clarify here, The move of code should happen so that <
EditorProfileData ... /> will never appear as a child component of
<ContainerGrid ... />
{editorDataOpen &&
<EditorProfileData
open = {editorDataOpen}
close = {closeEditorData}
width = {getEditorWidth(1, c.cardsPerRow, c.card.width, s.spacing.default)}
header = {{
text: 'Edit Profile Data',
}}
data = {unparsedData}
/>
}
If now it's working properly and you desperately need to maintain the above component hierarchy/structure. You can call
stopPropegation but you will need to have the native JS event with you. To explain how to do this I need to know what <EditorProfileData ... /> looks like. But assuming the close prop does return the native click event as a callback, the fix will look something like this.
{editorDataOpen &&
<EditorProfileData
open = {editorDataOpen}
//close = {closeEditorData}
// If close provides the native event use following
close = { e => {
e.stopPropagation();
closeEditorData();
}}
width = {getEditorWidth(1, c.cardsPerRow, c.card.width, s.spacing.default)}
header = {{
text: 'Edit Profile Data',
}}
data = {unparsedData}
/>
}
If not we need to find the original onClick event and pass up to that prop callback.

Selecting created option on menu close/select blur using creatable component

Is there some way to instruct react-select to select an option on menu close or select blur, but only if it is the one created (not from default list)?
Context:
I have a list of e-mail addresses and want to allow user to select from the list or type new e-mail address and then hit Submit button. I do the select part with react-select's Creatable component and it works.
import CreatableSelect from 'react-select/creatable';
<CreatableSelect
options={options}
isMulti={true}
isSearchable={true}
name={'emailAddresses'}
hideSelectedOptions={true}
isValidNewOption={(inputValue) => validateEmail(inputValue)}
/>
But what happens to my users is that they type new e-mail address, do not understand they need to click the newly created option in dropdown menu and directly hit the Submit button of the form. Thus the menu closes because select's focus is stolen and form is submitted with no e-mail address selected.
I look for a way how can I select the created option before the menu is closed and the typed option disappears.
You can keep track of the inputValue and add the inputValue as a new option when the onMenuClose and onBlur callbacks are triggered.
Keep in mind that both onBlur and onMenuClose will fire if you click anywhere outside of the select area. onMenuClose can also fire alone without onBlur if you press Esc key so you will need to write additional logic to handle that extra edge case.
function MySelect() {
const [value, setValue] = React.useState([]);
const [inputValue, setInputValue] = React.useState("");
const isInputPreviouslyBlurred = React.useRef(false);
const createOptionFromInputValue = () => {
if (!inputValue) return;
setValue((v) => {
return [...(v ? v : []), { label: inputValue, value: inputValue }];
});
};
const onInputBlur = () => {
isInputPreviouslyBlurred.current = true;
createOptionFromInputValue();
};
const onMenuClose = () => {
if (!isInputPreviouslyBlurred.current) {
createOptionFromInputValue();
}
else {} // option's already been created from the input blur event. Skip.
isInputPreviouslyBlurred.current = false;
};
return (
<CreatableSelect
isMulti
value={value}
onChange={setValue}
inputValue={inputValue}
onInputChange={setInputValue}
options={options}
onMenuClose={onMenuClose}
onBlur={onInputBlur}
/>
);
}
Live Demo

Programmatically focus and select value in react-select

I want to be able to programmatically focus() and select() a react-select. Clicking on Add new brand below:
should render something like this:
Here's what I have so far.
My Select component is wrapped in React.forwardRef:
const Select = React.forwardRef((props, ref) => {
return (
<Creatable ref={ref} {...props} />
)
})
so that I can style it with styled-components and still have a ref to its input, like so:
const BrandSelect = styled(Select)`...`
const Button = styled.button`...`
const MyComponent = () => {
const brandSelectRef = useRef()
const [newBrand, setNewBrand] = useState(false)
const handleAddBrand = () => {
setNewBrand(true)
console.log(brandSelectRef.current)
if (brandSelectRef && brandSelectRef.current) {
brandSelectRef.current.focus()
brandSelectRef.current.select()
}
}
return (
<BrandSelect
creatable
openMenuOnFocus
ref={brandSelectRef}
defaultInputValue={newBrand ? '...' : undefined}
// ...other required react-select props
/>
<Button onClick={handleAddBrand}>Add new brand</Button>
)
}
The problem is, though, that the above code doesn't work, i.e. react-select never gets focused. Also, the log brandSelectRef.current is undefined.
I'm clearly doing something wrong here, but I can't spot what.
I think the cause is that you have to use default value for your useRef hook
const brandSelectRef = useRef(null)

How to handle the state of multiple checkboxes in child component by the parent in react?

I have multiple check-boxes which are dynamic. These check boxes are placed (assume) in "B" component.
I have managed the state of these check boxes in "A" component which is the parent of "B".
When one or all the check boxes are clicked I want to render a div
expected result : render div if one or all the check boxes are clicked. If none is selected the div should be hidden. Basically the div should be visible even if a single checkbox is checked.
current result : When one check box is clicked the div gets rendered but if another checkbox is selected the div disappears.
Here's my A component (Parent)
const A = () => {
const [isChecked, setIsChecked] = useState(false) // state that holds state of check boxes
return (
{isChecked ? <div>Some Content..</div> : null} // rendering the div according to the state of the checkboxes
<Child isChecked={isChecked} setIsChecked={setIsChecked} />
)
}
Code of Child Component
const Child = props => {
return (
{checkBoxList.map((box, index) => (
<CustomCheckBox
key={index}
name={`check-box-${index}`} // to uniquely identify each check box
isChecked={props.isChecked}
setIsChecked={props.setIsChecked} />
))}
)
}
CustomCheckBox component :
const CustomCheckBox = props => {
const onChangeHandler = (event) => {
props.isChecked ? props.setIsChecked(false) : props.setIsChecked(true)
}
return <input name={props.name} type="checkBox" onChange={onChangeHandler} />;
};
CustomCheckBox.propTypes = {
name: string
}
I'm aware that the parent component state is only capable of handling a single check box right now.
The reason for the current result is because of the condition that I have included in onChangeHandler function.
props.isChecked ? props.setIsChecked(false) : props.setIsChecked(true)
I'm confused about how to handle the state of multiple check boxes in the above stated scenario using a single handler function !
you can keep track of checkbox indexes that you click instead of if it was clicked
const [isChecked, setIsChecked] = useState([]) // state that holds state of checked boxes
then handler would become:
const onChangeHandler = (index) => {
const result = props.isChecked;
var i = result.indexOf(index);
if (i> -1) {
result.splice(i, 1);
}
else {
result = [...result, index]
}
props.setIsChecked(result)
}
so in the parent you would check if isChecked.length> 0
Hope this helps

React dim element that is rendered from a map

I am working on showing images in a card using React. The images come from a API and are resolved when rendering the images page. Now I want to use the Dimmer component from the React Semantic-UI design library and dim an image on a mouseover. I tried the following example from the document page:
const dimmedID = this.state.dimmedID
const collections = this.state.collections
const collectionsList = collections.map((project) =>
<Dimmer.Dimmable
key = { project.id }
as = {Image}
dimmed = {dimmedID === project.id ? true : false}
dimmer = {{ active, content }}
onMouseEnter = {() => { this.handleShow(project.id) }}
onMouseLeave = {() => { this.handleShow('') }}
src = { project.thumbnail }
/>
)
When triggering the onMouseEnter, the dimmedID state object is set to the id of the image. However, all images that are rendered are being dimmed. Not the image which the mouse is on. I tried with a shorthand if-else on the dimmed parameter but that does not seem to work. When hovering with the mouse over one image, all images get dimmed.
Does someone know how to fix this?
Ok so apparently the fix is easy... so much for reading...
const dimmedID = this.state.dimmedID
const collections = this.state.collections
const collectionsList = collections.map((project) =>
<Dimmer.Dimmable
key = { project.id }
as = {Image}
dimmed = {dimmedID === project.id ? true : false}
dimmer = {{ active: dimmedID === project.id, content }}
onMouseEnter = {() => { this.handleShow(project.id) }}
onMouseLeave = {() => { this.handleShow('') }}
src = { project.thumbnail }
/>
)

Resources