diff --git a/packages/spindle-ui/src/NavigationTab/UnderlineTab/UnderlineTab.css b/packages/spindle-ui/src/NavigationTab/UnderlineTab/UnderlineTab.css new file mode 100644 index 000000000..27983d90e --- /dev/null +++ b/packages/spindle-ui/src/NavigationTab/UnderlineTab/UnderlineTab.css @@ -0,0 +1,213 @@ +.spui-UnderlineTab { + background-color: var(--color-surface-primary); +} + +.spui-UnderlineTab--border { + border-bottom: 1px solid var(--color-border-low-emphasis); +} + +.spui-UnderlineTab--scrollable { + position: relative; +} + +.spui-UnderlineTab-container { + align-items: center; + display: flex; +} + +.spui-UnderlineTab--scrollable .spui-UnderlineTab-container { + column-gap: 16px; + overflow-x: scroll; + padding: 0 16px; + scroll-behavior: smooth; +} + +.spui-UnderlineTab-button { + background-color: transparent; + border: none; + box-sizing: border-box; + flex-shrink: 0; + padding: 0 4px; + transition: background-color 0.15s ease-in-out; +} + +.spui-UnderlineTab--fixed .spui-UnderlineTab-button { + width: 100%; +} + +.spui-UnderlineTab--scrollable .spui-UnderlineTab-button { + min-width: 44px; +} + +.spui-UnderlineTab-button:hover { + background-color: var(--color-surface-tertiary); +} + +.spui-UnderlineTab-button:focus { + border-radius: 4px; + outline: 2px solid var(--color-focus-clarity); + outline-offset: -2px; +} + +.spui-UnderlineTab-button:not(:focus-visible) { + outline: none; +} + +.spui-UnderlineTab-labelWrapper { + align-items: center; + column-gap: 6px; + display: flex; + margin: 0 auto; + max-width: 100%; + padding: 14px 0; + position: relative; + width: fit-content; +} + +.spui-UnderlineTab-labelWrapper::after { + background-color: var(--color-border-accent-primary); + border-radius: 3px; + bottom: 0; + content: ''; + height: 3px; + left: 50%; + min-width: 40px; + opacity: 0; + position: absolute; + transform: translateX(-50%); + transition: opacity 0.35s ease-in-out; + width: 100%; +} + +/* stylelint-disable-next-line */ +.spui-UnderlineTab-button[aria-selected='true'] .spui-UnderlineTab-labelWrapper::after { + opacity: 1; +} + +.spui-UnderlineTab-label { + color: var(--color-text-low-emphasis); + font-size: 0.875rem; + font-weight: bold; + line-height: 1.3; +} + +.spui-UnderlineTab--fixed .spui-UnderlineTab-label { + min-width: 0; + overflow-wrap: break-word; +} + +/* stylelint-disable-next-line */ +.spui-UnderlineTab-button[aria-selected='true'] .spui-UnderlineTab-label { + color: var(--color-text-accent-primary); +} + +.spui-UnderlineTab-badge { + background-color: var(--color-surface-accent-primary); + border-radius: 8px; + box-sizing: border-box; + color: var(--color-text-high-emphasis-inverse); + flex-shrink: 0; + font-size: 0.6875rem; + font-weight: bold; + line-height: 1.3; + min-width: 20px; + padding: 1px 4px; +} + +.spui-UnderlineTab-visuallyHidden { + clip: rect(1px, 1px, 1px, 1px); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + width: 1px; +} + +.spui-UnderlineTab-arrow { + background-color: var(--color-surface-primary); + bottom: 0; + height: fit-content; + margin: auto; + opacity: 0; + position: absolute; + top: 0; + transition: + opacity 0.35s ease-in-out, + visibility 0.35s ease-in-out; + visibility: hidden; + z-index: 1; +} + +.spui-UnderlineTab-arrow.is-showed { + opacity: 1; + visibility: visible; +} + +.spui-UnderlineTab-arrow::before { + content: ''; + height: 100%; + position: absolute; + width: 10px; + z-index: -1; +} + +.spui-UnderlineTab-arrow--left { + left: 0; +} + +.spui-UnderlineTab-arrow--left::before { + background: linear-gradient( + 270deg, + rgba(255, 255, 255, 0) 0, + var(--color-surface-primary) 100% + ); + left: 44px; /* ボタンの横幅分 */ +} + +.spui-UnderlineTab-arrow--right { + right: 0; +} + +.spui-UnderlineTab-arrow--right::before { + background: linear-gradient( + 270deg, + var(--color-surface-primary) 0, + rgba(255, 255, 255, 0) 100% + ); + right: 44px; /* ボタンの横幅分 */ +} + +.spui-UnderlineTab-arrowButton { + background-color: transparent; + border: none; + border-radius: 8px; + color: var(--color-object-low-emphasis); + display: flex; + padding: 14px; + transition: background-color 0.15s ease-in-out; +} + +.spui-UnderlineTab-arrowButton:hover { + background-color: var(--color-surface-tertiary); +} + +.spui-UnderlineTab-arrowButton:focus { + outline: 2px solid var(--color-focus-clarity); +} + +.spui-UnderlineTab-arrowButton:not(:focus-visible) { + outline: none; +} + +@media (prefers-reduced-motion: reduce) { + .spui-UnderlineTab-button, + .spui-UnderlineTab-labelWrapper::after, + .spui-UnderlineTab-arrow, + .spui-UnderlineTab-arrowButton { + transition: 0.1ms; + } + + .spui-UnderlineTab--scrollable .spui-UnderlineTab-container { + scroll-behavior: auto; + } +} diff --git a/packages/spindle-ui/src/NavigationTab/UnderlineTab/UnderlineTab.stories.mdx b/packages/spindle-ui/src/NavigationTab/UnderlineTab/UnderlineTab.stories.mdx new file mode 100644 index 000000000..930de7b5a --- /dev/null +++ b/packages/spindle-ui/src/NavigationTab/UnderlineTab/UnderlineTab.stories.mdx @@ -0,0 +1,249 @@ +import { Description, Meta, Source, Story } from '@storybook/addon-docs/blocks'; +import { UnderlineTab } from './UnderlineTab'; +import { actions } from '@storybook/addon-actions'; + +# NavigationTab/UnderlineTab + + NavigationTabは表示内容を伝え、切り替える事を目的としたコンポーネントです。ページ上部に配置し、ナビゲーション(Header)的役割を担うことを想定しています。 + + + + + + + + +`} +/> + +## NavigationTabの種類 +NavigationTabには、Underline/Capsule/Inlineの3種類が存在します(※CapsuleとInlineはこれから実装予定です) + +Component | 用途 | 数字バッチを表示できるか | 区切り線を表示できるか | 横スクロールできるか +:-- | :-- | :-- | :-- | :-- | +UnderlineTab | 最も汎用的なコンポーネントです。よりHeaderらしく使いたい時に利用してください。また、表示内容が類推し易い時に利用してください。 | ○ | ○ | ○ | +CapsuleTab | UnderlineTabより象徴的に使ったり、目立たせたい時に利用してください。UnderlineTabと併用してサブナビゲーション的に利用することも可能です。 | × | ○ | ○ | +InlineTab | NavigationTabのいずれかと並列に配置したい時に利用してください。ただし、横スクロールできないため項目数が少ない時のみ利用してください。 | × | × | × | + +## 指定できるプロパティ + + - `defaultSelectedId`(必須): 初期選択状態にしたい項目の`id`を指定します。この`id`は`options`内のいずれかの`id`と一致している必要があります。 + + + - `options`(必須): 各項目の情報を指定します。`id`(必須)は各項目を識別するためのもので、配列内で一意である必要があります。`label`(必須)はUnderlineTabに表示されるラベルです。`countBadge`(任意)は`label`右横に数字バッチを表示したい時に指定してください。 + + + - `hasBorder`(任意): コンポーネント下に区切り線を表示するかどうかを指定します。デフォルトは`false`です。 + + + - `variant`(任意): UnderlineTabの種類を指定します。デフォルトは`fixed`で、その他に`scrollable`を指定することもできます。 + + + - `onClick`(任意): 各項目をクリックした際に追加で行いたい処理がある場合は指定します。第2引数の`id`には、選択された項目の`id`が渡されます。 + + +## アクセシビリティ +UnderlineTab内の各項目は`tab`roleを持ち、`id`には`options`の`id`が設定されます。また、`aria-controls`には`options`の`id`に`-tabpanel`を付与したものが設定されます。(例:`id`が`all`の場合、`id`には`all`、`aria-controls`には`all-tabpanel`が設定されます) + +上記を用いて、各項目と表示内容を関連付けてください。 + + +
+

「すべて」選択時の表示内容

+
+
+

「フォロー」選択時の表示内容

+
+
+

「フォロワー」選択時の表示内容

+
+ `} +/> + +## Variant(fixed) +`variant: fixed`を指定した場合、各項目の幅は項目数で等分した長さになります。`fixed`を指定している場合、長い項目名(`label`)や多数の項目が設定されることは期待していません。そのため、`variant: fixed`指定時は項目名を最大7文字程度に収めることを推奨しており、長い項目名や多数の項目を設定したい場合は`variant: scrollable`の利用を検討してください。 + + + + +
+
+
+ + + + +
+ + + +
+
+ `} +/> + +## Variant(scrollable) +`variant: scrollable`を指定した場合、各項目の幅は項目名(`label`)の長さと同等になります。(項目名が長くても省略されることはありません)UnderlineTabの全体が表示領域内に収まらなくなった瞬間から、横スクロール可能になります。 + + + + +
+
+
+
+ + + + +
+
+ +
+
+ + + + +
+
+ +
+
+
+ `} +/> + +## Variant(scrollable with short label) +UnderlineTabの全体が表示領域内に収まっている場合は、特にスクロールはしません。 + + + + +
+
+
+ + + +## HasBorder(true) +コンポーネント下に区切り線を表示するかどうかを指定します。デフォルトは`false`ですが、`true`を指定すれば表示できます。 + + + + +
+
+
+ + + +## CountBadge +`label`右横に数字バッチを表示したい時に指定してください。数字バッチの省略処理(例: `countBadge: 100`の場合に`99+`と表示)はコンポーネント内では特に行っていないので、必要に応じて利用元で処理してから指定してください。 + + + + +
+
+
+ + diff --git a/packages/spindle-ui/src/NavigationTab/UnderlineTab/UnderlineTab.test.tsx b/packages/spindle-ui/src/NavigationTab/UnderlineTab/UnderlineTab.test.tsx new file mode 100644 index 000000000..8de6f4b20 --- /dev/null +++ b/packages/spindle-ui/src/NavigationTab/UnderlineTab/UnderlineTab.test.tsx @@ -0,0 +1,159 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { jest } from '@jest/globals'; + +import { UnderlineTab } from './UnderlineTab'; + +const options = [ + { id: 'all', label: 'すべて' }, + { id: 'follow', label: 'フォロー' }, + { id: 'follower', label: 'フォロワー', countBadge: '100' }, +]; + +describe('', () => { + describe('defaultSelectedId Props', () => { + test('If defaultSelectedId is not included in options, nothing is selected.', () => { + render( + , + ); + + const notSelectedButtons = screen.getAllByRole('tab', { + selected: false, + }); + expect(notSelectedButtons.length).toEqual(options.length); + }); + + test('If defaultSelectedId is included in options, it must not be selected.', () => { + const defaultSelectedId = options[0].id; + render( + , + ); + + const notSelectedButtons = screen.getAllByRole('tab', { + selected: false, + }); + expect(notSelectedButtons.length).toEqual(options.length - 1); + + const selectedButton = screen.getByRole('tab', { selected: true }); + expect(selectedButton.getAttribute('id')).toEqual(defaultSelectedId); + }); + }); + + describe('options Props', () => { + test('Nothing is displayed when options is empty array.', () => { + const { container } = render( + , + ); + + expect(container.firstChild).toBeNull(); + }); + + test('If options are specified, they should be properly reflected.', () => { + const defaultSelectedId = options[1].id; + render( + , + ); + + const buttons = screen.getAllByRole('tab'); + buttons.forEach((button, i) => { + expect(button.getAttribute('aria-controls')).toEqual( + `${options[i].id}-tabpanel`, + ); + expect(button.getAttribute('aria-selected')).toEqual( + defaultSelectedId === button.id ? 'true' : 'false', + ); + expect(button.getAttribute('id')).toEqual(options[i].id); + expect(button.getAttribute('tabIndex')).toEqual( + defaultSelectedId === button.id ? '0' : '-1', + ); + if (options[i].countBadge) { + expect( + (button.textContent || button.innerText).includes( + options[i].countBadge || '', + ), + ).toEqual(true); + } + }); + }); + }); + + describe('hasBorder Props', () => { + test('Border must be displayed when hasBorder is true.', () => { + const { container } = render( + , + ); + + expect(container.firstChild).toHaveClass('spui-UnderlineTab--border'); + }); + + test('Border is not initially displayed.', () => { + const { container } = render( + , + ); + + expect(container.firstChild).not.toHaveClass('spui-UnderlineTab--border'); + }); + }); + + describe('variant Props', () => { + test('Border must be displayed when variant is fixed.', () => { + const { container } = render( + , + ); + + expect(container.firstChild).toHaveClass('spui-UnderlineTab--fixed'); + }); + + test('Border is not displayed when variant is scrollable.', () => { + const { container } = render( + , + ); + + expect(container.firstChild).toHaveClass('spui-UnderlineTab--scrollable'); + }); + }); + + describe('onClick Props', () => { + test('The selected item should change when onClick is called.', async () => { + const defaultSelectedId = options[0].id; + const selectedId = options[1].id; + const onClick = jest.fn(); + const user = userEvent.setup(); + + render( + , + ); + + const defaultSelectedButton = screen.getByRole('tab', { selected: true }); + expect(defaultSelectedButton.getAttribute('id')).toEqual( + defaultSelectedId, + ); + + if (defaultSelectedButton.nextElementSibling) { + await user.click(defaultSelectedButton.nextElementSibling); + } + expect(onClick).toBeCalled(); + const selectedButton = screen.getByRole('tab', { selected: true }); + expect(selectedButton.getAttribute('id')).toEqual(selectedId); + }); + }); +}); diff --git a/packages/spindle-ui/src/NavigationTab/UnderlineTab/UnderlineTab.tsx b/packages/spindle-ui/src/NavigationTab/UnderlineTab/UnderlineTab.tsx new file mode 100644 index 000000000..42f64c16d --- /dev/null +++ b/packages/spindle-ui/src/NavigationTab/UnderlineTab/UnderlineTab.tsx @@ -0,0 +1,251 @@ +import React, { + RefObject, + createRef, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import ChevronLeftBold from '../../Icon/ChevronLeftBold'; +import ChevronRightBold from '../../Icon/ChevronRightBold'; + +type Option = { + label: string; + id: string; + countBadge?: string; +}; + +type Variant = 'fixed' | 'scrollable'; + +type Props = { + defaultSelectedId: string; + options: Option[]; + hasBorder?: boolean; + variant?: Variant; + onClick?: ( + event: + | React.MouseEvent + | React.KeyboardEvent, + id: string, + ) => void; +}; + +const BLOCK_NAME = 'spui-UnderlineTab'; +const SCROLL_DISTANCE = 150; + +export const UnderlineTab: React.FC = ({ + defaultSelectedId, + options, + hasBorder = false, + variant = 'fixed', + onClick, +}) => { + const [selectedId, setSelectedId] = useState(defaultSelectedId); + const [showPrevButton, setShowPrevButton] = useState(false); + const [showNextButton, setShowNextButton] = useState(false); + + const buttonsRef = useRef[]>([]); + const containerRef = useRef(null); + + const handleScroll = useCallback((direction: 'prev' | 'next') => { + const containerElement = containerRef.current; + if (!containerElement) { + return; + } + + const scrollDistance = + direction === 'next' ? SCROLL_DISTANCE : -SCROLL_DISTANCE; + containerElement.scrollLeft = containerElement.scrollLeft + scrollDistance; + }, []); + + const handleClick = useCallback( + ( + e: + | React.MouseEvent + | React.KeyboardEvent, + id: string, + ) => { + const targetButtonElement = document.querySelector(`#${id}`); + // 既に選択中の項目に対してクリックした場合は何もしない + if (targetButtonElement?.getAttribute('aria-selected') === 'true') { + return; + } + + setSelectedId(id); + + if (typeof onClick === 'function') { + onClick(e, id); + } + }, + [onClick], + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent, index: number) => { + const totalLength = options.length; + + switch (e.key) { + case 'ArrowLeft': { + // 基本的には1つ前の要素に移動。ただし、既に先頭の要素にいる場合はリストの最後尾に移動 + const prevButtonIndex = index - 1 < 0 ? totalLength - 1 : index - 1; + const prevButtonRef = buttonsRef.current[prevButtonIndex]; + prevButtonRef.current?.focus(); + handleClick(e, options[prevButtonIndex].id); + break; + } + case 'ArrowRight': { + // 基本的には1つ後の要素に移動。ただし、既に最後尾の要素にいる場合はリストの先頭に移動 + const nextButtonIndex = index + 1 >= totalLength ? 0 : index + 1; + const nextButtonRef = buttonsRef.current[nextButtonIndex]; + nextButtonRef.current?.focus(); + handleClick(e, options[nextButtonIndex].id); + break; + } + case 'Enter': { + const targetButton = buttonsRef.current[index].current; + // 既に選択中の項目に対してEnterを押下した場合は無効にする + if (targetButton?.getAttribute('aria-selected') === 'true') { + e.preventDefault(); + } + break; + } + } + }, + [options, handleClick], + ); + + useEffect(() => { + buttonsRef.current = options.map( + (_, index) => buttonsRef.current[index] ?? createRef(), + ); + }, [options]); + + useEffect(() => { + if (variant !== 'scrollable') { + return; + } + const containerElement = containerRef.current; + if (!containerElement) { + return; + } + + const updateDisplayedButton = () => { + setShowPrevButton(containerElement.scrollLeft > 0); + setShowNextButton( + containerElement.scrollWidth > + Math.ceil(containerElement.clientWidth + containerElement.scrollLeft), + ); + }; + + updateDisplayedButton(); + window.addEventListener('resize', updateDisplayedButton); + containerElement.addEventListener('scroll', updateDisplayedButton); + + return () => { + window.removeEventListener('resize', updateDisplayedButton); + containerElement.removeEventListener('scroll', updateDisplayedButton); + }; + }, [variant]); + + if (options.length === 0) { + return null; + } + + return ( +
+ {variant === 'scrollable' && ( +
+ +
+ )} + +
+ {options.map((option, index) => { + const { label, id, countBadge } = option; + const isSelected = id === selectedId; + + return ( + + ); + })} +
+ + {variant === 'scrollable' && ( +
+ +
+ )} +
+ ); +}; diff --git a/packages/spindle-ui/src/NavigationTab/UnderlineTab/design-doc.md b/packages/spindle-ui/src/NavigationTab/UnderlineTab/design-doc.md index 7c72c6029..d9eb63aac 100644 --- a/packages/spindle-ui/src/NavigationTab/UnderlineTab/design-doc.md +++ b/packages/spindle-ui/src/NavigationTab/UnderlineTab/design-doc.md @@ -16,6 +16,8 @@ UnderlineTabはNavigationTabの3種(Underline/Capsule/Inline)の内の1つで #### Fixed, Scrollable 表示するタブアイテム数によってFixed, Scrollableの2パターンがあります。Fixedはタブアイテムが画面幅に収まる場合、Scrollableは収まらない場合にスクロール可能になり左右にボタンが配置されます。 +Scrollableパターンで左右に見切れたタブアイテムがある場合には、操作用途のボタンが表示されます。ボタンが押されると、左または右側に150pxスクロールします。この値は「想定する最小画面幅より大きくならない適当な値」として指定されています。画面幅に従って動的に割合で算出するプランもありましたが、よりシンプルな方で進めることにしました。 + ## スクリーンショット ![UnderlineTabのイメージ。Fixed, Scrollableの2パターンの表示がある。Fixedはアイテムが全て表示されている。Scrollableは画面内に表示し切れないアイテムがあり、画面の左端または右端に「<」または「>」アイコンが表示されている。他に、Borderをつけるものや、Count Badgeをつけるタイプのものがある。](https://github.com/openameba/spindle/assets/44389443/c0ef6998-603c-473a-ab5c-a022fdb478d3) @@ -28,7 +30,6 @@ linkUrlを指定するとリンクになります。 ```tsx @@ -42,7 +43,6 @@ linkUrlを指定しない場合はボタンとなります。ボタンには指 ```tsx @@ -57,7 +57,6 @@ linkUrlを指定しない場合はボタンとなります。ボタンには指 ```tsx Top @@ -66,21 +65,6 @@ linkUrlを指定しない場合はボタンとなります。ボタンには指 ``` -#### 直接buttonタグを使うパターン -`role`属性、`aria-selected`属性、`aria-controls`属性が必要です。また、表示されるコンテンツ側には、選択されているタブの`aria-controls`属性と一致した`id`属性が必要です。 -```tsx - - - - - -
表示されるコンテンツ
-``` - ### DO NOT サイドバーなどの補助的なナビゲーションには使用しないでください。 @@ -94,6 +78,8 @@ linkUrlを指定しない場合はボタンとなります。ボタンには指 - Border Low Emphasis (下線) - Text High Emphasis Inverse (数字バッジテキスト) - Surface Accent Primary (数字バッジ背景) +- Animation Content Change (タブhover時, 左右移動ボタンhover時) +- Animation Appear In (タブ下線の出現時, 左右移動ボタンの出現時) ### プロパティ #### UnderlineTab.tsx @@ -101,7 +87,6 @@ linkUrlを指定しない場合はボタンとなります。ボタンには指 type Props = { children: React.ReactNode; defaultSelectedId: string; // アイテムのidを指定します。指定したidが初期表示時に選択されているタブとして下線が表示されます。該当のidが存在しない場合は、下線が表示されません。 - countBadgeLabel?: string; // バッジの意味を表すための不可視のラベルに用います。ラベルは視覚的に伝わる以上の意味を含めたラベルをつけるべきではなく、必要最低限の簡潔なテキストにする必要があります。初期値は`件数`です。 hasBorder?: boolean; // タブに区切りとしての下線を表示します。初期値は`false`です onClick?: (e: React.MouseEvent, id) => void; }; @@ -126,14 +111,12 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; const UnderlineTabContext = React.createContext<{ selectedId: string; - countBadgeLabel: string; handleClick: ( e: React.MouseEvent, id: string, ) => void; }>({ selectedId: '', - countBadgeLabel: '', handleClick: () => {}, }); @@ -150,7 +133,6 @@ export const useUnderlineTabContextContext = () => { type Props = { children: React.ReactNode; defaultSelectedId: string; - countBadgeLabel?: string; hasBorder?: boolean; onClick?: ( e: React.MouseEvent, @@ -163,7 +145,6 @@ const BLOCK_NAME = 'spui-UnderlineTab'; export const UnderlineTab: React.FC = ({ children, defaultSelectedId, - countBadgeLabel = '件数', hasBorder = false, onClick, }) => { @@ -222,7 +203,7 @@ export const UnderlineTab: React.FC = ({ return (
( function Item({ label, id, linkUrl, countBadge }: Props) { const { selectedId, - countBadgeLabel = '件数', handleClick, } = useUnderlineTabContextContext(); @@ -293,9 +273,8 @@ export const UnderlineTabItem = forwardRef( const countBadgeElement = countBadge && ( <> - {countBadgeLabel} - {/* 100以上の場合は99+の表記にする */} {countBadge} + ); @@ -347,7 +326,6 @@ export const UnderlineTabItem = forwardRef( { id: 'team', linkUrl: '/team', label: 'Team' }, { id: 'ameba', linkUrl: '/about', label: 'Amebaとは' }, ]} - countBadgeLabel="未読" hasBorder /> ``` @@ -362,7 +340,6 @@ export const UnderlineTabItem = forwardRef( { id: 'team', label: 'Team', linkComponentProps: { to: '/team' } }, { id: 'ameba', label: 'Amebaとは', linkComponentProps: { to: '/about' } }, ]} - countBadgeLabel="未読" hasBorder onClick={handleClick} /> @@ -377,7 +354,6 @@ export const UnderlineTabItem = forwardRef( { id: 'team', label: 'Team' }, { id: 'ameba', label: 'Amebaとは' }, ]} - countBadgeLabel="新着" hasBorder onClick={handleClick} /> @@ -391,11 +367,11 @@ export const UnderlineTabItem = forwardRef( - [テキストや文字画像のコントラストを確保する](https://a11y-guidelines.ameba.design/1/4/3/)[基本必須] - [ ] SpindleカラーパレットのTheme Colorsを適切に使い分けている - [テキストサイズを拡大縮小できる](https://a11y-guidelines.ameba.design/1/4/4/)[基本必須] - - [ ] 画面を200%拡大・文字サイズを2倍に変更しても、横スクロールで全てのタブを表示できる + - [ ] 画面を200%拡大・文字サイズを2倍に変更しても、改行または横スクロールで全てのタブを確認できる - [リフローできる](https://a11y-guidelines.ameba.design/1/4/10/)[できれば] - - [ ] 画面を400%まで拡大しても、横スクロールで全てのタブを表示できる + - [ ] 画面を400%まで拡大しても、改行または横スクロールで全てのタブを表示できる - [キーボード、タッチデバイスで操作できる](https://a11y-guidelines.ameba.design/2/1/1/)[基本必須] - - [ ] リンクとボタンはTabキーでフォーカスでき、Enterキーで実行できる + - [ ] リンクとボタンはTabキーまたは左右キーでフォーカスでき、Enterキーで実行できる - [適切なフォーカス順序にする](https://a11y-guidelines.ameba.design/2/4/3/)[基本必須] - [ ] キーボード操作の順序が、見た目の順序と一致している - [リンクの目的を理解できるようにする](https://a11y-guidelines.ameba.design/2/4/4/)[基本必須] @@ -407,6 +383,8 @@ export const UnderlineTabItem = forwardRef( - [ ] 現在位置は `aria-selected="true"` を付与している。リンクの場合は、`aria-current="page"` を追加で付与している - [ポインタジェスチャを必須としない](https://a11y-guidelines.ameba.design/2/5/1/)[基本必須] - [ ] スクロールできる場合はスクロールボタンを配置している +- [ターゲットのサイズを理解する](https://a11y-guidelines.ameba.design/2/5/5/)[できれば] + - [ ] リンクやボタンのタップ領域は44px × 44px以上確保している - [HTMLを正しく記述する](https://a11y-guidelines.ameba.design/4/1/1/)[基本必須] - [ ] HTML仕様に準拠した実装をしている - [カスタムコントロールの操作性を担保する](https://a11y-guidelines.ameba.design/4/1/2/)[基本必須] diff --git a/packages/spindle-ui/src/NavigationTab/index.css b/packages/spindle-ui/src/NavigationTab/index.css new file mode 100644 index 000000000..5baa27f4c --- /dev/null +++ b/packages/spindle-ui/src/NavigationTab/index.css @@ -0,0 +1 @@ +@import './UnderlineTab/UnderlineTab.css'; diff --git a/packages/spindle-ui/src/NavigationTab/index.ts b/packages/spindle-ui/src/NavigationTab/index.ts new file mode 100644 index 000000000..c1e65802b --- /dev/null +++ b/packages/spindle-ui/src/NavigationTab/index.ts @@ -0,0 +1 @@ +export { UnderlineTab } from './UnderlineTab/UnderlineTab'; diff --git a/packages/spindle-ui/src/index.css b/packages/spindle-ui/src/index.css index 1ee6f4c32..2bd0f61a4 100644 --- a/packages/spindle-ui/src/index.css +++ b/packages/spindle-ui/src/index.css @@ -20,3 +20,4 @@ @import './Pagination/Pagination.css'; @import './InlineNotification/InlineNotification.css'; @import './SegmentedControl/SegmentedControl.css'; +@import './NavigationTab/index.css';