import React , { ComponentType , ReactNode , ReactElement } from 'react' ; import styled from 'styled-components' ; type FloatingLabelProps = { text : string ; children : ReactElement < HTMLInputElement > ; style ? : object ; placeholderStyle ? : object ; container ? : ComponentType | string ; label ? : ComponentType | string ; className ? : string ; } ; type FloatingLabelState = { inputStyle : CSSStyleDeclaration | null ; } ; const randomString = ( length ) : string => Math . random ( ) . toString ( 36 ) . substring ( length + 1 ) ; const pxValue = ( value ) : number => parseInt ( value && value . replace ( 'px' , '' ) ) ; class FloatingLabel extends React . Component < FloatingLabelProps , FloatingLabelState > { private inputRef : HTMLInputElement | null = null ; private inputClass : string = 'flf-' + randomString ( 6 ) ; public static defaultProps = { container : 'div' , label : 'label' , style : { } , placeholderStyle : { } , } ; public state : FloatingLabelState = { inputStyle : null } ; public componentDidMount ( ) { if ( this . inputRef === null ) { throw new Error ( 'FloatingLabelField: No input element. Did you pass a valid DOM input element?' ) ; } else { this . setState ( { inputStyle : window . getComputedStyle ( this . inputRef ) } ) ; } } private getClassNameStyle ( ) { const { className } = this . props ; if ( ! className ) { return null ; } const getCssStyleByClassName = ( className ) : CSSStyleDeclaration | null => { for ( const styleSheet of document . styleSheets ) { try { if ( styleSheet . cssRules ) { for ( const cssRule of styleSheet . cssRules ) { if ( cssRule . selectorText . includes ( className ) ) { return cssRule . style ; } } } } catch ( e ) { } } return null ; } ; const [ classNameStyle ] = className . split ( ' ' ) . map ( getCssStyleByClassName ) . filter ( cssRule => ! ! cssRule ) ; return classNameStyle ; } private getLabelStyleProperties ( inputStyle , classNameStyle ) { const { height , fontSize , borderTopWidth , paddingLeft , borderLeftWidth } = inputStyle ; const top = pxValue ( height ) / 2 - pxValue ( fontSize ) / 2 + pxValue ( borderTopWidth ) ; const labelStyle = { display : 'inline-block' , pointerEvents : 'none' , position : 'absolute' , top : top , left : pxValue ( paddingLeft ) + pxValue ( borderLeftWidth ) , transition : '200ms ease all' , transformOrigin : 'left top' , transform : `translateY(-${ top + pxValue ( fontSize ) * 0.75 }px)` , fontSize : pxValue ( fontSize ) * 0.75 , } ; if ( classNameStyle ) { for ( const property in labelStyle ) { if ( classNameStyle [ property ] ) { labelStyle [ property ] = classNameStyle [ property ] ; } } } if ( this . props . style ) { return { ... labelStyle , ... this . props . style , } ; } return labelStyle ; } private getPlaceholderStyleProperties ( inputStyle , classNameStyle ) { const { fontSize } = inputStyle ; const placeholderStyle = { transform : 'none' , fontSize : pxValue ( fontSize ) , fontWeight : 300 , color : 'rgb(117, 117, 117)' , margin : 0 , padding : 0 , } ; if ( classNameStyle ) { let property , value ; for ( let i = 0 ; ; ++ i ) { if ( ! classNameStyle . hasOwnProperty ( i ) ) { break ; } if ( classNameStyle [ i ] . startsWith ( '--placeholder-' ) ) { property = classNameStyle [ i ] . replace ( '--placeholder-' , '' ) ; value = classNameStyle . getPropertyValue ( classNameStyle [ i ] ) ; placeholderStyle [ property ] = value ; } } } if ( this . props . placeholderStyle ) { return { ... placeholderStyle , ... this . props . placeholderStyle , } ; } return placeholderStyle ; } public render ( ) { const input = React . Children . only ( this . props . children ) ; const { id , name } = input . props ; const labelHtmlFor = id || name ; const inputWithRef = React . cloneElement ( input , { ref : node => { if ( node !== null && node . classList ) { this . inputRef = node ; this . inputRef . classList . add ( this . inputClass ) ; this . inputRef . placeholder = ' ' ; } if ( input . hasOwnProperty ( 'ref' ) && typeof input [ 'ref' ] === 'function' ) { input [ 'ref' ] ( node ) ; } } , } ) ; const { inputStyle } = this . state ; const { text , className , container , label } = this . props ; if ( ! inputStyle ) { return ( < div > { inputWithRef } < label className = { className } style = { { display : 'none' } } /> < /div>); } const classNameStyle = this . getClassNameStyle ( ) ; const Label = styled ( label ) ( this . getLabelStyleProperties ( inputStyle , classNameStyle ) ) ; const Container = styled ( container ) ( { position : 'relative' , [ `& .${ this . inputClass }:not(:focus).${ this . inputClass }:placeholder-shown + ${ Label }` ] : this . getPlaceholderStyleProperties ( inputStyle , classNameStyle ) , } ) ; return ( < Container > { inputWithRef } < Label htmlFor = { labelHtmlFor } className = { className } > { text } < /Label> < /Container>); } } export default FloatingLabel ;