-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #13 from codeforjapan/wip/ui
UI プロトタイプ実装
- Loading branch information
Showing
61 changed files
with
5,110 additions
and
1,711 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
name: CI | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
branches: | ||
- main | ||
|
||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.ref }} | ||
cancel-in-progress: true | ||
|
||
defaults: | ||
run: | ||
shell: bash | ||
|
||
permissions: | ||
contents: read | ||
|
||
jobs: | ||
ci: | ||
name: Code Problem Check | ||
runs-on: ubuntu-24.04 | ||
timeout-minutes: 10 | ||
steps: | ||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
with: | ||
persist-credentials: false | ||
|
||
- name: Setup Node.js and yarn | ||
uses: ./.github/workflows/composite/setup | ||
|
||
- name: Build | ||
run: yarn run build | ||
|
||
- name: Run ESLint | ||
run: yarn run lint | ||
|
||
- name: Run Prettier | ||
run: yarn run format:ci | ||
|
||
- name: Run typecheck | ||
run: yarn run typecheck | ||
|
||
- name: Run Vitest | ||
run: yarn run test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
name: Setup Node.js | ||
description: Setup Node.js and yarn | ||
|
||
runs: | ||
using: composite | ||
steps: | ||
- name: Setup yarn | ||
shell: bash | ||
run: corepack enable yarn | ||
|
||
- name: Get yarn cache directory path | ||
id: yarn-store | ||
shell: bash | ||
run: echo "store_path=$(yarn cache dir)" >> "$GITHUB_OUTPUT" | ||
|
||
- name: Setup Node.js | ||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 | ||
with: | ||
node-version-file: package.json | ||
|
||
- name: Restore yarn cache | ||
uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 | ||
with: | ||
path: ${{ steps.yarn-store.outputs.store_path }} | ||
key: ${{ runner.os }}-yarn-store-${{ hashFiles('**/yarn.lock') }} | ||
restore-keys: | | ||
${{ runner.os }}-yarn-store- | ||
- name: Install Dependencies | ||
shell: bash | ||
run: yarn install --frozen-lockfile | ||
|
||
- name: Save pnpm cache if main branch | ||
if: github.ref_name == 'main' | ||
id: save-yarn-cache | ||
uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 | ||
with: | ||
path: ${{ steps.yarn-store.outputs.store_path }} | ||
key: ${{ runner.os }}-yarn-store-${{ hashFiles('**/yarn.lock') }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,7 @@ node_modules | |
/.cache | ||
/build | ||
.env | ||
|
||
.vscode/* | ||
!.vscode/settings.example.json | ||
eslint-typegen.d.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"editor.defaultFormatter": "esbenp.prettier-vscode", | ||
"editor.formatOnSave": true, | ||
"editor.codeActionsOnSave": { | ||
"source.fixAll.eslint": "explicit" | ||
}, | ||
"eslint.validate": [ | ||
"javascript", | ||
"javascriptreact", | ||
"typescript", | ||
"typescriptreact" | ||
], | ||
"typescript.tsdk": "node_modules/typescript/lib", | ||
"typescript.enablePromptUseWorkspaceTsdk": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
@import "tailwindcss"; | ||
|
||
@utility content-visibility-auto { | ||
content-visibility: auto; | ||
} | ||
|
||
body { | ||
color: #222; | ||
} | ||
|
||
* { | ||
letter-spacing: 0.05em; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import type { MantineSize } from "@mantine/core"; | ||
import { List, ListItem } from "@mantine/core"; | ||
import { useMemo } from "react"; | ||
|
||
type FormErrorProps = { | ||
errors: Array<string[] | null | undefined> | string[] | null | undefined; | ||
/** | ||
* @default "xs" | ||
*/ | ||
size?: MantineSize; | ||
}; | ||
|
||
export const FormError = ({ errors, size }: FormErrorProps) => { | ||
size ??= "xs"; | ||
|
||
const flattenErrors = useMemo( | ||
() => errors?.flat().filter((v) => v != null), | ||
[errors], | ||
); | ||
if (flattenErrors == null || flattenErrors.length === 0) { | ||
return undefined; | ||
} | ||
|
||
return ( | ||
<List listStyleType="none" size={size}> | ||
{flattenErrors.map((error, index) => ( | ||
<ListItem key={index}>{error}</ListItem> | ||
))} | ||
</List> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import type { ButtonProps, PolymorphicComponentProps } from "@mantine/core"; | ||
import { Button } from "@mantine/core"; | ||
import type React from "react"; | ||
|
||
type SubmitButtonProps = Omit< | ||
PolymorphicComponentProps<"button", ButtonProps>, | ||
"type" | ||
> & { | ||
children: React.ReactNode; | ||
loading?: boolean; | ||
}; | ||
|
||
export const SubmitButton = ({ disabled, ...rest }: SubmitButtonProps) => { | ||
// React19 へアップデートしたら useFormStatus() を追加して送信中はボタンを無効化することを検討する | ||
|
||
return <Button disabled={disabled} type="submit" {...rest} />; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { getFormProps, useForm } from "@conform-to/react"; | ||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; | ||
|
||
import { render, screen } from "../../../test/test-react"; | ||
import { userEvent } from "../../../test/vitest-setup"; | ||
import { DateRangePicker } from "./DateRangePicker"; | ||
|
||
type Form = { | ||
/** | ||
* ISO String | ||
*/ | ||
start: string | null; | ||
/** | ||
* ISO String | ||
*/ | ||
end: string | null; | ||
}; | ||
|
||
type PageProps = { | ||
defaultValue: Form; | ||
}; | ||
|
||
beforeEach(() => { | ||
vi.useFakeTimers({ shouldAdvanceTime: true }); | ||
// GitHub Actions では TZ が UTC になっているため、テスト時の TZ も UTC に設定する | ||
process.env.TZ = "UTC"; | ||
}); | ||
|
||
afterEach(() => { | ||
vi.useRealTimers(); | ||
}); | ||
|
||
const Page = ({ defaultValue }: PageProps) => { | ||
const [form, fields] = useForm<Form>({ | ||
defaultValue, | ||
}); | ||
|
||
return ( | ||
<div> | ||
<form {...getFormProps(form)}> | ||
<DateRangePicker | ||
convertFormValueToMantine={(value) => | ||
value ? new Date(value) : null | ||
} | ||
convertMantineValueToForm={(date) => date?.toISOString()} | ||
fromField={fields.start} | ||
label="Date Range" | ||
toField={fields.end} | ||
/> | ||
<button type="submit">Submit</button> | ||
</form> | ||
<span aria-label="result"> | ||
{fields.start.value} – {fields.end.value} | ||
</span> | ||
</div> | ||
); | ||
}; | ||
|
||
describe("DateRangePicker", () => { | ||
test("convert で指定した処理を用いて UI の値をフォームに反映できる", async () => { | ||
// 確実に 2025 年 1 月のカレンダーを表示するため、システム時刻を固定する | ||
vi.setSystemTime(new Date("2025-01-15T00:00:00Z")); | ||
render(<Page defaultValue={{ start: null, end: null }} />); | ||
|
||
const button = screen.getByRole("button", { name: "Date Range" }); | ||
await userEvent.click(button); | ||
|
||
// start: 2025-01-09T15:00:00.000Z | ||
const date1 = screen.getByRole("button", { name: "10 1月 2025" }); | ||
await userEvent.click(date1); | ||
|
||
// end: 2025-01-14T15:00:00.000Z | ||
const date2 = screen.getByRole("button", { name: "15 1月 2025" }); | ||
await userEvent.click(date2); | ||
|
||
const span = screen.getByLabelText("result"); | ||
expect(span).toHaveTextContent( | ||
"2025-01-10T00:00:00.000Z – 2025-01-15T00:00:00.000Z", | ||
); | ||
}); | ||
|
||
test("convert で指定した処理を用いてフォームの値を UI に反映できる", () => { | ||
render( | ||
<Page | ||
defaultValue={{ | ||
start: "2025-01-09T15:00:00.000Z", | ||
end: "2025-01-14T15:00:00.000Z", | ||
}} | ||
/>, | ||
); | ||
|
||
const button = screen.getByRole("button", { name: "Date Range" }); | ||
expect(button).toHaveTextContent("2025.01.09 (木) – 2025.01.14 (火)"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { DatePickerInput } from "@mantine/dates"; | ||
|
||
import { useDateRangeInputControl } from "../../hooks/useDateRangeInputControl"; | ||
import { containsNonNullValues } from "../../utils/array"; | ||
import { FormError } from "../FormError"; | ||
|
||
type DateRangePickerProps = Parameters<typeof useDateRangeInputControl>[0] & { | ||
disabled?: boolean; | ||
label: string; | ||
/** | ||
* 入力した値を表示する際のフォーマット | ||
* | ||
* @default "YYYY.MM.DD (ddd)" | ||
* | ||
* @see {@link https://day.js.org/docs/en/display/format Day.js のフォーマット仕様} | ||
*/ | ||
valueFormat?: string; | ||
}; | ||
|
||
export const DateRangePicker = ({ | ||
disabled, | ||
toField, | ||
fromField, | ||
label, | ||
valueFormat, | ||
convertMantineValueToForm, | ||
convertFormValueToMantine, | ||
}: DateRangePickerProps) => { | ||
valueFormat ??= "YYYY.MM.DD (ddd)"; | ||
|
||
const { | ||
value, | ||
change: onChange, | ||
focus: onFocus, | ||
blur: onBlur, | ||
} = useDateRangeInputControl({ | ||
fromField, | ||
toField, | ||
convertMantineValueToForm, | ||
convertFormValueToMantine, | ||
}); | ||
|
||
return ( | ||
<DatePickerInput | ||
clearable | ||
disabled={disabled} | ||
error={ | ||
containsNonNullValues(fromField.errors, toField.errors) && ( | ||
<FormError errors={[fromField.errors, toField.errors]} /> | ||
) | ||
} | ||
errorProps={{ component: "div" }} | ||
label={label} | ||
onBlur={onBlur} | ||
onChange={onChange} | ||
onFocus={onFocus} | ||
type="range" | ||
value={value} | ||
valueFormat={valueFormat} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import "./fieldset.css"; | ||
|
||
import type { FieldsetProps as MantineFieldsetProps } from "@mantine/core"; | ||
// このファイルは no-restricted-imports で提案される代替コンポーネントなので問題ない | ||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports | ||
import { Fieldset as MantineFieldSet } from "@mantine/core"; | ||
type FieldSetProps = Omit<MantineFieldsetProps, "variant">; | ||
|
||
export const Fieldset = (props: FieldSetProps) => { | ||
return <MantineFieldSet variant="unstyled" {...props} />; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import type { TextInputProps as MantineTextInputProps } from "@mantine/core"; | ||
// このファイルは no-restricted-imports で提案される代替コンポーネントなので問題ない | ||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports | ||
import { TextInput as MantineTextInput } from "@mantine/core"; | ||
|
||
import { mantineInputOrder } from "../../config/mantine"; | ||
|
||
type TextInputProps = Omit<MantineTextInputProps, "inputWrapperOrder">; | ||
|
||
export const TextInput = (props: TextInputProps) => { | ||
const { autoComplete, ...rest } = props; | ||
|
||
return ( | ||
<MantineTextInput | ||
autoComplete={autoComplete} | ||
inputWrapperOrder={mantineInputOrder} | ||
{...(autoComplete === "off" && { "data-1p-ignore": true })} | ||
{...rest} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.mantine-Fieldset-legend { | ||
font-size: 1.25em; | ||
font-weight: 700; | ||
} |
Oops, something went wrong.