Skip to content

Commit

Permalink
Create Select component that supports masks (#716)
Browse files Browse the repository at this point in the history
* Refactor Select to allow masks into the component

* Add autocomplete, key navigation and clear button to new select

* Improve Autocomplete

now it autocomplete with tab, and it doesnt open the dropdown when
clearing it

* Separate BaseSelect into smaller components

* Add first test to Select component

* Add Label to ClearInput

* Return enter keypress on form to its default

* Add keyboard accessibility to the select

* Update displayValue when value is changed outside of select

* Add automatic scroliing to select dropdown

* Add specs for strings and hash options

* Disable clearInput when the select is disabled

* Fix label not moving on timepicker

* Fix dropdown not opening with only 1 option
  • Loading branch information
negreirosleo authored Nov 1, 2023
1 parent 26d0aaa commit 419747c
Show file tree
Hide file tree
Showing 18 changed files with 1,255 additions and 188 deletions.
10 changes: 6 additions & 4 deletions frontend/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import '@testing-library/jest-dom'

Element.prototype.scrollIntoView = () => {}

Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
dispatchEvent: jest.fn()
}))
})
80 changes: 80 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"next-auth": "^4.23.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-imask": "^7.1.3",
"typescript": "5.2.2"
},
"devDependencies": {
Expand Down
53 changes: 22 additions & 31 deletions frontend/src/app/tasks/TaskForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import Stack from '@mui/joy/Stack'
import Button from '@mui/joy/Button'
import { Select, FreeSoloSelect } from '@/ui/Select/Select'
import { Select } from '@/ui/Select/Select'
import { Input } from '@/ui/Input/Input'
import { TextArea } from '@/ui/TextArea/TextArea'
import Typography from '@mui/joy/Typography'
Expand All @@ -13,13 +13,7 @@ import { Project } from '@/domain/Project'
import { TaskType } from '@/domain/TaskType'

import { useTaskForm } from './hooks/useTaskForm'

const timeOptions = () => {
const hours = Array.from({ length: 24 }, (_, i) => i)
const minutes = ['00', '15', '30', '45']

return hours.flatMap((h) => minutes.map((m) => `${h}:${m}`))
}
import { TimePicker } from './components/TimePicker'

type TaskFormProps = {
projects: Array<Project>
Expand Down Expand Up @@ -53,12 +47,15 @@ export const TaskForm = ({ projects, taskTypes, userId }: TaskFormProps) => {
ref={formRef}
>
<Select
onChange={(_, option) => handleChange('projectId', option?.id || '')}
value={projects.find((project) => project.id === task.projectId) || null}
onChange={(value) => handleChange('projectId', value)}
value={task.projectId}
name="projectId"
label="Select project"
options={projects}
getOptionLabel={(option) => option.description}
placeholder="Select project"
options={projects.map((project) => ({
label: project.description,
value: project.id.toString()
}))}
required
/>
<Stack flexDirection="row" gap="30px">
Expand All @@ -73,31 +70,24 @@ export const TaskForm = ({ projects, taskTypes, userId }: TaskFormProps) => {
</>
)}
</Button>
<FreeSoloSelect
sx={{ width: '166px' }}
<TimePicker
name="startTime"
label="From"
value={task.startTime}
onChange={(_, option) => {
if (option) {
selectStartTime(option)
}
}}
options={timeOptions()}
onChange={selectStartTime}
sx={{ width: '166px' }}
disabled={isTimerRunning}
required
/>
<FreeSoloSelect
sx={{ width: '166px' }}

<TimePicker
name="endTime"
label="To"
value={task.endTime}
onChange={(_, option) => {
if (option) {
handleChange('endTime', option)
}
onChange={(option: string) => {
handleChange('endTime', option)
}}
options={timeOptions()}
sx={{ width: '166px' }}
disabled={isTimerRunning}
required
/>
Expand All @@ -122,10 +112,10 @@ export const TaskForm = ({ projects, taskTypes, userId }: TaskFormProps) => {
<Select
name="taskType"
label="Select task type"
value={taskTypes.find((taskType) => taskType.slug === task.taskType) || null}
onChange={(_, option) => handleChange('taskType', option?.slug || '')}
options={taskTypes}
getOptionLabel={(option) => option.name}
value={task.taskType}
onChange={(value) => handleChange('taskType', value)}
options={taskTypes.map((taskType) => ({ label: taskType.name, value: taskType.slug }))}
placeholder="Select task type"
/>
<Input
value={task.story}
Expand All @@ -141,6 +131,7 @@ export const TaskForm = ({ projects, taskTypes, userId }: TaskFormProps) => {
name="moreActions"
label="More Actions"
options={[]}
value=""
/>
<Stack
sx={{
Expand Down
60 changes: 60 additions & 0 deletions frontend/src/app/tasks/components/TimePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { IMask } from 'react-imask'
import { MaskedSelect, MaskedSelectProps } from '@/ui/Select/MaskedSelect'

const timeOptions = () => {
const hours = Array.from({ length: 24 }, (_, i) => i)
const minutes = ['00', '15', '30', '45']

return hours.flatMap((h) => minutes.map((m) => `${h}:${m}`))
}

const hourMinuteMask = [
{
mask: '0:@0',
definitions: {
'@': /[0-5]/
}
},
{
mask: 'HH:MM',
blocks: {
HH: {
mask: IMask.MaskedRange,
from: 0,
to: 23
},
MM: {
mask: IMask.MaskedRange,
from: 0,
to: 59
}
}
}
]

type TimePickerProps = Omit<MaskedSelectProps, 'options' | 'mask'>

export const TimePicker = ({
name,
label,
value,
onChange,
sx,
disabled,
required
}: TimePickerProps) => {
return (
<MaskedSelect
options={timeOptions()}
name={name}
label={label}
value={value}
onChange={onChange}
placeholder=" "
sx={sx}
mask={hourMinuteMask}
disabled={disabled}
required={required}
/>
)
}
2 changes: 1 addition & 1 deletion frontend/src/app/tasks/hooks/useTaskForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const useTaskForm = ({ userId }: { userId: TaskIntent['userId'] }) => {

useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
if ((event.key === 's' && event.ctrlKey) || event.key === 'Enter') {
if (event.key === 's' && event.ctrlKey) {
event.preventDefault()
formRef.current?.requestSubmit()
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/domain/Project.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type Project = {
id: string
id: number
area_id: number
customer_id: number
description: string
Expand Down
Loading

0 comments on commit 419747c

Please sign in to comment.