Skip to content

Commit

Permalink
add fallback process for old browsers/mobile, replace classes by css …
Browse files Browse the repository at this point in the history
…pseudo-classes
  • Loading branch information
soywod committed Mar 14, 2021
1 parent b48c653 commit feedeb0
Show file tree
Hide file tree
Showing 13 changed files with 445 additions and 223 deletions.
7 changes: 4 additions & 3 deletions package.json
Expand Up @@ -27,6 +27,7 @@
"lib/pin-field/pin-field.{types.js|types.js.map|types.d.ts|js|js.map|d.ts}",
"lib/mvu/mvu.{types.js|types.js.map|types.d.ts|js|js.map|d.ts}",
"lib/kb-event/kb-event.{types.js|types.js.map|types.d.ts|js|js.map|d.ts}",
"lib/utils/utils.{js|js.map|d.ts}",
"lib/**/index.{js|js.map|d.ts}",
"lib/index.{js|js.map|d.ts}"
],
Expand Down Expand Up @@ -73,9 +74,6 @@
"react": "^16.8 || ^17",
"react-dom": "^16.8 || ^17"
},
"dependencies": {
"classnames": "^2.2.6"
},
"scripts": {
"start": "parcel serve -d lib -p 3000 src/_demo/index.html",
"build": "tsc",
Expand All @@ -84,5 +82,8 @@
"test:unit": "jest",
"test:e2e": "cypress run",
"test": "run-p test:unit test:e2e"
},
"dependencies": {
"classnames": "^2.2.6"
}
}
56 changes: 30 additions & 26 deletions src/_demo/script.tsx
@@ -1,6 +1,6 @@
import React, {FC, useRef, useState} from "react";
import ReactDOM from "react-dom";
import classNames from "classnames";
import cn from "classnames";

import PinField from "..";

Expand Down Expand Up @@ -47,9 +47,9 @@ const App: FC = () => {
</a>
<kbd>$ yarn add react-pin-field</kbd>
</div>
<div className="container-a">
<div className="pin-field-container">
<PinField
className={classNames("field-a", {"field-a-complete": demoCompleted})}
className={cn("pin-field", {complete: demoCompleted})}
onComplete={() => setDemoCompleted(true)}
format={k => k.toUpperCase()}
autoFocus
Expand All @@ -61,11 +61,15 @@ const App: FC = () => {

<div className="container mb-5">
<h2 className="display-5 mb-4">Default</h2>
<PinField data-cy="pin-field" />
<div>
<PinField data-cy="pin-field" />
</div>

<h2 className="display-5 mt-4">With ref</h2>
<p className="mb-4 text-muted">You can control each inputs with the pin field ref:</p>
<PinField ref={ref} />
<div>
<PinField ref={ref} />
</div>
<div>
<button onClick={() => ref && ref.current && ref.current[1].focus()}>Focus 2nd input</button>
<button onClick={() => ref && ref.current && ref.current.forEach(input => (input.value = ""))}>
Expand All @@ -75,7 +79,7 @@ const App: FC = () => {

<h2 className="display-5 mt-5">With custom style</h2>
<p className="mb-4 text-muted">
React pin field follows the <a href="https://css-tricks.com/abem-useful-adaptation-bem/">ABEM</a> convention.
React PIN Field follows the <a href="https://css-tricks.com/abem-useful-adaptation-bem/">ABEM</a> convention.
Each input has a class named <code>a-reactPinField__input</code>, plus:
</p>
<ul>
Expand All @@ -93,28 +97,24 @@ const App: FC = () => {
<code>-error</code> when a key is rejected
</li>
</ul>
<PinField
style={{
width: 50,
height: 50,
borderRadius: "50%",
border: "1px solid gray",
outline: "none",
textAlign: "center",
margin: 10,
}}
/>
<div>
<PinField className="pin-field" />
</div>

<h2 className="display-5 mt-5">With custom length</h2>
<p className="mb-4 text-muted">You can set the number of chars with the length prop. Default set to 5 chars.</p>
<PinField className="field-a" length={3} />
<div>
<PinField className="pin-field" length={3} />
</div>

<h2 className="display-5 mt-5">With custom validation</h2>
<p className="mb-4 text-muted">
You can restrict input with a string of allowed chars, or a regex, or a function.
</p>
<p>Only numbers:</p>
<PinField className="field-a" validate="0123456789" />
<div>
<PinField className="pin-field" validate="0123456789" inputMode="numeric" />
</div>

<h2 className="display-5 mt-5">With custom events</h2>
<ul className="mb-4 text-muted">
Expand All @@ -123,18 +123,22 @@ const App: FC = () => {
<li>onResolveKey: when receive a good key</li>
<li>onRejectKey: when receive a bad key</li>
</ul>
<PinField
className="field-a"
onChange={setCode}
onComplete={() => setCompleted(true)}
format={k => k.toUpperCase()}
/>
<div>
<PinField
className="pin-field"
onChange={setCode}
onComplete={() => setCompleted(true)}
format={k => k.toUpperCase()}
/>
</div>
<div>Current code: {code}</div>
<div>Completed: {String(completed)}</div>

<h2 className="display-5 mt-5">With custom InputHTMLAttributes</h2>
<p className="mb-4 text-muted">Props inherit from InputHTMLAttributes. For eg. with a password type prop:</p>
<PinField className="field-a" type="password" />
<div>
<PinField className="pin-field" type="password" />
</div>
</div>
</>
);
Expand Down
83 changes: 59 additions & 24 deletions src/_demo/styles.scss
Expand Up @@ -2,48 +2,83 @@ $black: #333333;
$blue: #686de0;
$gray: #d3d3d3;
$green: #6ab04c;
$red: #dc3545;
$white: #ffffff;

code {
padding: 5px 10px;
display: inline-block;
padding: 0.25rem 0.5rem;
}

.container-a {
.pin-field-container {
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
grid-column-gap: 10px;
grid-auto-flow: column;
justify-content: center;
margin: 64px 0;
margin: 4rem 0;
}

.field-a {
width: 75px;
height: 75px;
font-size: 40px;
text-align: center;
outline: none;
border-radius: 5px;
.pin-field {
border: 1px solid $gray;
transition-property: color, border, box-shadow, transform;
border-right: none;
font-size: 2rem;
height: 4rem;
outline: none;
text-align: center;
transition-duration: 250ms;
transition-property: color, border, box-shadow, transform;
width: 4rem;

&:first-of-type {
border-radius: 0.5rem 0 0 0.5rem;
}

&:last-of-type {
border-radius: 0 0.5rem 0.5rem 0;
border-right: 1px solid $gray;
}

&:focus {
border-color: $blue;
box-shadow: 0 0 0.25rem rgba($blue, 0.5);
outline: none;
box-shadow: 0 0 7px rgba($blue, 0.5);
border: 1px solid $blue;
transform: scale(1.05);

& + .pin-field {
border-left-color: $blue;
}
}

&:invalid {
animation: shake 5 linear 75ms;
border-color: $red;
box-shadow: 0 0 0.25rem rgba($red, 0.5);

& + .pin-field {
border-left-color: $red;
}
}
}

.field-a-complete {
border: 1px solid $green;
color: $green;
&.complete {
border-color: $green;
color: $green;

&[disabled] {
background: rgba($green, 0.1);
opacity: 0.5;
cursor: not-allowed;
&[disabled] {
background: rgba($green, 0.1);
cursor: not-allowed;
opacity: 0.5;
}

& + .pin-field {
border-left-color: $green;
}
}
}

@keyframes shake {
from {
transform: scale(1.05) translateX(-3%);
}
to {
transform: scale(1.05) translateX(3%);
}
}
13 changes: 1 addition & 12 deletions src/kb-event/kb-event.test.ts
@@ -1,4 +1,4 @@
import {getKeyFromKeyboardEvent, getKeyFromInputEvent} from "./kb-event";
import {getKeyFromKeyboardEvent} from "./kb-event";

test("getKeyFromKeyboardEvent", () => {
const cases = [
Expand All @@ -13,14 +13,3 @@ test("getKeyFromKeyboardEvent", () => {
expect(getKeyFromKeyboardEvent(evt)).toEqual(expected);
});
});

test("getKeyFromInputEvent", () => {
const cases = [
[{}, "Unidentified"],
[{data: "a"}, "a"],
];

cases.forEach(([evt, expected]) => {
expect(getKeyFromInputEvent(evt as InputEvent)).toEqual(expected);
});
});
4 changes: 0 additions & 4 deletions src/kb-event/kb-event.ts
Expand Up @@ -107,7 +107,3 @@ export function getKeyFromKeyboardEvent(evt: KeyboardEvent) {

return key;
}

export function getKeyFromInputEvent(evt: InputEvent) {
return evt.data || "Unidentified";
}
41 changes: 29 additions & 12 deletions src/pin-field/pin-field.spec.tsx
@@ -1,27 +1,26 @@
import React from "react";
import enzyme, {shallow, mount} from "enzyme";
import noop from "lodash/fp/noop";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17";

import PinField from "./pin-field";
import {noop} from "../utils";

enzyme.configure({adapter: new Adapter()});

jest.spyOn(console, "debug").mockImplementation(noop);

test("structure", () => {
const wrapper = shallow(<PinField />);
const inputs = wrapper.find("input");

expect(inputs).toHaveLength(5);
inputs.forEach((input, idx) => {
inputs.forEach(input => {
expect(input.prop("type")).toBe("text");
expect(input.hasClass("a-reactPinField__input")).toBe(true);
expect(input.hasClass(`-${idx}`)).toBe(true);
expect(input.prop("autoFocus")).toBe(false);
expect(typeof input.prop("onFocus")).toBe("function");
expect(typeof input.prop("onKeyDown")).toBe("function");
expect(typeof input.prop("onPaste")).toBe("function");
expect(input.prop("maxLength")).toBe(1);
expect(input.prop("style")).toEqual({});
});
});

Expand All @@ -47,7 +46,6 @@ test("autoFocus", () => {

inputs.forEach((input, idx) => {
expect(input.prop("autoFocus")).toBe(idx === 0);
expect(input.hasClass("-focus")).toBe(idx === 0);
});
});

Expand All @@ -56,7 +54,6 @@ test("className", () => {
const inputs = wrapper.find("input");

inputs.forEach(input => {
expect(input.hasClass("a-reactPinField__input")).toBe(true);
expect(input.hasClass("custom-class-name")).toBe(true);
});
});
Expand All @@ -77,12 +74,32 @@ test("events", () => {
const input = wrapper.find("input").first();

input.simulate("focus");
input.simulate("keydown", {preventDefault: noop, nativeEvent: {key: "a"}});
input.simulate("keydown", {preventDefault: noop, nativeEvent: {which: 66}});
input.simulate("input", {preventDefault: noop, nativeEvent: {data: "c"}});
input.simulate("paste", {clipboardData: {getData: () => "d"}});
input.simulate("keydown", {preventDefault: noop, nativeEvent: {key: "Alt", target: document.createElement("input")}});
input.simulate("keydown", {preventDefault: noop, nativeEvent: {key: "a", target: document.createElement("input")}});
input.simulate("keydown", {preventDefault: noop, nativeEvent: {which: 66, target: document.createElement("input")}});
input.simulate("paste", {clipboardData: {getData: () => "cde"}});

expect(handleChangeMock).toHaveBeenCalledTimes(4);
expect(handleChangeMock).toHaveBeenCalledTimes(3);
expect(handleCompleteMock).toHaveBeenCalledTimes(1);
expect(handleCompleteMock).toHaveBeenCalledWith("abcd");
});

test("fallback events", () => {
const handleChangeMock = jest.fn();
const handleCompleteMock = jest.fn();
const wrapper = mount(<PinField length={4} onChange={handleChangeMock} onComplete={handleCompleteMock} />);
const input = wrapper.find("input").first();

const keyDownInputMock = document.createElement("input");
keyDownInputMock.value = "";
const keyUpInputMock = document.createElement("input");
keyUpInputMock.value = "a";

input.simulate("focus");
input.simulate("keydown", {preventDefault: noop, nativeEvent: {key: "Unidentified", target: keyDownInputMock}});
input.simulate("keyup", {preventDefault: noop, nativeEvent: {target: keyUpInputMock}});
input.simulate("keydown", {preventDefault: noop, nativeEvent: {key: "Unidentified", target: keyDownInputMock}});
input.simulate("keyup", {preventDefault: noop, nativeEvent: {target: {value: "b"}}});
expect(handleChangeMock).toHaveBeenCalledTimes(1);
expect(handleChangeMock).toHaveBeenCalledWith("a");
});

0 comments on commit feedeb0

Please sign in to comment.