dh_demo

DreamHanks demo project
git clone git://git.lair.cx/dh_demo
Log | Files | Refs | README

commit b871dc707efa9313ae4e7b18e614eccb05ee02df
parent 720926afc98f5ab2db7dd29caf060f446309a35e
Author: Yongbin Kim <iam@yongbin.kim>
Date:   Wed, 25 Jan 2023 16:26:18 +0900

feat(components): Field 컴포넌트에 textarea 지원 추가

Signed-off-by: Yongbin Kim <iam@yongbin.kim>

Diffstat:
Mcomponents/form/Field.module.css | 2+-
Mcomponents/form/Field.module.css.map | 4++--
Mcomponents/form/Field.module.scss | 11++++++++++-
Mcomponents/form/Field.tsx | 53+++++++++++++++++++++++++++++++++++++++++------------
4 files changed, 54 insertions(+), 16 deletions(-)

diff --git a/components/form/Field.module.css b/components/form/Field.module.css @@ -1 +1 @@ -.field{display:flex;position:relative;border:1px solid rgba(0,0,0,0);margin-bottom:1rem;border-radius:4px;background-color:inherit;box-shadow:0 0 0 1px rgba(0,0,0,0);transition:all 200ms;color:#191c1b;border-color:#6f7975}.field.is-disabled{color:rgba(25,28,27,.5);border-color:rgba(111,121,117,.5)}.field.is-not-readonly.has-no-color:focus-within{border-color:#006b5a;box-shadow:0 0 0 1px #006b5a}.field.is-primary{color:#006b5a;border-color:#006b5a}.field.is-primary:focus-within{box-shadow:0 0 0 1px #006b5a}.field.is-secondary{color:#7b4998;border-color:#7b4998}.field.is-secondary:focus-within{box-shadow:0 0 0 1px #7b4998}.field.is-tertiary{color:#426278;border-color:#426278}.field.is-tertiary:focus-within{box-shadow:0 0 0 1px #426278}.field.is-error{color:red;border-color:red}.field.is-error:focus-within{box-shadow:0 0 0 1px red}@media(prefers-color-scheme: dark){.field{color:#e1e3e0;border-color:#89938f}.field.is-disabled{color:rgba(225,227,224,.5);border-color:rgba(137,147,143,.5)}.field.is-not-readonly.has-no-color:focus-within{border-color:#2cdebf;box-shadow:0 0 0 1px #2cdebf}.field.is-primary{color:#2cdebf;border-color:#2cdebf}.field.is-primary:focus-within{box-shadow:0 0 0 1px #2cdebf}.field.is-secondary{color:#e6b4ff;border-color:#e6b4ff}.field.is-secondary:focus-within{box-shadow:0 0 0 1px #e6b4ff}.field.is-tertiary{color:#aacbe4;border-color:#aacbe4}.field.is-tertiary:focus-within{box-shadow:0 0 0 1px #aacbe4}.field.is-error{color:#ffb4ab;border-color:#ffb4ab}.field.is-error:focus-within{box-shadow:0 0 0 1px #ffb4ab}}.input{width:100%;height:3.5rem;border:0;padding:0 1rem;color:inherit;background:rgba(0,0,0,0);outline:none}.field.has-icon-left .input{padding-left:3.25rem}.field.has-icon-right .input{padding-right:3.25rem}.label-background{position:absolute;top:-3px;left:.6875rem;height:4px;padding:0 .3125rem;line-height:0;color:rgba(0,0,0,0);background-color:inherit;transform:translateX(-10%) scaleX(0);transition:200ms transform ease-in-out}.field.is-not-readonly:focus-within .label-background,.field.has-content .label-background{transform:translateX(-10%) scaleX(0.8)}.label{position:absolute;top:50%;left:1rem;pointer-events:none;transform-origin:left top;transform:translateY(-50%);transition:all .2s ease-in-out;z-index:1;color:#3f4946}.field.has-icon-left .label{left:.75rem}.field.is-not-readonly:focus-within .label,.field.has-content .label{top:0;transform:scale(0.8) translateY(-50%)}.field.is-not-readonly.has-no-color:focus-within .label{color:#006b5a}.field.is-disabled .label{color:rgba(63,73,70,.5)}.is-primary .label{color:#006b5a}.is-secondary .label{color:#7b4998}.is-tertiary .label{color:#426278}.is-error .label{color:red}@media(prefers-color-scheme: dark){.label{color:#bfc9c4}.field.is-not-readonly.has-no-color:focus-within .label{color:#2cdebf}.field.is-disabled .label{color:rgba(191,201,196,.5)}.is-primary .label{color:#2cdebf}.is-secondary .label{color:#e6b4ff}.is-tertiary .label{color:#aacbe4}.is-error .label{color:#ffb4ab}}.message{line-height:1.25rem;font-size:.875rem;letter-spacing:.0178571429rem;font-weight:400;margin-top:-0.75rem;margin-bottom:1rem;color:#3f4946}.is-primary .message{color:#006b5a}.is-secondary .message{color:#7b4998}.is-tertiary .message{color:#426278}.is-error .message{color:red}@media(prefers-color-scheme: dark){.message{color:#bfc9c4}.is-primary .message{color:#2cdebf}.is-secondary .message{color:#e6b4ff}.is-tertiary .message{color:#aacbe4}.is-error .message{color:#ffb4ab}}/*# sourceMappingURL=Field.module.css.map */ +.field{display:flex;position:relative;border:1px solid rgba(0,0,0,0);margin-bottom:1rem;border-radius:4px;background-color:inherit;box-shadow:0 0 0 1px rgba(0,0,0,0);transition:all 200ms;color:#191c1b;border-color:#6f7975}.field.is-disabled{color:rgba(25,28,27,.5);border-color:rgba(111,121,117,.5)}.field.is-not-readonly.has-no-color:focus-within{border-color:#006b5a;box-shadow:0 0 0 1px #006b5a}.field.is-primary{color:#006b5a;border-color:#006b5a}.field.is-primary:focus-within{box-shadow:0 0 0 1px #006b5a}.field.is-secondary{color:#7b4998;border-color:#7b4998}.field.is-secondary:focus-within{box-shadow:0 0 0 1px #7b4998}.field.is-tertiary{color:#426278;border-color:#426278}.field.is-tertiary:focus-within{box-shadow:0 0 0 1px #426278}.field.is-error{color:red;border-color:red}.field.is-error:focus-within{box-shadow:0 0 0 1px red}@media(prefers-color-scheme: dark){.field{color:#e1e3e0;border-color:#89938f}.field.is-disabled{color:rgba(225,227,224,.5);border-color:rgba(137,147,143,.5)}.field.is-not-readonly.has-no-color:focus-within{border-color:#2cdebf;box-shadow:0 0 0 1px #2cdebf}.field.is-primary{color:#2cdebf;border-color:#2cdebf}.field.is-primary:focus-within{box-shadow:0 0 0 1px #2cdebf}.field.is-secondary{color:#e6b4ff;border-color:#e6b4ff}.field.is-secondary:focus-within{box-shadow:0 0 0 1px #e6b4ff}.field.is-tertiary{color:#aacbe4;border-color:#aacbe4}.field.is-tertiary:focus-within{box-shadow:0 0 0 1px #aacbe4}.field.is-error{color:#ffb4ab;border-color:#ffb4ab}.field.is-error:focus-within{box-shadow:0 0 0 1px #ffb4ab}}.input,.textarea{width:100%;height:3.5rem;border:0;padding:0 1rem;color:inherit;background:rgba(0,0,0,0);outline:none}.field.has-icon-left .input,.field.has-icon-left .textarea{padding-left:3.25rem}.field.has-icon-right .input,.field.has-icon-right .textarea{padding-right:3.25rem}.textarea{resize:none;line-height:1.5rem;padding:1rem 1rem;overflow-y:hidden}.label-background{position:absolute;top:-3px;left:.6875rem;height:4px;padding:0 .3125rem;line-height:0;color:rgba(0,0,0,0);background-color:inherit;transform:translateX(-10%) scaleX(0);transition:200ms transform ease-in-out}.field.is-not-readonly:focus-within .label-background,.field.has-content .label-background{transform:translateX(-10%) scaleX(0.8)}.label{position:absolute;top:50%;left:1rem;pointer-events:none;transform-origin:left top;transform:translateY(-50%);transition:all .2s ease-in-out;z-index:1;color:#3f4946}.field.has-icon-left .label{left:.75rem}.field.is-not-readonly:focus-within .label,.field.has-content .label{top:0;transform:scale(0.8) translateY(-50%)}.field.is-not-readonly.has-no-color:focus-within .label{color:#006b5a}.field.is-disabled .label{color:rgba(63,73,70,.5)}.is-primary .label{color:#006b5a}.is-secondary .label{color:#7b4998}.is-tertiary .label{color:#426278}.is-error .label{color:red}@media(prefers-color-scheme: dark){.label{color:#bfc9c4}.field.is-not-readonly.has-no-color:focus-within .label{color:#2cdebf}.field.is-disabled .label{color:rgba(191,201,196,.5)}.is-primary .label{color:#2cdebf}.is-secondary .label{color:#e6b4ff}.is-tertiary .label{color:#aacbe4}.is-error .label{color:#ffb4ab}}.message{line-height:1.25rem;font-size:.875rem;letter-spacing:.0178571429rem;font-weight:400;margin-top:-0.75rem;margin-bottom:1rem;color:#3f4946}.is-primary .message{color:#006b5a}.is-secondary .message{color:#7b4998}.is-tertiary .message{color:#426278}.is-error .message{color:red}@media(prefers-color-scheme: dark){.message{color:#bfc9c4}.is-primary .message{color:#2cdebf}.is-secondary .message{color:#e6b4ff}.is-tertiary .message{color:#aacbe4}.is-error .message{color:#ffb4ab}}/*# sourceMappingURL=Field.module.css.map */ diff --git a/components/form/Field.module.css.map b/components/form/Field.module.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["Field.module.scss","../../styles/core/_colors.scss","../../styles/core/_typography.scss"],"names":[],"mappings":"AAWA,OACE,aACA,kBACA,+BACA,mBACA,kBACA,yBACA,mCACA,qBAGE,cACA,qBAEA,mBACE,wBACA,kCAGF,iDACE,qBACA,6BAIA,kBACE,MC2GM,QD1GN,aC0GM,QDxGN,+BACE,6BALJ,oBACE,MC2GM,QD1GN,aC0GM,QDxGN,iCACE,6BALJ,mBACE,MC2GM,QD1GN,aC0GM,QDxGN,gCACE,6BALJ,gBACE,MC2GM,ID1GN,aC0GM,IDxGN,6BACE,yBCuFR,mCDrHF,OAWI,cACA,qBAEA,mBACE,2BACA,kCAGF,iDACE,qBACA,6BAIA,kBACE,MC2GM,QD1GN,aC0GM,QDxGN,+BACE,6BALJ,oBACE,MC2GM,QD1GN,aC0GM,QDxGN,iCACE,6BALJ,mBACE,MC2GM,QD1GN,aC0GM,QDxGN,gCACE,6BALJ,gBACE,MC2GM,QD1GN,aC0GM,QDxGN,6BACE,8BAOV,OACE,WACA,OA9CO,OA+CP,SACA,eACA,cACA,yBACA,aAIA,4BACE,aAHwB,QAM1B,6BACE,cAPwB,QAW5B,kBAGE,kBACA,SACA,cACA,WACA,mBACA,cACA,oBACA,yBACA,qCACA,uCAEA,2FAEE,uCAIJ,OACE,kBACA,QACA,KApFqB,KAqFrB,oBACA,0BACA,2BACA,+BACA,UAaE,cAXF,4BACE,KA3FgB,OA8FlB,qEAEE,MACA,sCAMA,wDACE,cAGF,0BACE,wBAIA,mBACE,MCuBM,QDxBR,qBACE,MCuBM,QDxBR,oBACE,MCuBM,QDxBR,iBACE,MCuBM,IAhBZ,mCDxCF,OAqBI,cAEA,wDACE,cAGF,0BACE,2BAIA,mBACE,MCuBM,QDxBR,qBACE,MCuBM,QDxBR,oBACE,MCuBM,QDxBR,iBACE,MCuBM,SDjBd,SETI,oBACA,kBACA,8BACA,gBFQF,oBACA,mBAGE,cAGE,qBACE,MCOM,QDRR,uBACE,MCOM,QDRR,sBACE,MCOM,QDRR,mBACE,MCOM,IAhBZ,mCDDF,SAMI,cAGE,qBACE,MCOM,QDRR,uBACE,MCOM,QDRR,sBACE,MCOM,QDRR,mBACE,MCOM","file":"Field.module.css"} -\ No newline at end of file +{"version":3,"sourceRoot":"","sources":["Field.module.scss","../../styles/core/_colors.scss","../../styles/core/_typography.scss"],"names":[],"mappings":"AAYA,OACE,aACA,kBACA,+BACA,mBACA,kBACA,yBACA,mCACA,qBAGE,cACA,qBAEA,mBACE,wBACA,kCAGF,iDACE,qBACA,6BAIA,kBACE,MC0GM,QDzGN,aCyGM,QDvGN,+BACE,6BALJ,oBACE,MC0GM,QDzGN,aCyGM,QDvGN,iCACE,6BALJ,mBACE,MC0GM,QDzGN,aCyGM,QDvGN,gCACE,6BALJ,gBACE,MC0GM,IDzGN,aCyGM,IDvGN,6BACE,yBCsFR,mCDpHF,OAWI,cACA,qBAEA,mBACE,2BACA,kCAGF,iDACE,qBACA,6BAIA,kBACE,MC0GM,QDzGN,aCyGM,QDvGN,+BACE,6BALJ,oBACE,MC0GM,QDzGN,aCyGM,QDvGN,iCACE,6BALJ,mBACE,MC0GM,QDzGN,aCyGM,QDvGN,gCACE,6BALJ,gBACE,MC0GM,QDzGN,aCyGM,QDvGN,6BACE,8BAOV,iBAEE,WACA,OAhDO,OAiDP,SACA,eACA,cACA,yBACA,aAIA,2DACE,aAHwB,QAM1B,6DACE,cAPwB,QAW5B,UACE,YACA,YA9DqB,OA+DrB,kBACA,kBAGF,kBAGE,kBACA,SACA,cACA,WACA,mBACA,cACA,oBACA,yBACA,qCACA,uCAEA,2FAEE,uCAIJ,OACE,kBACA,QACA,KA7FqB,KA8FrB,oBACA,0BACA,2BACA,+BACA,UAaE,cAXF,4BACE,KApGgB,OAuGlB,qEAEE,MACA,sCAMA,wDACE,cAGF,0BACE,wBAIA,mBACE,MCcM,QDfR,qBACE,MCcM,QDfR,oBACE,MCcM,QDfR,iBACE,MCcM,IAhBZ,mCD/BF,OAqBI,cAEA,wDACE,cAGF,0BACE,2BAIA,mBACE,MCcM,QDfR,qBACE,MCcM,QDfR,oBACE,MCcM,QDfR,iBACE,MCcM,SDRd,SElBI,oBACA,kBACA,8BACA,gBFiBF,oBACA,mBAGE,cAGE,qBACE,MCFM,QDCR,uBACE,MCFM,QDCR,sBACE,MCFM,QDCR,mBACE,MCFM,IAhBZ,mCDQF,SAMI,cAGE,qBACE,MCFM,QDCR,uBACE,MCFM,QDCR,sBACE,MCFM,QDCR,mBACE,MCFM","file":"Field.module.css"} +\ No newline at end of file diff --git a/components/form/Field.module.scss b/components/form/Field.module.scss @@ -8,6 +8,7 @@ $field-gap: 1rem; $padding-without-icon: 1rem; $padding-with-icon: 0.75rem; $label-focused-scale: 0.8; +$textarea-line-height: 1.5rem; .field { display: flex; @@ -46,7 +47,8 @@ $label-focused-scale: 0.8; } } -.input { +.input, +.textarea { width: 100%; height: $height; border: 0; @@ -66,6 +68,13 @@ $label-focused-scale: 0.8; } } +.textarea { + resize: none; + line-height: $textarea-line-height; + padding: math.div(($height - $textarea-line-height), 2) $padding-without-icon; + overflow-y: hidden; +} + .label-background { $label-background-padding: math.div(0.25rem, $label-focused-scale); diff --git a/components/form/Field.tsx b/components/form/Field.tsx @@ -1,5 +1,5 @@ import classNames from '@/lib/classnames' -import { FormEvent, HTMLAttributes, useMemo } from 'react' +import { createRef, FormEvent, HTMLAttributes, useEffect, useMemo } from 'react' import styles from './Field.module.css' export interface FieldProps extends HTMLAttributes<HTMLLabelElement> { @@ -9,6 +9,7 @@ export interface FieldProps extends HTMLAttributes<HTMLLabelElement> { readOnly?: boolean type: 'date' | 'datetime-local' | 'email' | 'month' | 'number' | 'password' | 'search' | 'tel' | 'time' | 'url' | 'week' | 'text' + | 'textarea' value?: string onValueChange?: (value: string) => void @@ -26,22 +27,39 @@ export default function Field (props: FieldProps) { placeholder, readOnly, type, - value: initialValue, + value, onValueChange, ...restProps } = props - const handleInput = (e: FormEvent<HTMLInputElement>) => { + const handleInput = (e: FormEvent<HTMLInputElement | HTMLTextAreaElement>) => { onValueChange?.(e.currentTarget.value) } + const textareaRef = createRef<HTMLTextAreaElement>() + + useEffect(() => { + if (type !== 'textarea') { + return + } + + const textarea = textareaRef.current + if (textarea === null) { + return + } + + textarea.style.height = '0px' + const scrollHeight = textarea.scrollHeight + textarea.style.height = `${scrollHeight}px` + }, [type, value]) + return ( <> <label {...classNames( className, styles['field'], - (props.value?.length ?? 0) > 0 ? styles['has-content'] : null, + (value?.length ?? 0) > 0 ? styles['has-content'] : null, color == null ? styles[`has-no-color`] : undefined, color != null ? styles[`is-${color}`] : undefined, readOnly !== true ? styles['is-not-readonly'] : null, @@ -49,14 +67,25 @@ export default function Field (props: FieldProps) { )} {...restProps} > - <input - className={styles['input']} - type={type} - disabled={disabled} - readOnly={readOnly} - value={props.value} - onInput={handleInput} - /> + {type === 'textarea' ? ( + <textarea + className={styles['textarea']} + disabled={disabled} + readOnly={readOnly} + value={value} + onInput={handleInput} + ref={textareaRef} + /> + ) : ( + <input + className={styles['input']} + type={type} + disabled={disabled} + readOnly={readOnly} + value={value} + onInput={handleInput} + /> + )} <span className={styles['label']}>{placeholder}</span> <div className={styles['label-background']}>{placeholder}</div> </label>