Part 1: Setting Up the Basic Dialog Component with Minimize/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
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);
}
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;
`;
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 theisExpanded
value is changed. -
DialogBody
andDialogFooter
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``;
// 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')};
`;
// 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;
`;
// 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;
`;
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 theDialog
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;
// 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')
);
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.