@@ -1,6 +1,6 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { TextControl } from '@tesseract-design/web-base'; | import { TextControl } from '@tesseract-design/web-base'; | ||||
import { delegateTriggerEvent, useClientSide } from '@modal-sh/react-utils'; | |||||
import { useClientSide, useProxyInput } from '@modal-sh/react-utils'; | |||||
import PhoneInput, { Country, Value } from 'react-phone-number-input/input'; | import PhoneInput, { Country, Value } from 'react-phone-number-input/input'; | ||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
@@ -82,49 +82,13 @@ export const PhoneNumberInput = React.forwardRef< | |||||
const labelId = React.useId(); | const labelId = React.useId(); | ||||
const defaultId = React.useId(); | const defaultId = React.useId(); | ||||
const id = idProp ?? defaultId; | const id = idProp ?? defaultId; | ||||
const defaultRef = React.useRef<PhoneNumberInputDerivedElement>(null); | |||||
React.useEffect(() => { | |||||
const { current: currentRaw } = defaultRef; | |||||
const current = currentRaw as unknown as PhoneNumberInputDerivedElement; | |||||
const forwardedRefProxy = new Proxy(current, { | |||||
get(target, prop, receiver) { | |||||
return Reflect.get(target, prop, receiver) as unknown; | |||||
}, | |||||
set(target, prop, newValue, receiver) { | |||||
if (prop === 'value') { | |||||
setPhoneNumber(newValue as string); | |||||
current.value = newValue as string; | |||||
return true; | |||||
} | |||||
return Reflect.set(target, prop, newValue, receiver); | |||||
}, | |||||
}) as unknown as PhoneNumberInputDerivedElement; | |||||
if (typeof forwardedRef === 'function') { | |||||
forwardedRef(forwardedRefProxy); | |||||
return; | |||||
} | |||||
if (typeof forwardedRef === 'object' && forwardedRef) { | |||||
const mutableForwardedRef = forwardedRef as React.MutableRefObject< | |||||
PhoneNumberInputDerivedElement | |||||
>; | |||||
mutableForwardedRef.current = forwardedRefProxy; | |||||
} | |||||
}, [forwardedRef, defaultRef]); | |||||
const handlePhoneInputChange = (phoneNumberValue: Value) => { | |||||
if (!(typeof defaultRef === 'object' && defaultRef)) { | |||||
return; | |||||
} | |||||
const { current: input } = defaultRef; | |||||
if (!input) { | |||||
return; | |||||
} | |||||
setPhoneNumber(phoneNumberValue); | |||||
delegateTriggerEvent('change', input, phoneNumberValue ?? ''); | |||||
}; | |||||
const { | |||||
defaultRef, | |||||
handleChange: handlePhoneInputChange, | |||||
} = useProxyInput<PhoneNumberInputDerivedElement, Value>({ | |||||
forwardedRef, | |||||
valueSetterFn: (v) => setPhoneNumber(v), | |||||
}); | |||||
const commonInputStyles = clsx( | const commonInputStyles = clsx( | ||||
'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums', | 'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums', | ||||
@@ -0,0 +1,78 @@ | |||||
import * as React from 'react'; | |||||
import { delegateTriggerEvent } from '../event'; | |||||
type ValueType = string | number | readonly string[]; | |||||
interface HTMLElementWithValue extends HTMLElement { | |||||
value: ValueType; | |||||
} | |||||
export interface UseProxyInputOptions<Element extends HTMLElementWithValue, Value = ValueType> { | |||||
forwardedRef: React.Ref<Element>; | |||||
valueSetterFn: (value: Value) => void; | |||||
transformChangeHandlerArgs?: (...e: unknown[]) => Value; | |||||
} | |||||
export const useProxyInput = < | |||||
T extends HTMLElementWithValue = HTMLElementWithValue, | |||||
V = ValueType, | |||||
>(options: UseProxyInputOptions<T, V>) => { | |||||
const { | |||||
forwardedRef, | |||||
valueSetterFn, | |||||
transformChangeHandlerArgs = (e) => e as V, | |||||
} = options; | |||||
const defaultRef = React.useRef<T>(null); | |||||
React.useEffect(() => { | |||||
const { current: currentRaw } = defaultRef; | |||||
const current = currentRaw as unknown as T; | |||||
const forwardedRefProxy = new Proxy(current, { | |||||
get(target, prop, receiver) { | |||||
return Reflect.get(target, prop, receiver) as unknown; | |||||
}, | |||||
set(target, prop, newValue, receiver) { | |||||
if (prop === 'value') { | |||||
const thisNewValue = newValue as V; | |||||
const actualNewValue = ( | |||||
Array.isArray(thisNewValue) ? thisNewValue.join('') : thisNewValue?.toString() ?? '' | |||||
) as V; | |||||
valueSetterFn(actualNewValue); | |||||
current.value = actualNewValue as string; | |||||
return true; | |||||
} | |||||
return Reflect.set(target, prop, newValue, receiver); | |||||
}, | |||||
}) as unknown as T; | |||||
if (typeof forwardedRef === 'function') { | |||||
forwardedRef(forwardedRefProxy); | |||||
return; | |||||
} | |||||
if (typeof forwardedRef === 'object' && forwardedRef) { | |||||
const mutableForwardedRef = forwardedRef as React.MutableRefObject<T>; | |||||
mutableForwardedRef.current = forwardedRefProxy; | |||||
} | |||||
}); | |||||
const handleChange = React.useCallback((e: unknown) => { | |||||
if (!(typeof defaultRef === 'object' && defaultRef)) { | |||||
return; | |||||
} | |||||
const { current: input } = defaultRef; | |||||
if (!input) { | |||||
return; | |||||
} | |||||
valueSetterFn(transformChangeHandlerArgs(e)); | |||||
delegateTriggerEvent('change', input, e); | |||||
}, [valueSetterFn, transformChangeHandlerArgs, defaultRef]); | |||||
return React.useMemo(() => ({ | |||||
defaultRef, | |||||
handleChange, | |||||
}), [ | |||||
defaultRef, | |||||
handleChange, | |||||
]); | |||||
}; |
@@ -0,0 +1,6 @@ | |||||
import * as React from 'react'; | |||||
export const useFallbackId = (id?: string) => { | |||||
const defaultId = React.useId(); | |||||
return id || defaultId; | |||||
}; |
@@ -1,2 +1,4 @@ | |||||
export * from './hooks/client-side'; | export * from './hooks/client-side'; | ||||
export * from './hooks/form'; | |||||
export * from './hooks/id'; | |||||
export * from './event'; | export * from './event'; |
@@ -22,23 +22,51 @@ const TemporalPage: NextPage = () => { | |||||
/> | /> | ||||
</Subsection> | </Subsection> | ||||
<Subsection title="With Ref"> | <Subsection title="With Ref"> | ||||
<Formatted.PhoneNumberInput | |||||
label="Phone" | |||||
name="phone" | |||||
enhanced | |||||
border | |||||
ref={phoneNumberRef} | |||||
onChange={(e) => { console.log('change', e.currentTarget.name, e.currentTarget, e.currentTarget.value)}} | |||||
/> | |||||
<ActionButton | |||||
onClick={() => { | |||||
if (phoneNumberRef.current) { | |||||
phoneNumberRef.current.value = '+639123456789'; | |||||
} | |||||
}} | |||||
> | |||||
Set Value | |||||
</ActionButton> | |||||
<div className="flex gap-4 flex-wrap"> | |||||
<div> | |||||
<Formatted.PhoneNumberInput | |||||
label="Phone" | |||||
name="phone" | |||||
enhanced | |||||
border | |||||
ref={phoneNumberRef} | |||||
onChange={(e) => { console.log('change', e.currentTarget.name, e.currentTarget, e.currentTarget.value)}} | |||||
/> | |||||
</div> | |||||
<div> | |||||
<ActionButton | |||||
onClick={() => { | |||||
if (phoneNumberRef.current) { | |||||
phoneNumberRef.current.value = '+639123456789'; | |||||
} | |||||
}} | |||||
> | |||||
Set Value | |||||
</ActionButton> | |||||
</div> | |||||
<div> | |||||
<ActionButton | |||||
onClick={() => { | |||||
if (phoneNumberRef.current) { | |||||
phoneNumberRef.current.value = '+63465123456'; | |||||
} | |||||
}} | |||||
> | |||||
Set Other Value | |||||
</ActionButton> | |||||
</div> | |||||
<div> | |||||
<ActionButton | |||||
onClick={() => { | |||||
if (phoneNumberRef.current) { | |||||
phoneNumberRef.current.value = '+63288888888'; | |||||
} | |||||
}} | |||||
> | |||||
Set Yet Other Value | |||||
</ActionButton> | |||||
</div> | |||||
</div> | |||||
</Subsection> | </Subsection> | ||||
</Section> | </Section> | ||||
<Section title="EmailInput"> | <Section title="EmailInput"> | ||||