Web Components で作るアクセシブルなタブ UI:キーボード操作& ARIA 完備
Web アプリケーションのユーザーインターフェースにおいて、タブコンポーネントは非常によく使われるパターンです。しかし、見た目は美しくても、キーボード操作ができなかったり、スクリーンリーダーで正しく読み上げられなかったりと、アクセシビリティに課題を抱えているケースが少なくありません。
今回は、Web Components を活用して、WAI-ARIA のタブパターンに準拠した完全にアクセシブルなタブ UI を実装する方法をご紹介します。キーボード操作やフォーカス管理、ARIA 属性の正しい使い方まで、実践的なコード例とともに解説していきますね。
背景
Web Components は、再利用可能なカスタム要素を作成できる標準技術として、モダンな Web 開発で広く採用されています。フレームワークに依存せず、Shadow DOM によってスタイルやスクリプトをカプセル化できるため、コンポーネントライブラリの構築に最適です。
一方、アクセシビリティは Web サイトやアプリケーションを、障害の有無に関わらず誰もが利用できるようにするための取り組みです。W3C が策定する WAI-ARIA(Accessible Rich Internet Applications)は、動的なコンテンツやウィジェットに対して、支援技術が理解できる情報を付与するための仕様になります。
Web Components とアクセシビリティの関係
以下の図は、Web Components でアクセシブルなタブ UI を構築する際の技術要素を示しています。
mermaidflowchart TB
wc["Web Components"] --> shadow["Shadow DOM<br/>スタイル・構造の<br/>カプセル化"]
wc --> custom["Custom Elements<br/>独自タグの定義"]
wc --> template["Template要素<br/>再利用可能な<br/>HTML構造"]
aria["WAI-ARIA"] --> roles["Role属性<br/>要素の役割を定義"]
aria --> states["State/Property<br/>状態・プロパティ"]
aria --> keyboard["Keyboard<br/>Navigation<br/>キーボード操作"]
shadow --> accessible["アクセシブルな<br/>タブUI"]
custom --> accessible
template --> accessible
roles --> accessible
states --> accessible
keyboard --> accessible
Web Components と WAI-ARIA を組み合わせることで、再利用性とアクセシビリティを両立したコンポーネントが実現できます。
課題
従来のタブ UI 実装では、以下のようなアクセシビリティ上の問題が発生しがちです。
マウス操作のみに対応
多くの実装では、クリックイベントのみを処理し、キーボード操作が考慮されていません。これにより、キーボードユーザーや支援技術を使用するユーザーが操作できなくなってしまいます。
ARIA 属性の不足または誤用
role="tab" や aria-selected などの ARIA 属性が適切に設定されていない場合、スクリーンリーダーは要素の役割や状態を正しく伝えられません。
フォーカス管理の不備
タブキーで移動する際に、すべてのタブボタンがタブストップになってしまうと、キーボードナビゲーションの効率が著しく低下します。WAI-ARIA のタブパターンでは、矢印キーでタブを切り替え、Tab キーでタブリストとタブパネルを移動する「ローミングタブインデックス」という手法が推奨されています。
視覚的なフォーカスインジケーターの欠如
:focus 状態のスタイリングが不十分だと、キーボードユーザーは現在どこにフォーカスがあるのか把握できません。
以下の図は、アクセシビリティ課題とその影響範囲を示しています。
mermaidflowchart LR
issues["アクセシビリティ<br/>課題"] --> mouse["マウス操作<br/>のみ"]
issues --> aria_lack["ARIA属性<br/>不足"]
issues --> focus["フォーカス管理<br/>不備"]
issues --> visual["視覚的<br/>フィードバック<br/>欠如"]
mouse --> keyboard_user["キーボード<br/>ユーザー"]
aria_lack --> screen_reader["スクリーン<br/>リーダー<br/>ユーザー"]
focus --> nav_inefficient["ナビゲーション<br/>非効率"]
visual --> confusion["操作位置<br/>不明"]
keyboard_user --> impact["利用困難"]
screen_reader --> impact
nav_inefficient --> impact
confusion --> impact
これらの課題を解決するには、WAI-ARIA のガイドラインに準拠した実装が不可欠です。
解決策
WAI-ARIA のタブパターンに準拠したアクセシブルなタブ UI を実装するためには、以下の要素を組み込む必要があります。
適切な ARIA 属性の設定
タブリスト、タブボタン、タブパネルそれぞれに適切な role と状態属性を設定します。
| # | 要素 | role 属性 | 主な ARIA 属性 | 説明 |
|---|---|---|---|---|
| 1 | タブリスト | tablist | aria-label または aria-labelledby | タブボタンの親コンテナ |
| 2 | タブボタン | tab | aria-selected、aria-controls、tabindex | 各タブの切り替えボタン |
| 3 | タブパネル | tabpanel | aria-labelledby、tabindex | タブに対応するコンテンツ領域 |
キーボード操作のサポート
WAI-ARIA ガイドラインで推奨される以下のキーボード操作を実装します。
| # | キー | 動作 | 実装のポイント |
|---|---|---|---|
| 1 | 右矢印 / 下矢印 | 次のタブへフォーカス移動 | 最後のタブで最初のタブへループ |
| 2 | 左矢印 / 上矢印 | 前のタブへフォーカス移動 | 最初のタブで最後のタブへループ |
| 3 | Home | 最初のタブへフォーカス移動 | 即座に先頭へジャンプ |
| 4 | End | 最後のタブへフォーカス移動 | 即座に末尾へジャンプ |
| 5 | Tab | タブパネルへフォーカス移動 | タブリストから抜ける |
| 6 | Enter / Space | タブを選択・アクティブ化 | 自動選択モードでは不要な場合も |
以下の図は、キーボード操作のフローを示しています。
mermaidstateDiagram-v2
[*] --> TabList: Tabキーで入場
TabList --> Tab1: 最初のタブに<br/>フォーカス
Tab1 --> Tab2: 右矢印/下矢印
Tab2 --> Tab3: 右矢印/下矢印
Tab3 --> Tab1: 右矢印<br/>(ループ)
Tab3 --> Tab2: 左矢印/上矢印
Tab2 --> Tab1: 左矢印/上矢印
Tab1 --> Tab3: 左矢印<br/>(ループ)
Tab1 --> TabPanel1: Enter/Spaceで<br/>選択&Tabで移動
Tab2 --> TabPanel2: Enter/Spaceで<br/>選択&Tabで移動
Tab3 --> TabPanel3: Enter/Spaceで<br/>選択&Tabで移動
TabPanel1 --> [*]: Shift+Tabで<br/>タブリストへ戻る
ローミングタブインデックス
タブリスト内では、選択されているタブのみが tabindex="0"(タブストップ対象)となり、他のタブは tabindex="-1"(フォーカス可能だがタブストップ外)に設定します。これにより、Tab キーでページ内を移動する際の効率が向上します。
自動選択 vs 手動選択
タブパターンには 2 つのモードがあります。
| # | モード | 動作 | 利点 | 欠点 |
|---|---|---|---|---|
| 1 | 自動選択 | 矢印キーでフォーカス移動すると同時にタブが選択される | 操作がシンプル、ステップ数が少ない | タブパネルの内容が重い場合に負荷 |
| 2 | 手動選択 | 矢印キーでフォーカス移動後、Enter/Space で選択 | タブパネルの読み込みを制御可能 | 操作ステップが増える |
本記事では、より一般的な自動選択モードを実装します。
Web Components での実装戦略
Web Components を使うことで、以下のメリットが得られます。
- 再利用性:どのプロジェクトでも
<accessible-tabs>タグを使うだけで利用可能 - カプセル化:Shadow DOM により、外部スタイルの影響を受けない
- 保守性:タブのロジックとスタイルが 1 つのコンポーネントにまとまる
具体例
ここからは、実際のコードを段階的に見ていきましょう。完全なアクセシブルタブコンポーネントを構築します。
HTML 構造の定義
まず、タブコンポーネントの基本的な HTML 構造を定義します。以下は、コンポーネントの使用例です。
html<!-- タブコンポーネントの使用例 -->
<accessible-tabs>
<button slot="tab">プロフィール</button>
<div slot="panel">
<h3>プロフィール情報</h3>
<p>ユーザーの基本情報を表示します。</p>
</div>
<button slot="tab">設定</button>
<div slot="panel">
<h3>設定画面</h3>
<p>アカウント設定を変更できます。</p>
</div>
<button slot="tab">通知</button>
<div slot="panel">
<h3>通知一覧</h3>
<p>最新の通知を確認できます。</p>
</div>
</accessible-tabs>
slot 属性を使うことで、タブボタンとパネルを柔軟に定義できます。
Web Component クラスの基本構造
次に、Web Component のクラス定義を作成します。まずは基本的な構造から見ていきましょう。
typescript// accessible-tabs.ts
// タブコンポーネントのクラス定義
class AccessibleTabs extends HTMLElement {
// プライベートプロパティ
private tablist: HTMLElement | null = null;
private tabs: HTMLElement[] = [];
private panels: HTMLElement[] = [];
private selectedIndex: number = 0;
constructor() {
super();
// Shadow DOM を作成
this.attachShadow({ mode: 'open' });
}
// コンポーネントが DOM に追加されたときに呼ばれる
connectedCallback() {
this.render();
this.setupTabs();
this.attachEventListeners();
}
}
コンストラクタで Shadow DOM を作成し、connectedCallback でコンポーネントの初期化処理を行います。
Shadow DOM のレンダリング
Shadow DOM 内に、タブリストとパネルを配置するテンプレートを作成します。
typescript// render メソッド:Shadow DOM の構築
private render(): void {
if (!this.shadowRoot) return;
// テンプレートを作成
this.shadowRoot.innerHTML = `
<style>
${this.getStyles()}
</style>
<div class="tabs-container">
<div class="tablist" role="tablist" aria-label="コンテンツタブ">
<slot name="tab"></slot>
</div>
<div class="panels">
<slot name="panel"></slot>
</div>
</div>
`;
}
<slot> を使って、外部から渡されたタブボタンとパネルを配置します。role="tablist" と aria-label を設定することで、スクリーンリーダーがタブリストとして認識できます。
スタイルの定義
タブ UI の見た目とフォーカスインジケーターを定義します。視覚的なフィードバックは、キーボードユーザーにとって非常に重要です。
typescript// getStyles メソッド:CSS スタイルの定義
private getStyles(): string {
return `
.tabs-container {
font-family: system-ui, sans-serif;
}
.tablist {
display: flex;
border-bottom: 2px solid #e5e7eb;
gap: 4px;
}
/* slot された button のスタイル */
::slotted([slot="tab"]) {
padding: 12px 24px;
border: none;
background: transparent;
cursor: pointer;
font-size: 16px;
color: #6b7280;
border-bottom: 3px solid transparent;
transition: all 0.2s;
}
`;
}
::slotted() セレクタを使うことで、slot に配置された要素にスタイルを適用できます。
フォーカスと選択状態のスタイル
続いて、選択状態とフォーカス状態のスタイルを追加します。
typescript// getStyles メソッドの続き
private getStyles(): string {
return `
/* ...前のスタイル... */
/* 選択されたタブ */
::slotted([slot="tab"][aria-selected="true"]) {
color: #2563eb;
border-bottom-color: #2563eb;
font-weight: 600;
}
/* ホバー状態 */
::slotted([slot="tab"]:hover) {
background: #f3f4f6;
}
/* フォーカス状態(キーボード操作時) */
::slotted([slot="tab"]:focus) {
outline: 3px solid #93c5fd;
outline-offset: -3px;
z-index: 1;
}
/* パネルのスタイル */
::slotted([slot="panel"]) {
padding: 24px;
display: none;
}
/* 選択されたパネル */
::slotted([slot="panel"][aria-hidden="false"]) {
display: block;
}
`;
}
:focus スタイルで明確なアウトラインを表示し、キーボードユーザーが現在の位置を把握できるようにしています。
タブとパネルの初期化
次に、slot に配置されたタブとパネルを取得し、ARIA 属性を設定します。
typescript// setupTabs メソッド:タブとパネルの初期化
private setupTabs(): void {
if (!this.shadowRoot) return;
// タブリストを取得
this.tablist = this.shadowRoot.querySelector('[role="tablist"]');
// slot された要素を取得
const tabSlot = this.shadowRoot.querySelector('slot[name="tab"]') as HTMLSlotElement;
const panelSlot = this.shadowRoot.querySelector('slot[name="panel"]') as HTMLSlotElement;
if (tabSlot && panelSlot) {
this.tabs = tabSlot.assignedElements() as HTMLElement[];
this.panels = panelSlot.assignedElements() as HTMLElement[];
}
// 各タブに ARIA 属性を設定
this.setupAriaAttributes();
}
assignedElements() メソッドで、slot に配置された実際の要素を取得できます。
ARIA 属性の設定
タブとパネルに必要な ARIA 属性を設定します。これにより、支援技術が正しく情報を伝えられるようになります。
typescript// setupAriaAttributes メソッド:ARIA 属性の設定
private setupAriaAttributes(): void {
this.tabs.forEach((tab, index) => {
const panel = this.panels[index];
// ユニークな ID を生成
const tabId = `tab-${index}`;
const panelId = `panel-${index}`;
// タブの ARIA 属性
tab.setAttribute('role', 'tab');
tab.setAttribute('id', tabId);
tab.setAttribute('aria-controls', panelId);
tab.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
tab.setAttribute('tabindex', index === 0 ? '0' : '-1');
// パネルの ARIA 属性
panel.setAttribute('role', 'tabpanel');
panel.setAttribute('id', panelId);
panel.setAttribute('aria-labelledby', tabId);
panel.setAttribute('aria-hidden', index === 0 ? 'false' : 'true');
panel.setAttribute('tabindex', '0');
});
}
aria-controls と aria-labelledby で、タブとパネルの関連性を明示しています。最初のタブのみ tabindex="0" とし、ローミングタブインデックスを実装します。
イベントリスナーの登録
タブのクリックとキーボード操作のイベントリスナーを登録します。
typescript// attachEventListeners メソッド:イベントリスナーの登録
private attachEventListeners(): void {
this.tabs.forEach((tab, index) => {
// クリックイベント
tab.addEventListener('click', () => {
this.selectTab(index);
});
// キーボードイベント
tab.addEventListener('keydown', (event) => {
this.handleKeydown(event as KeyboardEvent, index);
});
});
}
クリックとキーボード両方の操作をサポートすることで、すべてのユーザーがタブを操作できます。
タブ選択ロジック
タブが選択されたときの処理を実装します。ARIA 属性の更新とフォーカス管理を行います。
typescript// selectTab メソッド:タブの選択処理
private selectTab(index: number): void {
// 範囲チェック
if (index < 0 || index >= this.tabs.length) return;
// 前のタブの選択を解除
this.tabs[this.selectedIndex].setAttribute('aria-selected', 'false');
this.tabs[this.selectedIndex].setAttribute('tabindex', '-1');
this.panels[this.selectedIndex].setAttribute('aria-hidden', 'true');
// 新しいタブを選択
this.selectedIndex = index;
this.tabs[index].setAttribute('aria-selected', 'true');
this.tabs[index].setAttribute('tabindex', '0');
this.panels[index].setAttribute('aria-hidden', 'false');
// フォーカスを移動
this.tabs[index].focus();
}
選択されていないタブは tabindex="-1" に設定し、選択されたタブのみ tabindex="0" にすることで、ローミングタブインデックスを実現しています。
キーボードナビゲーションの実装
矢印キーや Home/End キーでタブを移動する処理を実装します。
typescript// handleKeydown メソッド:キーボード操作の処理
private handleKeydown(event: KeyboardEvent, currentIndex: number): void {
let newIndex = currentIndex;
switch (event.key) {
case 'ArrowRight':
case 'ArrowDown':
// 次のタブへ(ループ)
newIndex = (currentIndex + 1) % this.tabs.length;
event.preventDefault();
break;
case 'ArrowLeft':
case 'ArrowUp':
// 前のタブへ(ループ)
newIndex = (currentIndex - 1 + this.tabs.length) % this.tabs.length;
event.preventDefault();
break;
}
// インデックスが変更された場合、タブを選択
if (newIndex !== currentIndex) {
this.selectTab(newIndex);
}
}
矢印キーでタブを移動し、最後のタブから最初のタブへループできるようにしています。event.preventDefault() で、ページのスクロールを防止します。
Home/End キーのサポート
さらに、Home キーと End キーで先頭・末尾のタブへジャンプできるようにします。
typescript// handleKeydown メソッドの続き(switch 文内に追加)
private handleKeydown(event: KeyboardEvent, currentIndex: number): void {
let newIndex = currentIndex;
switch (event.key) {
// ...前のケース...
case 'Home':
// 最初のタブへ
newIndex = 0;
event.preventDefault();
break;
case 'End':
// 最後のタブへ
newIndex = this.tabs.length - 1;
event.preventDefault();
break;
}
if (newIndex !== currentIndex) {
this.selectTab(newIndex);
}
}
Home/End キーで瞬時に先頭・末尾へ移動できるため、タブが多い場合でも効率的にナビゲーションできます。
カスタム要素の登録
最後に、カスタム要素として登録します。
typescript// カスタム要素として登録
customElements.define('accessible-tabs', AccessibleTabs);
これで、<accessible-tabs> タグがブラウザで認識され、使用できるようになります。
完成した使用例
以下は、完成したタブコンポーネントの使用例です。
html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>アクセシブルタブデモ</title>
<script src="accessible-tabs.js" defer></script>
</head>
<body>
<main>
<h1>ユーザーダッシュボード</h1>
<accessible-tabs>
<button slot="tab">プロフィール</button>
<div slot="panel">
<h2>プロフィール情報</h2>
<p>名前:山田太郎</p>
<p>メール:yamada@example.com</p>
<button>編集する</button>
</div>
<button slot="tab">設定</button>
<div slot="panel">
<h2>アカウント設定</h2>
<label>
<input type="checkbox" checked />
メール通知を受け取る
</label>
<button>保存</button>
</div>
<button slot="tab">通知</button>
<div slot="panel">
<h2>通知一覧</h2>
<ul>
<li>新しいメッセージが届きました</li>
<li>プロフィールが更新されました</li>
</ul>
</div>
</accessible-tabs>
</main>
</body>
</html>
このコードをブラウザで開くと、完全にアクセシブルなタブ UI が動作します。
アクセシビリティの検証方法
実装したタブコンポーネントが正しくアクセシブルかどうか、以下の方法で検証できます。
| # | 検証項目 | 確認方法 | 期待される動作 |
|---|---|---|---|
| 1 | キーボード操作 | Tab、矢印キー、Home/End を使用 | すべてのタブがキーボードで操作可能 |
| 2 | フォーカスインジケーター | キーボードでフォーカス移動 | 明確なアウトラインが表示される |
| 3 | スクリーンリーダー | NVDA や VoiceOver で読み上げ | タブの役割、状態、パネルが正しく読まれる |
| 4 | ARIA 属性 | ブラウザの開発者ツールで検証 | すべての必須属性が正しく設定されている |
| 5 | 自動テスト | axe DevTools や Lighthouse を実行 | アクセシビリティエラーがゼロ |
特に、スクリーンリーダーでは「タブ、プロフィール、1 / 3、選択済み」のように、役割、ラベル、位置、状態が正しく読み上げられることを確認しましょう。
TypeScript での型定義
TypeScript を使用する場合、より型安全な実装が可能です。
typescript// types.ts
// タブコンポーネントの型定義
interface TabElements {
tab: HTMLElement;
panel: HTMLElement;
id: string;
}
interface TabConfig {
autoActivate?: boolean; // 自動選択モード
orientation?: 'horizontal' | 'vertical'; // タブの向き
}
型定義を追加することで、コードの保守性が向上し、エラーを事前に防げます。
実装のポイント整理
図解:アクセシブルなタブ UI の実装要素と、それぞれが解決する課題の関係性を以下に示します。
mermaidflowchart TD
impl["アクセシブル<br/>タブUI実装"]
impl --> aria["ARIA属性<br/>設定"]
impl --> keyboard["キーボード<br/>操作"]
impl --> focus["フォーカス<br/>管理"]
impl --> visual["視覚的<br/>フィードバック"]
aria --> sr["スクリーンリーダー<br/>対応"]
keyboard --> kb_user["キーボード<br/>ユーザー対応"]
focus --> efficient["効率的<br/>ナビゲーション"]
visual --> clear["明確な<br/>操作状態"]
sr --> accessible_final["すべてのユーザーが<br/>利用可能"]
kb_user --> accessible_final
efficient --> accessible_final
clear --> accessible_final
図で理解できる要点:
- ARIA 属性が支援技術へ情報を提供
- キーボード操作とフォーカス管理が操作性を向上
- 視覚的フィードバックが状態理解を助ける
まとめ
Web Components を使ってアクセシブルなタブ UI を実装する方法を、WAI-ARIA のタブパターンに準拠した形でご紹介しました。
重要なポイントを振り返ると、以下の通りです。
ARIA 属性の適切な使用は、支援技術が要素の役割と状態を理解するために不可欠でした。role="tablist"、role="tab"、role="tabpanel" に加えて、aria-selected、aria-controls、aria-labelledby などの属性を正しく設定することで、スクリーンリーダーユーザーにも情報が正確に伝わります。
キーボード操作のサポートでは、矢印キーでのタブ移動、Home/End キーでの先頭・末尾へのジャンプを実装しました。これにより、マウスを使わないユーザーも快適にタブを操作できるようになります。
ローミングタブインデックスという手法を用いることで、タブリスト内では矢印キーで移動し、Tab キーでタブパネルへ移動するという直感的なナビゲーションが実現できました。これは WAI-ARIA が推奨するベストプラクティスです。
視覚的なフィードバックとして、明確なフォーカスインジケーターと選択状態のスタイリングを実装しました。これにより、キーボードユーザーは現在の位置を常に把握できますね。
Web Components を活用することで、フレームワークに依存しない再利用可能なコンポーネントとして、どんなプロジェクトでも利用できる点も大きなメリットです。
アクセシビリティは、すべてのユーザーに平等な体験を提供するための重要な取り組みです。今回のタブコンポーネントの実装方法を参考に、他の UI コンポーネントにもアクセシビリティを組み込んでいくことで、より多くの人に使いやすい Web アプリケーションが実現できるでしょう。
ぜひ、この実装を基に、あなたのプロジェクトでもアクセシブルなコンポーネント開発に挑戦してみてください。
関連リンク
articleWeb Components で作るアクセシブルなタブ UI:キーボード操作& ARIA 完備
articleWeb Components のスロット設計術:命名付き slot/fallback/`slotchange` の実戦パターン
articleWeb Components イベント設計チート:`CustomEvent`/`composed`/`bubbles` 実例集
articleWeb Components のポリフィル戦略:@webcomponents 系を最小限で入れる判断基準
articleWeb Components と Declarative Shadow DOM の現在地:HTML だけで描くサーバー発 UI
articleWeb Components のパッケージ配布戦略:types/CEM(Custom Elements Manifest)/ドキュメント自動
articleCline で何が自動化できる?設計・実装・テスト・運用のユースケース地図
articleWeb Components で作るアクセシブルなタブ UI:キーボード操作& ARIA 完備
articleVue.js 可観測性:Sentry/OpenTelemetry/Web Vitals で UX を数値化
articleClaude Code SRE 実務:レート制限・キューイング・指数バックオフの実装指針
articleAnsible 実行モデル解体新書:コントローラからターゲットまでの裏側
articleACF かブロックか:WordPress 入力 UI の設計判断と移行戦略
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来