queelp/ui

Dynamic button

This component is used as a better alternative to confirmation modal windows and popups. It provides an inline confirmation mechanism, enhancing user experience by reducing context switches. This component is currently a work in progress.

Preview

Are you sure?

Usage

import DynamicButton from '@/components/DynamicButton';

function MyComponent() {
  return (
    <DynamicButton />
  );
}

Component Code

'use client';

import React, { useState, useRef, useEffect } from 'react';

const DynamicButton: React.FC = () => {
  const [isDialogOpen, setIsDialogOpen] = useState(false);
  const dialogRef = useRef<HTMLDivElement>(null);
  const buttonRef = useRef<HTMLButtonElement>(null);
  const submitRef = useRef<HTMLButtonElement>(null);
  const resetRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    if (isDialogOpen && dialogRef.current) {
      dialogRef.current.style.width = `${dialogRef.current.firstElementChild?.clientWidth}px`;
    } else if (dialogRef.current) {
      dialogRef.current.removeAttribute('style');
    }
  }, [isDialogOpen]);

  const handleButtonClick = () => {
    setIsDialogOpen(true);
    buttonRef.current?.blur();
    buttonRef.current?.setAttribute('tabindex', '-1');
    submitRef.current?.removeAttribute('tabindex');
    resetRef.current?.removeAttribute('tabindex');
  };

  const handleReset = () => {
    setIsDialogOpen(false);
    resetRef.current?.blur();
    buttonRef.current?.removeAttribute('tabindex');
    submitRef.current?.setAttribute('tabindex', '-1');
    resetRef.current?.setAttribute('tabindex', '-1');
  };

  return (
    <div className="flex">
      <div id="dynamic-button" className="h-10 flex flex-row items-stretch bg-gray-950 rounded-xl whitespace-nowrap select-none">
        <div className="flex flex-row">
          <button
            ref={buttonRef}
            type="button"
            className={`flex flex-row items-center gap-3 px-3 rounded-xl text-sm text-gray-100 transition-all ease-out duration-75 lg:hover:text-green-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-850 focus-visible:transition-none ${isDialogOpen ? 'pointer-events-none' : ''}`}
            onClick={handleButtonClick}
          >
            <svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" viewBox="0 0 16 16" fill="none">
              <path className="fill-green-500" d="M3 13L2.125 13L2.125 13.875H3V13ZM13 8.00001L13.6187 7.38129C13.4546 7.2172 13.2321 7.12501 13 7.12501C12.7679 7.12501 12.5454 7.2172 12.3813 7.38129L13 8.00001ZM13.8813 10.1187C14.223 10.4604 14.777 10.4604 15.1187 10.1187C15.4604 9.77703 15.4604 9.22301 15.1187 8.8813L13.8813 10.1187ZM10.8813 8.8813C10.5396 9.22301 10.5396 9.77703 10.8813 10.1187C11.223 10.4604 11.777 10.4604 12.1187 10.1187L10.8813 8.8813ZM3 13.875H11.5V12.125H3V13.875ZM13.875 11.5V8.00001H12.125V11.5H13.875ZM3.875 13L3.87503 1.74977L2.12503 1.74976L2.125 13L3.875 13ZM12.3813 8.61873L13.8813 10.1187L15.1187 8.8813L13.6187 7.38129L12.3813 8.61873ZM12.3813 7.38129L10.8813 8.8813L12.1187 10.1187L13.6187 8.61873L12.3813 7.38129ZM3.375 13.0003C3.375 13.2074 3.20711 13.3753 3 13.3753V15.1253C4.17361 15.1253 5.125 14.1739 5.125 13.0003H3.375ZM3 13.3753C2.79289 13.3753 2.625 13.2074 2.625 13.0003H0.875C0.875 14.1739 1.82639 15.1253 3 15.1253V13.3753ZM2.625 13.0003C2.625 12.7932 2.79289 12.6253 3 12.6253V10.8753C1.82639 10.8753 0.875 11.8267 0.875 13.0003H2.625ZM3 12.6253C3.20711 12.6253 3.375 12.7932 3.375 13.0003H5.125C5.125 11.8267 4.17361 10.8753 3 10.8753V12.6253ZM3.37503 2.99977C3.37503 3.20687 3.20714 3.37477 3.00003 3.37477V5.12477C4.17363 5.12477 5.12503 4.17337 5.12503 2.99977H3.37503ZM3.00003 3.37477C2.79292 3.37477 2.62503 3.20687 2.62503 2.99977H0.87503C0.87503 4.17337 1.82642 5.12477 3.00003 5.12477V3.37477ZM2.62503 2.99977C2.62503 2.79266 2.79292 2.62477 3.00003 2.62477V0.874767C1.82642 0.874767 0.87503 1.82616 0.87503 2.99977H2.62503ZM3.00003 2.62477C3.20714 2.62477 3.37503 2.79266 3.37503 2.99977H5.12503C5.12503 1.82616 4.17364 0.874767 3.00003 0.874767V2.62477ZM13.375 2.99975C13.375 3.20686 13.2071 3.37475 13 3.37475V5.12475C14.1736 5.12475 15.125 4.17335 15.125 2.99975H13.375ZM13 3.37475C12.7929 3.37475 12.625 3.20686 12.625 2.99975H10.875C10.875 4.17335 11.8264 5.12475 13 5.12475V3.37475ZM12.625 2.99975C12.625 2.79264 12.7929 2.62475 13 2.62475V0.874748C11.8264 0.874748 10.875 1.82614 10.875 2.99975H12.625ZM13 2.62475C13.2071 2.62475 13.375 2.79264 13.375 2.99975H15.125C15.125 1.82614 14.1736 0.874748 13 0.874748V2.62475ZM12.125 1.74975V4.24975H13.875V1.74975H12.125ZM11.5 13.875C12.8117 13.875 13.875 12.8117 13.875 11.5H12.125C12.125 11.8452 11.8452 12.125 11.5 12.125V13.875Z"/>
            </svg>
            Deploy
          </button>
          <div
            ref={dialogRef}
            className={`overflow-hidden flex transition-all ease-out duration-300 ${isDialogOpen ? 'w-0' : 'w-0 scale-50 opacity-0 blur'} origin-left`}
          >
            <div className="flex flex-row items-stretch gap-3">
              <div className="flex flex-row items-center">
                <span className="text-sm text-gray-300 cursor-default">Are you sure?</span>
              </div>
              <div className="flex flex-row gap-2 py-2 pr-2">
                <button
                  ref={submitRef}
                  tabIndex={isDialogOpen ? 0 : -1}
                  type="submit"
                  className="px-1.5 bg-green-800 rounded text-xs leading-6 text-green-400 transition-all ease-out duration-75 lg:hover:bg-green-700 lg:hover:text-green-200"
                >
                  Submit
                </button>
                <button
                  ref={resetRef}
                  tabIndex={isDialogOpen ? 0 : -1}
                  type="reset"
                  className="px-1.5 bg-gray-800 rounded text-xs leading-6 text-gray-300 transition-all ease-out duration-75 lg:hover:bg-gray-700 lg:hover:text-gray-200"
                  onClick={handleReset}
                >
                  Cancel
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default DynamicButton;

Props

The DynamicButton component doesn't accept any props. Its behavior is entirely self-contained.

Installation

To use the DynamicButton component, you need to have React and Tailwind CSS installed in your project. Here are the steps:

React

React should already be installed in your Next.js project. If not, install it:

npm install react react-dom

Tailwind CSS

Ensure Tailwind CSS is installed and configured in your project. If not, follow these steps:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Then, update your tailwind.config.js:

module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

After setting up these dependencies and configurations, you can use the DynamicButton component as shown in the usage example above.