-
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.
- Loading branch information
1 parent
3f0359f
commit de86333
Showing
9 changed files
with
382 additions
and
27 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,23 @@ | ||
import { Decorator } from "@storybook/react"; | ||
import React, { useState } from "react"; | ||
|
||
export const FORM_TEST_DECORATOR: Decorator = (Story) => { | ||
// Solely here to force re-rendering story on change. | ||
const [count, setCount] = useState(0); | ||
|
||
const getData = () => { | ||
const form = document.forms[0]; | ||
const formData = new FormData(form); | ||
|
||
// Convert FormData to JSON using Array.from and reduce | ||
return Array.from(formData.entries()).reduce< | ||
Record<string, FormDataEntryValue> | ||
>((acc, [key, value]) => ({ ...acc, [key]: value }), {}); | ||
}; | ||
return ( | ||
<form onChange={() => setCount(count + 1)} aria-label="form"> | ||
<Story /> | ||
<pre role="log">{JSON.stringify(getData())}</pre> | ||
</form> | ||
); | ||
}; |
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 |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from "./input"; | ||
export * from "./select"; |
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 @@ | ||
export * from "./input"; |
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,59 @@ | ||
.mykn-input { | ||
appearance: none; | ||
align-items: center; | ||
background: var(--typography-color-background); | ||
border: 1px solid var(--theme-color-primary-800); | ||
border-radius: 6px; | ||
box-sizing: border-box; | ||
color: var(--typography-color-body); | ||
font-family: Inter, sans-serif; | ||
font-size: var(--typography-font-size-body-s); | ||
line-height: var(--typography-line-height-body-s); | ||
padding: var(--spacing-v-s) var(--spacing-h-s); | ||
position: relative; | ||
width: min(320px, 100%); | ||
max-width: 100%; | ||
|
||
&[size] { | ||
width: auto; | ||
} | ||
|
||
&[type="color"] { | ||
min-height: 38px; | ||
overflow: hidden; | ||
padding: 0; | ||
|
||
&::-webkit-color-swatch-wrapper { | ||
padding: 0; | ||
} | ||
|
||
&::-webkit-color-swatch { | ||
border: none; | ||
} | ||
|
||
&:before, | ||
&:after { | ||
align-items: center; | ||
color: var(--typography-color-body); | ||
content: attr(value); | ||
display: flex; | ||
height: 50%; | ||
left: 50%; | ||
position: absolute; | ||
top: 50%; | ||
transform: translate(-50%, -50%); | ||
} | ||
|
||
&:before { | ||
opacity: 0.9; | ||
background-color: var(--typography-color-background); | ||
border-radius: 6px; | ||
color: transparent; | ||
padding: var(--spacing-v-s) var(--spacing-h-s); | ||
} | ||
} | ||
|
||
&[type="file"] { | ||
border: none; | ||
} | ||
} |
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,221 @@ | ||
import { action } from "@storybook/addon-actions"; | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
import { expect, fn, userEvent, waitFor, within } from "@storybook/test"; | ||
import { Formik } from "formik"; | ||
import React from "react"; | ||
|
||
import { Button } from "../../button"; | ||
import { FORM_TEST_DECORATOR } from "../.storybook/decorators"; | ||
import { Input } from "./input"; | ||
|
||
const meta = { | ||
title: "Form/Input", | ||
component: Input, | ||
play: async ({ canvasElement, args }) => { | ||
const testValue = | ||
args.value || args.placeholder?.replace("e.g. ", "") || "Hello world!"; | ||
const canvas = within(canvasElement); | ||
let input; | ||
|
||
switch (args.type) { | ||
case "number": | ||
input = await canvas.getByRole("spinbutton"); | ||
break; | ||
case "password": | ||
input = await canvas.getByPlaceholderText("Enter password"); | ||
break; | ||
default: | ||
input = await canvas.getByRole("textbox"); | ||
break; | ||
} | ||
|
||
const spy = fn(); | ||
input.addEventListener("change", spy); | ||
|
||
await userEvent.click(input, { delay: 10 }); | ||
await userEvent.clear(input); | ||
await userEvent.type(input, String(testValue)); | ||
|
||
// Test that event listener on the (custom) select gets called. | ||
await waitFor(testEventListener); | ||
|
||
async function testEventListener() { | ||
await expect(spy).toHaveBeenCalled(); | ||
} | ||
|
||
// Test that the FormData serialization returns the correct value. | ||
await waitFor(testFormDataSerialization, { | ||
timeout: String(testValue).length * 100, | ||
}); | ||
|
||
async function testFormDataSerialization() { | ||
const pre = await canvas.findByRole("log"); | ||
const data = JSON.parse(pre?.textContent || "{}"); | ||
await expect(data.input).toBe(testValue); | ||
} | ||
}, | ||
} satisfies Meta<typeof Input>; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
const FORM_TEST_ARG_TYPES = { | ||
onChange: { action: "onChange" }, | ||
}; | ||
|
||
export const InputComponent: Story = { | ||
args: { | ||
name: "input", | ||
placeholder: "e.g. John Doe", | ||
type: "text", | ||
}, | ||
argTypes: FORM_TEST_ARG_TYPES, | ||
decorators: [FORM_TEST_DECORATOR], | ||
}; | ||
|
||
export const InputTypeColor: Story = { | ||
args: { | ||
name: "input", | ||
type: "color", | ||
value: "#00bfcb", | ||
}, | ||
argTypes: FORM_TEST_ARG_TYPES, | ||
decorators: [FORM_TEST_DECORATOR], | ||
play: () => undefined, | ||
}; | ||
|
||
// TODO: DateInput. | ||
export const InputTypeDate: Story = { | ||
args: { | ||
name: "input", | ||
placeholder: "e.g. 15-09-2023", | ||
type: "date", | ||
}, | ||
argTypes: FORM_TEST_ARG_TYPES, | ||
play: () => undefined, | ||
}; | ||
|
||
export const InputTypeEmail: Story = { | ||
args: { | ||
name: "input", | ||
placeholder: "e.g. [email protected]", | ||
type: "email", | ||
}, | ||
argTypes: FORM_TEST_ARG_TYPES, | ||
decorators: [FORM_TEST_DECORATOR], | ||
}; | ||
|
||
// TODO: FileInput. | ||
export const InputTypeFile: Story = { | ||
args: { | ||
name: "input", | ||
type: "file", | ||
}, | ||
argTypes: FORM_TEST_ARG_TYPES, | ||
decorators: [FORM_TEST_DECORATOR], | ||
play: () => undefined, | ||
}; | ||
|
||
export const InputTypeNumber: Story = { | ||
args: { | ||
name: "input", | ||
placeholder: "e.g. 3", | ||
type: "number", | ||
}, | ||
argTypes: FORM_TEST_ARG_TYPES, | ||
decorators: [FORM_TEST_DECORATOR], | ||
}; | ||
|
||
export const InputTypePassword: Story = { | ||
args: { | ||
name: "input", | ||
placeholder: "Enter password", | ||
type: "password", | ||
value: "p4$$w0rd", | ||
}, | ||
argTypes: FORM_TEST_ARG_TYPES, | ||
decorators: [FORM_TEST_DECORATOR], | ||
}; | ||
|
||
export const InputTypeTel: Story = { | ||
args: { | ||
name: "input", | ||
placeholder: "e.g. +31 (0)20 753 05 23", | ||
type: "tel", | ||
}, | ||
argTypes: FORM_TEST_ARG_TYPES, | ||
decorators: [FORM_TEST_DECORATOR], | ||
}; | ||
|
||
export const InputTypeUrl: Story = { | ||
args: { | ||
name: "input", | ||
type: "Url", | ||
placeholder: "e.g. https://www.maykinmedia.nl", | ||
}, | ||
argTypes: FORM_TEST_ARG_TYPES, | ||
decorators: [FORM_TEST_DECORATOR], | ||
}; | ||
|
||
export const InputWithCustomSize: Story = { | ||
args: { | ||
name: "input", | ||
placeholder: "e.g. 1015CJ", | ||
size: 6, | ||
type: "text", | ||
}, | ||
argTypes: FORM_TEST_ARG_TYPES, | ||
decorators: [FORM_TEST_DECORATOR], | ||
}; | ||
|
||
export const UsageWithFormik: Story = { | ||
args: { | ||
name: "input", | ||
placeholder: "e.g. John Doe", | ||
type: "text", | ||
}, | ||
argTypes: { | ||
// @ts-expect-error - Using FormikProps here while SelectProps is expected. | ||
validate: { action: "validate" }, | ||
onSubmit: { action: "onSubmit" }, | ||
}, | ||
render: (args) => { | ||
return ( | ||
<Formik | ||
initialValues={{ input: "" }} | ||
validate={action("validate")} | ||
onSubmit={action("onSubmit")} | ||
> | ||
{({ handleChange, handleSubmit, values }) => ( | ||
<form onSubmit={handleSubmit}> | ||
<Input | ||
value={values.input} | ||
onChange={handleChange} | ||
{...args} | ||
></Input> | ||
<pre role="log">{JSON.stringify(values)}</pre> | ||
<Button type="submit">Verzenden</Button> | ||
</form> | ||
)} | ||
</Formik> | ||
); | ||
}, | ||
decorators: [(Story) => <Story />], | ||
play: async ({ canvasElement }) => { | ||
const canvas = within(canvasElement); | ||
const input = canvas.getByRole("textbox"); | ||
|
||
userEvent.clear(input); | ||
userEvent.click(input, { delay: 10 }); | ||
userEvent.type(input, "John Doe"); | ||
|
||
// Test that the FormData serialization returns the correct value. | ||
await waitFor(testFormikSerialization); | ||
|
||
async function testFormikSerialization() { | ||
const pre = await canvas.findByRole("log"); | ||
const data = JSON.parse(pre?.textContent || "{}"); | ||
await expect(data.input).toBe("John Doe"); | ||
} | ||
}, | ||
}; |
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,70 @@ | ||
import React, { useEffect, useState } from "react"; | ||
|
||
import { eventFactory } from "../eventFactory"; | ||
import "./input.scss"; | ||
|
||
export type InputProps = Omit< | ||
React.InputHTMLAttributes<HTMLInputElement>, | ||
"value" | ||
> & { | ||
/** Gets called when the value is changed */ | ||
onChange?: (event: Event) => void; | ||
|
||
/** Input value. */ | ||
value?: string | number; | ||
}; | ||
|
||
/** | ||
* Input component | ||
* @param children | ||
* @param props | ||
* @constructor | ||
*/ | ||
export const Input: React.FC<InputProps> = ({ | ||
type = "text", | ||
value, | ||
onChange, | ||
...props | ||
}) => { | ||
const inputRef = React.useRef<HTMLInputElement>(null); | ||
const [valueState, setValueState] = useState(value || ""); | ||
|
||
/** | ||
* Syncs value state with value prop change. | ||
*/ | ||
useEffect(() => setValueState(value || ""), [value]); | ||
|
||
/** | ||
* Handles a change of value. | ||
* @param event | ||
*/ | ||
const _onChange: React.ChangeEventHandler<HTMLInputElement> = (event) => { | ||
setValueState(event.target.value); | ||
|
||
/* | ||
* Dispatch change event. | ||
* | ||
* A custom "change" event with `detail` set to the `event.target.value` is | ||
* dispatched on `input.current`. | ||
* | ||
* This aims to improve compatibility with various approaches to dealing | ||
* with forms. | ||
*/ | ||
const input = inputRef.current as HTMLInputElement; | ||
const detail = type === "file" ? input.files : event.target.value; | ||
const changeEvent = eventFactory("change", detail, true, false, false); | ||
input.dispatchEvent(changeEvent); | ||
onChange && onChange(changeEvent); | ||
}; | ||
|
||
return ( | ||
<input | ||
ref={inputRef} | ||
className="mykn-input" | ||
type={type} | ||
value={valueState} | ||
onChange={_onChange} | ||
{...props} | ||
/> | ||
); | ||
}; |
Oops, something went wrong.