├── .codeclimate.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── Dev ├── demo.tsx └── index.tsx ├── GALLERY.md ├── KEYSUPPORT.md ├── LICENSE ├── README.md ├── __tests__ ├── Keyboard.spec.tsx ├── KeyboardKey.spec.tsx ├── index.spec.ts └── layouts.spec.ts ├── example ├── README.md ├── custom_layouts │ ├── README.md │ ├── index.tsx │ ├── package.json │ ├── tsconfig.json │ └── webpack.config.js ├── custom_textField │ ├── README.md │ ├── index.tsx │ ├── package.json │ ├── tsconfig.json │ ├── typings.json │ └── webpack.config.js ├── no_keyboard │ ├── README.md │ ├── index.tsx │ ├── package.json │ ├── tsconfig.json │ ├── typings.json │ └── webpack.config.js └── simple_usage │ ├── README.md │ ├── index.tsx │ ├── package.json │ ├── tsconfig.json │ ├── typings.json │ └── webpack.config.js ├── package.json ├── screenshots ├── alphanumeric.png ├── capsed.png ├── extended.png ├── iphone6_landscape.png ├── iphone6_portrait.png ├── keyboard_icon.png ├── numeric.png ├── screen_1200x600.png ├── screen_200x100.png ├── screen_600x300.png ├── show.png ├── size.png └── textField.png ├── server.js ├── src ├── ActiveElement.ts ├── Keyboard.tsx ├── KeyboardKey.tsx ├── index.tsx └── layouts.ts ├── tsconfig.json └── webpack.config.js /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | fixme: 4 | enabled: true 5 | exclude_paths: 6 | - LICENSE 7 | - "*.md" 8 | - "*.json" 9 | - "*.js" 10 | - Dev/* 11 | - example/* 12 | - screenshots/* 13 | - __*/* 14 | ratings: 15 | paths: 16 | - src/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | typings 40 | index.html 41 | main.js 42 | __*/*.js 43 | Dev/*.js 44 | src/*.js 45 | index.js 46 | Keyboard.js 47 | KeyboardKey.js 48 | layouts.js 49 | ActiveElement.js 50 | *.map 51 | *.d.ts 52 | coverage 53 | lcov.info 54 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | node_modules 3 | src 4 | Dev 5 | screenshots 6 | typings 7 | __tests__ 8 | coverage 9 | .git 10 | .vscode 11 | lcov.info 12 | index.html 13 | main.js 14 | server.js 15 | tsconfig.json 16 | typings.json 17 | webpack.config.js 18 | .npmignore 19 | .codeclimate.yml 20 | .gitignore 21 | GALLERY.md -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## [v1.0.2](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/1.0.2) 4 | 5 | ### Key support 6 | 7 | - Backspace 8 | - Enter 9 | - Escape 10 | - CapsLock 11 | - Keyboard (simulates Keyboard Layout Switcher) 12 | - Any Single Char Key 13 | - Spacing (Blank spot) 14 | 15 | ### Keyboard layouts 16 | 17 | - Numeric Keyboard 18 | - AlphaNumeric Keyboard 19 | 20 | ## [v1.1.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/1.1.0) 21 | 22 | ### Feutures 23 | 24 | - Keyboard key's size (Can be changed by passing numbers to `keyboardKeyWidth`, `keyboardKeyHeight` & `keyboardKeySymbolSize` props) 25 | 26 | ## [v1.2.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/1.2.0) 27 | 28 | ### Feutures 29 | 30 | - react-material-ui-keyboard can be used in none material-ui based projects (peerDependencie of material-ui is required) 31 | 32 | ### Key support 33 | 34 | - Spacebar (Spacebar key width can be controlled by the number of spaces used for the key 35 | 36 | ### Keyboard layouts 37 | 38 | - Extended Keyboard 39 | 40 | ## [v1.2.2](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/1.2.2) 41 | 42 | ### Feutures 43 | 44 | - Can be used in TypeScript based projects: d.ts file is included (npm typings support) 45 | 46 | ## [v1.3.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/1.3.0) 47 | 48 | ### Feuters 49 | 50 | - Add support for regular JavaScript users by including propTypes 51 | 52 | ### Keyboard layouts 53 | 54 | - Numeric Keyboard now has `-` instead of blank space. 55 | 56 | ## [v1.4.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/1.4.0) 57 | 58 | ### Properties 59 | 60 | - adding new prop `type` of type `'string' | 'number'` which adds support for `textField` which uses `value` of type `number` 61 | 62 | ## [v1.5.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/1.5.0) 63 | 64 | ### Deprecated 65 | 66 | - prop `type` added in *v1.4.0* is deprecated due to it's probably never to be used as `'number'` `TextFieldInput` from material-ui v16.0 uses `value` of type `string` including for `` 67 | 68 | ### Bug fixes 69 | 70 | - fixing when inputs are `focus`ed and `blur`ed. 71 | 72 | ## [v2.0.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/2.0.0) 73 | 74 | ### Deprecated 75 | 76 | - `Keyboard.id` 77 | 78 | ### Implementaion 79 | 80 | - Switching from `id`s to `red`s 81 | - exposing public methods `getTextField` & `getKeyboardField` 82 | - using polyfilled `Object.assign` in from of `'object-assign'` 83 | 84 | ## [v2.0.1](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/2.0.1) 85 | 86 | Example changes 87 | 88 | ## [v2.0.3](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/2.0.3) 89 | 90 | ### Bug fixes 91 | 92 | - enusre Keyboard input field value is always in sync with `textField.props.value` 93 | 94 | ## [v3.0.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/3.0.0) 95 | 96 | ### Bug fixes 97 | 98 | - Fix bug where changing keyboard key size via passing `keyboardKey*` `prop` changed current `muiTheme` 99 | 100 | ### Implementaion 101 | 102 | - No logner using `context` to size keyboard key instead properties are passed directly to `KeyboardKey` 103 | 104 | ## [v3.0.1](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/3.0.1) 105 | 106 | ### Bug fixes 107 | 108 | - Fix bug where `KeyboardKey` would not re-render if any of `prop`s introduced in `v3.0.0` changes due to using old version of `shouldComponentUpdate` 109 | 110 | ### Implementaion 111 | 112 | - Droping inmplementation of `shouldComponentUpdate` for `KeyboardKey` 113 | 114 | ## [v3.1.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/3.1.0) 115 | 116 | ### Feuters 117 | 118 | - When `open` changes to `true` `Keyboard` will listen for `'keydown'` events on `window` and when `open` changes to `false` listener will be removed. 119 | 120 | ## [v3.1.1](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/3.1.1) 121 | 122 | ### Bug fixes 123 | 124 | - Fix bug introduced with `v3.1.0` which wouldd call twice `_onKeyDown` if keyboard input field is focused due to listening both on `textField` clone and `window` 125 | 126 | ### Implementaion 127 | 128 | - textField` clone no longer recives `onKeyDown` handler 129 | 130 | ## [v3.1.2](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/3.1.2) 131 | 132 | ### Bug fixes 133 | 134 | - Fixing keyboard key caps locking which got broken in `v3.0.0` 135 | 136 | ## [v3.1.3](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/3.1.3) 137 | 138 | ### Implementaion 139 | 140 | - Replacing `List`s with plain old `div`s. `List` is a wrapper container build from `div` and just adds more overheap to `render`ing. It waas initially used in conjuction with `ListItem`, which is no longer used since `v2.0.0` 141 | 142 | ## [v4.0.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/4.0.0) 143 | 144 | ### Properties 145 | 146 | - New prop `automatic` added. Which should remove the boilerplate of opening a keyboard when `textField.props.onFocus` is tiggered and closing it when `props.onRequestClose` is fired 147 | - Props `open` and `onRequestClose` are now optional due to adding of the new `automatic` `prop` 148 | 149 | ## [v4.0.1](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/4.0.1) 150 | 151 | Example updates 152 | 153 | ## [v4.1.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/4.1.0) 154 | 155 | ### Properties 156 | 157 | - New prop `nativeVirtualKeyboard` added. Which controlls when to prevent the native vertual keyboard on `textField` by setting `readOnly`. 158 | 159 | Note: `readOnly` is always `true` on the cloned `textField` used for input when keyboard is opened 160 | 161 | Note: `readOnly` is setted to `true` on `textField` when `active` is also `true` 162 | 163 | ## [v4.1.1](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/4.1.1) 164 | 165 | ### Bug fixes 166 | 167 | - Fixing a bug which falsely synced keyboard input value with `textField.props.value` each time a `componentWillReciveProps` is called. Now values are synced only when `textField.props.value` did really changed. Bug was introduced with `v2.0.3` 168 | 169 | ## [v5.0.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/5.0.0) 170 | 171 | ### New 172 | 173 | - `Keyboard` now resizes on when `window` `'resize'`s 174 | - `Keyboard` now fits on screen if calculated size based on `keyboardKeyWidth`, `keyboardKeyHeight` & `keyboardKeySymbolSize` `props`s is less than calculated 175 | - `Keyboard` now exposes new `public` `static` member `automaitcOpenPredicate` which is `function` with signature: `function() => boolean` that is called when `automatic` is `true` and the attached `onFocus` handler on `textField` gets fired to determinate should keyboard `open` and disable native virtual keyboard by assigning `readOnly` at `textField` in the `render`. Default `automaitcOpenPredicate` behaviour is to always return `true`. You can override it to change when to `automatic`lly open keyboard `onFocus` 176 | 177 | Chech for examples [GALLERY](https://github.com/NoHomey/react-material-ui-keyboard/blob/master/GALLERY.md) 178 | 179 | ### Properties 180 | 181 | - `onInput` is now optional 182 | 183 | ### Changes 184 | 185 | - `readOnly` is assigned to the result of invoking `Keyboard.automaitcOpenPredicate` on `textField` when `active` is`true` 186 | 187 | ## [v5.0.1](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/5.0.1) 188 | 189 | ### Bug fixes 190 | 191 | - Fixing a bug which would set `Dialog` `dialogContentStyle` prop with `height`: `NaN` for custom `textFields` which dose not support `row` `prop` 192 | 193 | ## [v5.0.2](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/5.0.2) 194 | 195 | ### Bug fixes 196 | 197 | - Fixing possible styling bugs due to wrapping `textFiled` and `Dialog` in `div`. If `style` `prop` is passed to `textFiled` it will be also passed and to the wrapping `div` 198 | - Fixing possible styling bugs when `textFiled` is passed a `style` `prop` that would set any of `['minWidth', 'width', 'maxWidth', 'minHeight', 'height', 'maxHeight']`, those styling props are `delete`d on the cloned `textField`, used for keyboard input, in favour of unified user experiance of `fullWidth` `input` 199 | 200 | ## [v5.0.3](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/5.0.3) 201 | 202 | ### Bug fixes 203 | 204 | - Fixing possible styling bugs when other than `style` `prop`s, such as `inputStyle`, `underlineStyle` or any other posible custom `prop`, is passed to `textField` would make keyboard input "style boken". `textField` `style` `prop` `properties` `['minHeight', 'height', 'maxHeight']` are no longer `delete`d 205 | - Fixing possible styling bugs when `style` `prop` contains any of `['minHeight', 'height', 'maxHeight']` would prevent `Keyboard` from calculating it's height correctly 206 | 207 | ## [v6.0.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/6.0.0) 208 | 209 | ### Properties 210 | 211 | #### New 212 | 213 | - `correctionName` is a `string` which is the name of the cloned `textField` `prop` to which to bind `corrector`. 214 | - `corrector` is a `function` which is bound to the the cloned `textField` at `correctorName` `prop`. `this` is bound to the `Keyboard`, `public` method `makeCorrection` can be used to apply a correction to the keyboard input. 215 | 216 | ## [v6.0.1](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/6.0.1) 217 | 218 | - Re-exporting `default` from `'./Keboard'` 219 | 220 | ## [v6.0.2](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/6.0.2) 221 | 222 | - Using npm badge for README.md#Install 223 | 224 | ## [v6.0.3](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/6.0.3) 225 | 226 | ### Bug fixes 227 | 228 | - Fixing bug: `Uncaught TypeError: Cannot read property 'minHeight' of undefined` when no `style`s are passed to `textField` 229 | 230 | ## [v6.0.4](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/6.0.4) 231 | 232 | ### Changes 233 | 234 | - keyboard layouts are now moved to ``react-material-ui-keyboard/layouts``, in adition their names are in lower camel case now 235 | - Code is completetly refactored, in addition code is now memory and performance optimizated 236 | 237 | ### TypeScript 238 | 239 | - `Keyboard` and `KeyboardKey` can be used in `call` to `React.createElement` 240 | 241 | ## [v6.0.5](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/6.0.5) 242 | 243 | ### TypeScript 244 | 245 | - Fixing `[ts] Cannot find module 'react-material-ui-keyboard/layouts'` 246 | 247 | ## [v6.0.6](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/6.0.6) 248 | 249 | ### Properties 250 | 251 | - `nativeVirtualKeyboard` is now deprecated. `readOnly` is now computed based on `automatic` and `open` for the input. For keyboard input field it's always `true` 252 | 253 | ### Bug fixes 254 | 255 | - Fixing keyboard width computaion. Bug was introduced with `v6.0.4` update 256 | 257 | ## [v6.1.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/6.1.0) 258 | 259 | ### Depreacted 260 | 261 | - Usage of `ref`s is deprecated: The need of `ref`s was because of controlling when `input`s gets `focus`ed and `blur`ed, which is no more required, instead `document.activeElement.tagName.toLowerCase()` is checked is it `'input'` when `open` becomes `true` and if it is the `activeElement` is `blur()`ed 262 | - `public` methods: `getTextField` and `getKeyboardField` are both removed 263 | 264 | ### Bug fixes 265 | 266 | - If any of the following `prop`s is passed to `textField` it will be overwritten for the keyboard input 267 | 268 | ### Changes 269 | 270 | - Instad of doing twice `React.cloneElement(textField, someMergedProps)` `React.cloneElement` is called for the input and `React.createElement(textField.type, keyboardFieldProps`) for the keyboard input field 271 | 272 | ## [v6.1.1](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/6.1.1) 273 | 274 | ### New 275 | 276 | - `'#'` is added to `extendedKeyboard` layout 277 | - [warning](https://design.google.com/icons/#ic_warning) icon is used when given special key is not supported 278 | - Code is now have a 100% code coverage 279 | 280 | ## [v6.1.2](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/6.1.2) 281 | 282 | ### Bug fixes 283 | 284 | - Fixing how `keyboardKeySymbolSize` is calculated when `Keyboard` resizes 285 | 286 | ### Changes 287 | 288 | - Updating project dependencies 289 | - Updating project to use `typescript` `2.0.3` and `@types` instead of `typings` 290 | 291 | ## [v6.2.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/6.2.0) 292 | 293 | ### New 294 | 295 | `disableEffects` is a new `prop` supported by both `Keyboard` and `KeyboardKey`. When it is set to `true` it dissables `FocusRipple`, `KeyboardFocus`, `TouchRipple` and `hover` effects on the underlinig `button`/s 296 | 297 | ## [v6.2.1](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/6.2.1) 298 | 299 | ### New 300 | 301 | `onInputValueChange` is a new optional `prop` of `Keyboard` which is a callback that is triggered once keyboard's input change it's value. (closes #45) 302 | 303 | ### Changes 304 | 305 | - Updating project dependencies -------------------------------------------------------------------------------- /Dev/demo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import TextField from 'material-ui/TextField'; 3 | import { Keyboard, InputHandler } from './../src/index'; 4 | import { extendedKeyboard } from './../src/layouts'; 5 | 6 | export interface DemoState { 7 | value?: string; 8 | }; 9 | 10 | export interface TextEnterTarget { 11 | value?: string; 12 | }; 13 | 14 | export interface TextEnterEvent { 15 | target: TextEnterTarget; 16 | }; 17 | 18 | export default class Demo extends React.Component<{}, DemoState> { 19 | private _onInput: InputHandler; 20 | private _onInputChange: InputHandler; 21 | 22 | private _handleInput(input: string): void { 23 | this.setState({ value: input }); 24 | } 25 | 26 | private _handleInputChange(value: string): void { 27 | console.warn(`change: ${value}`); 28 | } 29 | 30 | public constructor() { 31 | super(); 32 | this.state = { value: '12' }; 33 | this._onInput = this._handleInput.bind(this); 34 | this._onInputChange = this._handleInputChange.bind(this); 35 | } 36 | 37 | public componentDidMount(): void { 38 | setTimeout(() => this.setState({ value: '123456' }), 1000); 39 | } 40 | 41 | public render(): JSX.Element { 42 | const { state, _onInput, _onInputChange } = this; 43 | const { value } = state; 44 | const textField: JSX.Element = ( 45 | 50 | ); 51 | 52 | return ( 53 |
54 | 55 | 65 |
66 | ); 67 | } 68 | }; -------------------------------------------------------------------------------- /Dev/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render as ReactDomRender } from 'react-dom'; 3 | import * as injectTapEventPlugin from 'react-tap-event-plugin'; 4 | import Demo from './demo'; 5 | 6 | injectTapEventPlugin(); 7 | let bootstrapNode = document.createElement('div'); 8 | ReactDomRender(, bootstrapNode); 9 | document.body.appendChild(bootstrapNode); -------------------------------------------------------------------------------- /GALLERY.md: -------------------------------------------------------------------------------- 1 | # Gallery of Keyboard Screen fitting with props: 2 | 3 | - keyboardKeyHeight={50} 4 | - keyboardKeyWidth={100} 5 | - keyboardKeySymbolSize={36} 6 | 7 | ## Screen 1200x600 8 | 9 | ![Screenshot](https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/master/screenshots/screen_1200x600.png) 10 | 11 | ## Screen 600x300 12 | 13 | ![Screenshot](https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/master/screenshots/screen_600x300.png) 14 | 15 | ## Screen 200x100 16 | 17 | ![Screenshot](https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/master/screenshots/screen_200x100.png) 18 | 19 | ## Iphone 6 Portrait 20 | 21 | ![Screenshot](https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/master/screenshots/iphone6_portrait.png) 22 | 23 | ## Iphone 6 Landscape 24 | 25 | ![Screenshot](https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/master/screenshots/iphone6_landscape.png) 26 | -------------------------------------------------------------------------------- /KEYSUPPORT.md: -------------------------------------------------------------------------------- 1 | # Key Support 2 | 3 | ## [v1.0.2](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/1.0.2) 4 | 5 | - Backspace 6 | - Enter 7 | - Escape 8 | - CapsLock 9 | - Keyboard (simulates Keyboard Layout Switcher) 10 | - Any Single Char Key 11 | - Spacing (Blank spot) 12 | 13 | ## [v1.2.0](https://github.com/NoHomey/react-material-ui-keyboard/releases/tag/1.2.0) 14 | 15 | - Spacebar (Spacebar key width can be controlled by the number of spaces used for the key see [Extended Keyboard for an example](https://github.com/NoHomey/react-material-ui-keyboard#extended-keyboard)) 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ivo Stratev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-material-ui-keyboard 2 | 3 | **The project is now archived since it requires a complete rewrite with modern day React and Material-UI!** 4 | 5 | Virtual keyboard for TextField when needed. 6 | 7 | [![npm version](https://badge.fury.io/js/react-material-ui-keyboard.svg)](https://badge.fury.io/js/react-material-ui-keyboard) 8 | [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NoHomey/react-material-ui-keyboard) 9 | [![Build Status](https://semaphoreci.com/api/v1/nohomey/react-material-ui-keyboard/branches/master/badge.svg)](https://semaphoreci.com/nohomey/react-material-ui-keyboard) 10 | [![Code Climate](https://codeclimate.com/github/NoHomey/react-material-ui-keyboard/badges/gpa.svg)](https://codeclimate.com/github/NoHomey/react-material-ui-keyboard) 11 | [![Test Coverage](https://codeclimate.com/github/NoHomey/react-material-ui-keyboard/badges/coverage.svg)](https://codeclimate.com/github/NoHomey/react-material-ui-keyboard/coverage) 12 | [![Issue Count](https://codeclimate.com/github/NoHomey/react-material-ui-keyboard/badges/issue_count.svg)](https://codeclimate.com/github/NoHomey/react-material-ui-keyboard) 13 | ![TypeScript](https://img.shields.io/badge/%3C%20%2F%3E-TypeScript-blue.svg) 14 | ![Typings](https://img.shields.io/badge/typings-%E2%9C%93-brightgreen.svg) 15 | 16 | You controll when to open it which allows cross platform App optimizations and code reusability for diferent platoforms such as Progressive Web Apps, Hybrid Apps, Electron Apps, Touch Devices, Smart TVs, Desktops, and all other Compatible JavaScript Enviroments. 17 | 18 | You have the freedom to choose on which of them to `open` the `Keyboard` and on which to just use a `textField`! 19 | 20 | ![Screenshot](https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/master/screenshots/textField.png) 21 | 22 | ![Screenshot](https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/master/screenshots/show.png) 23 | 24 | # Install 25 | 26 | Install with npm: 27 | 28 | ```bash 29 | $ npm install react-material-ui-keyboard 30 | ``` 31 | 32 | [![NPM](https://nodei.co/npm/react-material-ui-keyboard.png?downloads=true&stars=true)](https://nodei.co/npm/react-material-ui-keyboard/) 33 | 34 | # Changelog 35 | 36 | **Check [Change log](https://github.com/NoHomey/react-material-ui-keyboard/blob/master/CHANGELOG.md) for changes.** 37 | 38 | # Properties 39 | 40 | | Name | Type | Default | Description | 41 | | --------------------- | ---------------| -------------------------------------------- | -------------------- | 42 | | automatic | *bool* | | If true, keyboard will automaticlly: open when textField gets focused and close instead of firing onRequestClose. | 43 | | disableEffects | *bool* | | If true, disables all effects (ripples, focus, hover) on all `keyboardKey`s | 44 | | open | *bool* | | Controls whether the Keyboard is opened or not. | 45 | | layouts* | *string[][][]* | | Keybaord layouts that can be changed when user clicks on 'Keyboard' key. | 46 | | keyboardKeyWidth | *number* | *this.context.muiThemet.button.minWidth* | Override keyboard key's max width. | 47 | | keyboardKeyHeight | *number* | *this.context.muiThemet.button.height* | Override keyboard key's max height. | 48 | | keyboardKeySymbolSize | *number* | *this.context.muiThemet.flatButton.fontSize* | Override keyboard key's max symbol size. | 49 | | textField* | *element* | | Input field used when keyboard is closed and cloned when it's opened. | 50 | | onRequestClose | *function* | | Fired when keyboard recives 'Enter' or 'Escape' eighter from onKeyDown listener or keyboard key touch/click event. | 51 | | onInput | *function* | | Fired when keyboard recives 'Enter' **Signature:** `function(input: string) => void`. | 52 | | onInputValueChange | *function* | | Fired when keyboard's input chages value **Signature:** `function(input: string) => void`. | 53 | | correctorName | *string* | | Name of the cloned textField prop to which to bind corrector. | 54 | | corrector** | *function* | | Function which is bound to the the cloned textField at correctorName prop. this is bound to the Keyboard, public method makeCorrection can be used to apply a correction to the keyboard input. | 55 | 56 | Props marked with \* are required. 57 | 58 | \*\* corrector is required when correctorName is provided. 59 | 60 | # Requirements 61 | 62 | ## `textField` must be a controlled input 63 | 64 | ## Node passed to `textField` Prop must support the following props: 65 | 66 | - `value`\* of type `string` 67 | - `readOnly`\* of type `bool` 68 | 69 | Props marked with \* must be passed down to the native `input` element. 70 | 71 | # Implementation 72 | 73 | react-material-ui-keyboard is implemented using the followong Material-Ui Elements 74 | 75 | - Dialog 76 | - FlatButtton 77 | - SVG Icons 78 | 79 | and uses `React.cloneElement` to clone `textFiled` for the kyboard input field. 80 | 81 | The used `Dialog` is `modal` which guaranties that only one keyboard can be opened which allows memory and performance optimizations. 82 | 83 | `Keyboard` Compoment uses `MuiTheme`, `props`, `window.innerWidth` and `window.innerHeight` information to calculate it's size and keyboard keys size (width x height) to ensure it always fits best on screen chech [GALLERY](https://github.com/NoHomey/react-material-ui-keyboard/blob/master/GALLERY.md). 84 | 85 | # Key Support 86 | 87 | For supported keys read [KEYSUPPORT](https://github.com/NoHomey/react-material-ui-keyboard/blob/master/KEYSUPPORT.md) 88 | 89 | # Included Layouts 90 | 91 | The following keyboard layouts are exported from `'react-material-ui-keyboard/layouts'` 92 | 93 | ## numericKeyboard 94 | 95 | ```js 96 | const numericKeyboard = [ 97 | ['Escape', '-', 'Backspace'], 98 | ['7', '8', '9'], 99 | ['4', '5', '6'], 100 | ['1', '2', '3'], 101 | ['0', '.', 'Enter'] 102 | ]; 103 | ``` 104 | 105 | ![Screenshot](https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/master/screenshots/numeric.png) 106 | 107 | ## alphaNumericKeyboard 108 | 109 | ```js 110 | const alphaNumericKeyboard = [ 111 | ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], 112 | ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'], 113 | ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'Backspace'], 114 | ['Escape', 'CapsLock', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'Enter'] 115 | ]; 116 | ``` 117 | 118 | ![Screenshot](https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/master/screenshots/alphanumeric.png) 119 | 120 | ### With CapsLock On 121 | 122 | ![Screenshot](https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/master/screenshots/capsed.png) 123 | 124 | ## extendedKeyboard 125 | 126 | ```js 127 | const extendedKeyboard = [ 128 | ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], 129 | ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'], 130 | ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'Backspace'], 131 | ['CapsLock', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '-', 'CapsLock'], 132 | ['Escape', '@', '#', ' ', '.', 'Enter'] 133 | ]; 134 | ``` 135 | ### Demonstrating Spacebar and keyboard key size futers 136 | 137 | ![Screenshot](https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/master/screenshots/extended.png) 138 | 139 | # Creating Custom Keyboard Layout 140 | 141 | - All single chars suppoted as String can be used as a symbol key! 142 | - Empty strings can be used for blank spaces 143 | - Use `KeyboardEvent.key` names for all [Special keys] (https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key) 144 | - Use `'Keyboard'` for key with which user can change keyboard layout 145 | 146 | **All spacial keys (none Symbol will have an Icon and support at some point*)** 147 | 148 | **Check supported keys!** 149 | 150 | **If a key you want to use is not supported open an Issue.** 151 | 152 | # Public methods 153 | 154 | `Keyboard` exposes `public` method `makeCorrection` which can be used to apply keyboard input value corrections when keyboard is opened or within `correction` handller. 155 | 156 | # Public members 157 | 158 | `Keyboard` has one `public` `static` member which is designed to be overwritten: `automaitcOpenPredicate` it's signature is `function() => boolean`. It is called when `automatic` is `true` and the attached `onFocus` handler on `textField` gets fired to determinate should keyboard `open` and disable the native virtual keyboard by assigning `readOnly` at `textField` in the `render`. Default `automaitcOpenPredicate` behaviour is to always return `true`. You can override it to change when to `automatic`lly open keyboard `onFocus`. 159 | 160 | # Examples 161 | 162 | ```js 163 | import * as React from 'react'; 164 | import TextField from 'material-ui/TextField'; 165 | import Keyboard from 'react-material-ui-keyboard'; 166 | import { extendedKeyboard } from 'react-material-ui-keyboard/layouts'; 167 | 168 | class Demo extends React.Component { 169 | constructor(props) { 170 | super(props); 171 | this.state = { 172 | open: false, 173 | value: '' 174 | }; 175 | this.onInput = this.handleInput.bind(this); 176 | } 177 | 178 | handleInput(input) { 179 | this.setState({ value: input }); 180 | } 181 | 182 | render() { 183 | 189 | } 190 | automatic 191 | onInput={this.onInput} 192 | layouts={[extendedKeyboard]} 193 | />; 194 | } 195 | }; 196 | ``` 197 | 198 | # Example using custom textField and controlling when to open the keyboard and when to prevent the native virtual keyboard 199 | 200 | ```js 201 | import * as React from 'react'; 202 | import NumberInput from 'material-ui-number-input'; 203 | import Keyboard from 'react-material-ui-keyboard'; 204 | import { numericKeyboard } from 'react-material-ui-keyboard/layouts'; 205 | 206 | function corrector(value) { 207 | console.log(`correction ${value}`); 208 | this.makeCorrection(value); 209 | } 210 | 211 | class Demo extends React.Component { 212 | constructor(props) { 213 | super(props); 214 | this.state = { open: false, value: '2' }; 215 | this.onFocus = this.handleFocus.bind(this); 216 | this.onChange = this.handleChange.bind(this); 217 | this.onRequestClose = this.handleRequestClose.bind(this); 218 | this.onInput = this.handleInput.bind(this); 219 | this.onError = this.handleError.bind(this); 220 | this.onValid = this.handleValid.bind(this); 221 | } 222 | 223 | canOpenKeyboard() { 224 | return (this.state.value.length % 2) === 0; 225 | } 226 | 227 | handleFocus(event) { 228 | if(this.canOpenKeyboard()) { 229 | this.setState({ open: true }); 230 | } 231 | } 232 | 233 | handleChange(event, value) { 234 | console.log(value); 235 | this.setState({ value: value }); 236 | } 237 | 238 | handleRequestClose() { 239 | this.setState({ open: false }); 240 | } 241 | 242 | handleInput(input) { 243 | console.log(input); 244 | this.setState({ value: input }); 245 | } 246 | 247 | handleError(error) { 248 | let errorText; 249 | switch (error) { 250 | case 'required': 251 | errorText = 'This field is required'; 252 | break; 253 | case 'invalidSymbol': 254 | errorText = 'You are tring to enter none number symbol'; 255 | break; 256 | case 'incompleteNumber': 257 | errorText = 'Number is incomplete'; 258 | break; 259 | case 'singleMinus': 260 | errorText = 'Minus can be use only for negativity'; 261 | break; 262 | case 'singleFloatingPoint': 263 | errorText = 'There is already a floating point'; 264 | break; 265 | case 'singleZero': 266 | errorText = 'Floating point is expected'; 267 | break; 268 | case 'min': 269 | errorText = 'You are tring to enter number less than -10'; 270 | break; 271 | case 'max': 272 | errorText = 'You are tring to enter number greater than 12'; 273 | break; 274 | } 275 | this.setState({ errorText: errorText }); 276 | } 277 | 278 | handleValid(value) { 279 | console.debug(`valid ${value}`); 280 | } 281 | 282 | componentDidMount() { 283 | setTimeout(() => this.setState({ value: '89' }), 1000); 284 | } 285 | 286 | render() { 287 | const { state, onFocus, onChange, onError, onValid, onInput } = this; 288 | const { value, errorText } = state; 289 | const textField = ( 290 | 303 | ); 304 | 305 | return ( 306 | 318 | ); 319 | } 320 | } 321 | ``` 322 | 323 | # Written in Typescript and Typescript Ready! ([check examples](https://github.com/NoHomey/react-material-ui-keyboard/blob/master/example)) 324 | 325 | # Supports propTypes for regular JavaScript users 326 | 327 | # It is possible to use react-material-ui-keyboard in none material-ui project. 328 | 329 | ## Limitations 330 | 331 | If you need to change theme eg. gutter, spacing, colors or any other option you need to wrapp `````` in ```MuiThemeProvider``` or to manually provide a ```muiTheme``` to parent's ```context```. 332 | 333 | # Testing 334 | 335 | 1. `npm install` 336 | 337 | 2. `npm test` 338 | 339 | # Contributing 340 | 341 | 1. `npm install` 342 | 343 | 2. Make changes 344 | 345 | 3. If necessary add some tests to `__tests__` 346 | 347 | 4. `npm test` 348 | 349 | 5. Make a Pull Request 350 | -------------------------------------------------------------------------------- /__tests__/Keyboard.spec.tsx: -------------------------------------------------------------------------------- 1 | import { Keyboard, KeyboardProps, KeyboardState, RequestCloseHandler, InputHandler, KeyboardLayout } from './../src/Keyboard'; 2 | import * as React from 'react'; 3 | import { shallow, ShallowWrapper } from 'enzyme'; 4 | import TextField from 'material-ui/TextField'; 5 | import Dialog from 'material-ui/Dialog'; 6 | import { KeyboardKey, KeyboardKeyProps } from './../src/KeyboardKey'; 7 | import { extendedKeyboard, numericKeyboard } from './../src/layouts'; 8 | import EventListenerService from 'event-listener-service'; 9 | import ActiveElement from './../src/ActiveElement'; 10 | import * as injectTapEventPlugin from 'react-tap-event-plugin'; 11 | 12 | type KeyboardShallowWrapper = ShallowWrapper; 13 | type Listener = (eventName: string, listener: (event: any) => void, capture: boolean) => void; 14 | 15 | injectTapEventPlugin(); 16 | 17 | describe('Keyboard', () => { 18 | let addListener: jest.Mock; 19 | let removeListener: jest.Mock; 20 | let isInput: jest.Mock<() => boolean>; 21 | let blur: jest.Mock<() => void>; 22 | 23 | beforeEach(() => { 24 | addListener = jest.fn(); 25 | removeListener = jest.fn(); 26 | isInput = jest.fn<() => boolean>(() => false); 27 | blur = jest.fn<() => void>(); 28 | EventListenerService.setImplementation({ addListener, removeListener }); 29 | ActiveElement.isInput = isInput; 30 | ActiveElement.blur = blur; 31 | }); 32 | 33 | describe('when rendering', () => { 34 | it('undefines onChange prop for keyboard input field', () => { 35 | const onChange: () => void = (): void => { }; 36 | const wrapper: KeyboardShallowWrapper = shallow( 37 | } 40 | layouts={[extendedKeyboard]} /> 41 | , { lifecycleExperimental: true } 42 | ); 43 | expect(wrapper.find(TextField).first().prop('onChange')).toBe(onChange); 44 | expect(wrapper.find(TextField).last().prop('onChange')).toBeUndefined(); 45 | }); 46 | 47 | it('undefines onFocus prop for keyboard input field and overwrites it for input when automatic', () => { 48 | const onFocus: () => void = (): void => { }; 49 | const wrapper: KeyboardShallowWrapper = shallow( 50 | } 53 | layouts={[extendedKeyboard]} /> 54 | , { lifecycleExperimental: true } 55 | ); 56 | expect(wrapper.find(TextField).first().prop('onFocus')).not.toBe(onFocus); 57 | expect(typeof wrapper.find(TextField).first().prop('onFocus')).toBe('function'); 58 | expect(wrapper.find(TextField).last().prop('onFocus')).toBeUndefined(); 59 | }); 60 | 61 | it('undefines onFocus prop for keyboard input field and nulls it for input when not automatic and opened', () => { 62 | const onFocus: () => void = (): void => { }; 63 | const wrapper: KeyboardShallowWrapper = shallow( 64 | } 67 | layouts={[extendedKeyboard]} /> 68 | , { lifecycleExperimental: true } 69 | ); 70 | expect(wrapper.find(TextField).first().prop('onFocus')).not.toBe(onFocus); 71 | expect(wrapper.find(TextField).first().prop('onFocus')).toBeUndefined(); 72 | expect(wrapper.find(TextField).last().prop('onFocus')).toBeUndefined(); 73 | }); 74 | 75 | it('undefines onFocus prop for keyboard input field', () => { 76 | const onFocus: () => void = (): void => { }; 77 | const wrapper: KeyboardShallowWrapper = shallow( 78 | } 80 | layouts={[extendedKeyboard]} /> 81 | , { lifecycleExperimental: true } 82 | ); 83 | expect(wrapper.find(TextField).first().prop('onFocus')).toBe(onFocus); 84 | expect(wrapper.find(TextField).last().prop('onFocus')).toBeUndefined(); 85 | }); 86 | 87 | it('undefines onBlur prop for keyboard input field', () => { 88 | const onBlur: () => void = (): void => { }; 89 | const wrapper: KeyboardShallowWrapper = shallow( 90 | } 93 | layouts={[extendedKeyboard]} /> 94 | , { lifecycleExperimental: true } 95 | ); 96 | expect(wrapper.find(TextField).first().prop('onBlur')).toBe(onBlur); 97 | expect(wrapper.find(TextField).last().prop('onBlur')).toBeUndefined(); 98 | }); 99 | 100 | it('undefines onKeyUp prop for keyboard input field', () => { 101 | const onKeyUp: () => void = (): void => { }; 102 | const wrapper: KeyboardShallowWrapper = shallow( 103 | } 106 | layouts={[extendedKeyboard]} /> 107 | , { lifecycleExperimental: true } 108 | ); 109 | expect(wrapper.find('input').first().prop('onKeyUp')).toBe(onKeyUp); 110 | expect(wrapper.find('input').last().prop('onKeyUp')).toBeUndefined(); 111 | }); 112 | 113 | it('undefines onKeyDown prop for keyboard input field', () => { 114 | const onKeyDown: () => void = (): void => { }; 115 | const wrapper: KeyboardShallowWrapper = shallow( 116 | } 119 | layouts={[extendedKeyboard]} /> 120 | , { lifecycleExperimental: true } 121 | ); 122 | expect(wrapper.find(TextField).first().prop('onKeyDown')).toBe(onKeyDown); 123 | expect(wrapper.find(TextField).last().prop('onKeyDown')).toBeUndefined(); 124 | }); 125 | 126 | it('undefines onKeyPress prop for keyboard input field', () => { 127 | const onKeyPress: () => void = (): void => { }; 128 | const wrapper: KeyboardShallowWrapper = shallow( 129 | } 132 | layouts={[extendedKeyboard]} /> 133 | , { lifecycleExperimental: true } 134 | ); 135 | expect(wrapper.find('input').first().prop('onKeyPress')).toBe(onKeyPress); 136 | expect(wrapper.find('input').last().prop('onKeyPress')).toBeUndefined(); 137 | }); 138 | 139 | it('generates keyboard key for each key in the current layout', () => { 140 | const wrapper: KeyboardShallowWrapper = shallow( 141 | } 144 | layouts={[extendedKeyboard]} /> 145 | , { lifecycleExperimental: true } 146 | ); 147 | expect(extendedKeyboard.every((row: Array): boolean => row.every((key: string): boolean => wrapper.find({ keyboardKey: key.match(/\ +/) === null ? key : ' ' }).length >= 1))).toBe(true); 148 | }); 149 | 150 | it('transfers keyboardKey props to every keyboard key from current layout', () => { 151 | const wrapper: KeyboardShallowWrapper = shallow( 152 | } 155 | layouts={[extendedKeyboard]} 156 | keyboardKeyHeight={30} 157 | keyboardKeyWidth={60} 158 | keyboardKeySymbolSize={20} /> 159 | , { lifecycleExperimental: true } 160 | ); 161 | extendedKeyboard.forEach((row: Array): void => row.forEach((key: string): void => { 162 | const isNotSpacebar: boolean = key.match(/\ +/) === null; 163 | const wrappedKey: ShallowWrapper = wrapper.find({ keyboardKey: isNotSpacebar ? key : ' ' }).first(); 164 | expect(wrappedKey.prop('keyboardKeyHeight')).toBe(30); 165 | isNotSpacebar ? expect(wrappedKey.prop('keyboardKeyWidth')).toBe(60) : expect(wrappedKey.prop('keyboardKeyWidth')).toBeGreaterThan(60); 166 | expect(wrappedKey.prop('keyboardKeySymbolSize')).toBe(20); 167 | })); 168 | }); 169 | }); 170 | 171 | describe('once mounted', () => { 172 | const textField: JSX.Element = ; 173 | const layouts: KeyboardLayout[] = [extendedKeyboard]; 174 | let wrapper: KeyboardShallowWrapper; 175 | 176 | beforeEach(() => { 177 | wrapper = shallow( 178 | 182 | , { lifecycleExperimental: true } 183 | ); 184 | }); 185 | 186 | describe('componentDidMount', () => { 187 | it('adds event listener for resize', () => { 188 | expect(addListener).toBeCalled(); 189 | const args: any[] = addListener.mock.calls[0]; 190 | expect(args[0]).toBe('resize'); 191 | expect(typeof args[1]).toBe('function'); 192 | expect(args[2]).toBe(false); 193 | }); 194 | 195 | it('syncs values', () => { 196 | expect(wrapper.state('value')).toBe('new'); 197 | }); 198 | }); 199 | 200 | describe('shouldComponentUpdate', () => { 201 | it('should re-render when state.value changes', () => { 202 | expect(wrapper.state('value')).toBe('new'); 203 | wrapper.setState({ value: 'old' }); 204 | expect(wrapper.state('value')).toBe('old'); 205 | }); 206 | 207 | it('should re-render when state.open changes', () => { 208 | expect(wrapper.state('open')).toBe(false); 209 | wrapper.setState({ open: true }); 210 | expect(wrapper.state('open')).toBe(true); 211 | }); 212 | 213 | it('should re-render when state.capsLock changes', () => { 214 | expect(wrapper.state('capsLock')).toBe(false); 215 | wrapper.setState({ capsLock: true }); 216 | expect(wrapper.state('capsLock')).toBe(true); 217 | }); 218 | 219 | it('should re-render when state.layout changes', () => { 220 | wrapper.setProps({ textField: textField, layouts: [extendedKeyboard, numericKeyboard] }); 221 | expect(wrapper.state('layout')).toBe(0); 222 | wrapper.setState({ layout: 1 }); 223 | expect(wrapper.state('layout')).toBe(1); 224 | }); 225 | 226 | it('should re-render when props.open changes', () => { 227 | wrapper.setProps({ textField: textField, layouts: layouts, automatic: false }); 228 | expect(wrapper.find(Dialog).prop('open')).toBe(false); 229 | wrapper.setProps({ textField: textField, layouts: layouts, open: true }); 230 | expect(wrapper.find(Dialog).prop('open')).toBe(true); 231 | }); 232 | 233 | it('should re-render when props.keyboardKeyHeight changes', () => { 234 | wrapper.setProps({ textField: textField, layouts: layouts, keyboardKeyHeight: 24 }); 235 | expect(wrapper.find(KeyboardKey).first().prop('keyboardKeyHeight')).toBe(24); 236 | wrapper.setProps({ textField: textField, layouts: layouts, keyboardKeyHeight: 32 }); 237 | expect(wrapper.find(KeyboardKey).first().prop('keyboardKeyHeight')).toBe(32); 238 | }); 239 | 240 | it('should re-render when props.keyboardKeySymbolSize changes', () => { 241 | wrapper.setProps({ textField: textField, layouts: layouts, keyboardKeySymbolSize: 24 }); 242 | expect(wrapper.find(KeyboardKey).first().prop('keyboardKeySymbolSize')).toBe(24); 243 | wrapper.setProps({ textField: textField, layouts: layouts, keyboardKeySymbolSize: 32 }); 244 | expect(wrapper.find(KeyboardKey).first().prop('keyboardKeySymbolSize')).toBe(32); 245 | }); 246 | 247 | it('should re-render when props.keyboardKeyWidth changes', () => { 248 | wrapper.setProps({ textField: textField, layouts: layouts, keyboardKeyWidth: 24 }); 249 | expect(wrapper.find(KeyboardKey).first().prop('keyboardKeyWidth')).toBe(24); 250 | wrapper.setProps({ textField: textField, layouts: layouts, keyboardKeyWidth: 32 }); 251 | expect(wrapper.find(KeyboardKey).first().prop('keyboardKeyWidth')).toBe(32); 252 | }); 253 | 254 | it('should re-render when props.automatic changes', () => { 255 | expect(wrapper.state('open')).toBe(false); 256 | expect(wrapper.find(Dialog).prop('open')).toBe(false); 257 | wrapper.setState({ open: true }); 258 | expect(wrapper.state('open')).toBe(true); 259 | expect(wrapper.find(Dialog).prop('open')).toBe(true); 260 | wrapper.setProps({ textField: textField, layouts: layouts, open: false }); 261 | expect(wrapper.state('open')).toBe(true); 262 | expect(wrapper.find(Dialog).prop('open')).not.toBe(false); 263 | wrapper.setProps({ textField: textField, layouts: layouts, automatic: false }); 264 | wrapper.setProps({ textField: textField, layouts: layouts, open: false }); 265 | expect(wrapper.state('open')).toBe(true); 266 | expect(wrapper.find(Dialog).prop('open')).toBe(false); 267 | }); 268 | 269 | it('should re-render when props.disableEffects changes', () => { 270 | wrapper.setProps({ textField: textField, layouts: layouts, disableEffects: false }); 271 | expect(wrapper.find(KeyboardKey).first().prop('disableEffects')).toBe(false); 272 | wrapper.setProps({ textField: textField, layouts: layouts, disableEffects: true }); 273 | expect(wrapper.find(KeyboardKey).first().prop('disableEffects')).toBe(true); 274 | }); 275 | 276 | it('should re-render when props.correctorName changes', () => { 277 | const corrector: jest.Mock<(value: string) => void> = jest.fn<(value: string) => void>(); 278 | wrapper.setProps({ 279 | automatic: true, 280 | textField: textField, 281 | layouts: layouts, 282 | correctorName: 'onRequestChange', 283 | corrector: corrector 284 | }); 285 | wrapper.find(TextField).last().simulate('RequestChange', 'old'); 286 | expect(corrector).toBeCalledWith('old'); 287 | corrector.mockClear(); 288 | wrapper.setProps({ 289 | automatic: true, 290 | textField: textField, 291 | layouts: layouts, 292 | correctorName: 'onPleaseChange', 293 | corrector: corrector 294 | }); 295 | wrapper.find(TextField).last().simulate('PleaseChange', 'please'); 296 | expect(corrector).toBeCalledWith('please'); 297 | }); 298 | 299 | it('should re-render when props.corrector changes', () => { 300 | const corrector1: jest.Mock<(value: string) => void> = jest.fn<(value: string) => void>(); 301 | const corrector2: jest.Mock<(value: string) => void> = jest.fn<(value: string) => void>(); 302 | wrapper = shallow( 303 | 309 | , { lifecycleExperimental: true } 310 | ); 311 | wrapper.find(TextField).last().simulate('RequestChange', 'correct1'); 312 | expect(corrector1).toBeCalledWith('correct1'); 313 | wrapper.setProps({ 314 | automatic: true, 315 | textField: textField, 316 | layouts: layouts, 317 | correctorName: 'onRequestChange', 318 | corrector: corrector2 319 | }); 320 | wrapper.find(TextField).last().simulate('RequestChange', 'correct2'); 321 | expect(corrector1).not.toBeCalledWith('correct2'); 322 | expect(corrector2).toBeCalledWith('correct2'); 323 | }); 324 | 325 | it('should re-render when props.onInput', () => { 326 | const onInput1:jest.Mock = jest.fn(); 327 | const onInput2:jest.Mock = jest.fn(); 328 | const keydownEvent: any = { 329 | key: 'Enter', 330 | stopImmediatePropagation: jest.fn(), 331 | stopPropagation: jest.fn(), 332 | preventDefault: jest.fn() 333 | }; 334 | wrapper.setProps({ automatic: true, textField: textField, layouts: layouts, onInput: onInput1 }); 335 | wrapper.setState({ open: true }); 336 | EventListenerService.emit('keydown', keydownEvent); 337 | expect(onInput1).toBeCalledWith('new'); 338 | wrapper.setProps({ automatic: true, textField: textField, layouts: layouts, onInput: onInput2 }); 339 | wrapper.setState({ open: true, value: 'old' }); 340 | EventListenerService.emit('keydown', keydownEvent); 341 | expect(onInput1).not.toBeCalledWith('old'); 342 | expect(onInput2).toBeCalledWith('old'); 343 | }); 344 | 345 | it('should re-render when props.onRequestClose', () => { 346 | const onRequestClose1:jest.Mock = jest.fn(); 347 | const keydownEvent: any = { 348 | key: 'Escape', 349 | stopImmediatePropagation: jest.fn(), 350 | stopPropagation: jest.fn(), 351 | preventDefault: jest.fn() 352 | }; 353 | wrapper.setProps({ textField: textField, layouts: layouts, automatic: false, open: true }); 354 | EventListenerService.emit('keydown', keydownEvent); 355 | expect(onRequestClose1).not.toBeCalled(); 356 | onRequestClose1.mockClear(); 357 | wrapper.setProps({ textField: textField, layouts: layouts, onRequestClose: onRequestClose1, automatic: false, open: true }); 358 | EventListenerService.emit('keydown', keydownEvent); 359 | expect(onRequestClose1).toBeCalled(); 360 | }); 361 | 362 | it('should re-render when textField.type changes', () => { 363 | expect(wrapper.find(TextField).length >= 2).toBe(true); 364 | wrapper.setProps({ automatic: true, layouts: layouts, textField: }); 365 | expect(wrapper.find(TextField).length).toBe(0); 366 | expect(wrapper.find('input').length >= 2).toBe(true); 367 | }); 368 | 369 | it('should re-render when props.layouts changes', () => { 370 | expect(wrapper.find({ keyboardKey: 'a' }).length).toBe(1); 371 | wrapper.setProps({ automatic: true, textField: textField, layouts: [numericKeyboard] }); 372 | expect(wrapper.find({ keyboardKey: 'a' }).length).toBe(0); 373 | }); 374 | 375 | it('should re-render when textField.props changes', () => { 376 | expect(wrapper.find(TextField).first().prop('value')).toBe('new'); 377 | wrapper.setProps({ automatic: true, layouts: layouts, textField: }); 378 | expect(wrapper.find(TextField).first().prop('value')).toBe('old'); 379 | }); 380 | }); 381 | 382 | describe('componentWillReciveProps', () => { 383 | it('syncs values if they differ', () => { 384 | expect(wrapper.state('value')).toBe('new'); 385 | wrapper.setProps({ textField: textField, layouts: layouts }); 386 | expect(wrapper.state('value')).toBe('new'); 387 | wrapper.setProps({ textField: , layouts: layouts }); 388 | expect(wrapper.state('value')).toBe('old'); 389 | wrapper.setProps({ textField: textField, layouts: layouts }); 390 | expect(wrapper.state('value')).toBe('new'); 391 | }); 392 | }); 393 | 394 | describe('componentDidUpdate', () => { 395 | describe('when automatic', () => { 396 | describe('when state.open changes to true', () => { 397 | beforeEach(() => { 398 | wrapper.setState({ open: true }); 399 | }); 400 | 401 | describe('when ActiveElement.isInput', () => { 402 | it('ActiveElement.blur()s', () => { 403 | wrapper.setState({ open: false }); 404 | isInput.mockReturnValueOnce(true); 405 | wrapper.setState({ open: true }); 406 | expect(blur).toBeCalled(); 407 | }); 408 | }); 409 | 410 | describe('when ActiveElement is not input', () => { 411 | it('dose not call ActiveElement.blur', () => { 412 | expect(blur).not.toBeCalled(); 413 | }); 414 | }); 415 | 416 | it('tests if ActiveElement.isInput', () => { 417 | expect(isInput).toBeCalled(); 418 | }); 419 | 420 | it('adds listener for global keydown event', () => { 421 | expect(addListener).toBeCalled(); 422 | const { calls } = addListener.mock; 423 | const args: any[] = calls[calls.length - 1]; 424 | expect(args[0]).toBe('keydown'); 425 | expect(typeof args[1]).toBe('function'); 426 | expect(args[2]).toBe(true); 427 | }); 428 | }); 429 | 430 | describe('when state.open changes from true to false', () => { 431 | beforeEach(() => { 432 | wrapper.setState({ open: true }); 433 | wrapper.setState({ open: false }); 434 | }); 435 | 436 | it('removes listener for global keydown event', () => { 437 | wrapper.setState({ open: false }); 438 | expect(removeListener).toBeCalled(); 439 | const { calls } = removeListener.mock; 440 | const args: any[] = calls[calls.length - 1]; 441 | expect(args[0]).toBe('keydown'); 442 | expect(typeof args[1]).toBe('function'); 443 | expect(args[2]).toBe(true); 444 | }); 445 | }); 446 | }); 447 | 448 | describe('when not automatic', () => { 449 | beforeEach(() => { 450 | wrapper = shallow( 451 | 455 | , { lifecycleExperimental: true } 456 | ); 457 | }); 458 | 459 | describe('when props.open changes to true', () => { 460 | beforeEach(() => { 461 | wrapper.setProps({ textField: textField, layouts: layouts, open: true }); 462 | }); 463 | 464 | describe('when ActiveElement.isInput', () => { 465 | it('ActiveElement.blur()s', () => { 466 | wrapper.setProps({ textField: textField, layouts: layouts, open: false }); 467 | isInput.mockReturnValueOnce(true); 468 | wrapper.setProps({ textField: textField, layouts: layouts, open: true }); 469 | expect(blur).toBeCalled(); 470 | }); 471 | }); 472 | 473 | describe('when ActiveElement is not input', () => { 474 | it('dose not call ActiveElement.blur', () => { 475 | expect(blur).not.toBeCalled(); 476 | }); 477 | }); 478 | 479 | it('tests if ActiveElement.isInput', () => { 480 | expect(isInput).toBeCalled(); 481 | }); 482 | 483 | it('adds listener for global keydown event', () => { 484 | expect(addListener).toBeCalled(); 485 | const { calls } = addListener.mock; 486 | const args: any[] = calls[calls.length - 1]; 487 | expect(args[0]).toBe('keydown'); 488 | expect(typeof args[1]).toBe('function'); 489 | expect(args[2]).toBe(true); 490 | }); 491 | }); 492 | 493 | describe('when props.open changes from true to false', () => { 494 | it('removes listener for global keydown event', () => { 495 | wrapper.setProps({ textField: textField, layouts: layouts, open: true }); 496 | wrapper.setProps({ textField: textField, layouts: layouts, open: false }); 497 | expect(removeListener).toBeCalled(); 498 | const { calls } = removeListener.mock; 499 | const args: any[] = calls[calls.length - 1]; 500 | expect(args[0]).toBe('keydown'); 501 | expect(typeof args[1]).toBe('function'); 502 | expect(args[2]).toBe(true); 503 | }); 504 | }); 505 | }); 506 | }); 507 | 508 | describe('componentWillUnmount', () => { 509 | it('removes resize event listner', () => { 510 | wrapper.unmount(); 511 | expect(removeListener).toBeCalled(); 512 | const args: any[] = removeListener.mock.calls[0]; 513 | expect(args[0]).toBe('resize'); 514 | expect(typeof args[1]).toBe('function'); 515 | expect(args[2]).toBe(false); 516 | }); 517 | }); 518 | 519 | describe('onKeyboard', () => { 520 | beforeEach(() => wrapper.find(TextField).first().simulate('focus')); 521 | 522 | describe('when Enter is recived', () => { 523 | let onInput: jest.Mock; 524 | 525 | beforeEach(() => { 526 | onInput = jest.fn(); 527 | wrapper.setProps({ textField: textField, layouts: layouts, automatic: true, onInput: onInput }); 528 | }); 529 | 530 | it('emits onInput and closes the Keyboard', () => { 531 | expect(wrapper.find(Dialog).prop('open')).toBe(true); 532 | expect(onInput).not.toBeCalled(); 533 | wrapper.find({ keyboardKey: 'Enter' }).shallow().simulate('touchTap'); 534 | wrapper.update(); 535 | expect(onInput).toBeCalled(); 536 | expect(onInput).toBeCalledWith('new'); 537 | expect(wrapper.find(Dialog).prop('open')).toBe(false); 538 | }); 539 | }); 540 | 541 | describe('when Backspace key is recived', () => { 542 | it('deletes last character as long as there are characters to be deleted', () => { 543 | expect(wrapper.state('value')).toBe('new'); 544 | wrapper.find({ keyboardKey: 'Backspace' }).shallow().simulate('touchTap'); 545 | wrapper.update(); 546 | expect(wrapper.state('value')).toBe('ne'); 547 | wrapper.find({ keyboardKey: 'Backspace' }).shallow().simulate('touchTap'); 548 | wrapper.update(); 549 | expect(wrapper.state('value')).toBe('n'); 550 | wrapper.find({ keyboardKey: 'Backspace' }).shallow().simulate('touchTap'); 551 | wrapper.update(); 552 | expect(wrapper.state('value')).toBe(''); 553 | wrapper.find({ keyboardKey: 'Backspace' }).shallow().simulate('touchTap'); 554 | wrapper.update(); 555 | expect(wrapper.state('value')).toBe(''); 556 | }); 557 | }); 558 | 559 | describe('when Escape key is recived', () => { 560 | it('closes the Keyboard', () => { 561 | expect(wrapper.find(Dialog).prop('open')).toBe(true); 562 | wrapper.find({ keyboardKey: 'Escape' }).shallow().simulate('touchTap'); 563 | wrapper.update(); 564 | expect(wrapper.find(Dialog).prop('open')).toBe(false); 565 | }); 566 | }); 567 | 568 | describe('when CapsLock key is recived', () => { 569 | it('CapsLocks Keyboard', () => { 570 | expect(wrapper.find({ keyboardKey: 'a' }).length).toBe(1); 571 | expect(wrapper.find({ keyboardKey: 'A' }).length).toBe(0); 572 | wrapper.find({ keyboardKey: 'CapsLock' }).first().shallow().simulate('touchTap'); 573 | wrapper.update(); 574 | expect(wrapper.find({ keyboardKey: 'a' }).length).toBe(0); 575 | expect(wrapper.find({ keyboardKey: 'A' }).length).toBe(1); 576 | }); 577 | }); 578 | 579 | describe('when Keyboard key is recived', () => { 580 | beforeEach(() => wrapper.setProps({ textField: textField, layouts: [[['a', 'b'], ['Keyboard', 'Enter']], [['a', 'b', 'Keyboard', 'Enter']]] })) 581 | 582 | it('Changes Keyboard\'s layout', () => { 583 | expect(wrapper.state('layout')).toBe(0); 584 | wrapper.find({ keyboardKey: 'Keyboard' }).first().shallow().simulate('touchTap'); 585 | wrapper.update(); 586 | expect(wrapper.state('layout')).toBe(1); 587 | }); 588 | 589 | it('should make circular layout changes', () => { 590 | expect(wrapper.state('layout')).toBe(0); 591 | wrapper.find({ keyboardKey: 'Keyboard' }).first().shallow().simulate('touchTap'); 592 | wrapper.update(); 593 | expect(wrapper.state('layout')).toBe(1); 594 | wrapper.find({ keyboardKey: 'Keyboard' }).first().shallow().simulate('touchTap'); 595 | wrapper.update(); 596 | expect(wrapper.state('layout')).toBe(0); 597 | }); 598 | }); 599 | 600 | describe('when symbol key is pressed', () => { 601 | it('should add character to value', () => { 602 | expect(wrapper.state('value')).toBe('new'); 603 | wrapper.find({ keyboardKey: '@' }).first().shallow().simulate('touchTap'); 604 | expect(wrapper.state('value')).toBe('new@'); 605 | wrapper.find({ keyboardKey: 't' }).first().shallow().simulate('touchTap'); 606 | expect(wrapper.state('value')).toBe('new@t'); 607 | wrapper.find({ keyboardKey: 's' }).first().shallow().simulate('touchTap'); 608 | expect(wrapper.state('value')).toBe('new@ts'); 609 | wrapper.find({ keyboardKey: '#' }).first().shallow().simulate('touchTap'); 610 | expect(wrapper.state('value')).toBe('new@ts#'); 611 | wrapper.find({ keyboardKey: '9' }).first().shallow().simulate('touchTap'); 612 | expect(wrapper.state('value')).toBe('new@ts#9'); 613 | }); 614 | }); 615 | 616 | describe('when not supported Special Key is pressed', () => { 617 | it('dose not do anything', () => { 618 | wrapper.setProps({ textField: textField, layouts: [[['TS']]] }); 619 | const props: KeyboardProps = wrapper.props(); 620 | const state: KeyboardState = wrapper.state(); 621 | wrapper.find({ keyboardKey: 'TS' }).first().shallow().simulate('touchTap'); 622 | wrapper.update(); 623 | expect(wrapper.props()).toBe(props); 624 | expect(wrapper.state()).toBe(state); 625 | }); 626 | }); 627 | }); 628 | 629 | describe('onKeyDown', () => { 630 | beforeEach(() => wrapper.find(TextField).first().simulate('focus')); 631 | 632 | describe('when Enter is recived', () => { 633 | let onInput: jest.Mock; 634 | 635 | beforeEach(() => { 636 | onInput = jest.fn(); 637 | wrapper.setProps({ textField: textField, layouts: layouts, automatic: true, onInput: onInput }); 638 | }); 639 | 640 | it('emits onInput and closes the Keyboard when is pressed', () => { 641 | const keydownEvent: any = { 642 | key: 'Enter', 643 | stopImmediatePropagation: jest.fn(), 644 | stopPropagation: jest.fn(), 645 | preventDefault: jest.fn() 646 | }; 647 | expect(wrapper.find(Dialog).prop('open')).toBe(true); 648 | expect(onInput).not.toBeCalled(); 649 | EventListenerService.emit('keydown', keydownEvent); 650 | wrapper.update(); 651 | expect(onInput).toBeCalled(); 652 | expect(onInput).toBeCalledWith('new'); 653 | expect(wrapper.find(Dialog).prop('open')).toBe(false); 654 | }); 655 | }); 656 | 657 | describe('when Backspace key is recived', () => { 658 | it('deletes last character as long as there are characters to be deleted when is pressed', () => { 659 | const keydownEvent: any = { 660 | key: 'Backspace', 661 | stopImmediatePropagation: jest.fn(), 662 | stopPropagation: jest.fn(), 663 | preventDefault: jest.fn() 664 | }; 665 | expect(wrapper.state('value')).toBe('new'); 666 | EventListenerService.emit('keydown', keydownEvent); 667 | wrapper.update(); 668 | expect(wrapper.state('value')).toBe('ne'); 669 | EventListenerService.emit('keydown', keydownEvent); 670 | wrapper.update(); 671 | expect(wrapper.state('value')).toBe('n'); 672 | EventListenerService.emit('keydown', keydownEvent); 673 | wrapper.update(); 674 | expect(wrapper.state('value')).toBe(''); 675 | EventListenerService.emit('keydown', keydownEvent); 676 | wrapper.update(); 677 | expect(wrapper.state('value')).toBe(''); 678 | }); 679 | }); 680 | 681 | describe('when Escape key is recived', () => { 682 | it('closes the Keyboard when is pressed', () => { 683 | const keydownEvent: any = { 684 | key: 'Escape', 685 | stopImmediatePropagation: jest.fn(), 686 | stopPropagation: jest.fn(), 687 | preventDefault: jest.fn() 688 | }; 689 | expect(wrapper.find(Dialog).prop('open')).toBe(true); 690 | EventListenerService.emit('keydown', keydownEvent); 691 | wrapper.update(); 692 | expect(wrapper.find(Dialog).prop('open')).toBe(false); 693 | }); 694 | }); 695 | 696 | describe('when CapsLock key is recived', () => { 697 | it('CapsLocks Keyboard when is pressed', () => { 698 | const keydownEvent: any = { 699 | key: 'CapsLock', 700 | stopImmediatePropagation: jest.fn(), 701 | stopPropagation: jest.fn(), 702 | preventDefault: jest.fn() 703 | }; 704 | expect(wrapper.find({ keyboardKey: 'a' }).length).toBe(1); 705 | expect(wrapper.find({ keyboardKey: 'A' }).length).toBe(0); 706 | EventListenerService.emit('keydown', keydownEvent); 707 | wrapper.update(); 708 | expect(wrapper.find({ keyboardKey: 'a' }).length).toBe(0); 709 | expect(wrapper.find({ keyboardKey: 'A' }).length).toBe(1); 710 | }); 711 | }); 712 | 713 | describe('when symbol key is pressed', () => { 714 | it('should add character to value', () => { 715 | let keydownEvent: any = { 716 | key: '@', 717 | stopImmediatePropagation: jest.fn(), 718 | stopPropagation: jest.fn(), 719 | preventDefault: jest.fn() 720 | }; 721 | expect(wrapper.state('value')).toBe('new'); 722 | EventListenerService.emit('keydown', keydownEvent); 723 | expect(wrapper.state('value')).toBe('new@'); 724 | keydownEvent.key = 't'; 725 | EventListenerService.emit('keydown', keydownEvent); 726 | expect(wrapper.state('value')).toBe('new@t'); 727 | keydownEvent.key = 's'; 728 | EventListenerService.emit('keydown', keydownEvent); 729 | expect(wrapper.state('value')).toBe('new@ts'); 730 | keydownEvent.key = '#'; 731 | EventListenerService.emit('keydown', keydownEvent); 732 | expect(wrapper.state('value')).toBe('new@ts#'); 733 | keydownEvent.key = '9'; 734 | EventListenerService.emit('keydown', keydownEvent); 735 | expect(wrapper.state('value')).toBe('new@ts#9'); 736 | }); 737 | }); 738 | 739 | describe('when not supported Special Key is pressed', () => { 740 | it('just stops event propagation', () => { 741 | const keydownEvent: any = { 742 | key: 'TS', 743 | stopImmediatePropagation: jest.fn(), 744 | stopPropagation: jest.fn(), 745 | preventDefault: jest.fn() 746 | }; 747 | expect(wrapper.state('value')).toBe('new'); 748 | EventListenerService.emit('keydown', keydownEvent); 749 | expect(wrapper.state('value')).toBe('new'); 750 | expect(keydownEvent.stopImmediatePropagation).toBeCalled(); 751 | expect(keydownEvent.stopPropagation).toBeCalled(); 752 | expect(keydownEvent.preventDefault).not.toBeCalled(); 753 | }); 754 | }); 755 | }); 756 | 757 | describe('onResize', () => { 758 | function extractTransformTopHelper(transform: string): number { 759 | return Number(transform.match(/translate\(0, (\d+)px\)/)![1]); 760 | } 761 | 762 | beforeEach(() => wrapper.setProps({ 763 | textField: textField, 764 | layouts: layouts, 765 | keyboardKeyHeight: 60, 766 | keyboardKeySymbolSize: 32 767 | })); 768 | 769 | it('decreses Keyboard with if window.innerWidth is less than needed', () => { 770 | const { width, maxWidth } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 771 | const { innerWidth: oldWidth } = window; 772 | (window as any).innerWidth = oldWidth / 10; 773 | EventListenerService.emit('resize'); 774 | wrapper.update(); 775 | const { width: newWidth, maxWidth: newMaxWidth } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 776 | expect(newWidth).toBeLessThan(width); 777 | expect(newMaxWidth).toBeLessThan(maxWidth); 778 | const { keyboardKeySymbolSize } = wrapper.find(KeyboardKey as any).first().props(); 779 | expect(keyboardKeySymbolSize).toBeLessThan(32); 780 | expect(keyboardKeySymbolSize).toBeGreaterThan(0); 781 | (window as any).innerWidth = oldWidth; 782 | }); 783 | 784 | it('decreses Keyboard translate(0, top) if window.innerHeight is less than needed but greater than the need for height resize', () => { 785 | const { height, maxHeight, transform } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 786 | const { innerHeight: oldHeight } = window; 787 | (window as any).innerHeight = (oldHeight / 2) + 50; 788 | EventListenerService.emit('resize'); 789 | wrapper.update(); 790 | const { height: newHeight, maxHeight: newMaxHeight, transform: newTransform } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 791 | expect(newHeight).toEqual(height); 792 | expect(newMaxHeight).toBeLessThan(maxHeight); 793 | expect(extractTransformTopHelper(newTransform)).toBeLessThan(extractTransformTopHelper(transform)); 794 | (window as any).innerHeight = oldHeight; 795 | }); 796 | 797 | it('decreses Keyboard height if window.innerHeight is less than needed and sets transform to translate(0, 0)', () => { 798 | const { height, maxHeight } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 799 | const { innerHeight: oldHeight } = window; 800 | (window as any).innerHeight = oldHeight / 2; 801 | EventListenerService.emit('resize'); 802 | wrapper.update(); 803 | const { height: newHeight, maxHeight: newMaxHeight, transform: newTransform } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 804 | expect(newHeight).toBeLessThan(height); 805 | expect(newMaxHeight).toBeLessThan(maxHeight); 806 | expect(extractTransformTopHelper(newTransform)).toEqual(0); 807 | (window as any).innerHeight = oldHeight; 808 | }); 809 | 810 | it('decreses Keyboard size if window inners* are less than requested with keyboardKey* props', () => { 811 | const { width, maxWidth, height, maxHeight } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 812 | const { innerHeight: oldHeight, innerWidth: oldWidth } = window; 813 | (window as any).innerWidth = oldWidth / 10; 814 | (window as any).innerHeight = oldHeight / 10; 815 | EventListenerService.emit('resize'); 816 | wrapper.update(); 817 | const { width: newWidth, maxWidth: newMaxWidth, height: newHeight, maxHeight: newMaxHeight, transform: newTransform } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 818 | expect(newWidth).toBeLessThan(width); 819 | expect(newMaxWidth).toBeLessThan(maxWidth); 820 | expect(newHeight).toBeLessThan(height); 821 | expect(newMaxHeight).toBeLessThan(maxHeight); 822 | expect(extractTransformTopHelper(newTransform)).toEqual(0); 823 | (window as any).innerWidth = oldWidth; 824 | (window as any).innerHeight = oldHeight; 825 | }); 826 | 827 | it('it dose not change Keyboard width if window.innerWidth is greater than needed', () => { 828 | const { width, maxWidth } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 829 | const { innerWidth: oldWidth } = window; 830 | (window as any).innerWidth = oldWidth * 10; 831 | EventListenerService.emit('resize'); 832 | wrapper.update(); 833 | const { width: newWidth, maxWidth: newMaxWidth } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 834 | expect(newWidth).toEqual(width); 835 | expect(newMaxWidth).toBeGreaterThan(maxWidth); 836 | (window as any).innerWidth = oldWidth; 837 | }); 838 | 839 | it('it dose not change Keyboard height if window.innerHeight is greater than needed', () => { 840 | const { height, maxHeight, transform } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 841 | const { innerHeight: oldHeight } = window; 842 | (window as any).innerHeight = oldHeight * 10; 843 | EventListenerService.emit('resize'); 844 | wrapper.update(); 845 | const { height: newHeight, maxHeight: newMaxHeight, transform: newTransform } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 846 | expect(newHeight).toEqual(height); 847 | expect(newMaxHeight).toBeGreaterThan(maxHeight); 848 | expect(extractTransformTopHelper(newTransform)).toEqual(extractTransformTopHelper(transform)); 849 | (window as any).innerHeight = oldHeight; 850 | }); 851 | 852 | it('it dose not change Keyboard size if window inners* are greater than requested with keyboardKey* props', () => { 853 | const { width, maxWidth, height, maxHeight, transform } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 854 | const { innerHeight: oldHeight, innerWidth: oldWidth } = window; 855 | (window as any).innerWidth = oldWidth * 10; 856 | (window as any).innerHeight = oldHeight * 10; 857 | EventListenerService.emit('resize'); 858 | wrapper.update(); 859 | const { width: newWidth, maxWidth: newMaxWidth, height: newHeight, maxHeight: newMaxHeight, transform: newTransform } = wrapper.find(Dialog).prop('contentStyle') as React.CSSProperties; 860 | expect(newWidth).toEqual(width); 861 | expect(newMaxWidth).toBeGreaterThan(maxWidth); 862 | expect(newHeight).toEqual(height); 863 | expect(newMaxHeight).toBeGreaterThan(maxHeight); 864 | expect(extractTransformTopHelper(newTransform)).toEqual(extractTransformTopHelper(transform)); 865 | const { keyboardKeySymbolSize } = wrapper.find(KeyboardKey as any).first().props(); 866 | expect(keyboardKeySymbolSize).toBe(32); 867 | (window as any).innerWidth = oldWidth; 868 | (window as any).innerHeight = oldHeight; 869 | }); 870 | }); 871 | 872 | describe('makeCorrection', () => { 873 | it('corrects value', () => { 874 | expect(wrapper.state('value')).toBe('new'); 875 | (wrapper.instance() as Keyboard).makeCorrection('old'); 876 | expect(wrapper.state('value')).toBe('old'); 877 | }); 878 | }); 879 | 880 | describe('when automatic', () => { 881 | describe('onFocus', () => { 882 | describe('when Keyboard.automaitcOpenPredicate return true', () => { 883 | it('sets state.open to true', () => { 884 | expect(wrapper.state('open')).toBe(false); 885 | wrapper.find(TextField).first().simulate('focus'); 886 | expect(wrapper.state('open')).toBe(true); 887 | }); 888 | }); 889 | 890 | describe('when Keyboard.automaitcOpenPredicate return false', () => { 891 | it('sets state.open to true', () => { 892 | Keyboard.automaitcOpenPredicate = () => false; 893 | expect(wrapper.state('open')).toBe(false); 894 | wrapper.find(TextField).first().simulate('focus'); 895 | expect(wrapper.state('open')).toBe(false); 896 | }); 897 | }); 898 | }); 899 | }); 900 | }); 901 | }); -------------------------------------------------------------------------------- /__tests__/KeyboardKey.spec.tsx: -------------------------------------------------------------------------------- 1 | import { KeyboardKey, KeyboardKeyProps, KeyboardKeyPressHandler } from './../src/KeyboardKey'; 2 | import * as React from 'react'; 3 | import FlatButton from 'material-ui/FlatButton'; 4 | import Backspace from 'material-ui/svg-icons/content/backspace'; 5 | import Enter from 'material-ui/svg-icons/hardware/keyboard-return'; 6 | import Escape from 'material-ui/svg-icons/action/exit-to-app'; 7 | import Keyboard from 'material-ui/svg-icons/hardware/keyboard'; 8 | import CapsLock from 'material-ui/svg-icons/hardware/keyboard-capslock'; 9 | import Spacebar from 'material-ui/svg-icons/editor/space-bar'; 10 | import Warning from 'material-ui/svg-icons/alert/warning'; 11 | import { shallow, ShallowWrapper } from 'enzyme'; 12 | import * as injectTapEventPlugin from 'react-tap-event-plugin'; 13 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 14 | 15 | type KeyboardKeyWrapper = ShallowWrapper; 16 | 17 | type IconType = {type: typeof React.Component}; 18 | 19 | injectTapEventPlugin(); 20 | 21 | const options: any = { 22 | context: { 23 | muiTheme: getMuiTheme() 24 | } 25 | }; 26 | 27 | describe('KeyboardKey', () => { 28 | describe('when rendering', () => { 29 | describe('common for all KeyboardKeys', () => { 30 | let wrapper: KeyboardKeyWrapper; 31 | 32 | beforeEach(() => { 33 | const onKeyPress: jest.Mock = jest.fn(); 34 | wrapper = shallow( 35 | , 42 | options 43 | ); 44 | }); 45 | 46 | it('renders primary FlatButton', () => { 47 | expect(wrapper.is(FlatButton)).toBeTruthy(); 48 | expect(wrapper.prop('primary')).toBeTruthy(); 49 | }); 50 | 51 | it('has onTouchTap handler', () => { 52 | expect(typeof wrapper.prop('onTouchTap')).toBe('function'); 53 | }); 54 | 55 | it('has keyboardKeyHeight for style.height', () => { 56 | expect((wrapper.prop('style') as React.CSSProperties).height).toEqual(40); 57 | }); 58 | 59 | it('has keyboardKeyWidth for style.width', () => { 60 | expect((wrapper.prop('style') as React.CSSProperties).width).toEqual(70); 61 | }); 62 | 63 | it('has keyboardKeyWidth for style.minWidth', () => { 64 | expect((wrapper.prop('style') as React.CSSProperties).width).toEqual(70); 65 | }); 66 | 67 | it('has enabled disableFocusRipple, disableKeyboardFocus, disableTouchRipple', () => { 68 | expect(wrapper.prop('disableFocusRipple')).toBe(false); 69 | expect(wrapper.prop('disableKeyboardFocus')).toBe(false); 70 | expect(wrapper.prop('disableTouchRipple')).toBe(false); 71 | }); 72 | }); 73 | describe('disableEffects', () => { 74 | it('has special effects when disableEffects is false', () => { 75 | const onKeyPress: jest.Mock = jest.fn(); 76 | const wrapper: KeyboardKeyWrapper = shallow( 77 | , 84 | options 85 | ); 86 | 87 | expect(wrapper.prop('disableFocusRipple')).toBe(false); 88 | expect(wrapper.prop('disableKeyboardFocus')).toBe(false); 89 | expect(wrapper.prop('disableTouchRipple')).toBe(false); 90 | expect(wrapper.prop('hoverColor')).toBeUndefined(); 91 | }); 92 | 93 | it('has no special effects when disableEffects is true', () => { 94 | const onKeyPress: jest.Mock = jest.fn(); 95 | const wrapper: KeyboardKeyWrapper = shallow( 96 | , 103 | options 104 | ); 105 | 106 | expect(wrapper.prop('disableFocusRipple')).toBe(true); 107 | expect(wrapper.prop('disableKeyboardFocus')).toBe(true); 108 | expect(wrapper.prop('disableTouchRipple')).toBe(true); 109 | expect(wrapper.prop('hoverColor')).toBe(options.context.muiTheme.flatButton.color); 110 | }); 111 | }); 112 | describe('when keyboardKey.length less than or equal to 1 and is not \' \'', () => { 113 | describe('common for all when keyboardKey.length less than or equal to 1 and is not \' \'', () => { 114 | let wrapper: KeyboardKeyWrapper; 115 | 116 | beforeEach(() => { 117 | const onKeyPress: jest.Mock = jest.fn(); 118 | wrapper = shallow( 119 | , 126 | options 127 | ); 128 | }); 129 | 130 | it('has keyboardKeySymbolSize for labelStyle.fontSize', () => { 131 | expect((wrapper.prop('labelStyle') as React.CSSProperties).fontSize).toBe(20); 132 | }); 133 | 134 | it('has \'none\' for labelStyle.textTransform', () => { 135 | expect((wrapper.prop('labelStyle') as React.CSSProperties).textTransform).toBe('none'); 136 | }); 137 | }); 138 | 139 | describe('when keyboardKey.length is 1', () => { 140 | it('has keyboardKey for label', () => { 141 | const onKeyPress: jest.Mock = jest.fn(); 142 | let wrapper: KeyboardKeyWrapper = shallow( 143 | , 150 | options 151 | ); 152 | expect(wrapper.prop('label')).toBe('f'); 153 | }); 154 | }); 155 | 156 | describe('when keyboardKey.length is less tnan 1', () => { 157 | let wrapper: KeyboardKeyWrapper; 158 | 159 | beforeEach(() => { 160 | const onKeyPress: jest.Mock = jest.fn(); 161 | wrapper = shallow( 162 | , 169 | options 170 | ); 171 | }); 172 | 173 | it('is disabled', () => { 174 | expect(wrapper.prop('disabled')).toBeTruthy(); 175 | }); 176 | 177 | it('has \' \' for a label', () => { 178 | expect(wrapper.prop('label')).toBe(' '); 179 | }); 180 | }); 181 | }); 182 | 183 | describe('when keyboardKey.length is greater than 1 or keyboardKey is \' \'', () => { 184 | describe('common for all when keyboardKey.length is greater than 1 or keyboardKey is \' \'', () => { 185 | let wrapper: KeyboardKeyWrapper; 186 | 187 | beforeEach(() => { 188 | const onKeyPress: jest.Mock = jest.fn(); 189 | wrapper = shallow( 190 | , 197 | options 198 | ); 199 | }); 200 | 201 | type IconProps = {props: {style: React.CSSProperties}}; 202 | 203 | it('has keyboardKeySymbolSize for icon.props.style.width', () => { 204 | expect((wrapper.prop('icon') as IconProps).props.style.width).toBe(20); 205 | }); 206 | 207 | it('has keyboardKeySymbolSize for icon.props.style.height', () => { 208 | expect((wrapper.prop('icon') as IconProps).props.style.height).toBe(20); 209 | }); 210 | }); 211 | 212 | describe('when keyboardKey is \'Enter\'', () => { 213 | it('has Enter for icon', () => { 214 | const onKeyPress: jest.Mock = jest.fn(); 215 | let wrapper: KeyboardKeyWrapper = shallow( 216 | , 223 | options 224 | ); 225 | expect((wrapper.prop('icon') as IconType).type).toBe(Enter); 226 | }); 227 | }); 228 | 229 | describe('when keyboardKey is \'Backspace\'', () => { 230 | it('has Backspace for icon', () => { 231 | const onKeyPress: jest.Mock = jest.fn(); 232 | let wrapper: KeyboardKeyWrapper = shallow( 233 | , 240 | options 241 | ); 242 | expect((wrapper.prop('icon') as IconType).type).toBe(Backspace); 243 | }); 244 | }); 245 | 246 | describe('when keyboardKey is \'Escape\'', () => { 247 | it('has Escape for icon', () => { 248 | const onKeyPress: jest.Mock = jest.fn(); 249 | let wrapper: KeyboardKeyWrapper = shallow( 250 | , 257 | options 258 | ); 259 | expect((wrapper.prop('icon') as IconType).type).toBe(Escape); 260 | }); 261 | }); 262 | 263 | describe('when keyboardKey is \'CapsLock\'', () => { 264 | it('has CapsLock for icon', () => { 265 | const onKeyPress: jest.Mock = jest.fn(); 266 | let wrapper: KeyboardKeyWrapper = shallow( 267 | , 274 | options 275 | ); 276 | expect((wrapper.prop('icon') as IconType).type).toBe(CapsLock); 277 | }); 278 | }); 279 | 280 | describe('when keyboardKey is \'Keyboard\'', () => { 281 | it('has Keyboard for icon', () => { 282 | const onKeyPress: jest.Mock = jest.fn(); 283 | let wrapper: KeyboardKeyWrapper = shallow( 284 | , 291 | options 292 | ); 293 | expect((wrapper.prop('icon') as IconType).type).toBe(Keyboard); 294 | }); 295 | }); 296 | 297 | describe('when keyboardKey is \' \'', () => { 298 | it('has Spacebar for icon', () => { 299 | const onKeyPress: jest.Mock = jest.fn(); 300 | let wrapper: KeyboardKeyWrapper = shallow( 301 | , 308 | options 309 | ); 310 | expect((wrapper.prop('icon') as IconType).type).toBe(Spacebar); 311 | }); 312 | }); 313 | 314 | describe('when keyboardKey is not recognized', () => { 315 | it('has Warning for icon', () => { 316 | const onKeyPress: jest.Mock = jest.fn(); 317 | let wrapper: KeyboardKeyWrapper = shallow( 318 | , 325 | options 326 | ); 327 | expect((wrapper.prop('icon') as IconType).type).toBe(Warning); 328 | }); 329 | }); 330 | }); 331 | }); 332 | 333 | describe('when shouldComponentUpdate and once touchTap-ed', () => { 334 | let onKeyPress: jest.Mock; 335 | let wrapper: KeyboardKeyWrapper; 336 | 337 | beforeEach(() => { 338 | onKeyPress = jest.fn(); 339 | wrapper = shallow( 340 | , 347 | options 348 | ); 349 | }); 350 | 351 | describe('when touchTaped', () => { 352 | describe('when keyboardKey length is 1 or it is a supported special key', () => { 353 | it('calls onKeyPress with keyboardKey', () => { 354 | wrapper.simulate('touchTap'); 355 | expect(onKeyPress).lastCalledWith('i'); 356 | wrapper.setProps({ 357 | keyboardKey: 'Enter', 358 | onKeyPress: onKeyPress, 359 | keyboardKeySymbolSize: 16, 360 | keyboardKeyHeight :40, 361 | keyboardKeyWidth: 70, 362 | disableEffects: false 363 | }); 364 | wrapper.simulate('touchTap'); 365 | expect(onKeyPress).lastCalledWith('Enter'); 366 | }); 367 | }); 368 | 369 | describe('when keyboardKey is not recognized and supported', () => { 370 | it('dose not call onKeyPress', () => { 371 | wrapper.setProps({ 372 | keyboardKey: 'TS', 373 | onKeyPress: onKeyPress, 374 | keyboardKeySymbolSize: 16, 375 | keyboardKeyHeight :40, 376 | keyboardKeyWidth: 70, 377 | disableEffects: false 378 | }); 379 | wrapper.simulate('touchTap'); 380 | expect(onKeyPress).not.toBeCalled(); 381 | }); 382 | }); 383 | }); 384 | 385 | describe('when shouldComponentUpdate', () => { 386 | it('re-renders when keyboardKey changes', () => { 387 | expect(wrapper.prop('label')).toEqual('i'); 388 | wrapper.setProps({ 389 | keyboardKey: 's', 390 | onKeyPress: onKeyPress, 391 | keyboardKeySymbolSize: 16, 392 | keyboardKeyHeight: 40, 393 | keyboardKeyWidth: 70, 394 | disableEffects: false 395 | }); 396 | expect(wrapper.prop('label')).toEqual('s'); 397 | }); 398 | 399 | it('re-renders when onKeyPress changes', () => { 400 | const changedHandler: jest.Mock = jest.fn(); 401 | wrapper.simulate('touchTap'); 402 | expect(onKeyPress).toBeCalledWith('i'); 403 | wrapper.setProps({ 404 | keyboardKey: 'i', 405 | onKeyPress: changedHandler, 406 | keyboardKeySymbolSize: 16, 407 | keyboardKeyHeight: 40, 408 | keyboardKeyWidth: 70, 409 | disableEffects: false 410 | }); 411 | wrapper.simulate('touchTap'); 412 | expect(changedHandler).toBeCalledWith('i'); 413 | expect(onKeyPress.call.length).toEqual(1); 414 | }); 415 | 416 | it('re-renders when keyboardKeySymbolSize changes', () => { 417 | expect((wrapper.prop('labelStyle') as React.CSSProperties).fontSize).toEqual(16); 418 | wrapper.setProps({ 419 | keyboardKey: 'i', 420 | onKeyPress: onKeyPress, 421 | keyboardKeySymbolSize: 20, 422 | keyboardKeyHeight: 40, 423 | keyboardKeyWidth: 70, 424 | disableEffects: false 425 | }); 426 | expect((wrapper.prop('labelStyle') as React.CSSProperties).fontSize).toEqual(20); 427 | }); 428 | 429 | it('re-renders when keyboardKeyHeight changes', () => { 430 | expect((wrapper.prop('style') as React.CSSProperties).height).toEqual(40); 431 | wrapper.setProps({ 432 | keyboardKey: 'i', 433 | onKeyPress: onKeyPress, 434 | keyboardKeySymbolSize: 16, 435 | keyboardKeyHeight: 50, 436 | keyboardKeyWidth: 70, 437 | disableEffects: false 438 | }); 439 | expect((wrapper.prop('style') as React.CSSProperties).height).toEqual(50); 440 | }); 441 | 442 | it('re-renders when keyboardKeyWidth changes', () => { 443 | expect((wrapper.prop('style') as React.CSSProperties).width).toEqual(70); 444 | wrapper.setProps({ 445 | keyboardKey: 'i', 446 | onKeyPress: onKeyPress, 447 | keyboardKeySymbolSize: 16, 448 | keyboardKeyHeight: 40, 449 | keyboardKeyWidth: 60, 450 | disableEffects: false 451 | }); 452 | expect((wrapper.prop('style') as React.CSSProperties).width).toEqual(60); 453 | }); 454 | it('re-renders when disableEffects changes', () => { 455 | expect(wrapper.prop('disableFocusRipple')).toBe(true); 456 | wrapper.setProps({ 457 | keyboardKey: 'i', 458 | onKeyPress: onKeyPress, 459 | keyboardKeySymbolSize: 16, 460 | keyboardKeyHeight: 40, 461 | keyboardKeyWidth: 60, 462 | disableEffects: false 463 | }); 464 | expect(wrapper.prop('disableFocusRipple')).toBe(false); 465 | }); 466 | 467 | it('dose not re-renders when no prop has changed', () => { 468 | const props: any = wrapper.props(); 469 | wrapper.setProps({ 470 | keyboardKey: 'i', 471 | onKeyPress: onKeyPress, 472 | keyboardKeySymbolSize: 16, 473 | keyboardKeyHeight: 40, 474 | keyboardKeyWidth: 70, 475 | disableEffects: true 476 | }); 477 | expect(wrapper.props()).toBe(props); 478 | }); 479 | }); 480 | }); 481 | }); -------------------------------------------------------------------------------- /__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import ReExportedKeyboard from './../src'; 2 | import Keyboard from './../src/Keyboard'; 3 | import * as ReExportedEverythingFromKeyboard from './../src'; 4 | import * as EverythingFromKeyboard from './../src/Keyboard'; 5 | 6 | describe('index', function () { 7 | it('should re-export everything from Keyboard.tsx', function () { 8 | expect(ReExportedEverythingFromKeyboard).toEqual(EverythingFromKeyboard); 9 | }); 10 | 11 | it('should re-export default from Keyboard.tsx', function () { 12 | expect(ReExportedKeyboard).toBe(Keyboard); 13 | }); 14 | }); -------------------------------------------------------------------------------- /__tests__/layouts.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | alphaNumericKeyboard, 3 | extendedKeyboard, 4 | numericKeyboard, 5 | kyeboardCapsLockLayout 6 | } from './../src/layouts'; 7 | 8 | describe('layots', function () { 9 | describe('alphaNumericKeyboard', function () { 10 | it('is a keyboard layout containg only numbers and letters symbols, Excape, CapsLock, Backspace and Enter', function () { 11 | expect(alphaNumericKeyboard).toEqual([ 12 | ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], 13 | ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'], 14 | ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'Backspace'], 15 | ['Escape', 'CapsLock', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'Enter'] 16 | ]); 17 | }); 18 | }); 19 | 20 | describe('extendedKeyboard', function () { 21 | it('is a keyboard layout extending alphaNumericKeyboard by adding second CapsLock, -, @, , #, Spacebar and . keys', function () { 22 | expect(extendedKeyboard).toEqual([ 23 | ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], 24 | ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'], 25 | ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'Backspace'], 26 | ['CapsLock', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '-', 'CapsLock'], 27 | ['Escape', '@', '#', ' ', '.', 'Enter'] 28 | ]); 29 | }); 30 | }); 31 | 32 | describe('numericKeyboard', function () { 33 | it('is a keyboard layout for inputing numbers only', function () { 34 | expect(numericKeyboard).toEqual([ 35 | ['Escape', '-', 'Backspace'], 36 | ['7', '8', '9'], 37 | ['4', '5', '6'], 38 | ['1', '2', '3'], 39 | ['0', '.', 'Enter'] 40 | ]); 41 | }); 42 | }); 43 | 44 | describe('kyeboardCapsLockLayout', function () { 45 | describe('when second argument is true', function () { 46 | it('will upper case all keyboard layout keys which are not special keys (strings with lengh equal to 1)', function () { 47 | expect(kyeboardCapsLockLayout([ 48 | ['0', '9'], 49 | ['a', 'z'], 50 | ['а', 'я'], 51 | ['Ю', 'F'], 52 | ['@', '#'], 53 | ['-', '+'], 54 | ['S7s', 't3M'], 55 | ['', ''] 56 | ], true)).toEqual([ 57 | ['0', '9'], 58 | ['A', 'Z'], 59 | ['А', 'Я'], 60 | ['Ю', 'F'], 61 | ['@', '#'], 62 | ['-', '+'], 63 | ['S7s', 't3M'], 64 | ['', ''] 65 | ]); 66 | }); 67 | 68 | it('will not upper case keyboard layout keys which are special keys (strings with lengh different from 1)', function () { 69 | expect(kyeboardCapsLockLayout([ 70 | ['Escape', 'Backspace'], 71 | ['Enter', 'Alt'], 72 | ['Shift', 'CapsLock'], 73 | ['', 'iv'], 74 | ['S7s', 't3M'], 75 | ], true)).toEqual([ 76 | ['Escape', 'Backspace'], 77 | ['Enter', 'Alt'], 78 | ['Shift', 'CapsLock'], 79 | ['', 'iv'], 80 | ['S7s', 't3M'], 81 | ]); 82 | }); 83 | }); 84 | 85 | describe('when second argument is false', function () { 86 | it('will lower case all keyboard layout keys which are not special keys (strings with lengh equal to 1)', function () { 87 | expect(kyeboardCapsLockLayout([ 88 | ['0', '9'], 89 | ['A', 'Z'], 90 | ['А', 'Я'], 91 | ['ю', 'f'], 92 | ['@', '#'], 93 | ['-', '+'], 94 | ['S7s', 't3M'], 95 | ['', ''] 96 | ], false)).toEqual([ 97 | ['0', '9'], 98 | ['a', 'z'], 99 | ['а', 'я'], 100 | ['ю', 'f'], 101 | ['@', '#'], 102 | ['-', '+'], 103 | ['S7s', 't3M'], 104 | ['', ''] 105 | ]); 106 | }); 107 | 108 | it('will not lower case keyboard layout keys which are special keys (strings with lengh different from 1)', function () { 109 | expect(kyeboardCapsLockLayout([ 110 | ['Escape', 'Backspace'], 111 | ['Enter', 'Alt'], 112 | ['Shift', 'CapsLock'], 113 | ['', 'iv'] 114 | ], false)).toEqual([ 115 | ['Escape', 'Backspace'], 116 | ['Enter', 'Alt'], 117 | ['Shift', 'CapsLock'], 118 | ['', 'iv'] 119 | ]); 120 | }); 121 | }); 122 | }); 123 | }); -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # custom_layouts 2 | 3 | Demonstrates a Keyboard with all layouts beeing custom and how the `'Keyboard'` key can be used to switch between layouts 4 | 5 | # custom_textField 6 | 7 | Demonstrates a custom textField element ('material-ui-number-input') a TextField wrapper 8 | 9 | # no_keyboard 10 | 11 | Demonstrates a Keyboard that never opens, showing that when user don't needs on screen keyboard the input field is just a TextField 12 | 13 | # simple_example 14 | 15 | Demonstrates a Keyboard that opens every time an input field is focused -------------------------------------------------------------------------------- /example/custom_layouts/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | npm install 3 | npm start 4 | // Open ./demo/index.html in your favorite browser 5 | ``` 6 | -------------------------------------------------------------------------------- /example/custom_layouts/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import TextField from 'material-ui/TextField'; 3 | import { Keyboard, InputHandler, KeyboardLayout } from 'react-material-ui-keyboard'; 4 | import { render as ReactDomRender } from 'react-dom'; 5 | import * as injectTapEventPlugin from 'react-tap-event-plugin'; 6 | 7 | interface DemoState { 8 | value?: string; 9 | }; 10 | 11 | const layout1: KeyboardLayout = [ 12 | ['Escape', 'Keyboard', 'CapsLock', 'Backspace', 'Enter'], 13 | ['a', 'b', 'c', 'd', 'e'] 14 | ]; 15 | 16 | const layout2: KeyboardLayout = [ 17 | ['z', 'x', 'y', 'u', 'q'], 18 | ['Escape', 'Keyboard', 'CapsLock', 'Backspace', 'Enter'] 19 | ]; 20 | 21 | const layouts: Array = [layout1, layout2]; 22 | 23 | class Demo extends React.Component { 24 | private _onInput: InputHandler; 25 | 26 | private _handleInput(input: string): void { 27 | this.setState({ value: input }); 28 | } 29 | 30 | public constructor(props: void) { 31 | super(props); 32 | this.state = { value: '' }; 33 | this._onInput = this._handleInput.bind(this); 34 | } 35 | 36 | public render(): JSX.Element { 37 | const { state, _onInput } = this; 38 | const { value } = state; 39 | const textField: JSX.Element = ( 40 | 44 | ); 45 | 46 | return ( 47 |
48 | 49 | 58 |
59 | ); 60 | } 61 | }; 62 | 63 | injectTapEventPlugin(); 64 | let bootstrapNode = document.createElement('div'); 65 | ReactDomRender(, bootstrapNode); 66 | document.body.appendChild(bootstrapNode); 67 | -------------------------------------------------------------------------------- /example/custom_layouts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./demo/index.html", 3 | "scripts": { 4 | "test": "echo \"Error: no test specified\" && exit 1", 5 | "start": "webpack" 6 | }, 7 | "author": "Ivo Stratev", 8 | "license": "MIT", 9 | "dependencies": { 10 | "material-ui": "^0.16.1", 11 | "react": "^15.3.2", 12 | "react-dom": "^15.3.2", 13 | "react-tap-event-plugin": "^1.0.0", 14 | "react-material-ui-keyboard": "^6.1.2", 15 | "@types/node": "^6.0.46", 16 | "@types/react": "^0.14.43", 17 | "@types/react-dom": "^0.14.18", 18 | "@types/react-tap-event-plugin": "^0.0.30", 19 | "@types/material-ui": "^0.16.38" 20 | }, 21 | "devDependencies": { 22 | "html-webpack-plugin": "^2.24.0", 23 | "ts-loader": "^0.9.5", 24 | "typescript": "^2.0.6", 25 | "webpack": "^1.13.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/custom_layouts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": true, 6 | "strictNullChecks": true, 7 | "noImplicitThis": true, 8 | "noUnusedParameters": true, 9 | "noUnusedLocals": true, 10 | "experimentalDecorators": true, 11 | "jsx": "react" 12 | }, 13 | "include": [ 14 | "index.tsx" 15 | ], 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } 20 | 21 | -------------------------------------------------------------------------------- /example/custom_layouts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: [ 7 | "./index.tsx" 8 | ], 9 | 10 | output: { 11 | path: path.join(__dirname, "demo"), 12 | filename: "[name].js", 13 | publicPath: '' 14 | }, 15 | 16 | plugins: [ 17 | new HtmlWebpackPlugin({ title: "React Material-Ui Keyboard" }) 18 | ], 19 | 20 | resolve: { 21 | extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"] 22 | }, 23 | 24 | module: { 25 | loaders: [ 26 | { 27 | test: /\.tsx?$/, 28 | loaders: ["ts-loader"] 29 | } 30 | ], 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /example/custom_textField/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | npm install 3 | npm start 4 | // Open ./demo/index.html in your favorite browser 5 | ``` 6 | -------------------------------------------------------------------------------- /example/custom_textField/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { NumberInput, NumberInputChangeHandler, NumberInputError, EventValue, NumberInputErrorHandler, NumberInputValidHandler } from 'material-ui-number-input'; 3 | import { Keyboard, RequestCloseHandler, InputHandler } from 'react-material-ui-keyboard'; 4 | import { numericKeyboard } from 'react-material-ui-keyboard/layouts'; 5 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 6 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 7 | import { MuiTheme } from 'material-ui/styles'; 8 | import { render as ReactDomRender } from 'react-dom'; 9 | import * as injectTapEventPlugin from 'react-tap-event-plugin'; 10 | 11 | const { div, link } = React.DOM; 12 | 13 | interface DemoState { 14 | open?: boolean; 15 | value?: string; 16 | errorText?: string; 17 | }; 18 | 19 | function corrector(value: string): void { 20 | (this as Keyboard).makeCorrection(value); 21 | } 22 | 23 | class Demo extends React.Component { 24 | private onFocus: React.FocusEventHandler; 25 | private onChange: React.FormEventHandler; 26 | private onRequestClose: RequestCloseHandler; 27 | private onInput: InputHandler; 28 | private onError: NumberInputErrorHandler; 29 | private onValid: NumberInputValidHandler; 30 | 31 | private canOpenKeyboard(): boolean { 32 | return (this.state.value.length % 2) === 0; 33 | } 34 | 35 | private handleFocus(event: React.FocusEvent): void { 36 | if(this.canOpenKeyboard()) { 37 | this.setState({ open: true }); 38 | } 39 | } 40 | 41 | private handleChange(event: React.FormEvent, value: string): void { 42 | console.log(value); 43 | this.setState({ value: value }); 44 | } 45 | 46 | private handleRequestClose(): void { 47 | this.setState({ open: false }); 48 | } 49 | 50 | private handleInput(input: string): void { 51 | console.log(input); 52 | this.setState({ value: input }); 53 | } 54 | 55 | private handleError(error: NumberInputError): void { 56 | let errorText: string; 57 | switch(error) { 58 | case 'required': 59 | errorText = 'This field is required'; 60 | break; 61 | case 'invalidSymbol': 62 | errorText = 'You are tring to enter none number symbol'; 63 | break; 64 | case 'incompleteNumber': 65 | errorText = 'Number is incomplete'; 66 | break; 67 | case 'singleMinus': 68 | errorText = 'Minus can be use only for negativity'; 69 | break; 70 | case 'singleFloatingPoint': 71 | errorText = 'There is already a floating point'; 72 | break; 73 | case 'singleZero': 74 | errorText = 'Floating point is expected'; 75 | break; 76 | case 'min': 77 | errorText = 'You are tring to enter number less than -10'; 78 | break; 79 | case 'max': 80 | errorText = 'You are tring to enter number greater than 120'; 81 | break; 82 | } 83 | this.setState({ errorText: errorText }); 84 | } 85 | 86 | private handleValid(value: number): void { 87 | console.debug(`valid ${value}`); 88 | } 89 | 90 | public constructor(props: void) { 91 | super(props); 92 | this.state = { open: false, value: '' }; 93 | this.onFocus = this.handleFocus.bind(this); 94 | this.onChange = this.handleChange.bind(this); 95 | this.onRequestClose = this.handleRequestClose.bind(this); 96 | this.onInput = this.handleInput.bind(this); 97 | this.onError = this.handleError.bind(this); 98 | this.onValid = this.handleValid.bind(this); 99 | } 100 | 101 | public componentDidMount(): void { 102 | setTimeout(() => this.setState({ value: '89' }), 1000); 103 | } 104 | 105 | public render(): JSX.Element { 106 | const { state, onFocus, onChange, onError, onValid } = this; 107 | const { value, errorText } = state; 108 | const textField: JSX.Element = ( 109 | 122 | ); 123 | 124 | return ( 125 |
126 | 127 | 139 |
140 | ); 141 | } 142 | }; 143 | 144 | injectTapEventPlugin(); 145 | let bootstrapNode = document.createElement('div'); 146 | ReactDomRender(, bootstrapNode); 147 | document.body.appendChild(bootstrapNode); 148 | -------------------------------------------------------------------------------- /example/custom_textField/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./demo/index.html", 3 | "scripts": { 4 | "test": "echo \"Error: no test specified\" && exit 1", 5 | "postinstall": "typings install", 6 | "start": "webpack" 7 | }, 8 | "author": "Ivo Stratev", 9 | "license": "MIT", 10 | "dependencies": { 11 | "material-ui": "^0.15.4", 12 | "react": "^15.3.1", 13 | "react-dom": "^15.3.1", 14 | "react-tap-event-plugin": "^1.0.0", 15 | "react-material-ui-keyboard": "^6.1.0", 16 | "material-ui-number-input": "^5.0.8" 17 | }, 18 | "devDependencies": { 19 | "html-webpack-plugin": "^2.22.0", 20 | "ts-loader": "^0.8.2", 21 | "typescript": "^1.8.10", 22 | "typings": "^1.3.3", 23 | "webpack": "^1.13.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/custom_textField/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es5", 6 | "jsx": "react", 7 | "noImplicitAny": true 8 | }, 9 | "files": [ 10 | "typings/index.d.ts", 11 | "index.tsx" 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | 18 | -------------------------------------------------------------------------------- /example/custom_textField/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalDependencies": { 3 | "node": "registry:dt/node", 4 | "react": "registry:dt/react", 5 | "react-dom": "registry:dt/react-dom", 6 | "react-tap-event-plugin": "registry:dt/react-tap-event-plugin", 7 | "material-ui": "registry:dt/material-ui" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/custom_textField/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: [ 7 | "./index.tsx" 8 | ], 9 | 10 | output: { 11 | path: path.join(__dirname, "demo"), 12 | filename: "[name].js", 13 | publicPath: '' 14 | }, 15 | 16 | plugins: [ 17 | new HtmlWebpackPlugin({ title: "React Material-Ui Keyboard" }) 18 | ], 19 | 20 | resolve: { 21 | extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"] 22 | }, 23 | 24 | module: { 25 | loaders: [ 26 | { 27 | test: /\.tsx?$/, 28 | loaders: ["ts-loader"] 29 | } 30 | ], 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /example/no_keyboard/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | npm install 3 | npm start 4 | // Open ./demo/index.html in your favorite browser 5 | ``` 6 | -------------------------------------------------------------------------------- /example/no_keyboard/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import TextField from 'material-ui/TextField'; 3 | import { Keyboard, RequestCloseHandler, InputHandler } from 'react-material-ui-keyboard'; 4 | import { extendedKeyboard } from 'react-material-ui-keyboard/layouts'; 5 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 6 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 7 | import { MuiTheme } from 'material-ui/styles'; 8 | import { render as ReactDomRender } from 'react-dom'; 9 | import * as injectTapEventPlugin from 'react-tap-event-plugin'; 10 | 11 | const { div, link } = React.DOM; 12 | 13 | interface DemoState { 14 | value?: string; 15 | } 16 | 17 | interface TextEnterTarget { 18 | value?: string; 19 | } 20 | 21 | interface TextEnterEvent { 22 | target: TextEnterTarget; 23 | } 24 | 25 | class Demo extends React.Component { 26 | private _onChange: React.FormEventHandler; 27 | 28 | private _handleChange(event: React.FormEvent): void { 29 | const textEnterEvent: TextEnterEvent = event as TextEnterEvent; 30 | const value: string = textEnterEvent.target.value; 31 | this.setState({ value: value }); 32 | } 33 | 34 | public constructor(props: void) { 35 | super(props); 36 | this.state = { value: '' }; 37 | this._onChange = this._handleChange.bind(this); 38 | } 39 | 40 | public render(): JSX.Element { 41 | const { state, _onChange } = this; 42 | const { value } = state; 43 | const textField: JSX.Element = ( 44 | 49 | ); 50 | 51 | return ( 52 |
53 | 54 | 59 |
60 | ); 61 | } 62 | }; 63 | 64 | injectTapEventPlugin(); 65 | let bootstrapNode = document.createElement('div'); 66 | ReactDomRender(, bootstrapNode); 67 | document.body.appendChild(bootstrapNode); 68 | -------------------------------------------------------------------------------- /example/no_keyboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./demo/index.html", 3 | "scripts": { 4 | "test": "echo \"Error: no test specified\" && exit 1", 5 | "postinstall": "typings install", 6 | "start": "webpack" 7 | }, 8 | "author": "Ivo Stratev", 9 | "license": "MIT", 10 | "dependencies": { 11 | "material-ui": "^0.15.4", 12 | "react": "^15.3.1", 13 | "react-dom": "^15.3.1", 14 | "react-tap-event-plugin": "^1.0.0", 15 | "react-material-ui-keyboard": "^6.1.0" 16 | }, 17 | "devDependencies": { 18 | "html-webpack-plugin": "^2.22.0", 19 | "ts-loader": "^0.8.2", 20 | "typescript": "^1.8.10", 21 | "typings": "^1.3.3", 22 | "webpack": "^1.13.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/no_keyboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es5", 6 | "jsx": "react", 7 | "noImplicitAny": true 8 | }, 9 | "files": [ 10 | "typings/index.d.ts", 11 | "index.tsx" 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | 18 | -------------------------------------------------------------------------------- /example/no_keyboard/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalDependencies": { 3 | "node": "registry:dt/node", 4 | "react": "registry:dt/react", 5 | "react-dom": "registry:dt/react-dom", 6 | "react-tap-event-plugin": "registry:dt/react-tap-event-plugin", 7 | "material-ui": "registry:dt/material-ui" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/no_keyboard/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: [ 7 | "./index.tsx" 8 | ], 9 | 10 | output: { 11 | path: path.join(__dirname, "demo"), 12 | filename: "[name].js", 13 | publicPath: '' 14 | }, 15 | 16 | plugins: [ 17 | new HtmlWebpackPlugin({ title: "React Material-Ui Keyboard" }) 18 | ], 19 | 20 | resolve: { 21 | extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"] 22 | }, 23 | 24 | module: { 25 | loaders: [ 26 | { 27 | test: /\.tsx?$/, 28 | loaders: ["ts-loader"] 29 | } 30 | ], 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /example/simple_usage/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | npm install 3 | npm start 4 | // Open ./demo/index.html in your favorite browser 5 | ``` 6 | -------------------------------------------------------------------------------- /example/simple_usage/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import TextField from 'material-ui/TextField'; 3 | import { Keyboard, RequestCloseHandler, InputHandler } from 'react-material-ui-keyboard'; 4 | import { extendedKeyboard } from 'react-material-ui-keyboard/layouts'; 5 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 6 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 7 | import { MuiTheme } from 'material-ui/styles'; 8 | import { render as ReactDomRender } from 'react-dom'; 9 | import * as injectTapEventPlugin from 'react-tap-event-plugin'; 10 | 11 | const { div, link } = React.DOM; 12 | 13 | interface DemoState { 14 | value?: string; 15 | }; 16 | 17 | interface TextEnterTarget { 18 | value?: string; 19 | }; 20 | 21 | interface TextEnterEvent { 22 | target: TextEnterTarget; 23 | }; 24 | 25 | class Demo extends React.Component { 26 | private _onInput: InputHandler; 27 | 28 | private _handleInput(input: string): void { 29 | this.setState({ value: input }); 30 | } 31 | 32 | public constructor(props: void) { 33 | super(props); 34 | this.state = { value: '' }; 35 | this._onInput = this._handleInput.bind(this); 36 | } 37 | 38 | public render(): JSX.Element { 39 | const { state, _onInput } = this; 40 | const { value } = state; 41 | const textField: JSX.Element = ( 42 | 46 | ); 47 | 48 | return ( 49 |
50 | 51 | 57 |
58 | ); 59 | } 60 | }; 61 | 62 | injectTapEventPlugin(); 63 | let bootstrapNode = document.createElement('div'); 64 | ReactDomRender(, bootstrapNode); 65 | document.body.appendChild(bootstrapNode); 66 | -------------------------------------------------------------------------------- /example/simple_usage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./demo/index.html", 3 | "scripts": { 4 | "test": "echo \"Error: no test specified\" && exit 1", 5 | "postinstall": "typings install", 6 | "start": "webpack" 7 | }, 8 | "author": "Ivo Stratev", 9 | "license": "MIT", 10 | "dependencies": { 11 | "material-ui": "^0.15.4", 12 | "react": "^15.3.1", 13 | "react-dom": "^15.3.1", 14 | "react-tap-event-plugin": "^1.0.0", 15 | "react-material-ui-keyboard": "^6.1.0" 16 | }, 17 | "devDependencies": { 18 | "html-webpack-plugin": "^2.22.0", 19 | "ts-loader": "^0.8.2", 20 | "typescript": "^1.8.10", 21 | "typings": "^1.3.3", 22 | "webpack": "^1.13.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/simple_usage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es5", 6 | "jsx": "react", 7 | "noImplicitAny": true 8 | }, 9 | "files": [ 10 | "typings/index.d.ts", 11 | "index.tsx" 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | 18 | -------------------------------------------------------------------------------- /example/simple_usage/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalDependencies": { 3 | "node": "registry:dt/node", 4 | "react": "registry:dt/react", 5 | "react-dom": "registry:dt/react-dom", 6 | "react-tap-event-plugin": "registry:dt/react-tap-event-plugin", 7 | "material-ui": "registry:dt/material-ui" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/simple_usage/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: [ 7 | "./index.tsx" 8 | ], 9 | 10 | output: { 11 | path: path.join(__dirname, "demo"), 12 | filename: "[name].js", 13 | publicPath: '' 14 | }, 15 | 16 | plugins: [ 17 | new HtmlWebpackPlugin({ title: "React Material-Ui Keyboard" }) 18 | ], 19 | 20 | resolve: { 21 | extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"] 22 | }, 23 | 24 | module: { 25 | loaders: [ 26 | { 27 | test: /\.tsx?$/, 28 | loaders: ["ts-loader"] 29 | } 30 | ], 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-material-ui-keyboard", 3 | "version": "6.2.0", 4 | "description": "Virtual keyboard for TextField when needed.", 5 | "main": "index.js", 6 | "typings": "index.d.ts", 7 | "scripts": { 8 | "test": "tsc && jest --verbose", 9 | "jest-cov": "tsc --sourceMap -p . && jest --verbose --coverage --testPathPattern __tests__/.+.spec.js", 10 | "map-cov": "remap-istanbul -i ./coverage/coverage-final.json -t lcovonly -o ./lcov.info", 11 | "report-cov": "codeclimate-test-reporter < ./lcov.info", 12 | "coverage": "npm run jest-cov && npm run map-cov && npm run report-cov", 13 | "start": "webpack && node ./server.js", 14 | "npm": "tsc -d -p . && mv ./src/*.js . && mv ./src/*.d.ts .", 15 | "clean": "npm run clean:some && npm run clean:rest", 16 | "clean:some": "rm -f coverage ./src/*.map ./src/*.d.ts ./Dev/*.d.ts ./__tests__/*.d.ts", 17 | "clean:rest": "rm -f ./index* ./layouts* ./Keyboard* ./__tests__/*.js ./src/*.js ./Dev/*.js main.js lcov.info" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "material-ui", 22 | "keyboard", 23 | "on-screen-keyboard", 24 | "virtual-keyboard" 25 | ], 26 | "author": "Ivo Stratev", 27 | "license": "MIT", 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/NoHomey/react-material-ui-keyboard.git" 31 | }, 32 | "peerDependencies": { 33 | "material-ui": "^0.18.6", 34 | "react": "15.6.1", 35 | "react-dom": "15.6.1", 36 | "react-tap-event-plugin": "^2.0.1" 37 | }, 38 | "devDependencies": { 39 | "@types/deep-equal": "^1.0.0", 40 | "@types/enzyme": "^2.8.3", 41 | "@types/jest": "^20.0.4", 42 | "@types/material-ui": "^0.17.17", 43 | "@types/node": "^8.0.14", 44 | "@types/object-assign": "^4.0.30", 45 | "@types/prop-types": "^15.5.1", 46 | "@types/react": "^15.0.38", 47 | "@types/react-dom": "^15.5.1", 48 | "@types/react-tap-event-plugin": "^0.0.30", 49 | "codeclimate-test-reporter": "^0.5.0", 50 | "enzyme": "^2.9.1", 51 | "express": "^4.15.3", 52 | "html-webpack-plugin": "^2.29.0", 53 | "jest": "^20.0.4", 54 | "material-ui": "^0.18.6", 55 | "react": "15.6.1", 56 | "react-addons-test-utils": "15.6.0", 57 | "react-dom": "15.6.1", 58 | "react-hot-loader": "^1.3.1", 59 | "react-tap-event-plugin": "^2.0.1", 60 | "remap-istanbul": "^0.9.5", 61 | "ts-loader": "^2.3.1", 62 | "typescript": "^2.4.2", 63 | "webpack": "^2.3.3", 64 | "webpack-dev-middleware": "^1.11.0", 65 | "webpack-hot-middleware": "^2.18.2" 66 | }, 67 | "dependencies": { 68 | "bind-decorator": "^1.0.11", 69 | "deep-equal": "^1.0.1", 70 | "event-listener-service": "^1.0.1", 71 | "object-assign": "^4.1.1", 72 | "prop-types": "^15.5.10" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /screenshots/alphanumeric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/d4f1eb31bb5a6ff264055c4d9effcc88a1a30c6c/screenshots/alphanumeric.png -------------------------------------------------------------------------------- /screenshots/capsed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/d4f1eb31bb5a6ff264055c4d9effcc88a1a30c6c/screenshots/capsed.png -------------------------------------------------------------------------------- /screenshots/extended.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/d4f1eb31bb5a6ff264055c4d9effcc88a1a30c6c/screenshots/extended.png -------------------------------------------------------------------------------- /screenshots/iphone6_landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/d4f1eb31bb5a6ff264055c4d9effcc88a1a30c6c/screenshots/iphone6_landscape.png -------------------------------------------------------------------------------- /screenshots/iphone6_portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/d4f1eb31bb5a6ff264055c4d9effcc88a1a30c6c/screenshots/iphone6_portrait.png -------------------------------------------------------------------------------- /screenshots/keyboard_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/d4f1eb31bb5a6ff264055c4d9effcc88a1a30c6c/screenshots/keyboard_icon.png -------------------------------------------------------------------------------- /screenshots/numeric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/d4f1eb31bb5a6ff264055c4d9effcc88a1a30c6c/screenshots/numeric.png -------------------------------------------------------------------------------- /screenshots/screen_1200x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/d4f1eb31bb5a6ff264055c4d9effcc88a1a30c6c/screenshots/screen_1200x600.png -------------------------------------------------------------------------------- /screenshots/screen_200x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/d4f1eb31bb5a6ff264055c4d9effcc88a1a30c6c/screenshots/screen_200x100.png -------------------------------------------------------------------------------- /screenshots/screen_600x300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/d4f1eb31bb5a6ff264055c4d9effcc88a1a30c6c/screenshots/screen_600x300.png -------------------------------------------------------------------------------- /screenshots/show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/d4f1eb31bb5a6ff264055c4d9effcc88a1a30c6c/screenshots/show.png -------------------------------------------------------------------------------- /screenshots/size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/d4f1eb31bb5a6ff264055c4d9effcc88a1a30c6c/screenshots/size.png -------------------------------------------------------------------------------- /screenshots/textField.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoHomey/react-material-ui-keyboard/d4f1eb31bb5a6ff264055c4d9effcc88a1a30c6c/screenshots/textField.png -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config'); 5 | 6 | config.plugins.pop(); 7 | 8 | var app = express(); 9 | var compiler = webpack(config); 10 | 11 | app.use(require('webpack-dev-middleware')(compiler, { 12 | noInfo: true, 13 | publicPath: config.output.publicPath 14 | })); 15 | 16 | app.use(require('webpack-hot-middleware')(compiler)); 17 | 18 | app.get('*', function (req, res) { 19 | res.sendFile(path.join(__dirname, 'index.html')); 20 | }); 21 | 22 | app.listen(3000, function (err) { 23 | if (err) { 24 | console.log(err); 25 | return; 26 | } 27 | 28 | console.log('Listening at http://localhost:3000'); 29 | }); -------------------------------------------------------------------------------- /src/ActiveElement.ts: -------------------------------------------------------------------------------- 1 | export class ActiveElement { 2 | public static isInput: () => boolean; 3 | public static blur: () => void; 4 | } 5 | 6 | export default ActiveElement; -------------------------------------------------------------------------------- /src/Keyboard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as PropTypes from 'prop-types'; 3 | import bind from 'bind-decorator'; 4 | import Dialog from 'material-ui/Dialog'; 5 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 6 | import { KeyboardKey, KeyboardKeyProps } from './KeyboardKey'; 7 | import { KeyboardLayout, kyeboardCapsLockLayout } from './layouts'; 8 | import { MuiTheme } from 'material-ui/styles'; 9 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 10 | import EventListenerService from 'event-listener-service'; 11 | import ActiveElement from './ActiveElement'; 12 | import objectAssign = require('object-assign'); 13 | import deepEqual = require('deep-equal'); 14 | 15 | export { KeyboardLayout }; 16 | 17 | export type RequestCloseHandler = () => void; 18 | 19 | export type InputHandler = (input: string) => void; 20 | 21 | export interface TextFieldRequiredProps { 22 | style?: React.CSSProperties; 23 | readOnly: boolean; 24 | value: string; 25 | onFocus?: React.FocusEventHandler; 26 | } 27 | 28 | export interface TextFieldAccessedProps extends TextFieldRequiredProps { 29 | rows?: number; 30 | floatingLabelText?: string; 31 | } 32 | 33 | export type TextFieldElement = React.ReactElement; 34 | 35 | export type CreatableTextField = React.ComponentClass; 36 | 37 | export type KeyboardRow = React.ReactElement; 38 | 39 | export type KeyboardRowKey = React.ReactElement; 40 | 41 | export interface KeyboardProps { 42 | open?: boolean; 43 | automatic?: boolean; 44 | layouts: Array; 45 | keyboardKeyWidth?: number; 46 | keyboardKeyHeight?: number; 47 | keyboardKeySymbolSize?: number; 48 | textField: TextFieldElement; 49 | onRequestClose?: RequestCloseHandler; 50 | onInput?: InputHandler; 51 | onInputValueChange?: InputHandler; 52 | correctorName?: string; 53 | corrector?: Function; 54 | disableEffects?: boolean; 55 | } 56 | 57 | export interface KeyboardState { 58 | value?: string; 59 | layout?: number; 60 | capsLock?: boolean; 61 | open?: boolean; 62 | }; 63 | 64 | export interface KeyboardContext { 65 | muiTheme?: MuiTheme; 66 | } 67 | 68 | export type AutomaitcOpenPredicate = () => boolean; 69 | 70 | namespace constants { 71 | export const minusOne: number = -1; 72 | export const zero: number = 0; 73 | export const one: number = 1; 74 | export const two: number = 2; 75 | export const three: number = 3; 76 | export const four: number = 4; 77 | export const sixteen: number = 16; 78 | export const twentyFour: number = 24; 79 | export const fourtyEight: number = 48; 80 | export const seventyTwo: number = 72; 81 | export const emptyString: string = ''; 82 | export const space: string = ' '; 83 | export const keydown: string = 'keydown'; 84 | export const resize: string = 'resize'; 85 | export const input: string = 'input'; 86 | export const fullWidth: string = '100%'; 87 | export const typeofString: string = 'string'; 88 | export const typeofNumber: string = 'number'; 89 | export const typeofFunction: string = 'function'; 90 | export const boolTrue: boolean = true; 91 | export const boolFalse: boolean = false; 92 | export const isSpaceBar: RegExp = /^\ +$/; 93 | export const strictCompare: { strict: boolean } = { strict: boolTrue }; 94 | } 95 | 96 | function allwaysTruePredicate(): boolean { 97 | return constants.boolTrue; 98 | } 99 | 100 | EventListenerService.setImplementation({ 101 | addListener: window.addEventListener.bind(window), 102 | removeListener: window.removeEventListener.bind(window) 103 | }); 104 | 105 | ActiveElement.isInput = () => document.activeElement.tagName.toLowerCase() === constants.input; 106 | ActiveElement.blur = () => (document.activeElement as HTMLElement).blur(); 107 | 108 | export class Keyboard extends React.Component { 109 | private static calculatedTextFieldHeight(props: TextFieldAccessedProps): number { 110 | const { rows, floatingLabelText } = props; 111 | const normalHeight: number = floatingLabelText ? constants.seventyTwo : constants.fourtyEight; 112 | return (rows ? ((rows - constants.one) * constants.twentyFour) : constants.zero) + normalHeight; 113 | } 114 | 115 | public static getSupportedSpecialKeys(): Array { 116 | return Keyboard.supportedSpecialKeys; 117 | } 118 | 119 | private static supportedSpecialKeys: Array = ['Enter', 'Backspace', 'Escape', 'CapsLock', 'Keyboard']; 120 | private static overwrittenProps: Array = ['onChange', 'onFocus', 'onBlur', 'onKeyUp', 'onKeyDown', 'onKeyPress']; 121 | private static noStyleHeight: React.CSSProperties = { 122 | minHeight: constants.zero, 123 | height: constants.zero, 124 | maxHeight: constants.zero 125 | }; 126 | public static propTypes: React.ValidationMap = { 127 | open: PropTypes.bool, 128 | automatic: PropTypes.bool, 129 | layouts: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string))).isRequired, 130 | keyboardKeyWidth: PropTypes.number, 131 | keyboardKeyHeight: PropTypes.number, 132 | keyboardKeySymbolSize: PropTypes.number, 133 | textField: PropTypes.element.isRequired, 134 | onRequestClose: PropTypes.func, 135 | onInput: PropTypes.func, 136 | onInputValueChange: PropTypes.func, 137 | correctorName: PropTypes.string, 138 | corrector: PropTypes.func, 139 | disableEffects: PropTypes.bool 140 | }; 141 | public static contextTypes: any = { muiTheme: PropTypes.object }; 142 | public static automaitcOpenPredicate: AutomaitcOpenPredicate = allwaysTruePredicate; 143 | public context: KeyboardContext; 144 | private corrector: Function; 145 | 146 | @bind 147 | private onInputValueChange(): void { 148 | if(typeof this.props.onInputValueChange === constants.typeofFunction) { 149 | this.props.onInputValueChange!(this.state.value!); 150 | } 151 | } 152 | 153 | private setValue(value: string): void { 154 | if(this.state.value !== value) { 155 | this.setState({ value: value }, this.onInputValueChange); 156 | } 157 | } 158 | 159 | private syncValue(value: string): void { 160 | if(value !== this.state.value) { 161 | this.setValue(value); 162 | } 163 | } 164 | 165 | private setAutomaitcOpen(open: boolean): void { 166 | this.setState({ open: open }); 167 | } 168 | 169 | private requestClose(): void { 170 | const { automatic, onRequestClose } = this.props; 171 | if(automatic) { 172 | this.setAutomaitcOpen(constants.boolFalse); 173 | } else if(onRequestClose) { 174 | onRequestClose(); 175 | } 176 | } 177 | 178 | @bind 179 | private onFocus(): void { 180 | if(Keyboard.automaitcOpenPredicate()) { 181 | this.setAutomaitcOpen(constants.boolTrue); 182 | } 183 | } 184 | 185 | @bind 186 | private onKeyboard(key: string): void { 187 | const { supportedSpecialKeys } = Keyboard; 188 | const { props, state } = this; 189 | const { onInput, layouts: propsLayout } = props; 190 | const { capsLock, layout } = state; 191 | const value: string = state.value!; 192 | switch(key) { 193 | case supportedSpecialKeys[constants.zero]: 194 | if(onInput) { 195 | onInput(value); 196 | } 197 | return this.requestClose(); 198 | case supportedSpecialKeys[constants.one]: 199 | return this.setValue(value.substring(constants.zero, value.length - constants.one)); 200 | case supportedSpecialKeys[constants.two]: 201 | return this.requestClose(); 202 | case supportedSpecialKeys[constants.three]: return this.setState({ capsLock: !capsLock }); 203 | case supportedSpecialKeys[constants.four]: 204 | return this.setState({ 205 | layout: 206 | (layout === (propsLayout.length - constants.one)) 207 | ? constants.zero : layout! + constants.one 208 | }); 209 | default: return this.setValue(value + key); 210 | } 211 | } 212 | 213 | @bind 214 | private onKeyDown(event: KeyboardEvent): void { 215 | const { key } = event; 216 | event.stopImmediatePropagation(); 217 | event.stopPropagation(); 218 | if((key.length === constants.one) || (Keyboard.getSupportedSpecialKeys().indexOf(key) !== constants.minusOne)) { 219 | event.preventDefault(); 220 | this.onKeyboard(key); 221 | } 222 | } 223 | 224 | @bind 225 | private onResize(): void { 226 | this.forceUpdate(); 227 | } 228 | 229 | public makeCorrection(value: string): void { 230 | this.setValue(value); 231 | } 232 | 233 | public constructor(props: KeyboardProps, context: KeyboardContext) { 234 | super(props, context); 235 | this.state = { 236 | value: constants.emptyString, 237 | layout: constants.zero, 238 | capsLock: constants.boolFalse, 239 | open: constants.boolFalse 240 | }; 241 | this.context = context; 242 | } 243 | 244 | public componentWillMount(): void { 245 | const { corrector } = this.props; 246 | if(typeof corrector === constants.typeofFunction) { 247 | this.corrector = corrector!.bind(this); 248 | } 249 | } 250 | 251 | public componentDidMount(): void { 252 | EventListenerService.addListener(constants.resize, this.onResize, constants.boolFalse); 253 | this.syncValue(this.props.textField.props.value); 254 | } 255 | 256 | public shouldComponentUpdate(props: KeyboardProps, state: KeyboardState): boolean { 257 | const { textField } = props; 258 | const { textField: thisTextField } = this.props; 259 | if(this.state.value !== state.value) { 260 | return constants.boolTrue; 261 | } 262 | if(this.state.open !== state.open) { 263 | return constants.boolTrue; 264 | } 265 | if(this.state.capsLock !== state.capsLock) { 266 | return constants.boolTrue; 267 | } 268 | if(this.state.layout !== state.layout) { 269 | return constants.boolTrue; 270 | } 271 | if(this.props.open !== props.open) { 272 | return constants.boolTrue; 273 | } 274 | if(this.props.keyboardKeyHeight !== props.keyboardKeyHeight) { 275 | return constants.boolTrue; 276 | } 277 | if(this.props.keyboardKeySymbolSize !== props.keyboardKeySymbolSize) { 278 | return constants.boolTrue; 279 | } 280 | if(this.props.keyboardKeyWidth !== props.keyboardKeyWidth) { 281 | return constants.boolTrue; 282 | } 283 | if(this.props.automatic !== props.automatic) { 284 | return constants.boolTrue; 285 | } 286 | if(this.props.disableEffects !== props.disableEffects) { 287 | return constants.boolTrue; 288 | } 289 | if(this.props.correctorName !== props.correctorName) { 290 | return constants.boolTrue; 291 | } 292 | if(this.props.corrector !== props.corrector) { 293 | return constants.boolTrue; 294 | } 295 | if(this.props.onInput !== props.onInput) { 296 | return constants.boolTrue; 297 | } 298 | if(this.props.onRequestClose !== props.onRequestClose) { 299 | return constants.boolTrue; 300 | } 301 | if(thisTextField.type !== textField.type) { 302 | return constants.boolTrue; 303 | } 304 | if(!deepEqual(this.props.layouts, props.layouts, constants.strictCompare)) { 305 | return constants.boolTrue; 306 | } 307 | if(!deepEqual(thisTextField.props, textField.props, constants.strictCompare)) { 308 | return constants.boolTrue; 309 | } 310 | 311 | return constants.boolFalse; 312 | } 313 | 314 | public componentWillReceiveProps(props: KeyboardProps): void { 315 | this.syncValue(props.textField.props.value); 316 | if(this.props.corrector !== props.corrector) { 317 | this.corrector = props.corrector!.bind(this); 318 | } 319 | } 320 | 321 | public componentDidUpdate(props: KeyboardProps, state: KeyboardState): void { 322 | const { automatic } = this.props; 323 | const open: boolean = automatic ? this.state.open! : this.props.open!; 324 | const prev: boolean = automatic ? state.open! : props.open!; 325 | if(open !== prev) { 326 | if(open) { 327 | if(ActiveElement.isInput()) { 328 | ActiveElement.blur(); 329 | } 330 | EventListenerService.addListener(constants.keydown, this.onKeyDown, constants.boolTrue); 331 | } else { 332 | EventListenerService.removeListener(constants.keydown, this.onKeyDown, constants.boolTrue); 333 | } 334 | } 335 | } 336 | 337 | public componentWillUnmount(): void { 338 | EventListenerService.removeListener(constants.resize, this.onResize, constants.boolFalse); 339 | } 340 | 341 | public render(): JSX.Element { 342 | const { props, state, context } = this; 343 | const { textField, layouts, keyboardKeyHeight, keyboardKeyWidth, keyboardKeySymbolSize, automatic, correctorName, disableEffects } = props; 344 | const { value, layout: stateLayout, capsLock } = state; 345 | const { muiTheme } = context; 346 | const open: boolean = automatic ? state.open! : (props.open ? constants.boolTrue : constants.boolFalse); 347 | const theme: MuiTheme = muiTheme ? muiTheme : getMuiTheme(); 348 | const styles: React.CSSProperties = textField.props.style!; 349 | let keyboardFieldProps: any = objectAssign({}, textField.props); 350 | let inputTextFieldProps: TextFieldAccessedProps = objectAssign({}, textField.props, { readOnly: open }); 351 | if(automatic || open) { 352 | inputTextFieldProps.onFocus = automatic ? this.onFocus : undefined; 353 | } 354 | keyboardFieldProps.style = objectAssign({}, styles); 355 | keyboardFieldProps.style.minWidth = constants.fullWidth; 356 | keyboardFieldProps.style.width = constants.fullWidth; 357 | keyboardFieldProps.style.maxWidth = constants.fullWidth; 358 | keyboardFieldProps.readOnly = constants.boolTrue; 359 | keyboardFieldProps.value = value; 360 | if(typeof correctorName === constants.typeofString) { 361 | keyboardFieldProps[correctorName!] = this.corrector; 362 | } 363 | const overwrittenProps: Array = Keyboard.overwrittenProps; 364 | const { length: overwrittenPropsLength } = overwrittenProps; 365 | let propIndex: number; 366 | let prop: string; 367 | for(propIndex = constants.zero; propIndex < overwrittenPropsLength; ++propIndex) { 368 | prop = overwrittenProps[propIndex]; 369 | if(keyboardFieldProps.hasOwnProperty(prop)) { 370 | keyboardFieldProps[prop] = undefined; 371 | } 372 | } 373 | const inputTextField: TextFieldElement = React.cloneElement(textField, inputTextFieldProps); 374 | const keyboardTextField: TextFieldElement = React.createElement(textField.type as CreatableTextField, keyboardFieldProps); 375 | const keyboardLayout: KeyboardLayout = kyeboardCapsLockLayout(layouts[stateLayout!], capsLock!); 376 | const keyboardRowLength: number = keyboardLayout.length; 377 | const keyboardRowLengths: Array = []; 378 | let rowIndex: number; 379 | let keyIndex: number; 380 | let spacebar: number; 381 | let rowLength: number; 382 | let row: Array; 383 | let key: string; 384 | for(rowIndex = constants.zero; rowIndex < keyboardRowLength; ++rowIndex) { 385 | spacebar = constants.one; 386 | row = keyboardLayout[rowIndex]; 387 | rowLength = row.length; 388 | for(keyIndex = constants.zero; keyIndex < rowLength; ++keyIndex) { 389 | key = row[keyIndex]; 390 | if(key.match(constants.isSpaceBar)) { 391 | spacebar = key.length; 392 | } 393 | } 394 | keyboardRowLengths.push(rowLength + spacebar - constants.one); 395 | } 396 | const maxKeyboardRowLength: number = Math.max(...keyboardRowLengths); 397 | let keyHeight: number = typeof keyboardKeyHeight === constants.typeofNumber ? keyboardKeyHeight! : theme.button!.height!; 398 | let keyWidth: number = typeof keyboardKeyWidth === constants.typeofNumber ? keyboardKeyWidth! : theme.button!.minWidth!; 399 | let keySymbolSize: number = typeof keyboardKeySymbolSize === constants.typeofNumber ? keyboardKeySymbolSize! : theme.flatButton!.fontSize!; 400 | const { desktopGutter, desktopKeylineIncrement } = theme.baseTheme!.spacing!; 401 | const { innerHeight, innerWidth } = window; 402 | const { minHeight, height, maxHeight } = (styles ? styles : Keyboard.noStyleHeight); 403 | const dialogGutter: number = constants.two * desktopGutter!; 404 | const styleHeight: number = minHeight ? minHeight : (height ? height : (maxHeight ? maxHeight : constants.zero)); 405 | const textFieldHeight: number = styleHeight > constants.zero ? styleHeight : Keyboard.calculatedTextFieldHeight(inputTextFieldProps); 406 | let transformTop: number = desktopKeylineIncrement!; 407 | let dialogWidth: number = (maxKeyboardRowLength * keyWidth) + dialogGutter; 408 | let dialogHeight: number = (keyboardRowLength * keyHeight) + textFieldHeight + dialogGutter; 409 | const maxDialogHeight: number = innerHeight - constants.sixteen; 410 | const dialogSpacingTop: number = maxDialogHeight - dialogHeight; 411 | const overwriteWidth: boolean = dialogWidth > innerWidth; 412 | const overwriteHeight: boolean = dialogSpacingTop < transformTop; 413 | if(overwriteWidth || overwriteHeight) { 414 | if(overwriteWidth) { 415 | dialogWidth = innerWidth; 416 | keyWidth = (innerWidth - dialogGutter) / maxKeyboardRowLength; 417 | } 418 | if(overwriteHeight) { 419 | if(dialogSpacingTop >= constants.zero) { 420 | transformTop = dialogSpacingTop; 421 | } else { 422 | transformTop = constants.zero; 423 | dialogHeight = maxDialogHeight; 424 | keyHeight = (dialogHeight - textFieldHeight - dialogGutter) / keyboardRowLength; 425 | } 426 | } 427 | const smallerSymbolSize: number = (keyHeight < keyWidth ? keyHeight : keyWidth) - constants.four; 428 | if(smallerSymbolSize < keySymbolSize) { 429 | keySymbolSize = smallerSymbolSize; 430 | } 431 | } 432 | const dialogContentStyle: React.CSSProperties = { 433 | width: dialogWidth, 434 | maxWidth: innerWidth, 435 | height: dialogHeight, 436 | maxHeight: maxDialogHeight, 437 | transform: `translate(0, ${transformTop}px)` 438 | }; 439 | let keyboardRows: Array = []; 440 | let keyboardRowKeys: Array; 441 | let notSpacebar: boolean; 442 | for(let rowIndex: number = constants.zero; rowIndex < keyboardRowLength; ++rowIndex) { 443 | keyboardRowKeys = []; 444 | row = keyboardLayout[rowIndex]; 445 | rowLength = row.length; 446 | for(keyIndex = constants.zero; keyIndex < rowLength; ++keyIndex) { 447 | key = row[keyIndex]; 448 | notSpacebar = key.match(constants.isSpaceBar) === null; 449 | keyboardRowKeys.push( 450 | 459 | ); 460 | } 461 | keyboardRows.push(
{keyboardRowKeys}
); 462 | } 463 | const keyboard: JSX.Element = ( 464 |
465 | {inputTextField} 466 | 471 |
472 | {keyboardTextField} 473 | {keyboardRows} 474 |
475 |
476 |
477 | ); 478 | return muiTheme ? keyboard : {keyboard}; 479 | } 480 | }; 481 | 482 | export default Keyboard; -------------------------------------------------------------------------------- /src/KeyboardKey.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as PropTypes from 'prop-types'; 3 | import bind from 'bind-decorator'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import Backspace from 'material-ui/svg-icons/content/backspace'; 6 | import Enter from 'material-ui/svg-icons/hardware/keyboard-return'; 7 | import Escape from 'material-ui/svg-icons/action/exit-to-app'; 8 | import Keyboard from 'material-ui/svg-icons/hardware/keyboard'; 9 | import CapsLock from 'material-ui/svg-icons/hardware/keyboard-capslock'; 10 | import Spacebar from 'material-ui/svg-icons/editor/space-bar'; 11 | import Warning from 'material-ui/svg-icons/alert/warning'; 12 | import { MuiTheme } from 'material-ui/styles'; 13 | 14 | export type KeyboardKeyPressHandler = (key: string) => void; 15 | 16 | export interface KeyboardKeyProps { 17 | keyboardKey: string; 18 | onKeyPress: KeyboardKeyPressHandler; 19 | keyboardKeyWidth: number; 20 | keyboardKeyHeight: number; 21 | keyboardKeySymbolSize: number; 22 | disableEffects: boolean; 23 | } 24 | 25 | export interface KeyboardKeyContext { 26 | muiTheme: MuiTheme; 27 | } 28 | 29 | interface SpecialIcons { 30 | [index: string]: React.ComponentClass; 31 | } 32 | 33 | namespace constants { 34 | export const one: number = 1; 35 | export const spacebar: string = ' '; 36 | export const none: string = 'none'; 37 | export const notFound: string = 'notFound'; 38 | export const boolTrue: boolean = true; 39 | export const boolFalse: boolean = false; 40 | } 41 | 42 | export class KeyboardKey extends React.Component { 43 | public context: KeyboardKeyContext; 44 | 45 | private static specialIcons: SpecialIcons = { 46 | 'Enter': Enter, 47 | 'Backspace': Backspace, 48 | 'Escape': Escape, 49 | 'CapsLock': CapsLock, 50 | 'Keyboard': Keyboard, 51 | ' ' : Spacebar, 52 | 'notFound': Warning 53 | }; 54 | 55 | public static propTypes: React.ValidationMap = { 56 | keyboardKey: PropTypes.string.isRequired, 57 | onKeyPress: PropTypes.func.isRequired, 58 | keyboardKeyWidth: PropTypes.number.isRequired, 59 | keyboardKeyHeight: PropTypes.number.isRequired, 60 | keyboardKeySymbolSize: PropTypes.number.isRequired, 61 | }; 62 | public static contextTypes: any = { muiTheme: PropTypes.object.isRequired }; 63 | 64 | @bind 65 | private onTouchTap(): void { 66 | const { onKeyPress, keyboardKey } = this.props; 67 | if((keyboardKey.length === 1) || KeyboardKey.specialIcons.hasOwnProperty(keyboardKey)) { 68 | onKeyPress(keyboardKey); 69 | } 70 | } 71 | 72 | public constructor(props: KeyboardKeyProps, context: KeyboardKeyContext) { 73 | super(props); 74 | this.context = context; 75 | } 76 | 77 | public shouldComponentUpdate(props: KeyboardKeyProps): boolean { 78 | if(this.props.keyboardKey !== props.keyboardKey) { 79 | return constants.boolTrue; 80 | } 81 | if(this.props.keyboardKeyHeight !== props.keyboardKeyHeight) { 82 | return constants.boolTrue; 83 | } 84 | if(this.props.keyboardKeySymbolSize !== props.keyboardKeySymbolSize) { 85 | return constants.boolTrue; 86 | } 87 | if(this.props.keyboardKeyWidth !== props.keyboardKeyWidth) { 88 | return constants.boolTrue; 89 | } 90 | if(this.props.onKeyPress !== props.onKeyPress) { 91 | return constants.boolTrue; 92 | } 93 | if(this.props.disableEffects !== props.disableEffects) { 94 | return constants.boolTrue; 95 | } 96 | 97 | return constants.boolFalse; 98 | } 99 | 100 | public render(): JSX.Element { 101 | const { keyboardKey: key, keyboardKeyHeight: height, keyboardKeyWidth: width, keyboardKeySymbolSize: size, disableEffects } = this.props; 102 | let flatButtonProps: any = { 103 | style: { 104 | height: height, 105 | width: width, 106 | minWidth: width 107 | }, 108 | primary: constants.boolTrue, 109 | onTouchTap: this.onTouchTap, 110 | disableFocusRipple: disableEffects, 111 | disableKeyboardFocus: disableEffects, 112 | disableTouchRipple: disableEffects 113 | }; 114 | if(disableEffects) { 115 | flatButtonProps.hoverColor = this.context.muiTheme.flatButton!.color; 116 | } 117 | if((key.length <= constants.one) && (key !== constants.spacebar)) { 118 | if(key.length) { 119 | flatButtonProps.label = key; 120 | } else { 121 | flatButtonProps.disabled = constants.boolTrue; 122 | flatButtonProps.label = constants.spacebar; 123 | } 124 | flatButtonProps.labelStyle = { fontSize: size, textTransform: constants.none }; 125 | } else { 126 | const { specialIcons } = KeyboardKey; 127 | const icon: React.ComponentClass = specialIcons[specialIcons.hasOwnProperty(key) ? key : constants.notFound]; 128 | flatButtonProps.icon = React.createElement(icon, { style: { width: size, height: size } }); 129 | } 130 | return React.createElement(FlatButton, flatButtonProps); 131 | } 132 | }; 133 | 134 | export default KeyboardKey; -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import Keyboard from './Keyboard'; 2 | export * from './Keyboard'; 3 | export default Keyboard; -------------------------------------------------------------------------------- /src/layouts.ts: -------------------------------------------------------------------------------- 1 | export type KeyboardLayout = Array>; 2 | 3 | export const alphaNumericKeyboard: KeyboardLayout = [ 4 | ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], 5 | ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'], 6 | ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'Backspace'], 7 | ['Escape', 'CapsLock', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'Enter'] 8 | ]; 9 | 10 | export const extendedKeyboard: KeyboardLayout = [ 11 | ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], 12 | ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'], 13 | ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'Backspace'], 14 | ['CapsLock', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '-', 'CapsLock'], 15 | ['Escape', '@', '#', ' ', '.', 'Enter'] 16 | ]; 17 | 18 | export const numericKeyboard: KeyboardLayout = [ 19 | ['Escape', '-', 'Backspace'], 20 | ['7', '8', '9'], 21 | ['4', '5', '6'], 22 | ['1', '2', '3'], 23 | ['0', '.', 'Enter'] 24 | ]; 25 | 26 | export function kyeboardCapsLockLayout(layout: KeyboardLayout, caps: boolean): KeyboardLayout { 27 | return layout.map((row: Array): Array => { 28 | return row.map((key: string): string => { 29 | return (key.length === 1) ? (caps ? key.toUpperCase() : key.toLowerCase()) : key; 30 | }); 31 | }); 32 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": true, 6 | "strictNullChecks": true, 7 | "noImplicitThis": true, 8 | "noUnusedParameters": true, 9 | "noUnusedLocals": true, 10 | "experimentalDecorators": true, 11 | "lib": ["es5", "es6", "dom"], 12 | "jsx": "react" 13 | }, 14 | "include": [ 15 | "src/**/*", 16 | "Dev/**/*", 17 | "__tests__/**/*" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | "./Dev/index.tsx" 9 | ], 10 | 11 | output: { 12 | path: __dirname, 13 | filename: "[name].js", 14 | publicPath: '' 15 | }, 16 | 17 | plugins: [ 18 | new webpack.HotModuleReplacementPlugin(), 19 | new HtmlWebpackPlugin({ title: "React Material-Ui Keyboard" }) 20 | ], 21 | 22 | resolve: { 23 | extensions: [".ts", ".tsx", ".js"] 24 | }, 25 | 26 | module: { 27 | loaders: [ 28 | { 29 | test: /\.tsx?$/, 30 | loaders: ["ts-loader"] 31 | } 32 | ], 33 | } 34 | }; 35 | --------------------------------------------------------------------------------