Hide/Show React-slick arrows - reactjs

I am using react-slick and I have my own customised arrows. This Slider is NOT an infinite loop and I would like to hide the arrows at the start and end of the loop. So basically at start PrevArrow should be hidden and at the end the NextArrow should be hidden. Which I am trying to set hidden class name depending on state changes. However the class name is not changing although the state is changing correctly. Do you know what's the problem with this code? and how to get it work?
Below is my setting for Slider and the component which renders Slider.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Slider from 'react-slick';
import Arrow from '../slider/Arrow';
export default class CityCarousel extends Component {
constructor(props) {
super(props);
this.state = {
displayLeftArrow: true,
displayRightArrow: true
};
this.slidesToShow = 5;
this.sliderSetting = {
dots: false,
arrows: true,
infinite: false,
initialSlide: 0,
slidesToShow: this.slidesToShow,
slidesToScroll: 1,
speed: 500,
rows: 0,
nextArrow: <Arrow
styleClassName={`city-carousel__right ${
this.state.displayRightArrow ? '' : 'city-carousel__right--hide'
}`}
direction="right"
clickHandler={this.clickHandler}
/>,
prevArrow: <Arrow
styleClassName={`city-carousel__left ${
this.state.displayLeftArrow ? '' : 'city-carousel__left--hide'
}`}
direction="left"
clickHandler={this.clickHandler}
/>,
afterChange: currentSlide => this.setArrowDisplay(currentSlide)
};
}
clickHandler = direction => {
if (direction === 'left') {
this.slider.slickPrev();
} else if (direction === 'right') {
this.slider.slickNext();
}
};
setArrowDisplay = currentSlide => {
const { cityList } = this.props;
const displayLeftArrow = currentSlide !== 0;
const displayRightArrow = currentSlide !== cityList.length - this.slidesToShow;
this.setState({ displayRightArrow, displayLeftArrow });
};
render() {
const { cityList, tours } = this.props;
return (
<div>
<div className="city-selection">
<Slider
ref={c => this.slider = c }
{...this.sliderSetting}
>
{cityList.length > 0 ? this.renderCityList() : null}
</Slider>
</div>
</div>
);
}
}
Here is also code for Arrow component
import React from 'react';
import PropTypes from 'prop-types';
const Arrow = ({ styleClassName, direction, clickHandler }) => {
return(
<div
className={`slick-arrow--${direction} slider-arrow--${direction} ${styleClassName}`}
onClick={e => clickHandler(direction)}
/>
)};
Arrow.propTypes = {
styleClassName: PropTypes.string,
direction: PropTypes.string,
clickHandler: PropTypes.func
};
export default Arrow;

It seems react-slick is not re-rendering <Arrow /> with new props, and it's always with first initialized props setting.
I think the solution is to get <Arrow /> out of slider like this:
<div className="city-selection">
<Arrow
styleClassName={`city-carousel__right ${
this.state.displayRightArrow ? '' : 'city-carousel__right--hide'
}`}
direction="right"
clickHandler={this.clickHandler}
/>
<Arrow
styleClassName={`city-carousel__left ${
this.state.displayLeftArrow ? '' : 'city-carousel__left--hide'
}`}
direction="left"
clickHandler={this.clickHandler}
/>
<Slider
{/* list of items*/}
</Slider>
</div>
you can see how it's working in here

Pass the current slide prop to the component and do a check for the slide you want to trigger hiding the arrow:
function SamplePrevArrow(props) {
const { currentSlide, className, onClick } = props;
if (currentSlide === 0) {
return false;
} else {
return <div className={className} onClick={onClick} />;
}
}

In your this.sliderSettings set nextArrow & prevArrow for your Arrow component do something like this
<Arrow
styleClassName={'YOUR_CLASSES_HERE'}
direction="right"
clickHandler={this.clickHandler}
isHidden={this.state.isHidden}
/>
Where this.state.isHidden is the state variable where you are trying toggle the arrows.
Then in your Arrow component do something like this.
const Arrow = ({ styleClassName, direction, clickHandler, isHidden }) => {
return (
<div
className={`slick-arrow--${direction} slider-arrow--${direction}
${styleClassName}`}
style={{ display: isHidden: 'none': 'block' }}
onClick={e => clickHandler(direction)}
/>
)
};

If you want the best manual controls over the arrow then use a custom arrow.
Create it by using a simple Arrow component.
Set the onClick handler of that component by using a ref in the main Slider component.
const sliderRef = useRef<Slider>(null);
<Slider {...settings} ref={sliderRef}>
Use states to conditionally show or hide arrows.
I wrote a blog post which covers the workflow to achieve similar kind of functionalities you want. You can have a look there.
https://medium.com/#imasharaful/image-slider-with-react-slick-d54a049f043

In 2022, we have boolean arrows, just declare it false in settings.
const settings = {
...,
arrows: false,
};

Related

Material UI Carousel - with autoscroll

I am building a carousel component in react -- and the client wants there to be an autoscroll option - my concern though is interfering with the user interface -- what kind of logic to build to break/pause the autoscroll without interfering with the user trying to use it in the first place. So hover detection is needed?
//Original Carousel
https://codesandbox.io/s/holy-https-h0yv3
//With current autoscroll option on - but ignoring all UI considerations
https://codesandbox.io/s/nervous-brown-2inki?file=/src/Carousel/Carousel.js
also - the carousel after going through the slides - will just ZIP through the deck - without being able to snap to the slide - so that's another issue
import React, { Component } from 'react';
import Button from '#material-ui/core/Button';
import ChevronLeftIcon from '#material-ui/icons/ChevronLeft';
import ChevronRightIcon from '#material-ui/icons/ChevronRight';
import './Carousel.scss';
class Carousel extends Component {
constructor() {
super();
this.myRef = React.createRef();
this.state = {
currentSlide: 0,
slideWidth: 0,
slideCount: 0
};
this.updateDimensions = this.updateDimensions.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.prev = this.prev.bind(this);
this.next = this.next.bind(this);
}
componentDidMount() {
window.addEventListener("resize", this.updateDimensions);
this.updateDimensions();
this.setState({
slideCount: this.props.items.length
});
this.autoSlide();
}
componentWillUnmount() {
window.removeEventListener("resize", this.updateDimensions);
}
handleKeyPress(e) {
if (e.keyCode === 39) {
this.next();
}
if (e.keyCode === 37) {
this.prev();
}
}
updateDimensions(){
//update the dimensions of the slide for responsiveness
this.setState({
slideWidth: this.myRef.current.getBoundingClientRect().width
});
}
prev(){
//if the current slide won't hit negative slide - change current slide
if(this.state.currentSlide > 0){
let prevSlide = this.state.currentSlide-1;
this.setState({
currentSlide: prevSlide
});
}
}
next(){
//if the next slide will not go beyond the items - change current slide
if(this.state.currentSlide < this.props.items.length-1){
let nextSlide = this.state.currentSlide+1;
this.setState({
currentSlide: nextSlide
});
}
}
selected(slide){
console.log(slide)
this.setState({
currentSlide: slide
});
}
autoSlide(){
let that = this;
let count = this.props.items.length;
let i = 0;
setInterval(function(){
that.selected(i);
i++;
if(count === i){
i=0;//resets
}
}, 3000);
}
render() {
return (
<div ref={this.myRef} className="carousel" onKeyDown={this.handleKeyPress} tabIndex={-1}>
<div className="carousel-holder">
<div className="carousel-wrapper">
<div
className="slide-wrapper"
style={{
left: -this.state.slideWidth*this.state.currentSlide,
width: this.state.slideWidth*this.state.slideCount
}}
>
{
this.props.items.map((item, j) => {
return(
<div
key={j}
className="slide"
style={{
width: this.state.slideWidth
}}
>
<div className={"gutter " + (this.props.hasGutter? "has-gutter" : "")}>
{item.slide}
</div>
</div>
)
})
}
</div>
</div>
<Button
className="carousel-arrow prev-arrow"
onClick={this.prev}
disabled={(this.state.currentSlide === 0)}
>
<ChevronLeftIcon />
</Button>
<Button
className="carousel-arrow next-arrow"
onClick={this.next}
disabled={(this.state.currentSlide === this.props.items.length-1)}
>
<ChevronRightIcon />
</Button>
</div>
<div className="carousel-dots">
<ul className="dot-wrapper">
{
this.props.items.map((item, x) => {
return (
<li
key={x}
className={(this.state.currentSlide === x? "selected" : "")}
>
<Button
className="dot"
onClick={() => this.selected(x)}
/>
</li>
)
})
}
</ul>
</div>
</div>
);
}
}
export default Carousel;
You should create a boolean shouldAutoScroll which should be passed as prop to this Carousel component. Initially, it's value would be true, which means this slides should autoscroll, based on a time-interval you can keep changing the active slide.
One the user hovers over the images or once the user clicks the next/previous button, set that shouldAutoScroll to false, which would mean that logic to autoscroll should be stopped.
this.state = {
shouldAutoScroll: true,
...
}
and since you are already calling it on componentDidMount
componentDidMount() {
...
this.state.shouldAutoScroll && this.autoSlide();
}
Toggle it's value, on the next() and prev(), where it is certain that user wants to take control of the scrolling.
next/prev () {
this.setState(p => p.shouldAutoScroll && ({...p, shouldAutoScroll: false}));
... // further logic
}
I'm not sure how to adjust your code in the given scenario according to your expectation. But I also wanted the same thing to be done in my carousel. Then I found this npm package that can be used to display a carousel. I think you also can use this and rectify your problem.

Change Component property through onClick

I have a ButtonGroup with a few Buttons in it, and when one of the buttons gets clicked, I want to change its color, I kinda want to make them behave like radio buttons:
<ButtonGroup>
<Button
variant={"info"}
onClick={(e) => {
..otherFunctions..
handleClick(e);
}}
>
<img src={square} alt={".."} />
</Button>
</ButtonGroup>
function handleClick(e) {
console.log(e.variant);
}
But that doesnt work, e.variant is undefined.
If it was just a single button I would have used useState and I would be able to make this work, but how do I make it work when there are multiple buttons, how do I know which button is clicked and change the variant prop of that button? And then revert the other buttons to variant="info"
Another approach that I could think of is to create my own Button that wraps the bootstrap Button and that way I can have access to the inner state and use onClick inside to control each buttons state, but I'm not sure if that will work, as then how would I restore the other buttons that werent clicked..?
To further from my comment above, you could create your own button component to handle its own state and remove the need to have lots of state variables in your main component e.g.
const ColourButton = ({ children }) => {
const [colour, setColour] = React.useState(true)
return (
<button
onClick={ () => setColour(!colour) }
style = {{color: colour ? "red" : "blue"} }
>
{ children }
</button>
)
}
That way you can just wrap your image in your new ColourButton:
<ColourButton><img src={square} alt={".."} /></ColourButton>
Edit:
I actually like to use styled-components and pass a prop to them rather than change the style prop directly. e.g. https://styled-components.com/docs/basics#adapting-based-on-props
EDIT: Kitson response is a good way to handle your buttons state locally :)
I like to handle the generation of multiple elements with a function. It allows me to customize handleClick.
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [buttons, setButtons] = useState([
{
id: 1,
variant: "info"
},
{
id: 2,
variant: "alert"
}
]);
const handleClick = id => {
setButtons(previous_buttons => {
return previous_buttons.map(b => {
if (b.id !== id) return b;
return {
id,
variant: "other color"
};
});
});
};
const generateButtons = () => {
return buttons.map(button => {
return (
<button key={button.id} onClick={() => handleClick(button.id)}>
Hey {button.id} - {button.variant}
</button>
);
});
};
return <div>{generateButtons()}</div>;
}
https://jrjvv.csb.app/
You can maintain a state variable for your selected button.
export default class ButtonGroup extends Component {
constructor(props) {
super(props);
this.state = {
selected: null
};
}
handleClick = e => {
this.setState({
selected: e.target.name
});
};
render() {
const selected = this.state.selected;
return (
<>
<button
name="1"
style={{ backgroundColor: selected == 1 ? "red" : "blue" }}
onClick={this.handleClick}
/>
<button
name="2"
style={{ backgroundColor: selected == 2 ? "red" : "blue" }}
onClick={this.handleClick}
/>
<button
name="1"
style={{ backgroundColor: selected == 3 ? "red" : "blue" }}
onClick={this.handleClick}
/>
</>
);
}
}
Here is a working demo:
https://codesandbox.io/live/OXm3G

react-select focus() doesn't show cursor after change

As described in the official documentation for react-select, I'm trying to use ref and focus() to manually set the focus into the control input field. In most instances it works, but not immediately after selecting an Option from the dropdown.
After selecting an option from the dropdown, the control gets the focus but the cursor doesn't appear. It only appears if you start typing (including hitting the Esc key). On subsequent openings of the menu, the cursor appears along with the focus of the entire control field. Any ideas how to get this working?
I've created a sample code in codesandbox.io here
This is the code:
import React, { Component } from "react";
import ReactDOM from "react-dom";
import Select from "react-select";
import styled from "styled-components";
import { stateOptions } from "./data.js";
class PopoutExample extends Component {
selectRef = React.createRef();
state = {
isOpen: false,
option: undefined,
};
toggleOpen = () => {
const isOpening = !this.state.isOpen;
this.setState(
{
isOpen: isOpening,
},
() => isOpening && setTimeout(() => this.selectRef.focus(), 400),
);
};
onSelectChange = option => {
this.toggleOpen();
this.setState({ option });
};
render() {
const { isOpen, option } = this.state;
return (
<Dropdown
target={
<MainButton onClick={this.toggleOpen}>
{option ? option.label : "Select a State"}
</MainButton>
}
>
<Select
menuIsOpen
ref={ref => {
this.selectRef = ref;
}}
styles={{
container: provided => ({
...provided,
display: isOpen ? "block" : "none",
}),
}}
onChange={this.onSelectChange}
options={stateOptions}
value={option}
controlShouldRenderValue={false}
/>
</Dropdown>
);
}
}
const MainButton = styled.button`
padding: 10px;
background-color: aqua;
width: 100%;
`;
const Dropdown = ({ children, target }) => (
<div>
{target}
{children}
</div>
);
ReactDOM.render(<PopoutExample />, document.getElementById("root"));
You can notice that the bug also exists in the official react-select examples. Even clicking on the blur button after the selection is not solving the problem.
There's probably a small different in the code when user closes the menu and saves + automatically closes action.
I saw you've opened an issue on github. Let's keep an eye on it.
If I can offer an alternative to the behaviour you're trying to achieve, instead of hiding the Select with css why don't just mount / unmount it ?
class PopoutExample extends Component {
state = {
isOpen: false,
option: undefined
};
toggleOpen = () => {
this.setState({
isOpen: !this.state.isOpen
});
};
onSelectChange = option => {
this.setState({ option, isOpen: !this.state.isOpen });
};
render() {
const { isOpen, option } = this.state;
return (
<Dropdown
target={
<MainButton onClick={this.toggleOpen}>
{option ? option.label : "Select a State"}
</MainButton>
}
>
{isOpen && (
<Select
autoFocus
menuIsOpen
onChange={this.onSelectChange}
options={stateOptions}
value={option}
controlShouldRenderValue={false}
/>
)}
</Dropdown>
);
}
}
Here a live example of my solution.

Why does clicking expand button closes the side panel in react?

i have a side panel with items listed. when the list item content overflows expand button appears and clicking that expand btn would show the entire content of list item
For this i have created a expandable component. this will show arrow_down when list item content overflows and clicking arrow_down shows up arrow_up.
However with the below code, clicking button 1 just makes the sidpanel disappear instead of arrow_up appearing. could some one help me solve this. thanks.
export default class Expandable extends React.PureComponent{
constructor(props) {
super(props);
this.expandable_ref = React.createRef();
this.state = {
expanded: false,
overflow: false,
};
}
componentDidMount () {
if (this.expandable_ref.current.offsetHeight <
this.expandable_ref.current.scrollHeight) {
this.setState({overflow: true});
}
}
on_expand = () => {
this.setState({expanded: true});
console.log("in expnad");
};
on_collapse = () => {
this.setState({expanded: false});
};
render () {
return (
<div className={(this.state.overflow ?
this.props.container_classname : '')}>
<div className={(this.state.overflow ?
this.props.classname : '')} style={{overflow: 'hidden',
display: 'flex', height: (this.state.expanded ? null :
this.props.base_height)}}
ref={this.expandable_ref}>
{this.props.children}
</div>
{this.state.overflow && this.state.expanded &&
<div className={this.props.expand}>
<button onClick={this.on_collapse}>
{this.props.arrow_up}</button>
</div>}
{this.state.overflow && !this.state.expanded &&
<div className={this.props.expand}>
<button onClick={this.on_expand}>
{this.props.arrow_down}</button>
</div>}
</div>
);
}
}
In the above code i pass the base_height to be 42px.
Edit:
i have realised for the side panel component i add eventlistener click to close the side panel if user clicks anywhere outside sidepanel. When i remove that eventlistener it works fine....
class sidepanel extends React.PureComponent {
constructor(props) {
super(props);
this.sidepanel_ref = React.createRef();
}
handle_click = (event) => {
if (this.sidepanel_ref.current.contains(event.target)) {
return;
} else {
this.props.on_close();
}
};
componentDidMount() {
document.addEventListener('click', this.handle_click, false);
}
componentWillUnmount() {
document.removeEventListener('click', this.handle_click, false);
}
render() {
return (
<div>
<div className="sidepanel" ref=
{this.sidepanel_ref}>
{this.props.children}
</div>
</div>
);
}
}
when i log the event.target and sidepanel_ref.current i see the button element in both of them but svg seems different in both of them.
How can i fix this?
Probably it is because click events bubble up the component tree as they do in the DOM too. If you have an element with an onClick handler inside an element with another onClick handler it will trigger both. Use event.stopPropagation() in the handler of the inner element to stop the event from bubbling up:
export default class Expandable extends React.PureComponent{
constructor(props) {
super(props);
this.expandable_ref = React.createRef();
this.state = {
expanded: false,
overflow: false,
};
}
componentDidMount () {
if (this.expandable_ref.current.offsetHeight <
this.expandable_ref.current.scrollHeight) {
this.setState({overflow: true});
}
}
toggleCollapse = event => {
// use preventDefault here to stop the event from bubbling up
event.stopPropagation();
this.setState(({expanded}) => ({expanded: !expanded}));
};
render () {
const {className, container_classname, base_height, expand, arrow_up, arrow_down} = this.props;
const {overflow, expanded} = this.state;
return (
<div className={overflow ? container_classname : ''}>
<div
className={overflow ? classname : ''}
style={{
overflow: 'hidden',
display: 'flex',
height: expanded ? null : base_height
}}
ref={this.expandable_ref}
>
{this.props.children}
</div>
{overflow && (
<div className={expand}>
<button onClick={this.toggleCollapse}>
{expanded ? arrow_up : arrow_down}
</button>
</div>
)}
</div>
);
}
}

Callback function, responsible for updating state, passed as props to child component not triggering a state update

The callback function (lies in Images component) is responsible for making a state update. I'm passing that function as props to the Modal component, and within it it's being passed into the ModalPanel component.
That function is used to set the state property, display, to false which will close the modal. Currently, that function is not working as intended.
Image Component:
class Images extends Component {
state = {
display: false,
activeIndex: 0
};
handleModalDisplay = activeIndex => {
this.setState(() => {
return {
activeIndex,
display: true
};
});
};
closeModal = () => {
this.setState(() => {
return { display: false };
});
}
render() {
const { imageData, width } = this.props;
return (
<div>
{imageData.resources.map((image, index) => (
<a
key={index}
onClick={() => this.handleModalDisplay(index)}
>
<Modal
closeModal={this.closeModal}
display={this.state.display}
activeIndex={this.state.activeIndex}
selectedIndex={index}
>
<Image
cloudName={CLOUDINARY.CLOUDNAME}
publicId={image.public_id}
width={width}
crop={CLOUDINARY.CROP_TYPE}
/>
</Modal>
</a>
))}
</div>
);
}
}
export default Images;
Modal Component:
const overlayStyle = {
position: 'fixed',
zIndex: '1',
paddingTop: '100px',
left: '0',
top: '0',
width: '100%',
height: '100%',
overflow: 'auto',
backgroundColor: 'rgba(0,0,0,0.9)'
};
const button = {
borderRadius: '5px',
backgroundColor: '#FFF',
zIndex: '10'
};
class ModalPanel extends Component {
render() {
const { display } = this.props;
console.log(display)
const overlay = (
<div style={overlayStyle}>
<button style={button} onClick={this.props.closeModal}>
X
</button>
</div>
);
return <div>{display ? overlay : null}</div>;
}
}
class Modal extends Component {
render() {
const {
activeIndex,
children,
selectedIndex,
display,
closeModal
} = this.props;
let modalPanel = null;
if (activeIndex === selectedIndex) {
modalPanel = (
<ModalPanel display={this.props.display} closeModal={this.props.closeModal} />
);
}
return (
<div>
{modalPanel}
{children}
</div>
);
}
}
export default Modal;
links to code
https://github.com/philmein23/chez_portfolio/blob/chez_portfolio/components/Images.js
https://github.com/philmein23/chez_portfolio/blob/chez_portfolio/components/Modal.js
You're dealing with this modal through a very non-react and hacky way.
Essentially, in your approach, all the modals are always there, and when you click on image, ALL modals display state becomes true, and you match the index number to decide which content to show.
I suspect it's not working due to the multiple children of same key in Modal or Modal Panel.
I strongly suggest you to ditch current approach. Here's my suggestions:
Only a single <Modal/> in Images component.
Add selectedImage state to your Images component. Every time you click on an image, you set selectedImage to that clicked image object.
Pass selectedImage down to Modal to display the content you want.
This way, there is only ONE modal rendered at all time. The content changes dynamically depending on what image you click.
This is the working code I tweaked from your repo:
(I'm not sure what to display as Modal content so I display public_id of image)
Images Component
class Images extends Component {
state = {
display: false,
selectedImage: null
};
handleModalDisplay = selectedImage => {
this.setState({
selectedImage,
display: true
})
};
closeModal = () => {
//shorter way of writing setState
this.setState({display: false})
}
render() {
const { imageData, width } = this.props;
return (
<div>
<Modal
closeModal={this.closeModal}
display={this.state.display}
selectedImage={this.state.selectedImage}
/>
{imageData.resources.map((image, index) => (
<a
//Only use index as key as last resort
key={ image.public_id }
onClick={() => this.handleModalDisplay(image)}
>
<Image
cloudName={CLOUDINARY.CLOUDNAME}
publicId={image.public_id}
width={width}
crop={CLOUDINARY.CROP_TYPE}
/>
</a>
))}
</div>
);
}
}
Modal Component
class Modal extends Component {
render() {
const { display, closeModal, selectedImage } = this.props;
const overlayContent = () => {
if (!selectedImage) return null; //for when no image is selected
return (
//Here you dynamically display the content of modal using selectedImage
<h1 style={{color: 'white'}}>{selectedImage.public_id}</h1>
)
}
const overlay = (
<div style={overlayStyle}>
<button style={button} onClick={this.props.closeModal}>
X
</button>
{
//Show Modal Content
overlayContent()
}
</div>
);
return <div>{display ? overlay : null}</div>;
}
}

Resources