かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

TypeScript React クリップボードにコピーする hooks とコンポーネント作ってみた

input タグに入力された値をクリップボードにコピーする hooks と コンポーネントを作ってみたのでログとしてメモ

📝 Clipboard.writeText() を使ってクリップボードにテキストをコピーすることができる

clipboardjs のようなライブラリもあったのですが、ブラウザで動作するフロントエンドなら Clipboard API を使えばクリップボードに書き込みが可能そうだったのでライブラリなしに実装しました

Clipboard: writeText() method
The writeText() method of the Clipboard interface writes the specified text to the system clipboard, returning a Promise that is resolved once the system clipboard has been updated.
cf. Clipboard: writeText() method - Web APIs | MDN

const onCopy = async (text: string) => {
  try {
    await navigator.clipboard.writeText(text);
  } catch (error) {
    console.error(error.message);
  }
}

Clipboard にテキストをコピーする hooks

// /hooks/useCopyClipboard.ts
import { useCallback, useEffect, useState } from "react";

export const useCopyClipboard = () => {
  const [hasCopied, setHasCpied] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  const onCopy = useCallback(async (text: string) => {
    try {
      await navigator.clipboard.writeText(text);
      setHasCpied(true);
    } catch (err) {
      if (err instanceof Error) {
        console.error(err.message);
        setError(err);
        return;
      }

      console.error(err);
      setError(new Error("Failed to copy"));
    }
  }, []);

  useEffect(() => {
    let timeoutId: number | null = null;
    if (hasCopied) {
      timeoutId = window.setTimeout(() => {
        setHasCpied(false);
      }, 1500);
    }

    return () => {
      if (timeoutId) {
        window.clearTimeout(timeoutId);
      }
    };
  }, [hasCopied]);

  return {
    onCopy,
    hasCopied,
    isError: !!error,
    error,
  };
};

useCopyClipboard hooks から返される onCopy に渡した文字列をクリップボードにコピーするシンプルな hooks です (※ Chakra UI の useClipboard を参考にしました)

input に入力された文字列をクリップボードにコピーする

import { useRef } from "react";
import { useCopyClipboard } from "./hooks/useCopyClipboard";

export default function App() {
  const inputRef = useRef<HTMLInputElement>(null);
  const { onCopy, hasCopied, isError } = useCopyClipboard();

  const handleCopyClipboard = () => {
    if (!inputRef.current) {
      return;
    }
    onCopy(inputRef.current.value);
  };

  return (
    <div className="App">
      <input ref={inputRef} type="text" defaultValue=""  />
      <button type="button" onClick={handleCopyClipboard}>
        {hasCopied ? "Copied!" : "Copy!"}
      </button>
      {isError && <small className="text-error">Failed to copy<small>}
    </div>
  );
}

Sample

iframe だとクリップボードにコピーできないので codesandbox を直接開いてください

ライブラリを使うこと無くクリップボードに任意のテキストをコピーさせることができてびっくりしました!
と、同時に Clipboard API にはブラウザのサポートが完全なわけではないですがクリップボードから読み込みもあるので、evil なサイトではクリップボードの入力値を不正に API にも送るとか出来てしまうのかな〜なども気になるところでした。

Clipboard API - Browser compatibility | MDN Clipboard API - Web APIs | MDN

evil な使い方が出来てしまうんので navigator.clipboard.readText はサポートがマチマチなのかも?

おわり ₍ᐢ. ̫.ᐢ₎


[参考]

ES2023 JavaScript 配列から特定の要素を削除するのに toSpliced が便利

フロントエンドでやることはだいたい配列の操作だと感じてます。
その中でも元の配列を非破壊に特定のインデックスの要素を配列から削除するのが意外と面倒でした

今まで方法

1. filter で削除する要素を取り除く

const result = array.filter((item) => item.id !== deleteID);

この方法は非破壊に要素を削除した配列を削除できるが、配列を全て捜査する分処理が無駄になる

2. 削除する index を探して slice で取り除く

const deleteIndex = array.findIndex((item) => item.id === deleteID);
const result = [
  ...array.slice(0, deleteIndex),
  ...array.slice(deleteIndex + 1)
];

削除する要素の index を見つけるまで配列を捜査して、slice で配列から削除する要素を除いた配列を作成する方法
slice で index の要素を削除した配列を作成するコードが若干見通しが悪い

3. 配列を先にコピーして splice で要素を削除する

const deleteIndex = array.findIndex((item) => item.id === deleteID);
const copyArray = […array];
// or const Array = structuredClone(array); (deep copy)
copyArray.splice(deleteIndex, 1);
const result = copyArray;

.splice(deleteIndex, 1) で特定の index の要素を削除できるが、splice() は元の配列を変更してしまう破壊メソッドなので、先に配列を スプレッド構文 (Shallow copy)structuredClone (Deep copy) コピーしておく必要がある

toSpliced() メソッド

toSpliced() は Array インスタンスのメソッドで、 splice() メソッドに対応するコピーメソッドです。これは、指定された位置の要素を除去したり置き換えたりした新しい配列を返します。
cf. Array.prototype.toSpliced() - JavaScript | MDN

toSpliced() メソッドは splice() メソッドでできる配列の要素の削除・追加・置き換えを非破壊的にでき、新しい配列を返してくれる

const deleteIndex = array.findIndex((item) => item.id === deleteID);
const result = array.toSpliced(deleteIndex, 1);

従来の方法の 2, 3 を合わせてシンプルにしたように書ける!

Tips toSpliced()splice() の挙動の違い

splice() を使うことはあまりないと思いますが、破壊/非破壊だけでなく返り値に違いがあるので注意が必要です

  • toSpliced() は元の配列を変化させない (非破壊)・新しい配列を返す
  • splice() は元の配列を変化させる (破壊)・削除した要素の配列を返す
Sample

要素の削除

// toSpliced
const a1 = [1, 2, 3, 4, 5];
const res1 = a1.toSpliced(1, 2);

console.log(a1);   // [1, 2, 3, 4, 5]
console.log(res1); // [1, 4, 5]

// splice
const a2 = [1, 2, 3, 4, 5];
const res2 = a2.splice(1, 2);

console.log(a2);   // [1, 4, 5]
console.log(res2); // [2, 3]

要素を削除して追加

// toSpliced
const a1 = [1, 2, 3, 4, 5];
const res1 = a1.toSpliced(1, 2, 'a', 'b', 'c');

console.log(a1);   // [1, 2, 3, 4, 5]
console.log(res1); // [1, 'a', 'b', 'c', 4, 5]

// splice
const a2 = [1, 2, 3, 4, 5];
const res2 = a1.splice(1, 2, 'a', 'b', 'c');

console.log(a2);   // [1, 'a', 'b', 'c', 4, 5]
console.log(res2); // [2, 3]

これからは配列からの要素の削除は toSpliced() を使って見通しよく書いていけそうで嬉しいです!
配列からの削除めんどいなと思って調べなかったら気づくこと無く slicefilter で書いているところでした… ES2022, ES2023 で配列の非破壊メソッドが結構追加されているようなので、サボらずちゃんとキャッチアップしようと思ったのでした

おわり₍ᐢ. ̫.ᐢ₎


[参考]

ダンジョン飯の 九井 諒子 さん「竜の学校は山の上」の人だったのか!

SVG に alt の代わりになる属性を付けたい

React / Next で SVGコンポーネントとして使うことが多かったのだけれど、SEO が必要なメディアの制作で SVG に image のような alt が設定できないか気になって調べてみたメモ

img タグを使う

<img src="/path/to/logo.svg" alt="ロゴ" />

font-size で大きさを変えたり、color で色を変えたりする必要のない画像であれば通常の img タグを使えば、従来どおり画像として認識され alt 属性が SEO にも効く。ロゴなど動的な変化が必要ない SVG 画像ならこのパターンがシンプルで良さそう

Inline SVG

fill="current color" にしておいて color で色を動的に変えたりできるのが SVG の強みだと思っている。
SVG を React Component として読み込ませた場合は Inline SVG になるので、先の img タグのように alt 属性を使うことは出来ない。

Search engines don’t index inline images. For the type of images used (e.g. icons), for most websites they’re not the sort of images that should be indexed anyway.
cf. Inline SVG Images: Page Speed & SEO Guide // Salience

Currently at the time of writing, we'd recommend you to go with <img> tags as the simplest option and <object> tags only if you require interactivity in your graphics. Avoid Inline SVG in order to be indexed in Google Image.
cf. Best Practices for SVG SEO in Google Image

最近の記事はあまり見つけられなかったが、どうやら Inline SVG は画像として Google にはインデックスされないっぽい。
color を変更したいなどのインタラクティブ性が必要な SVG 画像はボタンのアイコンなどになるはずなので、SEO 対策よりアクセシビリティとして有用かどうかを考えたほうが良さそう

Note that while some screen readers may ignore an SVG if it has no role or accessible name, other screen readers may still find the element and announce it as a “group” without an accessible name. It’s best to avoid these situations by always using aria-hidden="true" if the SVG is meant to be decorative.
cf. Contextually Marking up accessible images and SVGs | scottohara.me

role か accessible name を持たない場合、スクリーンリーダーは SVG を無視する場合があるので意味を持つ SVG には設定をしておくのが良さそう

role="img"

<svg role="img"></svg>

role="img" があればスクリーンリーダーが画像またはグラフィックと認識するっぽい

role="img" + title タグ

<svg role="imge">
  <title>Button</title>
</svg>

role="img" + title タグは画像として扱われ title タグの中身がスクリーンリーダーに読み上げられ、hover 時にブラウザデフォルトの tooltip で title の内容が表示される

tooltip が表示されるのでアイコンボタンなどで使う場合はボタンにマウスオーバーした際に SVG の上にマウスがあると意図しない tooltip が表示されてしまう可能性があるので、SVGコンポーネントとして使い回す場合は扱いが難しそう

import EditIcon from './editIcon.svg';

const EditIconButton: FC = () => {
  return (
    <button title="Edit this article">
      <EditIcon />
    </button>
  );
};

// editIcon.svg
<svg role="img">
  <title>Edit</title>
</svg>

上記の例では <EditIconButton />Edit this article を title (tooltip) として表示させたいが、内部の <EditIcon /> コンポーネントは Inline SVG<title> 要素があるために、アイコンの上にマウスオーバーしていると Edit が tooltip として表示されてしまう

また title はスクリンリーダーで読み上げられないケースがあるらしい

また、インタラクティブな要素内の <svg> 要素の <title> 要素は、ブラウザとスクリーンリーダーの組み合わせによっては読み上げられないことがあります。実際にMac VoiceOverを使用した検証において、Google Chromeでは問題なく読み上げられましたが、Safariでは読み上げられませんでした。
cf. アイコンボタンのアクセシブルな名前はボタンが持つべきかアイコンが持つべきか

role="img" + arie-label

<svg role="img" aria-label="edit"></svg>

aria-label属性
この属性で指定したラベルは画面上には表示されません。この属性は、画面には表示させずに支援技術に対してのみラベルを設定しておきたい場合に利用できます(この属性で指定したラベルはスクリーンリーダーなどでは読み上げられます)。
cf. 知っていると便利なaria-label属性の使い方 – ofujimiki.jp

aria-label 属性はスクリーンリーダーで読み上げられるが <title> 要素と異なり tooltip として表示されることはないので、SVGアイコンをコンポーネント化してボタンのアイコンなどに使う場合でもこちらの方が属性を props で渡せるので良さそう

import EditIcon from './editIcon.svg';

const EditIconButton: FC = () => {
  return (
    <button title="Edit this article">
      <EditIcon aria-label="Edit this article" />
    </button>
  );
};

// editIcon.svg
<svg role="img" aria-label="edit"></svg>

React Component として読み込んだ SVG に props で aria-label を渡せば、出力される Inline SVGaria-label 属性は props で渡したものに上書きされるので、SVG そのものにデフォルトの aria-label を持たせて利用用途に合わせて props で aria-label を変更するといった使い方ができそうです!

結論

  1. ロゴなど検索結果に画像インデックスさせたい・SEO 用途の場合は <img> 要素を使って SVG を読み込ませて、alt 属性に SEO 用のテキストを書く
  2. 意味のあるアイコンやグラフィックとして使う場合は SVGrole="img" + aria-label を設定する

のが良さそうかな〜と思いました。
SEOアクセシビリティもめちゃめちゃ詳しいわけではないので、誤りなどあればご指摘ください!

おわり


[参考]