Tips & Tricks

设计and Implementing Modal Dialogs:Claude Code 实战指南

了解designing and implementing modal dialogs:Claude Code 实战. 包含实用代码示例。

模态框・对话框の设计原則

模态框は用户の注意を特定の操作に集中させるUIパターンです。然而、焦点管理やキーボード操作、スクリーンリーダー支持を怠ると、アクセシビリティの問題を引き起こします。借助 Claude Code,WAI-ARIA準拠の模态框を正确地实现可以。

HTML dialog要素使用…的实现

> HTML標準のdialog要素使用...的模态框组件を作って。
> アクセシビリティと动画に支持して。
import { useRef, useEffect } from 'react';

interface DialogProps {
  open: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
}

function Dialog({ open, onClose, title, children }: DialogProps) {
  const dialogRef = useRef<HTMLDialogElement>(null);

  useEffect(() => {
    const dialog = dialogRef.current;
    if (!dialog) return;

    if (open) {
      dialog.showModal();
    } else {
      dialog.close();
    }
  }, [open]);

  useEffect(() => {
    const dialog = dialogRef.current;
    if (!dialog) return;

    const handleClose = () => onClose();
    dialog.addEventListener('close', handleClose);

    const handleClick = (e: MouseEvent) => {
      if (e.target === dialog) onClose();
    };
    dialog.addEventListener('click', handleClick);

    return () => {
      dialog.removeEventListener('close', handleClose);
      dialog.removeEventListener('click', handleClick);
    };
  }, [onClose]);

  return (
    <dialog
      ref={dialogRef}
      aria-labelledby="dialog-title"
      className="rounded-xl shadow-2xl p-0 backdrop:bg-black/50 backdrop:backdrop-blur-sm
        max-w-lg w-full open:animate-fadeIn"
    >
      <div className="p-6">
        <div className="flex items-center justify-between mb-4">
          <h2 id="dialog-title" className="text-xl font-bold">{title}</h2>
          <button onClick={onClose} aria-label="Close"
            className="rounded-full p-1 hover:bg-gray-100">✕</button>
        </div>
        {children}
      </div>
    </dialog>
  );
}

确认对话框

> 「Deleteしてよろしいですか?」这样的确认对话框をPromiseベースで使えるようにして。
import { createRoot } from 'react-dom/client';

interface ConfirmOptions {
  title: string;
  message: string;
  confirmLabel?: string;
  cancelLabel?: string;
  variant?: 'danger' | 'default';
}

function confirm(options: ConfirmOptions): Promise<boolean> {
  return new Promise((resolve) => {
    const container = document.createElement('div');
    document.body.appendChild(container);
    const root = createRoot(container);

    const handleClose = (result: boolean) => {
      root.unmount();
      container.remove();
      resolve(result);
    };

    root.render(
      <Dialog open={true} onClose={() => handleClose(false)} title={options.title}>
        <p className="text-gray-600 mb-6">{options.message}</p>
        <div className="flex justify-end gap-3">
          <button onClick={() => handleClose(false)}
            className="px-4 py-2 border rounded-lg hover:bg-gray-50">
            {options.cancelLabel ?? 'キャンセル'}
          </button>
          <button onClick={() => handleClose(true)}
            className={`px-4 py-2 rounded-lg text-white ${
              options.variant === 'danger' ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700'
            }`}>
            {options.confirmLabel ?? '確認'}
          </button>
        </div>
      </Dialog>
    );
  });
}

// Usage example
async function handleDelete(id: string) {
  const ok = await confirm({
    title: 'Deleteの確認',
    message: 'この項目をDeleteしてよろしいですか?この操作は取り消せません。',
    confirmLabel: 'Deleteする',
    variant: 'danger',
  });
  if (ok) await deleteItem(id);
}

コマンドパレット

function CommandPalette({ commands, onClose }: { commands: Command[]; onClose: () => void }) {
  const [query, setQuery] = useState('');
  const [activeIndex, setActiveIndex] = useState(0);
  const filtered = commands.filter((c) =>
    c.label.toLowerCase().includes(query.toLowerCase())
  );

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      setActiveIndex((prev) => Math.min(prev + 1, filtered.length - 1));
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      setActiveIndex((prev) => Math.max(prev - 1, 0));
    } else if (e.key === 'Enter' && filtered[activeIndex]) {
      filtered[activeIndex].action();
      onClose();
    }
  };

  return (
    <Dialog open={true} onClose={onClose} title="コマンドパレット">
      <input
        autoFocus
        value={query}
        onChange={(e) => { setQuery(e.target.value); setActiveIndex(0); }}
        onKeyDown={handleKeyDown}
        placeholder="コマンドを検索..."
        className="w-full px-3 py-2 border rounded-lg mb-3"
        role="combobox"
        aria-expanded={true}
      />
      <ul role="listbox" className="max-h-64 overflow-y-auto">
        {filtered.map((cmd, i) => (
          <li key={cmd.id} role="option" aria-selected={i === activeIndex}
            onClick={() => { cmd.action(); onClose(); }}
            className={`px-3 py-2 rounded cursor-pointer ${i === activeIndex ? 'bg-blue-100' : 'hover:bg-gray-50'}`}>
            {cmd.label}
          </li>
        ))}
      </ul>
    </Dialog>
  );
}

总结

借助 Claude Code,HTML dialog要素を活用したアクセシブルな模态框から、确认对话框、コマンドパレットまで高效地构建可以。Toast通知との使い分けはToast通知实现を、アクセシビリティの基本はアクセシビリティ指南

dialog要素的规范请参阅MDN Web Docs - dialog

#Claude Code #modal #ダイアログ #React #accessibility