├── .eslintrc ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── components │ ├── Button.tsx │ ├── Example.tsx │ └── FieldGroup.tsx ├── examples │ ├── CustomPositioningExample.tsx │ ├── DefaultBehaviorExample.tsx │ ├── StyledPlaceholderExample.tsx │ ├── StylingWithPropsExample.tsx │ └── TypicalUsageExample.tsx ├── index.html ├── index.tsx ├── screen-recordings │ ├── custom-positioning.gif │ ├── styled-placeholder.gif │ ├── styling-with-props.gif │ └── typical-usage.gif └── webpack.demo.js ├── index.tsx ├── package-lock.json ├── package.json ├── tsconfig.json └── webpack.dist.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "plugins": [ 13 | "react", 14 | "@typescript-eslint", 15 | "filenames" 16 | ], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "jsx": true, 20 | "ecmaFeatures": { 21 | "jsx": true 22 | }, 23 | "ecmaVersion": 2018, 24 | "sourceType": "module" 25 | }, 26 | "settings": { 27 | "react": { 28 | "version": "detect" 29 | } 30 | }, 31 | "rules": { 32 | // General 33 | "indent": ["error", 4], 34 | "linebreak-style": ["error", "unix"], 35 | "quotes": ["error", "single"], 36 | "semi": ["error", "always"], 37 | 38 | // React 39 | "react/jsx-tag-spacing": [ 40 | "error", 41 | { 42 | "closingSlash": "never", 43 | "beforeSelfClosing": "always", 44 | "afterOpening": "never", 45 | "beforeClosing": "allow" 46 | } 47 | ], 48 | 49 | // Typescript 50 | "@typescript-eslint/explicit-member-accessibility": "off", 51 | "@typescript-eslint/prefer-interface": "off" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ 3 | npm-debug.log 4 | **/dist/*.js 5 | **/demo/build/js/* 6 | **/demo/build/*.html 7 | **/demo/build/*.map 8 | **/demo/build/*.json 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Ihor Burlachenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 8 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 11 | of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 14 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 15 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React-Styled-Floating-Label 2 | =========================== 3 | Floating label component which works with any HTML input. Supports styling with [styled-components](https://styled-components.com). Check this [live demo](http://ihor.burlachenko.com/react-styled-floating-label-demo/) to see it in action. 4 | 5 | ```jsx 6 | import FloatingLabel from 'react-styled-floating-label'; 7 | 8 | const email = ( 9 | 10 | 11 | 12 | ); 13 | ``` 14 | 15 | Installation 16 | ============ 17 | `npm i react-styled-floating-label styled-components --save` 18 | 19 | 20 | Usage 21 | ===== 22 | 23 | ### Typical Usage Example 24 | 25 | ```jsx 26 | import styled from 'styled-components'; 27 | import FloatingLabel from 'react-styled-floating-label'; 28 | 29 | const BlueFloatingLabel = styled(FloatingLabel)` 30 | color: #0070e0; 31 | `; 32 | 33 | const Input = styled.input` 34 | -webkit-appearance: none; 35 | -moz-appearance: none; 36 | appearance: none; 37 | box-sizing: border-box; 38 | 39 | border: none; 40 | border-bottom: 0.5px solid #bdbdbd; 41 | 42 | font-size: 1.25em; 43 | padding-left: 0.25em; 44 | padding-top: 0.25em; 45 | min-width: 20em; 46 | 47 | :focus { 48 | border-color: #5eaefe; 49 | outline: none; 50 | } 51 | `; 52 | 53 | const email = ( 54 | 55 | 56 | 57 | ); 58 | ``` 59 | 60 | ![Typical Usage Screen Recording](https://github.com/ihor/react-styled-floating-label/blob/master/demo/screen-recordings/typical-usage.gif?raw=true) 61 | 62 | ### Styling With Props 63 | 64 | ```jsx 65 | import FloatingLabel from 'react-styled-floating-label'; 66 | 67 | const address = ( 68 | 76 | 77 | 78 | ); 79 | ``` 80 | 81 | ![Styling With Props Screen Recording](https://github.com/ihor/react-styled-floating-label/blob/master/demo/screen-recordings/styling-with-props.gif?raw=true) 82 | 83 | ### Styled Placeholder 84 | 85 | ```jsx 86 | import styled from 'styled-components'; 87 | import FloatingLabel from 'react-styled-floating-label'; 88 | 89 | const FloatingLabelWithStyledPlaceholder = styled(FloatingLabel)` 90 | --placeholder-color: #328a09; 91 | --placeholder-font-weight: bold; 92 | `; 93 | 94 | const Input = styled.input` 95 | font-size: 1em; 96 | `; 97 | 98 | const address = ( 99 | 100 | 101 | 102 | ); 103 | ``` 104 | 105 | ![Styled Placeholder Screen Recording](https://github.com/ihor/react-styled-floating-label/blob/master/demo/screen-recordings/styled-placeholder.gif?raw=true) 106 | 107 | ### Custom Positioning 108 | 109 | ```jsx 110 | import styled from 'styled-components'; 111 | import FloatingLabel from 'react-styled-floating-label'; 112 | 113 | const VerticallyPositionedFloatingLabel = styled(FloatingLabel)` 114 | transform: translateY(-10px); 115 | `; 116 | 117 | const HorizontallyPositionedFloatingLabel = styled(FloatingLabel)` 118 | margin-left: 20px; 119 | `; 120 | 121 | const firstName = ( 122 | 123 | 124 | 125 | ); 126 | 127 | const lastName = ( 128 | 129 | 130 | 131 | ); 132 | ``` 133 | 134 | ![Custom Positioning Screen Recording](https://github.com/ihor/react-styled-floating-label/blob/master/demo/screen-recordings/custom-positioning.gif?raw=true) 135 | 136 | You can check all examples in action in this [live demo](http://ihor.burlachenko.com/react-styled-floating-label-demo/). 137 | 138 | API 139 | === 140 | 141 | ### Props 142 | 143 | | Prop | Required | Default | Description 144 | | :--- | ---: | ---: | :--- 145 | | text | Yes | | Label text 146 | | style | Optional | `{}` | Label style for projects which are not using `styled-components` 147 | | placeholderStyle | Optional | `{}` | Placeholder style for projects which are not using `styled-components` 148 | | container | Optional | `div` | Component container 149 | | label | Optional | `label` | Label component 150 | 151 | ### styled-components 152 | 153 | Label can be styled with [styled-components](https://styled-components.com): 154 | 155 | ```jsx 156 | import styled from 'styled-components'; 157 | import FloatingLabel from 'react-styled-floating-label'; 158 | 159 | const BlueFloatingLabel = styled(FloatingLabel)` 160 | color: #0070e0; 161 | `; 162 | ``` 163 | 164 | To style placeholder use standard CSS properties with the "--placeholder-" prefix: 165 | 166 | ```jsx 167 | const BlueFloatingLabelWithBoldPlaceholder = styled(BlueFloatingLabel)` 168 | --placeholder-font-weight: bold; 169 | `; 170 | ``` 171 | 172 | Demo 173 | ==== 174 | 175 | To run the demo, you need to clone the project and execute: 176 | ```bash 177 | npm i && npm run demo 178 | ``` 179 | 180 | Or you can check a live demo [here](http://ihor.burlachenko.com/react-styled-floating-label-demo/). 181 | 182 | Feedback 183 | ======== 184 | 185 | There are no mailing lists or discussion groups yet. Please use GitHub issues and pull request or follow me on Twitter [@IhorBurlachenko](https://twitter.com/IhorBurlachenko) 186 | -------------------------------------------------------------------------------- /demo/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Button = styled.button` 4 | display: inline-block; 5 | box-sizing: border-box; 6 | cursor: pointer; 7 | text-transform: none; 8 | outline: none; 9 | border-width: 0px; 10 | padding: 4px 22px; 11 | background: #0070e0; 12 | 13 | font-size: 1em; 14 | font-weight: 500; 15 | border-radius: 5px; 16 | padding-top: 0.75em; 17 | padding-bottom: 0.75em; 18 | padding-left: 2.5em; 19 | padding-right: 2.5em; 20 | color: #ffffff; 21 | `; 22 | 23 | export default Button; 24 | -------------------------------------------------------------------------------- /demo/components/Example.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import styled from 'styled-components'; 3 | import SyntaxHighlighter from 'react-syntax-highlighter'; 4 | import { github } from 'react-syntax-highlighter/dist/esm/styles/hljs'; 5 | 6 | const Form = styled.div` 7 | display: inline-block; 8 | vertical-align: top; 9 | padding-left: 3rem; 10 | padding-right: 3rem; 11 | padding-top: 2rem; 12 | padding-bottom: 2rem; 13 | margin-right: 30px; 14 | margin-bottom: 30px; 15 | width: 25rem; 16 | box-shadow: rgba(0,0,0,0.15) 2px 2px 7px 1px; 17 | `; 18 | 19 | const Code = styled(SyntaxHighlighter)` 20 | display: inline-block !important; 21 | margin-top: 0; 22 | padding: 30px !important; 23 | `; 24 | 25 | type ExampleProps = { 26 | title: string; 27 | code: string; 28 | children: ReactElement | ReactElement[]; 29 | }; 30 | 31 | const Example = ({ title, code, children }: ExampleProps): ReactElement => 32 |
33 |

{title}

34 |
35 | {children} 36 |
37 | 38 | {code} 39 | 40 |
; 41 | 42 | export default Example; 43 | -------------------------------------------------------------------------------- /demo/components/FieldGroup.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const FieldGroup = styled.div` 4 | margin-top: 2em; 5 | margin-bottom: 2em; 6 | `; 7 | 8 | export default FieldGroup; 9 | -------------------------------------------------------------------------------- /demo/examples/CustomPositioningExample.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled from 'styled-components'; 3 | import FloatingLabel from 'react-styled-floating-label'; 4 | 5 | import Example from '../components/Example'; 6 | import FieldGroup from '../components/FieldGroup'; 7 | 8 | const VerticallyPositionedFloatingLabel = styled(FloatingLabel)` 9 | transform: translateY(-10px); 10 | `; 11 | 12 | const HorizontallyPositionedFloatingLabel = styled(FloatingLabel)` 13 | margin-left: 20px; 14 | `; 15 | 16 | const Input = styled.input` 17 | font-size: 1em; 18 | `; 19 | 20 | const code = ` 21 | import styled from 'styled-components'; 22 | import FloatingLabel from 'react-styled-floating-label'; 23 | 24 | const VerticallyPositionedFloatingLabel = styled(FloatingLabel)\` 25 | transform: translateY(-10px); 26 | \`; 27 | 28 | const HorizontallyPositionedFloatingLabel = styled(FloatingLabel)\` 29 | margin-left: 20px; 30 | \`; 31 | 32 | const Input = styled.input\` 33 | font-size: 1em; 34 | \`; 35 | 36 | const firstName = ( 37 | 38 | 39 | 40 | ); 41 | 42 | const lastName = ( 43 | 44 | 45 | 46 | ); 47 | `; 48 | 49 | const CustomPositioningExample: FunctionComponent = () => 50 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ; 65 | 66 | export default CustomPositioningExample; 67 | 68 | -------------------------------------------------------------------------------- /demo/examples/DefaultBehaviorExample.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import FloatingLabel from 'react-styled-floating-label'; 3 | import styled from 'styled-components'; 4 | 5 | import Example from '../components/Example'; 6 | import FieldGroup from '../components/FieldGroup'; 7 | 8 | const BigInput = styled.input` 9 | height: 4em; 10 | font-size: 2em; 11 | `; 12 | 13 | const code = ` 14 | import FloatingLabel from 'react-styled-floating-label'; 15 | import styled from 'styled-components'; 16 | 17 | const BigInput = styled.input\` 18 | height: 4em; 19 | font-size: 2em; 20 | \`; 21 | 22 | const firstName = ( 23 | 24 | 25 | 26 | ); 27 | 28 | const lastName = ( 29 | 30 | 31 | 32 | ); 33 | `; 34 | 35 | const DefaultBehaviorExample: FunctionComponent = () => 36 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | ; 51 | 52 | export default DefaultBehaviorExample; 53 | 54 | -------------------------------------------------------------------------------- /demo/examples/StyledPlaceholderExample.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled from 'styled-components'; 3 | import FloatingLabel from 'react-styled-floating-label'; 4 | 5 | import Example from '../components/Example'; 6 | import FieldGroup from '../components/FieldGroup'; 7 | 8 | const FloatingLabelWithStyledPlaceholder = styled(FloatingLabel)` 9 | --placeholder-color: #328a09; 10 | --placeholder-font-weight: bold; 11 | `; 12 | 13 | const Input = styled.input` 14 | font-size: 1em; 15 | `; 16 | 17 | const code = ` 18 | import styled from 'styled-components'; 19 | import FloatingLabel from 'react-styled-floating-label'; 20 | 21 | const FloatingLabelWithStyledPlaceholder = styled(FloatingLabel)\` 22 | --placeholder-color: #328a09; 23 | --placeholder-font-weight: bold; 24 | \`; 25 | 26 | const Input = styled.input\` 27 | font-size: 1em; 28 | \`; 29 | 30 | const address = ( 31 | 32 | 33 | 34 | ); 35 | `; 36 | 37 | const StyledPlaceholderExample: FunctionComponent = () => 38 | 41 | 42 | 43 | 44 | 45 | 46 | ; 47 | 48 | export default StyledPlaceholderExample; 49 | 50 | -------------------------------------------------------------------------------- /demo/examples/StylingWithPropsExample.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled from 'styled-components'; 3 | import FloatingLabel from 'react-styled-floating-label'; 4 | 5 | import Example from '../components/Example'; 6 | import FieldGroup from '../components/FieldGroup'; 7 | 8 | const Input = styled.input` 9 | -webkit-appearance: none; 10 | -moz-appearance: none; 11 | appearance: none; 12 | box-sizing: border-box; 13 | 14 | border: none; 15 | border-bottom: 0.5px solid #bdbdbd; 16 | 17 | font-size: 1.25em; 18 | padding-left: 0.25em; 19 | padding-top: 0.25em; 20 | min-width: 20em; 21 | 22 | :focus { 23 | border-color: #5eaefe; 24 | outline: none; 25 | } 26 | `; 27 | 28 | const code = ` 29 | import styled from 'styled-components'; 30 | import FloatingLabel from 'react-styled-floating-label'; 31 | 32 | const address = ( 33 | 41 | 42 | 43 | ); 44 | `; 45 | 46 | const StylingWithPropsExample: FunctionComponent = () => 47 | 50 | 51 | 59 | 60 | 61 | 62 | ; 63 | 64 | export default StylingWithPropsExample; 65 | 66 | -------------------------------------------------------------------------------- /demo/examples/TypicalUsageExample.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled from 'styled-components'; 3 | import FloatingLabel from 'react-styled-floating-label'; 4 | 5 | import Example from '../components/Example'; 6 | import FieldGroup from '../components/FieldGroup'; 7 | import Button from '../components/Button'; 8 | 9 | const BlueFloatingLabel = styled(FloatingLabel)` 10 | color: #0070e0; 11 | `; 12 | 13 | const Input = styled.input` 14 | -webkit-appearance: none; 15 | -moz-appearance: none; 16 | appearance: none; 17 | box-sizing: border-box; 18 | 19 | border: none; 20 | border-bottom: 0.5px solid #bdbdbd; 21 | 22 | font-size: 1.25em; 23 | padding-left: 0.25em; 24 | padding-top: 0.25em; 25 | min-width: 20em; 26 | 27 | :focus { 28 | border-color: #5eaefe; 29 | outline: none; 30 | } 31 | `; 32 | 33 | const code = ` 34 | import styled from 'styled-components'; 35 | import FloatingLabel from 'react-styled-floating-label'; 36 | 37 | const BlueFloatingLabel = styled(FloatingLabel)\` 38 | color: #0070e0; 39 | \`; 40 | 41 | const Input = styled.input\` 42 | -webkit-appearance: none; 43 | -moz-appearance: none; 44 | appearance: none; 45 | box-sizing: border-box; 46 | 47 | border: none; 48 | border-bottom: 0.5px solid #bdbdbd; 49 | 50 | font-size: 1.25em; 51 | padding-left: 0.25em; 52 | padding-top: 0.25em; 53 | min-width: 20em; 54 | 55 | :focus { 56 | border-color: #5eaefe; 57 | outline: none; 58 | } 59 | \`; 60 | 61 | const email = ( 62 | 63 | 64 | 65 | ); 66 | `; 67 | 68 | const TypicalUsageExample: FunctionComponent = () => 69 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | ; 86 | 87 | export default TypicalUsageExample; 88 | 89 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Styled Floating Label Demo 8 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /demo/index.tsx: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill'; 2 | import 'react-hot-loader'; 3 | 4 | import React, { FunctionComponent } from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import styled from 'styled-components'; 7 | 8 | import TypicalUsageExample from './examples/TypicalUsageExample'; 9 | import StylingWithPropsExample from './examples/StylingWithPropsExample'; 10 | import DefaultBehaviorExample from './examples/DefaultBehaviorExample'; 11 | import StyledPlaceholderExample from './examples/StyledPlaceholderExample'; 12 | import CustomPositioningExample from './examples/CustomPositioningExample'; 13 | 14 | const Container = styled.div` 15 | padding: 30px; 16 | `; 17 | 18 | const Demo: FunctionComponent = () => 19 | 20 | 21 | 22 | 23 | 24 | 25 | ; 26 | 27 | ReactDOM.render(, document.getElementById('root')); 28 | -------------------------------------------------------------------------------- /demo/screen-recordings/custom-positioning.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihor/react-styled-floating-label/eb583e398a3ce4f263cc83ff8add42771c478319/demo/screen-recordings/custom-positioning.gif -------------------------------------------------------------------------------- /demo/screen-recordings/styled-placeholder.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihor/react-styled-floating-label/eb583e398a3ce4f263cc83ff8add42771c478319/demo/screen-recordings/styled-placeholder.gif -------------------------------------------------------------------------------- /demo/screen-recordings/styling-with-props.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihor/react-styled-floating-label/eb583e398a3ce4f263cc83ff8add42771c478319/demo/screen-recordings/styling-with-props.gif -------------------------------------------------------------------------------- /demo/screen-recordings/typical-usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihor/react-styled-floating-label/eb583e398a3ce4f263cc83ff8add42771c478319/demo/screen-recordings/typical-usage.gif -------------------------------------------------------------------------------- /demo/webpack.demo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const path = require('path'); 3 | const webpackDemo = require('webpack'); 4 | const CleanWebpackPlugin = require('clean-webpack-plugin/dist/clean-webpack-plugin'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | const paths = { 8 | app: path.resolve(__dirname, '.'), 9 | entry: path.resolve(__dirname, './index'), 10 | lib: path.resolve(__dirname, '../index'), 11 | template: path.resolve(__dirname, './index.html'), 12 | output: path.resolve(__dirname, './build'), 13 | bundle: path.resolve(__dirname, './build/js/*'), 14 | }; 15 | 16 | module.exports = { 17 | mode: 'development', 18 | entry: paths.entry, 19 | output: { 20 | path: paths.output, 21 | publicPath: '/', 22 | filename: './js/[name].[hash].js', 23 | }, 24 | devServer: { 25 | port: 8000, 26 | contentBase: paths.output, 27 | publicPath: '/', 28 | overlay: true, 29 | compress: true, 30 | historyApiFallback: true, 31 | }, 32 | devtool: 'source-map', 33 | resolve: { 34 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], 35 | symlinks: true, 36 | alias: { 37 | 'react-dom': '@hot-loader/react-dom', 38 | 'react-styled-floating-label': paths.lib, 39 | } 40 | }, 41 | module: { 42 | rules: [ 43 | { 44 | use: { 45 | loader: 'babel-loader', 46 | options: { 47 | cacheDirectory: true, 48 | babelrc: false, 49 | presets: [ 50 | '@babel/preset-env', 51 | '@babel/preset-react', 52 | '@babel/preset-typescript', 53 | ], 54 | plugins: [ 55 | 'react-hot-loader/babel', 56 | '@babel/plugin-proposal-class-properties', 57 | ], 58 | }, 59 | }, 60 | test: /\.(tsx?)|(jsx?)$/, 61 | include: [paths.app, paths.lib], 62 | }, 63 | 64 | ], 65 | }, 66 | plugins: [ 67 | new HtmlWebpackPlugin({ 68 | template: paths.template, 69 | }), 70 | new CleanWebpackPlugin(), 71 | new webpackDemo.HotModuleReplacementPlugin(), 72 | ], 73 | }; 74 | -------------------------------------------------------------------------------- /index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentType, ReactNode, ReactElement } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | type FloatingLabelProps = { 5 | text: string; 6 | children: ReactElement; 7 | style?: object; 8 | placeholderStyle?: object; 9 | container?: ComponentType|string; 10 | label?: ComponentType|string; 11 | className?: string; // for compatibility with styled-components 12 | }; 13 | 14 | type FloatingLabelState = { 15 | inputStyle: CSSStyleDeclaration|null; 16 | }; 17 | 18 | const randomString = (length: number): string => 19 | Math.random().toString(36).substring(length + 1); 20 | 21 | const pxValue = (value): number => 22 | parseInt(value && value.replace('px', '')); 23 | 24 | class FloatingLabel extends React.Component { 25 | private inputRef: HTMLInputElement|null = null; 26 | private inputClass: string = 'flf-' + randomString(6); 27 | 28 | public static defaultProps = { 29 | container: 'div', 30 | label: 'label', 31 | style: {}, 32 | placeholderStyle: {}, 33 | }; 34 | 35 | public state: FloatingLabelState = { 36 | inputStyle: null 37 | }; 38 | 39 | public componentDidMount(): void { 40 | if (this.inputRef === null) { 41 | throw new Error('FloatingLabelField: No input element. Did you pass a valid DOM input element?'); 42 | } 43 | else { 44 | this.setState({ 45 | inputStyle: window.getComputedStyle(this.inputRef) 46 | }); 47 | } 48 | } 49 | 50 | private getClassNameStyle(): CSSStyleDeclaration|null { 51 | const { className } = this.props; 52 | 53 | if (!className) { 54 | return null; 55 | } 56 | 57 | const getCssStyleByClassName = (className: string): CSSStyleDeclaration|null => { 58 | for (const styleSheet of document.styleSheets) { 59 | try { 60 | if (styleSheet.cssRules) { 61 | for (const cssRule of styleSheet.cssRules) { 62 | if (cssRule.selectorText.includes(className)) { 63 | return cssRule.style; 64 | } 65 | } 66 | } 67 | } 68 | catch (e) {} 69 | } 70 | 71 | return null; 72 | }; 73 | 74 | const [classNameStyle] = className 75 | .split(' ') 76 | .map(getCssStyleByClassName) 77 | .filter(cssRule => !!cssRule); 78 | 79 | return classNameStyle; 80 | } 81 | 82 | private getLabelStyleProperties( 83 | inputStyle: CSSStyleDeclaration, 84 | classNameStyle: CSSStyleDeclaration|null 85 | ): object { 86 | const { 87 | height, 88 | fontSize, 89 | borderTopWidth, 90 | paddingLeft, 91 | borderLeftWidth 92 | } = inputStyle; 93 | 94 | const top = pxValue(height) / 2 - pxValue(fontSize) / 2 + pxValue(borderTopWidth); 95 | 96 | const labelStyle = { 97 | display: 'inline-block', 98 | pointerEvents: 'none', 99 | 100 | position: 'absolute', 101 | top: top, 102 | left: pxValue(paddingLeft) + pxValue(borderLeftWidth), 103 | 104 | transition: '200ms ease all', 105 | transformOrigin: 'left top', 106 | transform: `translateY(-${top + pxValue(fontSize) * 0.75}px)`, 107 | 108 | fontSize: pxValue(fontSize) * 0.75, 109 | }; 110 | 111 | if (classNameStyle) { 112 | for (const property in labelStyle) { 113 | if (classNameStyle[property]) { 114 | labelStyle[property] = classNameStyle[property]; 115 | } 116 | } 117 | } 118 | 119 | if (this.props.style) { 120 | return { 121 | ...labelStyle, 122 | ...this.props.style, 123 | }; 124 | } 125 | 126 | return labelStyle; 127 | } 128 | 129 | private getPlaceholderStyleProperties( 130 | inputStyle: CSSStyleDeclaration, 131 | classNameStyle: CSSStyleDeclaration|null 132 | ): object { 133 | const { fontSize } = inputStyle; 134 | 135 | // We cannot automatically get placeholder styles because of the bug in Chrome 136 | // https://bugs.chromium.org/p/chromium/issues/detail?id=850744 137 | // https://bugs.chromium.org/p/chromium/issues/detail?id=884537 138 | const placeholderStyle = { 139 | transform: 'none', 140 | fontSize: pxValue(fontSize), 141 | fontWeight: 300, 142 | color: 'rgb(117, 117, 117)', 143 | margin: 0, 144 | padding: 0, 145 | }; 146 | 147 | if (classNameStyle) { 148 | let property, value; 149 | 150 | for (let i = 0; ; ++i) { 151 | if (!classNameStyle.hasOwnProperty(i)) { 152 | break; 153 | } 154 | 155 | if (classNameStyle[i].startsWith('--placeholder-')) { 156 | property = classNameStyle[i].replace('--placeholder-', ''); 157 | value = classNameStyle.getPropertyValue(classNameStyle[i]); 158 | 159 | placeholderStyle[property] = value; 160 | } 161 | } 162 | } 163 | 164 | if (this.props.placeholderStyle) { 165 | return { 166 | ...placeholderStyle, 167 | ...this.props.placeholderStyle, 168 | }; 169 | } 170 | 171 | return placeholderStyle; 172 | } 173 | 174 | public render(): ReactNode { 175 | const input = React.Children.only(this.props.children); 176 | const { id, name } = input.props; 177 | const labelHtmlFor = id || name; 178 | 179 | // https://github.com/facebook/react/issues/8873#issuecomment-275423780 180 | const inputWithRef = React.cloneElement(input, { 181 | ref: node => { 182 | // Keep your own reference 183 | if (node !== null && node.classList) { 184 | this.inputRef = node; 185 | 186 | // And mark it with our custom class to track the placeholder visibility 187 | this.inputRef.classList.add(this.inputClass); 188 | this.inputRef.placeholder = ' '; 189 | } 190 | 191 | // Call the original ref, if any 192 | if (input.hasOwnProperty('ref') && typeof input['ref'] === 'function') { 193 | input['ref'](node); 194 | } 195 | }, 196 | }); 197 | 198 | const { inputStyle } = this.state; 199 | const { 200 | text, 201 | className, 202 | container, 203 | label 204 | } = this.props; 205 | 206 | if (!inputStyle) { 207 | // Fooling styled-components to avoid "It looks like you've wrapped styled() around your React component, 208 | // but the className prop is not being passed down to a child." warnings 209 | return ( 210 |
211 | {inputWithRef} 212 |
214 | ); 215 | } 216 | 217 | const classNameStyle = this.getClassNameStyle(); 218 | 219 | // Label has to be defined inside the render function in order to 220 | // have linking to the container 221 | const Label = styled(label)(this.getLabelStyleProperties(inputStyle, classNameStyle)); 222 | 223 | const Container = styled(container)({ 224 | position: 'relative', 225 | [`& .${this.inputClass}:not(:focus).${this.inputClass}:placeholder-shown + ${Label}`]: 226 | this.getPlaceholderStyleProperties(inputStyle, classNameStyle), 227 | }); 228 | 229 | return ( 230 | 231 | {inputWithRef} 232 | 233 | 236 | 237 | ); 238 | } 239 | } 240 | 241 | export default FloatingLabel; 242 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-styled-floating-label", 3 | "version": "0.1.3", 4 | "main": "dist/index.js", 5 | "license": "MIT", 6 | "description": "Floating label component which works with any HTML input", 7 | "keywords": [ 8 | "react", 9 | "floating-label", 10 | "styled-components" 11 | ], 12 | "homepage": "https://github.com/ihor/react-styled-floating-label", 13 | "bugs": { 14 | "url": "https://github.com/ihor/react-styled-floating-label" 15 | }, 16 | "author": { 17 | "name": "Ihor Burlachenko", 18 | "email": "ihor.burlachenko@gmail.com", 19 | "url": "http://ihor.burlachenko.com" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/ihor/react-styled-floating-label.git" 24 | }, 25 | "scripts": { 26 | "build": "webpack --config webpack.dist.js", 27 | "demo": "webpack --watch --hot=false --config demo/webpack.demo.js & webpack-dev-server --lazy --config demo/webpack.demo.js", 28 | "build-live-demo": "webpack --config demo/webpack.demo.js --mode=production" 29 | }, 30 | "peerDependencies": { 31 | "react": "^16.8.0", 32 | "react-dom": "^16.8.0", 33 | "styled-components": "^4.2.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/cli": "^7.5.5", 37 | "@babel/core": "^7.5.5", 38 | "@babel/plugin-proposal-class-properties": "^7.5.5", 39 | "@babel/polyfill": "^7.4.4", 40 | "@babel/preset-env": "^7.5.5", 41 | "@babel/preset-react": "^7.0.0", 42 | "@babel/preset-typescript": "^7.3.3", 43 | "@hot-loader/react-dom": "^16.9.0", 44 | "@types/react": "^16.9.2", 45 | "@types/styled-components": "^4.1.18", 46 | "@typescript-eslint/eslint-plugin": "^1.13.0", 47 | "babel-eslint": "^10.0.3", 48 | "babel-loader": "^8.0.6", 49 | "clean-webpack-plugin": "^2.0.2", 50 | "eslint": "^5.16.0", 51 | "eslint-plugin-filenames": "^1.3.2", 52 | "eslint-plugin-react": "^7.14.3", 53 | "html-webpack-plugin": "^3.2.0", 54 | "react": "^16.9.0", 55 | "react-dom": "^16.9.0", 56 | "react-hot-loader": "^4.12.12", 57 | "react-syntax-highlighter": "^10.3.5", 58 | "styled-components": "^4.3.2", 59 | "typescript": "^3.6.2", 60 | "typescript-eslint-parser": "^22.0.0", 61 | "webpack": "^4.39.3", 62 | "webpack-cli": "^3.3.7", 63 | "webpack-dev-server": "^3.8.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "esnext", 5 | "module": "esnext", 6 | "lib": [ 7 | "dom", 8 | "es2018" 9 | ], 10 | "jsx": "preserve", 11 | "pretty": true, 12 | "isolatedModules": true, 13 | 14 | "strict": true, 15 | "noImplicitAny": false, 16 | "noImplicitThis": false, 17 | 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "allowSyntheticDefaultImports": true, 21 | "esModuleInterop": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /webpack.dist.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const path = require('path'); 3 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 4 | 5 | const paths = { 6 | lib: path.resolve(__dirname, '.'), 7 | entry: path.resolve(__dirname, './index'), 8 | output: path.resolve(__dirname, './dist'), 9 | }; 10 | 11 | module.exports = { 12 | mode: 'production', 13 | entry: paths.entry, 14 | output: { 15 | filename: 'index.js', 16 | path: paths.output, 17 | libraryTarget: 'umd' 18 | }, 19 | resolve: { 20 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], 21 | symlinks: true, 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | use: { 27 | loader: 'babel-loader', 28 | options: { 29 | cacheDirectory: true, 30 | babelrc: false, 31 | presets: [ 32 | '@babel/preset-env', 33 | '@babel/preset-react', 34 | '@babel/preset-typescript', 35 | ], 36 | plugins: [ 37 | '@babel/plugin-proposal-class-properties', 38 | ], 39 | }, 40 | }, 41 | test: /\.(tsx?)|(jsx?)$/, 42 | include: [paths.lib], 43 | }, 44 | 45 | ], 46 | }, 47 | plugins: [ 48 | new CleanWebpackPlugin(), 49 | ], 50 | }; 51 | --------------------------------------------------------------------------------