Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable shopping list title to be edited from shopping lists page #40

Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 19
- name: Install Node Modules
run: yarn
- name: lint
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
yarn 1.22.19
nodejs 19.6.1
nodejs 19.8.1
5 changes: 5 additions & 0 deletions docs/contexts/shopping-lists-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ The `ShoppingListsContext` keeps track of the active game and its shopping lists
- `attributes`: an object containing an optional `title` key with a string value, the attributes of the shopping list to create
- `onSuccess` (optional): a callback called on a successful response; no arguments are passed in and its return value, if any, is not used
- `onError` (optional): a callback called on an unsuccessful response; no arguments are passed in and its return value, if any, is not used
- `updateShoppingList`: a function that creates a shopping list for the current active game at the API, taking the following arguments:
- `listId`: the ID of the shopping list to be updated
- `attributes`: an object containing an optional `title` key with a string value, the attributes of the shopping list to create
- `onSuccess` (optional): a callback called on a successful response; no arguments are passed in and its return value, if any, is not used
- `onError` (optional): a callback called on an unsuccessful response; no arguments are passed in and its return value, if any, is not used
- `destroyShoppingList`: a function that destroys the selected shopping list at the API, taking the following arguments:
- `listId`: the `id` of the shopping list to be destroyed
- `onSuccess` (optional): a callback called on a successful response; no arguments are passed in and its return value, if any, is not used
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
"build-storybook": "NODE_OPTIONS=--openssl-legacy-provider build-storybook"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.3.0",
"@fortawesome/free-solid-svg-icons": "^6.3.0",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@react-hook/resize-observer": "^1.2.6",
"classnames": "^2.3.2",
"firebase": "^9.17.1",
"react": "^18.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ exports[`GameLineItem > matches snapshot 1`] = `
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 320 512"
viewBox="0 0 384 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M310.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L160 210.7 54.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L114.7 256 9.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 301.3 265.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L205.3 256 310.6 150.6z"
d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"
fill="currentColor"
/>
</svg>
Expand All @@ -47,7 +47,7 @@ exports[`GameLineItem > matches snapshot 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M471.6 21.7c-21.9-21.9-57.3-21.9-79.2 0L362.3 51.7l97.9 97.9 30.1-30.1c21.9-21.9 21.9-57.3 0-79.2L471.6 21.7zm-299.2 220c-6.1 6.1-10.8 13.6-13.5 21.9l-29.6 88.8c-2.9 8.6-.6 18.1 5.8 24.6s15.9 8.7 24.6 5.8l88.8-29.6c8.2-2.8 15.7-7.4 21.9-13.5L437.7 172.3 339.7 74.3 172.4 241.7zM96 64C43 64 0 107 0 160V416c0 53 43 96 96 96H352c53 0 96-43 96-96V320c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H96z"
d="M471.6 21.7c-21.9-21.9-57.3-21.9-79.2 0L362.3 51.7l97.9 97.9 30.1-30.1c21.9-21.9 21.9-57.3 0-79.2L471.6 21.7zm-299.2 220c-6.1 6.1-10.8 13.6-13.5 21.9l-29.6 88.8c-2.9 8.6-.6 18.1 5.8 24.6s15.9 8.7 24.6 5.8l88.8-29.6c8.2-2.7 15.7-7.4 21.9-13.5L437.7 172.3 339.7 74.3 172.4 241.7zM96 64C43 64 0 107 0 160V416c0 53 43 96 96 96H352c53 0 96-43 96-96V320c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H96z"
fill="currentColor"
/>
</svg>
Expand Down Expand Up @@ -100,11 +100,11 @@ exports[`GameLineItem > matches snapshot 1`] = `
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 320 512"
viewBox="0 0 384 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M310.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L160 210.7 54.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L114.7 256 9.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 301.3 265.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L205.3 256 310.6 150.6z"
d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"
fill="currentColor"
/>
</svg>
Expand All @@ -124,7 +124,7 @@ exports[`GameLineItem > matches snapshot 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M471.6 21.7c-21.9-21.9-57.3-21.9-79.2 0L362.3 51.7l97.9 97.9 30.1-30.1c21.9-21.9 21.9-57.3 0-79.2L471.6 21.7zm-299.2 220c-6.1 6.1-10.8 13.6-13.5 21.9l-29.6 88.8c-2.9 8.6-.6 18.1 5.8 24.6s15.9 8.7 24.6 5.8l88.8-29.6c8.2-2.8 15.7-7.4 21.9-13.5L437.7 172.3 339.7 74.3 172.4 241.7zM96 64C43 64 0 107 0 160V416c0 53 43 96 96 96H352c53 0 96-43 96-96V320c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H96z"
d="M471.6 21.7c-21.9-21.9-57.3-21.9-79.2 0L362.3 51.7l97.9 97.9 30.1-30.1c21.9-21.9 21.9-57.3 0-79.2L471.6 21.7zm-299.2 220c-6.1 6.1-10.8 13.6-13.5 21.9l-29.6 88.8c-2.9 8.6-.6 18.1 5.8 24.6s15.9 8.7 24.6 5.8l88.8-29.6c8.2-2.7 15.7-7.4 21.9-13.5L437.7 172.3 339.7 74.3 172.4 241.7zM96 64C43 64 0 107 0 160V416c0 53 43 96 96 96H352c53 0 96-43 96-96V320c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H96z"
fill="currentColor"
/>
</svg>
Expand Down
38 changes: 38 additions & 0 deletions src/components/listEditForm/listEditForm.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.root {
display: grid;
grid-template-columns: auto min-content;
max-width: var(--max-width);
background-color: var(--scheme-color);
align-content: center;
border-bottom: 1px solid var(--border-color);
}

.input {
font-family: 'Quattrocento Sans', Arial, Helvetica, sans-serif;
font-size: 1.25rem;
font-weight: 700;
padding: 0;
padding-right: 8px;
background-color: inherit;
color: var(--text-color);
border-style: none;
}

.submit {
background-color: inherit;
color: var(--text-color);
cursor: pointer;
flex: 0;
padding-right: 0;
border-style: none;
}

.fa {
color: var(--text-color);
font-size: 17px;
margin-bottom: -2px;
}

.fa:hover {
color: var(--icon-hover-color);
}
28 changes: 28 additions & 0 deletions src/components/listEditForm/listEditForm.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useRef, type FormEvent } from 'react'
import { PINK } from '../../utils/colorSchemes'
import { ColorProvider } from '../../contexts/colorContext'
import ListEditForm from './listEditForm'

const WrapperComponent = () => {
const formRef = useRef<HTMLFormElement>(null)

return (
<div>
<ListEditForm
className="foo"
formRef={formRef}
maxTotalWidth={256}
title="Severin Manor"
onSubmit={(e: FormEvent) => e.preventDefault()}
/>
</div>
)
}

export default { title: 'ListEditForm' }

export const Default = () => (
<ColorProvider colorScheme={PINK}>
<WrapperComponent />
</ColorProvider>
)
85 changes: 85 additions & 0 deletions src/components/listEditForm/listEditForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useRef, type FormEvent, type FormEventHandler } from 'react'
import { describe, test, expect, vitest } from 'vitest'
import { act, fireEvent } from '@testing-library/react'
import { render } from '../../support/testUtils'
import { BLUE } from '../../utils/colorSchemes'
import { ColorProvider } from '../../contexts/colorContext'
import ListEditForm from './listEditForm'

interface WrapperProps {
onSubmit: FormEventHandler
}

const WrapperComponent = ({ onSubmit }: WrapperProps) => {
const formRef = useRef(null)

return (
<div>
<ListEditForm
className="foo"
formRef={formRef}
maxTotalWidth={256}
title="Alchemy Ingredients"
onSubmit={onSubmit}
/>
</div>
)
}

describe('ListEditForm', () => {
test.skip('displays the title and submit button', () => {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is being skipped because rendering the form in JSDOM involves using the node-canvas package, which doesn't support worker_threads. What are worker_threads and are we using them in this application? Apparently we are. The maintainers have said they would like to add support this year, so I'm skipping this test rather than deleting it entirely.

const wrapper = render(
<ColorProvider colorScheme={BLUE}>
<WrapperComponent onSubmit={(e: FormEvent) => e.preventDefault()} />
</ColorProvider>
)

expect(wrapper).toBeTruthy()
expect(
wrapper.getByRole('textbox').attributes.getNamedItem('name')
).toEqual('title')
expect(wrapper.getByRole('button').attributes.getNamedItem('name')).toEqual(
'submit'
)
})

describe('submitting the form', () => {
test('calls the onSubmit function passed in when button is clicked', () => {
const onSubmit = vitest.fn()
const wrapper = render(
<ColorProvider colorScheme={BLUE}>
<WrapperComponent onSubmit={onSubmit} />
</ColorProvider>
)

const input = wrapper.getByRole('textbox')
const button = wrapper.getByRole('button')

act(() => {
fireEvent.change(input, { target: { value: 'Smithing Materials' } })
fireEvent.click(button)
})

expect(onSubmit).toHaveBeenCalledOnce()
})

test('calls the onSubmit function any time the form is submitted', () => {
const onSubmit = vitest.fn()
const wrapper = render(
<ColorProvider colorScheme={BLUE}>
<WrapperComponent onSubmit={onSubmit} />
</ColorProvider>
)

const input = wrapper.getByRole('textbox')
const form = wrapper.getByRole('form')

act(() => {
fireEvent.change(input, { target: { value: 'Smithing Materials' } })
fireEvent.submit(form)
})

expect(onSubmit).toHaveBeenCalledOnce()
})
})
})
122 changes: 122 additions & 0 deletions src/components/listEditForm/listEditForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
useState,
useRef,
useEffect,
type FormEventHandler,
type CSSProperties,
type ChangeEvent,
type RefObject,
} from 'react'
import classNames from 'classnames'
import { useColorScheme } from '../../hooks/contexts'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSquareCheck } from '@fortawesome/free-regular-svg-icons'
import styles from './listEditForm.module.css'

const FIXED_BUTTON_WIDTH = 72 // px

interface EditFormProps {
formRef: RefObject<any>
maxTotalWidth: number
className?: string
title: string
onSubmit: FormEventHandler
}

const ListEditForm = ({
formRef,
maxTotalWidth,
className,
title,
onSubmit,
}: EditFormProps) => {
const [maxTextWidth, setMaxTextWidth] = useState<number>(
danascheider marked this conversation as resolved.
Show resolved Hide resolved
maxTotalWidth - FIXED_BUTTON_WIDTH - 2
)

const getInputTextWidth = (text: string) => {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')

if (!context) return 0

context.font = '21px Quattrocento Sans'

const textWidth = context.measureText(text).width

return Math.min(textWidth, maxTextWidth)
}

const [inputValue, setInputValue] = useState(title)
const [inputWidth, setInputWidth] = useState(
`${getInputTextWidth(inputValue)}px`
)

const inputRef = useRef<HTMLInputElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)

const {
schemeColorDarkest,
textColorPrimary,
borderColor,
schemeColorLightest,
} = useColorScheme()

const rootStyles = {
'--scheme-color': schemeColorDarkest,
'--text-color': textColorPrimary,
'--border-color': borderColor,
'--icon-hover-color': schemeColorLightest,
'--max-width': `${maxTotalWidth}px`,
} as CSSProperties

const updateInputWidth = (e: ChangeEvent) => {
const newValue = (e.currentTarget as HTMLInputElement)?.value || ''
setInputValue(newValue)
setInputWidth(`${getInputTextWidth(newValue)}px`)
}

useEffect(() => {
inputRef.current?.focus()
}, [])

useEffect(() => {
setMaxTextWidth(maxTotalWidth - FIXED_BUTTON_WIDTH - 2)
setInputWidth(`${getInputTextWidth(inputValue)}px`)
}, [maxTotalWidth])

return (
<form
className={classNames(className, styles.root)}
style={rootStyles}
ref={formRef}
onSubmit={onSubmit}
aria-label="List title edit form"
>
<input
className={styles.input}
onClick={(e) => e.stopPropagation()}
onChange={updateInputWidth}
type="text"
name="title"
aria-label="title"
ref={inputRef}
style={{ width: inputWidth }}
defaultValue={inputValue}
pattern="^\s*[A-Za-z0-9 \-',]*\s*$"
title="Title can only contain alphanumeric characters, spaces, hyphens, commas, and apostrophes"
data-testid="editListTitle"
/>
<button
className={styles.submit}
ref={buttonRef}
name="submit"
type="submit"
>
<FontAwesomeIcon className={styles.fa} icon={faSquareCheck} />
</button>
</form>
)
}

export default ListEditForm
Loading