diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index e8b51bf6..1eceba15 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -2,6 +2,7 @@ import type { Decorator, Preview } from '@storybook/nextjs-vite';
import { useEffect, useRef, type ReactNode } from 'react';
import { ThemeProvider, useTheme } from 'next-themes';
import { useGlobals } from 'storybook/preview-api';
+import { INITIAL_VIEWPORTS } from 'storybook/viewport';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
import '@/styles/globals.css';
@@ -87,6 +88,9 @@ const preview: Preview = {
},
parameters: {
layout: 'fullscreen',
+ viewport: {
+ options: INITIAL_VIEWPORTS,
+ },
nextjs: {
appDirectory: true,
navigation: {
diff --git a/src/components/Common/BreadCrumb/index.stories.tsx b/src/components/Common/BreadCrumb/index.stories.tsx
index 7a5f6eae..4f51cc17 100644
--- a/src/components/Common/BreadCrumb/index.stories.tsx
+++ b/src/components/Common/BreadCrumb/index.stories.tsx
@@ -31,5 +31,5 @@ export const LongTitle: Story = {
},
},
},
- globals: { viewport: { value: 'mobile1', isRotated: false } },
+ globals: { viewport: { value: 'iphone13promax', isRotated: false } },
};
diff --git a/src/components/Common/CodeBlock/index.stories.tsx b/src/components/Common/CodeBlock/index.stories.tsx
index d366231d..3eedea2d 100644
--- a/src/components/Common/CodeBlock/index.stories.tsx
+++ b/src/components/Common/CodeBlock/index.stories.tsx
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
+import { expect } from 'storybook/test';
import CodeBlock, { codeBlockStyles } from '.';
const sampleCode = `export function greeting(name: string) {
@@ -20,6 +21,17 @@ export const Default: Story = {
},
};
+export const WithFilename: Story = {
+ args: {
+ filename: 'greeting.ts',
+ children: {sampleCode},
+ },
+ play: async ({ canvas }) => {
+ await expect(canvas.getByText('greeting.ts')).toBeVisible();
+ await expect(canvas.getByRole('region', { name: 'コードブロック: greeting.ts' })).toBeVisible();
+ },
+};
+
export const LongLine: Story = {
args: {
children: (
diff --git a/src/components/Common/CodeBlock/index.tsx b/src/components/Common/CodeBlock/index.tsx
index 9a6d3d52..56d466e7 100644
--- a/src/components/Common/CodeBlock/index.tsx
+++ b/src/components/Common/CodeBlock/index.tsx
@@ -6,7 +6,9 @@ import styles from './index.module.css';
/* eslint-disable jsx-a11y/no-noninteractive-tabindex -- Scrollable code regions need keyboard focus. */
-type Props = ComponentPropsWithoutRef<'pre'>;
+type Props = ComponentPropsWithoutRef<'pre'> & {
+ filename?: string;
+};
export const getReactNodeText = (node: ReactNode): string => {
if (typeof node === 'string' || typeof node === 'number') {
@@ -24,19 +26,20 @@ export const getReactNodeText = (node: ReactNode): string => {
return '';
};
-export default function CodeBlock({ children, className, ...props }: Props) {
+export default function CodeBlock({ children, className, filename, ...props }: Props) {
const [wrapped, setWrapped] = useState(false);
const wrapClassName = wrapped ? styles.wrapped : styles.unwrapped;
return (
table th {
background-color: var(--color-bg-sub);
+ color: var(--color-theme-light-text);
text-align: left;
padding: 1rem;
}
diff --git a/src/components/Common/Layouts/Header/index.stories.tsx b/src/components/Common/Layouts/Header/index.stories.tsx
index 965f2537..c8c5529f 100644
--- a/src/components/Common/Layouts/Header/index.stories.tsx
+++ b/src/components/Common/Layouts/Header/index.stories.tsx
@@ -29,7 +29,7 @@ export const Desktop: Story = {
};
export const Mobile: Story = {
- globals: { viewport: { value: 'mobile1', isRotated: false } },
+ globals: { viewport: { value: 'iphone13promax', isRotated: false } },
play: async ({ canvas }) => {
await expect(canvas.getByRole('button', { name: 'メニューを開く' })).toBeVisible();
},
diff --git a/src/components/Common/Layouts/ThemeSwitch/index.stories.tsx b/src/components/Common/Layouts/ThemeSwitch/index.stories.tsx
index 2a790d24..dbbc64b6 100644
--- a/src/components/Common/Layouts/ThemeSwitch/index.stories.tsx
+++ b/src/components/Common/Layouts/ThemeSwitch/index.stories.tsx
@@ -1,5 +1,4 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
-import { expect, userEvent } from 'storybook/test';
import ThemeSwitch from '.';
const meta = {
@@ -11,16 +10,4 @@ const meta = {
export default meta;
type Story = StoryObj;
-export const Default: Story = {
- play: async ({ canvas }) => {
- const switchToDarkButton = await canvas.findByRole('button', {
- name: 'ダークテーマに切り替え',
- });
-
- await userEvent.click(switchToDarkButton);
-
- await expect(
- canvas.getByRole('button', { name: 'ライトテーマに切り替え' }),
- ).toBeInTheDocument();
- },
-};
+export const Default: Story = {};
diff --git a/src/components/Common/Markdown/index.stories.tsx b/src/components/Common/Markdown/index.stories.tsx
index 4ee667ae..def9e6f1 100644
--- a/src/components/Common/Markdown/index.stories.tsx
+++ b/src/components/Common/Markdown/index.stories.tsx
@@ -5,13 +5,11 @@ const content = `## 固定ページの見出し
本文には[内部リンク](/profile)と[外部リンク](https://example.com)を含められます。
-### リストと引用
+### リスト
- ひとつ目の項目
- ふたつ目の項目
-> 引用文の表示例です。
-
### コード
~~~ts
@@ -30,5 +28,5 @@ export default meta;
type Story = StoryObj;
export const FixedPageContent: Story = {
- args: { content, headingIds: ['page-heading', 'list-and-quote', 'code'] },
+ args: { content, headingIds: ['page-heading', 'list', 'code'] },
};
diff --git a/src/components/Common/TableOfContent/index.stories.tsx b/src/components/Common/TableOfContent/index.stories.tsx
index 26f10703..6caca455 100644
--- a/src/components/Common/TableOfContent/index.stories.tsx
+++ b/src/components/Common/TableOfContent/index.stories.tsx
@@ -19,7 +19,16 @@ type Story = StoryObj;
export const Default: Story = { args: { headings } };
-export const Sidebar: Story = { args: { headings, sidebar: true } };
+export const Sidebar: Story = {
+ args: { headings, sidebar: true },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
export const DeepHierarchy: Story = {
args: {
@@ -30,5 +39,5 @@ export const DeepHierarchy: Story = {
{ id: 'verification', title: '動作確認で確認しておきたい長い見出し', level: 2 },
],
},
- globals: { viewport: { value: 'mobile1', isRotated: false } },
+ globals: { viewport: { value: 'iphone13promax', isRotated: false } },
};
diff --git a/src/components/Features/Article/Elements/Plugins/CustomHtml/index.module.css b/src/components/Features/Article/Elements/Plugins/CustomHtml/index.module.css
index e80ebace..715b6895 100644
--- a/src/components/Features/Article/Elements/Plugins/CustomHtml/index.module.css
+++ b/src/components/Features/Article/Elements/Plugins/CustomHtml/index.module.css
@@ -123,6 +123,11 @@
outline-offset: var(--focus-ring-offset);
}
+.content iframe[data-focus-visible='true'] {
+ outline: var(--focus-ring-width) solid var(--color-focus-ring) !important;
+ outline-offset: var(--focus-ring-offset);
+}
+
.content > figure {
margin: 2rem 0;
}
@@ -155,6 +160,7 @@
.content > table th {
background-color: var(--color-bg-sub);
+ color: var(--color-theme-light-text);
text-align: left;
padding: 1rem;
}
diff --git a/src/components/Features/Article/Elements/Plugins/CustomHtml/index.tsx b/src/components/Features/Article/Elements/Plugins/CustomHtml/index.tsx
index 24befaaf..43af2495 100644
--- a/src/components/Features/Article/Elements/Plugins/CustomHtml/index.tsx
+++ b/src/components/Features/Article/Elements/Plugins/CustomHtml/index.tsx
@@ -43,6 +43,27 @@ function CustomHtml({ html, sponsorUrl }: Props) {
const cleanupMoshimoEasyLinkFallback = setupMoshimoEasyLinkFallback(content);
+ let iframeFocusFrame: number | undefined;
+ const clearIframeFocus = () => {
+ content.querySelectorAll('iframe[data-focus-visible]').forEach((iframe) => {
+ iframe.removeAttribute('data-focus-visible');
+ });
+ };
+ const syncIframeFocus = () => {
+ window.cancelAnimationFrame(iframeFocusFrame ?? 0);
+ iframeFocusFrame = window.requestAnimationFrame(() => {
+ clearIframeFocus();
+ const activeElement = document.activeElement;
+
+ if (activeElement instanceof HTMLIFrameElement && content.contains(activeElement)) {
+ activeElement.dataset.focusVisible = 'true';
+ }
+ });
+ };
+
+ window.addEventListener('blur', syncIframeFocus);
+ window.addEventListener('focus', clearIframeFocus);
+
const observer = new MutationObserver(() => {
syncCustomHtmlEnhancements();
});
@@ -61,6 +82,10 @@ function CustomHtml({ html, sponsorUrl }: Props) {
return () => {
cleanupMoshimoEasyLinkFallback();
+ window.cancelAnimationFrame(iframeFocusFrame ?? 0);
+ window.removeEventListener('blur', syncIframeFocus);
+ window.removeEventListener('focus', clearIframeFocus);
+ clearIframeFocus();
observer.disconnect();
window.clearTimeout(timer);
};
diff --git a/src/components/Features/Article/Elements/Plugins/ImageSlider/index.stories.tsx b/src/components/Features/Article/Elements/Plugins/ImageSlider/index.stories.tsx
index f984a5ae..cb7a3d3d 100644
--- a/src/components/Features/Article/Elements/Plugins/ImageSlider/index.stories.tsx
+++ b/src/components/Features/Article/Elements/Plugins/ImageSlider/index.stories.tsx
@@ -26,7 +26,9 @@ type Story = StoryObj;
export const MultipleImages: Story = {};
export const SingleImage: Story = { args: { block: { image_slider: [images[0]] } } };
-export const Mobile: Story = { globals: { viewport: { value: 'mobile1', isRotated: false } } };
+export const Mobile: Story = {
+ globals: { viewport: { value: 'iphone13promax', isRotated: false } },
+};
export const NextImageInteraction: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
diff --git a/src/components/Features/Article/Elements/Plugins/RichText/index.module.css b/src/components/Features/Article/Elements/Plugins/RichText/index.module.css
index 473c809a..ef91c1c3 100644
--- a/src/components/Features/Article/Elements/Plugins/RichText/index.module.css
+++ b/src/components/Features/Article/Elements/Plugins/RichText/index.module.css
@@ -148,6 +148,7 @@
.content > table th {
background-color: var(--color-bg-sub);
+ color: var(--color-theme-light-text);
text-align: left;
padding: 1rem;
}
diff --git a/src/components/Features/Article/Elements/Plugins/RichText/index.stories.tsx b/src/components/Features/Article/Elements/Plugins/RichText/index.stories.tsx
index f0ec8ddd..9eb40784 100644
--- a/src/components/Features/Article/Elements/Plugins/RichText/index.stories.tsx
+++ b/src/components/Features/Article/Elements/Plugins/RichText/index.stories.tsx
@@ -24,7 +24,7 @@ const tableHtml = `
`;
const articleHtml = `
- 本文には強調やリンクが含まれます。
+ 本文には強調、青マーカー、赤マーカー、黄マーカー、inlineCode()、リンクが含まれます。
見出し2
段落の余白と行間を確認するための文章です。
見出し3
diff --git a/src/components/Features/Article/Elements/Plugins/SpeechBubble/index.stories.tsx b/src/components/Features/Article/Elements/Plugins/SpeechBubble/index.stories.tsx
index f2e662d6..a4a4c14c 100644
--- a/src/components/Features/Article/Elements/Plugins/SpeechBubble/index.stories.tsx
+++ b/src/components/Features/Article/Elements/Plugins/SpeechBubble/index.stories.tsx
@@ -25,5 +25,5 @@ export const LongText: Story = {
'文章が長い場合でも、画像と吹き出しの位置関係を保ちながら自然に折り返されることを確認します。スマートフォンの画面幅でも内容がはみ出さないことが重要です。',
},
},
- globals: { viewport: { value: 'mobile1', isRotated: false } },
+ globals: { viewport: { value: 'iphone13promax', isRotated: false } },
};
diff --git a/src/components/Features/Article/Elements/Plugins/SpeechBubble/index.tsx b/src/components/Features/Article/Elements/Plugins/SpeechBubble/index.tsx
index ff34ddff..871bbf35 100644
--- a/src/components/Features/Article/Elements/Plugins/SpeechBubble/index.tsx
+++ b/src/components/Features/Article/Elements/Plugins/SpeechBubble/index.tsx
@@ -15,6 +15,7 @@ export default function SpeechBubble({ block }: Props) {

対象となる利用環境を確認する 必要な設定を済ませる 表示と動作を確認する ',
+ },
+ common: true,
+ },
+};
diff --git a/src/components/Features/Article/Elements/Plugins/TabBox/index.tsx b/src/components/Features/Article/Elements/Plugins/TabBox/index.tsx
index cd49ce94..10b15269 100644
--- a/src/components/Features/Article/Elements/Plugins/TabBox/index.tsx
+++ b/src/components/Features/Article/Elements/Plugins/TabBox/index.tsx
@@ -36,7 +36,7 @@ export default function TabBox({
return (
<>
{merit && (
<>
diff --git a/src/components/Features/Article/Elements/Plugins/__tests__/Plugins.test.tsx b/src/components/Features/Article/Elements/Plugins/__tests__/Plugins.test.tsx
index 3ae394ae..d4802a89 100644
--- a/src/components/Features/Article/Elements/Plugins/__tests__/Plugins.test.tsx
+++ b/src/components/Features/Article/Elements/Plugins/__tests__/Plugins.test.tsx
@@ -141,6 +141,21 @@ describe('Article plugins', () => {
).toHaveAttribute('aria-pressed', 'false');
});
+ it('marks a focused custom html iframe for a visible focus indicator', async () => {
+ renderCustomHtml('');
+
+ const iframe = screen.getByTitle('Embedded content');
+ iframe.focus();
+ fireEvent.blur(window);
+
+ await waitFor(() => {
+ expect(iframe).toHaveAttribute('data-focus-visible', 'true');
+ });
+
+ fireEvent.focus(window);
+ expect(iframe).not.toHaveAttribute('data-focus-visible');
+ });
+
it('renders rich text and injects article ad units before h2 sections', () => {
render(
{
const image = container.querySelector('img');
expect(image).toHaveAttribute('alt', '');
expect(image).toHaveAttribute('src', expect.stringContaining('w=150'));
+ expect(image).toHaveAttribute('src', expect.stringContaining('h=150'));
+ expect(image).toHaveAttribute('src', expect.stringContaining('fit=crop'));
});
it('renders each tab box variant from its corresponding html field', () => {
diff --git a/src/styles/plugin.css b/src/styles/plugin.css
index e75be01f..1e3f1eeb 100644
--- a/src/styles/plugin.css
+++ b/src/styles/plugin.css
@@ -1,30 +1,24 @@
/* マーカー */
.underline_blue {
- background: linear-gradient(transparent 70%, skyblue 70%);
+ background: linear-gradient(transparent 70%, rgb(135 206 235 / 35%) 70%);
}
.underline_red {
- background: linear-gradient(transparent 70%, pink 70%);
+ background: linear-gradient(transparent 70%, rgb(255 192 203 / 35%) 70%);
}
.underline_yellow {
- background: linear-gradient(transparent 70%, yellow 70%);
+ background: linear-gradient(transparent 70%, rgb(255 255 0 / 35%) 70%);
}
.underline_code {
- background-color: #f7f7fc;
+ background-color: var(--color-bg-sub);
border: 1px solid var(--color-theme-border);
- border-radius: 3px;
- color: #2b2c30;
+ border-radius: var(--radius-small);
+ color: var(--color-theme-light-text);
margin: 0 2px;
padding: 2px 4px;
- font-family:
- Menlo,
- Monaco,
- Consolas,
- Liberation Mono,
- Courier New,
- monospace;
+ font-family: var(--font-family-content-code);
}
/* テーブル */
@@ -35,7 +29,8 @@ table {
table tr th {
border: 1px solid var(--color-theme-border);
- background-color: var(--color-accent-soft-bg) !important;
+ background-color: var(--color-bg-sub) !important;
+ color: var(--color-theme-light-text);
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
@@ -64,6 +59,10 @@ table tr td {
}
.bubble-image {
+ display: block;
+ width: 75px;
+ height: 75px;
+ object-fit: cover;
border-radius: 50%;
margin: 0 10px;
}
@@ -110,6 +109,7 @@ table tr td {
.bubble-image-wrapper {
display: flex;
align-items: center;
+ flex-shrink: 0;
}
.bubble-image-wrapper.left {
diff --git a/vitest.config.mts b/vitest.config.mts
index 4fc4b70b..d2ed0a31 100644
--- a/vitest.config.mts
+++ b/vitest.config.mts
@@ -14,7 +14,7 @@ export default defineConfig(async () => {
},
},
optimizeDeps: {
- include: ['react-slick'],
+ include: ['react-slick', 'storybook/viewport'],
},
test: {
projects: [