├── .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 | 
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 | 
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 | 
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 | 
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 |
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 |
213 |
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 |
--------------------------------------------------------------------------------