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

feat: New select component #169

Merged
merged 49 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
25d963d
component
Jul 26, 2024
f76fc4a
component
Jul 26, 2024
2c91b20
props cleaned
Jul 26, 2024
8025701
option processing moved to function
Jul 26, 2024
d630378
change input style
Jul 26, 2024
2fe2fcf
trying to use popper. Doesnt really work yet
Jul 26, 2024
3468768
revert resolve
Jul 26, 2024
29a1688
roll back
Jul 26, 2024
b364ba7
fix options text
Jul 26, 2024
b4290ea
better styles
Jul 26, 2024
4e7b455
input style change
Jul 26, 2024
bcd6ed0
optional icons
Jul 26, 2024
396e355
fix input processing
Jul 26, 2024
3804e9e
validation in create world
Jul 27, 2024
f35803e
Select in for proxy in serverlist
Jul 27, 2024
9d3af27
fix styling
Jul 27, 2024
ffad46d
disabled for input
Jul 27, 2024
5d6473c
validation for input
Jul 27, 2024
a79393e
border fix
Jul 28, 2024
0e9c05e
better select option component
Jul 28, 2024
4f564d6
floating fix
Jul 28, 2024
08bce44
validation color change fix
Jul 28, 2024
41ae8f3
fix input typing
Jul 28, 2024
d2bdf63
some dixes
Jul 28, 2024
b66a410
overflow for long options
Jul 28, 2024
c702661
fix text sselect in input
Jul 29, 2024
8cb9012
props for input change
Jul 29, 2024
40dbd65
component for versions
Jul 31, 2024
c47c7c9
version component in create world
Jul 31, 2024
8c815ba
clean up
Jul 31, 2024
c805c4f
storybook fix
Aug 2, 2024
4adef34
doesnt show options on clear value
Aug 3, 2024
b762b71
fix create world versions
Aug 3, 2024
cf1faf0
default value for world create
Aug 4, 2024
fbf5f54
select positioning
Aug 5, 2024
e4d045c
Merge branch 'next' into pr/gguio/169-1
zardoy Aug 6, 2024
c358a2e
open options on empty input
Aug 6, 2024
ebdf84e
fix show options on clear input
Aug 7, 2024
ca01926
clean
Aug 7, 2024
26a2bf6
fix singleplayer change focus with ctrl holding
Aug 8, 2024
f22b9b5
component integrated
Aug 15, 2024
f0b81e9
initial options
Aug 15, 2024
3df78d1
works
Aug 15, 2024
67a52bf
Merge branch 'next' into select
gguio Aug 15, 2024
f076310
can copy value
Aug 15, 2024
542192a
change style for chosen option
Aug 15, 2024
6bc0990
chosen option remains and create changed to use
Aug 16, 2024
0267b10
full list on first click
Aug 16, 2024
2237e2d
fix menu isnt full on subsequent clickes
Aug 16, 2024
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-select": "^5.8.0",
"react-transition-group": "^4.4.5",
"remark": "^15.0.1",
"sanitize-filename": "^1.6.3",
Expand Down
380 changes: 251 additions & 129 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

24 changes: 21 additions & 3 deletions src/react/AddServerOrConnect.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useEffect } from 'react'
import React from 'react'
import Screen from './Screen'
import Input from './Input'
import Button from './Button'
import SelectGameVersion from './SelectGameVersion'
import { useIsSmallWidth } from './simpleHooks'

export interface BaseServerInfo {
Expand All @@ -24,11 +25,12 @@ interface Props {
defaults?: Pick<BaseServerInfo, 'proxyOverride' | 'usernameOverride'>
accounts?: string[]
authenticatedAccounts?: number
versions?: string[]
}

const ELEMENTS_WIDTH = 190

export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, defaults, accounts, authenticatedAccounts }: Props) => {
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, defaults, accounts, versions, authenticatedAccounts }: Props) => {
const qsParams = parseQs ? new URLSearchParams(window.location.search) : undefined
const qsParamName = qsParams?.get('name')
const qsParamIp = qsParams?.get('ip')
Expand Down Expand Up @@ -93,7 +95,23 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
<InputWithLabel required label="Server IP" value={serverIp} disabled={lockConnect && qsIpParts?.[0] !== null} onChange={({ target: { value } }) => setServerIp(value)} />
<InputWithLabel label="Server Port" value={serverPort} disabled={lockConnect && qsIpParts?.[1] !== null} onChange={({ target: { value } }) => setServerPort(value)} placeholder='25565' />
<div style={{ gridColumn: smallWidth ? '' : 'span 2' }}>Overrides:</div>
<InputWithLabel label="Version Override" value={versionOverride} disabled={lockConnect && qsParamVersion !== null} onChange={({ target: { value } }) => setVersionOverride(value)} placeholder='Optional, but recommended to specify' />
<div style={{
display: 'flex',
flexDirection: 'column',
}}>
<label style={{ fontSize: 12, marginBottom: 1, color: 'lightgray' }}>Version Override</label>
<SelectGameVersion
versions={versions?.map(v => { return { value: v, label: v } }) ?? []}
onChange={(value) => {
setVersionOverride(value)
}}
// inputProps={{
// placeholder: 'Optional, but recommended to specify',
// disabled: lockConnect && qsParamVersion !== null
// }}
/>
</div>

<InputWithLabel label="Proxy Override" value={proxyOverride} disabled={lockConnect && qsParamProxy !== null} onChange={({ target: { value } }) => setProxyOverride(value)} placeholder={defaults?.proxyOverride} />
<InputWithLabel label="Username Override" value={usernameOverride} disabled={!noAccountSelected || lockConnect && qsParamUsername !== null} onChange={({ target: { value } }) => setUsernameOverride(value)} placeholder={defaults?.usernameOverride} />
<label style={{
Expand Down
19 changes: 8 additions & 11 deletions src/react/CreateWorld.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { filesize } from 'filesize'
import Input from './Input'
import Screen from './Screen'
import Button from './Button'
import SelectGameVersion from './SelectGameVersion'
import styles from './createWorld.module.css'

// const worldTypes = ['default', 'flat', 'largeBiomes', 'amplified', 'customized', 'buffet', 'debug_all_block_states']
Expand Down Expand Up @@ -43,18 +44,14 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer
}}
placeholder='World name'
/>
<select
value={version} style={{
background: 'gray',
color: 'white'
}} onChange={({ target: { value } }) => {
creatingWorldState.version = value
<SelectGameVersion
versions={versions.map((obj) => { return { value: obj.version, label: obj.version === defaultVersion ? obj.version + ' (available offline)' : obj.version } })}
selected={{ value: defaultVersion, label: defaultVersion + ' (available offline)' }}
onChange={(value) => {
creatingWorldState.version = value ?? defaultVersion
}}
>
{versions.map(({ version, label }) => {
return <option key={version} value={version}>{label}</option>
})}
</select>
containerStyle={{ width: '100px' }}
/>
</form>
<div style={{ display: 'flex' }}>
<Button onClick={() => {
Expand Down
21 changes: 17 additions & 4 deletions src/react/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
import React, { useEffect, useRef } from 'react'
import React, { CSSProperties, useEffect, useRef, useState } from 'react'
import { isMobile } from 'prismarine-viewer/viewer/lib/simpleUtils'
import styles from './input.module.css'

interface Props extends React.ComponentProps<'input'> {
rootStyles?: React.CSSProperties
autoFocus?: boolean
inputRef?: React.RefObject<HTMLInputElement>
validateInput?: (value: string) => CSSProperties | undefined
}

export default ({ autoFocus, rootStyles, inputRef, ...inputProps }: Props) => {
export default ({ autoFocus, rootStyles, inputRef, validateInput, ...inputProps }: Props) => {
const ref = useRef<HTMLInputElement>(null!)
const [validationStyle, setValidationStyle] = useState<CSSProperties>({})
const [value, setValue] = useState(inputProps.value ?? '')

useEffect(() => {
setValue(inputProps.value === '' || inputProps.value ? inputProps.value : value)
}, [inputProps.value])

useEffect(() => {
if (inputRef) (inputRef as any).current = ref.current
if (!autoFocus || isMobile()) return // Don't make screen keyboard popup on mobile
ref.current.focus()
}, [])

return <div className={styles.container} style={rootStyles}>

return <div id='input-container' className={styles.container} style={rootStyles}>
<input
ref={ref} className={styles.input} autoComplete='off' autoCapitalize='off' autoCorrect='off' autoSave='off' spellCheck='false'
{...inputProps}
style={{ ...validationStyle }} {...inputProps} value={value}
onChange={(e) => {
setValidationStyle(validateInput?.(e.target.value) ?? {})
setValue(e.target.value)
inputProps.onChange?.(e)
}}
/>
</div>
}
5 changes: 5 additions & 0 deletions src/react/Select.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

div.Mui-focused #input-container {
border: 1px solid white !important;
}

24 changes: 24 additions & 0 deletions src/react/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react'

import { CSSProperties } from 'react'
import Select from './Select'

const meta: Meta<typeof Select> = {
component: Select,
}

export default meta
type Story = StoryObj<typeof Select>

export const Primary: Story = {
args: {
initialOptions: [{ value: '1', label: 'option 1' }, { value: '2', label: 'option 2' }, { value: '3', label: 'option 3' },],
updateOptions (options) {},
processInput (input) {
console.log('input:', input)
if (input === 'option 3') return { border: '1px solid yellow' } as CSSProperties
},
iconInput: 'user',
iconOption: 'user'
},
}
109 changes: 109 additions & 0 deletions src/react/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { useState, CSSProperties } from 'react'
import Creatable from 'react-select/creatable'
import Input from './Input'
import './Select.css'
import styles from './select.module.css'


export interface OptionStorage {
value: string,
label: string
}

interface Props {
initialOptions: OptionStorage[]
updateOptions: (options: string) => void
processInput?: (input: string) => CSSProperties | undefined
processOption?: (option: string) => string
onValueChange?: (newVal: string) => void
defaultValue?: { value: string, label: string }
iconInput?: string
placeholder?: string
iconOption?: string
containerStyle?: CSSProperties
inputProps?: React.ComponentProps<typeof Input>
}

export default ({
initialOptions,
updateOptions,
processInput,
onValueChange,
defaultValue,
containerStyle,
placeholder
}: Props) => {
const [inputValue, setInputValue] = useState<string | undefined>(defaultValue?.label ?? '')
const [currValue, setCurrValue] = useState<string | undefined>(defaultValue?.label ?? '')
const [inputStyle, setInputStyle] = useState<CSSProperties>({})
const [isFirstClick, setIsFirstClick] = useState(true)

return <Creatable
options={initialOptions}
aria-invalid="true"
defaultValue={defaultValue}
blurInputOnSelect={true}
hideSelectedOptions={false}
maxMenuHeight={100}
isClearable={true}
formatCreateLabel={(value) => {
return 'Use "' + value + '"'
}}
placeholder={placeholder ?? ''}
onChange={(e, action) => {
console.log('value:', e?.value)
setCurrValue(e?.label)
setInputValue(e?.label)
onValueChange?.(e?.value ?? '')
updateOptions?.(e?.value ?? '')
setInputStyle(processInput?.(e?.value ?? '') ?? {})
}}
onInputChange={(e) => {
setIsFirstClick(false)
setInputValue(e)
}}
inputValue={inputValue}
onFocus={(state) => {
setInputValue(currValue)
}}
filterOption={(option, value) => {
return isFirstClick || option.label.includes(value)
}}
onMenuOpen={() => {
setIsFirstClick(true)
}}
classNames={{
control (state) {
return styles.container
},
input (state) {
return styles.input
},
option (state) {
return styles.container
}
}}
styles={{
container (base, state) { return { ...base, position: 'relative' } },
control (base, state) { return { ...containerStyle, ...inputStyle } },
menu (base, state) { return { position: 'absolute', zIndex: 10 } },
option (base, state) {
return {
boxSizing: 'border-box',
padding: '3px',
backgroundColor: 'black',
border: state.isFocused ? '1px solid white' : '1px solid grey',
height: 'fit-content',
...containerStyle
}
},
input (base, state) { return {} },
indicatorsContainer (base, state) { return { display: 'none' } },
placeholder (base, state) { return { ...base, padding: '3px', position: 'absolute' } },
singleValue (base, state) { return { ...base, margin: '0px', position: 'absolute', color: 'white' } },
valueContainer (base, state) { return { ...base, padding: '3px' } },
noOptionsMessage (base, state) { return { display: 'none' } }
}}
/>
}

51 changes: 51 additions & 0 deletions src/react/SelectGameVersion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { CSSProperties } from 'react'
import Select from './Select'
import Input from './Input'

type Version = { value: string, label: string }

export default (
{ versions, selected, onChange, updateOptions, containerStyle }:
{
versions: Version[],
selected?: Version,
inputProps?: React.ComponentProps<typeof Input>,
onChange?: (newValue: string) => void,
updateOptions?: (newSel: string) => void,
containerStyle?: CSSProperties
}
) => {
return <Select
initialOptions={versions}
defaultValue={selected}
updateOptions={(newSel) => {
updateOptions?.(newSel)
}}
onValueChange={onChange}
containerStyle={containerStyle ?? { width: '190px' }}
processInput={(value) => {
if (!versions || !value) return {}
const parsedsupportedVersions = versions.map(x => x.value.split('.').map(Number))
const parsedValue = value.split('.').map(Number)

const compareVersions = (v1, v2) => {
for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
const num1 = v1[i] || 0
const num2 = v2[i] || 0
if (num1 > num2) return 1
if (num1 < num2) return -1
}
return 0
}

parsedsupportedVersions.sort(compareVersions)
const minVersion = parsedsupportedVersions[0]
const maxVersion = parsedsupportedVersions.at(-1)

const isWithinRange = compareVersions(parsedValue, minVersion) >= 0 && compareVersions(parsedValue, maxVersion) <= 0
if (!isWithinRange) return { border: '1px solid red' }
if (!versions.some(x => x.value === value)) return { border: '1px solid yellow' }
}}
/>

}
Loading
Loading