DEV Community

Anthony Tambrin
Anthony Tambrin

Posted on • Edited on

Creating a Smooth Transitioning Dialog Component in React (Part 1/4)

Part 1: Setting Up the Basic Dialog Component with Minimise/Expand Functionality

Welcome to the first part of my four-part series on creating a responsive dialog component in React. In this series, I'll explore different approaches to achieve smooth animation transitions while maintaining the dialog's fluid dimensions. In this initial part, I'll set up the basic dialog component with minimise and expand functionality.

Please note that accessibility and responsive design are not included as part of the considerations in this series. The primary focus is on creating a reusable dialog component with smooth animation transitions.

This series is part of a proof of concept I've been working on, aimed at discussing and refining techniques for animating UI components. I invite feedback and insights from fellow developers to validate my approach or suggest improvements.

Setting Up the Basic Dialog Component

Let's start by creating a highly reusable dialog component that supports minimising and expanding. I'll use the compositional pattern to ensure the dialog can adapt to changing content.

File Structure:

src/
  components/
    FluidDialog/
      Dialog.js
      DialogContext.js
      DialogHeader.js
      DialogBody.js
      DialogFooter.js
      DialogContainer.js
      index.js
  App.js
  index.js
Enter fullscreen mode Exit fullscreen mode

Step 1: Dialog Context

First, I'll create a context to manage the state of our dialog component.

Key Points:

  • The DialogContext will hold the state and provide functions to toggle the dialog between minimised and expanded states.
  • The DialogProvider component initialises the state and provides it to the dialog components via context.
// src/components/FluidDialog/DialogContext.js
import { createContext, useContext, useId, useState } from 'react';

const DialogContext = createContext();

export function DialogProvider({
  rootRef,
  isExpandedByDefault,
  children,
  maxWidth,
}) {
  const dialogId = useId();
  const [isExpanded, setIsExpanded] = useState(isExpandedByDefault);

  return (
    <DialogContext.Provider
      value={{ dialogId, rootRef, isExpanded, setIsExpanded, maxWidth }}
    >
      {children}
    </DialogContext.Provider>
  );
}

export function useDialog() {
  return useContext(DialogContext);
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Dialog Component

Next, I'll create the main dialog component that uses the context to handle expansion and minimisation.

Key Points:

  • The Dialog component initialises the context provider with relevant props.
  • The DialogComponent styled-component handles the basic styling and layout of the dialog.
// src/components/FluidDialog/Dialog.js
import { useRef } from 'react';
import { styled } from 'styled-components';
import { DialogProvider } from './DialogContext';

export default function Dialog({
  id,
  isExpandedByDefault = true,
  maxWidth = 400,
  children,
}) {
  const rootRef = useRef(null);
  return (
    <DialogProvider
      dialogId={id}
      rootRef={rootRef}
      isExpandedByDefault={isExpandedByDefault}
    >
      <DialogComponent
        role="dialog"
        aria-labelledby={`${id}_label`}
        aria-describedby={`${id}_desc`}
        ref={rootRef}
        maxWidth={maxWidth}
      >
        {children}
      </DialogComponent>
    </DialogProvider>
  );
}

const DialogComponent = styled.section`
  max-width: ${({ maxWidth }) => (maxWidth ? `${maxWidth}px` : undefined)};
  position: absolute;
  right: 16px;
  bottom: 16px;
  border: 1px solid #ccc;
  border-radius: 6px;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
  overflow: hidden;
`;
Enter fullscreen mode Exit fullscreen mode

Step 3: Additional Components

I'll create additional components for the dialog header, body, footer, and container to ensure modularity and reusability.

Key Points:

  • DialogHeader includes a button to toggle between minimised and expanded states using the context.
  • DialogContainer wraps the body and footer content to automatically hide them when the isExpanded value is changed.
  • DialogBody and DialogFooter components are simple containers for the dialog's content.
// src/components/FluidDialog/DialogHeader.js
import { styled } from 'styled-components';
import { IconButton } from '../IconButton';
import { useDialog } from './DialogContext';

export default function DialogHeader({ children, expandedTitle }) {
  const { dialogId, isExpanded, setIsExpanded } = useDialog();

  return (
    <DialogHeaderComponent id={`${dialogId}_label`}>
      <ExpandedState isVisible={isExpanded}>
        <Title>{expandedTitle ?? children}</Title>
        <IconButtons>
          <IconButton
            icon="chevron-down"
            onClick={() => setIsExpanded(false)}
          />
        </IconButtons>
      </ExpandedState>
      <MinimizedState
        isVisible={!isExpanded}
        onClick={() => setIsExpanded(true)}
      >
        <Title>{children}</Title>
        <IconButtons>
          <IconButton icon="chevron-up" />
        </IconButtons>
      </MinimizedState>
    </DialogHeaderComponent>
  );
}

const DialogHeaderComponent = styled.div``;

const ExpandedState = styled.header`
  transition: opacity 0.3s;
  opacity: ${({ isVisible }) => (isVisible ? 1 : 0)};
  pointer-events: ${({ isVisible }) => (isVisible ? 'all' : 'none')};
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  background: #f3f3f3;
  display: flex;
  flex-direction: row;
`;

const MinimizedState = styled.header`
  transition: opacity 0.3s;
  opacity: ${({ isVisible }) => (isVisible ? 1 : 0)};
  pointer-events: ${({ isVisible }) => (isVisible ? 'all' : 'none')};
  background: #f3f3f3;
  display: flex;
  flex-direction: row;
  cursor: pointer;
`;

const Title = styled.span`
  flex-grow: 1;
  text-align: left;
  display: flex;
  align-items: center;
  padding: 0 16px;
`;

const IconButtons = styled.div``;
Enter fullscreen mode Exit fullscreen mode
// src/components/FluidDialog/DialogContainer.js
import { styled } from 'styled-components';
import { useDialog } from './DialogContext';

export default function DialogContainer({ children }) {
  const { isExpanded } = useDialog();

  return (
    <DialogContainerComponent isVisible={isExpanded}>
      {children}
    </DialogContainerComponent>
  );
}

const DialogContainerComponent = styled.div`
  display: ${({ isVisible }) => (isVisible ? undefined : 'none')};
`;
Enter fullscreen mode Exit fullscreen mode
// src/components/FluidDialog/DialogBody.js
import { styled } from 'styled-components';
import DialogContainer from './DialogContainer';
import { useDialog } from './DialogContext';

export default function DialogBody({ children }) {
  const { dialogId } = useDialog();

  return (
    <DialogBodyComponent>
      <DialogContainer>
        <DialogBodyContent id={`${dialogId}_desc`}>
          {children}
        </DialogBodyContent>
      </DialogContainer>
    </DialogBodyComponent>
  );
}

const DialogBodyComponent = styled.div``;

const DialogBodyContent = styled.div`
  padding: 8px 16px;
`;
Enter fullscreen mode Exit fullscreen mode
// src/components/FluidDialog/DialogFooter.js
import { styled } from 'styled-components';
import DialogContainer from './DialogContainer';

export default function DialogFooter({ children }) {
  return (
    <DialogFooterComponent>
      <DialogContainer>
        <DialogFooterContent>{children}</DialogFooterContent>
      </DialogContainer>
    </DialogFooterComponent>
  );
}

const DialogFooterComponent = styled.div`
  background: #f3f3f3;
`;

const DialogFooterContent = styled.div`
  padding: 8px 16px;
`;
Enter fullscreen mode Exit fullscreen mode

Step 4: Putting It All Together

Finally, I'll import and use the dialog component in the main app.

Key Points:

  • The App component includes the Dialog with its header, body, and footer components.
  • This setup ensures the dialog is ready for further enhancements and animations in the upcoming parts.
// src/App.js
import React from 'react';
import Dialog from './components/FluidDialog/Dialog';
import DialogHeader from './components/FluidDialog/DialogHeader';
import DialogBody from './components/FluidDialog/DialogBody';
import DialogFooter from './components/FluidDialog/DialogFooter';

function App() {
  return (
    <div className="App">
      <Dialog>
        <DialogHeader>My dialog/DialogHeader>
        <DialogBody>This is the content of the dialog.</DialogBody>
        <DialogFooter>This is the footer of the dialog.</DialogFooter>
      </Dialog>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

You can access the whole source code on CodeSandbox.

You can also see a live preview of the implementation:

Conclusion

In this first part, I've set up a basic dialog box in React with minimise and expand functionality. This foundational component will serve as the basis for further enhancements in the upcoming articles. The dialog component is designed to hug its content and adapt to changes, making it highly reusable and flexible.

Stay tuned for Part 2, where I'll delve into adding animations to the dialog transitions, exploring different options to achieve smooth effects.

I invite feedback and comments from fellow developers to help refine and improve this approach. Your insights are invaluable in making this proof of concept more robust and effective.

Top comments (0)