Skip to content

Commit

Permalink
Merge pull request #18 from ReoHakase/16/add-prefecture-state-bar
Browse files Browse the repository at this point in the history
都道府県の選択状態を表示するコンポーネント`<PrefectureStateBar>`を実装した
  • Loading branch information
ReoHakase authored May 25, 2024
2 parents 101a389 + 0ae3621 commit ef7b1b1
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 36 deletions.
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@pandacss/dev": "0.36.1",
"@radix-ui/colors": "3.0.0",
"@storybook/addon-a11y": "8.0.5",
"@storybook/addon-actions": "8.1.3",
"@storybook/addon-actions": "8.0.5",
"@storybook/addon-essentials": "8.0.5",
"@storybook/addon-interactions": "8.0.5",
"@storybook/addon-links": "8.0.5",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/Link/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ComponentPropsWithoutRef, ReactNode } from 'react';

// It is required to forward generics T to NextLinkProps in order to make DynamicRoute work.
// e.g. `/tag/[tagId]`
type LinkProps<T> = (
export type LinkProps<T> = (
| (NextLinkProps<T> & {
external?: false;
})
Expand All @@ -13,6 +13,7 @@ type LinkProps<T> = (
})
) & {
children: ReactNode;
className?: string;
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use client';

import { X } from 'lucide-react';
import { usePathname } from 'next/navigation';
import { ReactNode } from 'react';
import { Link } from '@/components/Link/Link';
import type { LinkProps } from '@/components/Link/Link';
import { css, cx } from 'styled-system/css';

export type PrefectureClearButtonProps<T> = Omit<LinkProps<T>, 'href' | 'children'>;

/**
* 都道府県選択をクリアするためのボタン。
*
* @param props 追加のプロパティ
* @returns 都道府県選択をクリアするためのボタン
*/
export const PrefectureClearButton = <T,>({ className, ...props }: PrefectureClearButtonProps<T>): ReactNode => {
// searchParamsは含まない
const pathname = usePathname();
return (
<Link
href={pathname}
className={cx(
css({
display: 'flex',
h: '44px',
justifyContent: 'center',
alignItems: 'center',
fontFamily: 'heading',
fontWeight: 'bold',
px: '4',
py: '2',
gap: '1',
bg: 'keyplate.12',
color: 'keyplate.1',
rounded: 'full',
}),
className,
)}
{...props}
>
<X
className={css({
display: 'inline',
w: '4',
h: '4',
})}
/>
クリア
</Link>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { useSearchParams } from 'next/navigation';
import { ReactNode } from 'react';
import { useMemo } from 'react';
import { prefCodesSchema } from '@/models/prefCode';
import type { PrefCode } from '@/models/prefCode';

/**
* 都道府県の数を表示するコンポーネントです。
*
* @returns {ReactNode} 都道府県の数を表示するためのコンポーネント
*/
export const PrefectureCount = (): ReactNode => {
const searchParams = useSearchParams();
const count = useMemo(() => {
const params = new URLSearchParams(searchParams.toString());
const currentPrefCodes = prefCodesSchema.parse(
params
.getAll('prefCodes')
.map((str) => str.split(','))
.flat(),
) as PrefCode[];
return currentPrefCodes.length;
}, [searchParams]);
return <>{count}</>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Meta, StoryObj } from '@storybook/react';
import { expect, within } from '@storybook/test';
import { PrefectureStateBar } from './PrefectureStateBar';

type Story = StoryObj<typeof PrefectureStateBar>;

const meta: Meta<typeof PrefectureStateBar> = {
component: PrefectureStateBar,
tags: ['autodocs'],
parameters: {
nextjs: {
appDirectory: true,
navigation: {
pathname: '/all',
query: {
prefCodes: '8,12,13,14',
},
},
},
},
argTypes: {},
};

export default meta;

export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const clearButton = canvas.getByRole('link');
const heading = canvas.getByRole('heading');

expect(heading).toHaveTextContent('4つの都道府県を選択中');
expect(clearButton.getAttribute('href')).toBe('/all');
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { ArrowDown } from 'lucide-react';
import { ReactNode, ComponentPropsWithoutRef } from 'react';
import { Suspense } from 'react';
import { Skeleton } from '../Skeleton/Skeleton';
import { PrefectureClearButton } from './PrefectureClearButton';
import { PrefectureCount } from './PrefectureCount';
import { css, cx } from 'styled-system/css';

export const SELECTION_STATE_LABEL_ID = 'selection-state-label' as const;

export type PrefectureStateBarProps = Omit<ComponentPropsWithoutRef<'div'>, 'children'>;

/**
* 選択中の都道府県の状態を表示するバー。
*
* @param children 子要素
* @param props 追加のプロパティ
* @returns 都道府県の状態を表示するバー
*/
export const PrefectureStateBar = ({ className, ...props }: PrefectureStateBarProps): ReactNode => {
return (
<div
id={SELECTION_STATE_LABEL_ID}
className={cx(
css({
bg: 'keyplate.1',
border: '1px solid',
borderColor: 'keyplate.6',
rounded: '26px', // 内側のクリアボタンに合わせる
display: 'flex',
flexDir: 'row',
justifyContent: 'start',
alignItems: 'center',
p: '2',
gap: '4',
}),
className,
)}
{...props}
>
<div
id={SELECTION_STATE_LABEL_ID}
className={cx(
css({
flexGrow: '1',
display: 'flex',
flexDir: 'row',
justifyContent: 'start',
alignItems: 'center',
pl: '3',
gap: '3',
}),
className,
)}
{...props}
>
<ArrowDown />
<h2 id={SELECTION_STATE_LABEL_ID}>
<Suspense
fallback={
<Skeleton
className={css({
w: '3',
})}
/>
}
>
<PrefectureCount />
</Suspense>
つの都道府県を選択中
</h2>
<div
aria-hidden
className={css({
pos: 'relative',
w: '3',
h: '3',
})}
>
<div
className={css({
pos: 'absolute',
animation: 'ping',
w: 'full',
h: 'full',
bg: 'info.6',
rounded: 'full',
})}
/>
<div
className={css({
pos: 'relative',
w: 'full',
h: 'full',
bg: 'info.9',
rounded: 'full',
border: '2px solid',
borderColor: 'info.7',
})}
/>
</div>
</div>
<PrefectureClearButton />
</div>
);
};
Loading

0 comments on commit ef7b1b1

Please sign in to comment.