diff --git a/docs/examples-docs/number-keyboard.md b/docs/examples-docs/number-keyboard.md new file mode 100644 index 000000000..362d1d1f8 --- /dev/null +++ b/docs/examples-docs/number-keyboard.md @@ -0,0 +1,101 @@ + + + + +## NumberKeyboard 数字键盘 + +### 使用指南 +``` javascript +import { NumberKeyboard } from 'vant'; + +Vue.component(NumberKeyboard.name, NumberKeyboard); +``` + +### 代码演示 + +#### 基础用法 + +:::demo 基础用法 +```html + + 弹出键盘 + + + + 收起键盘 + + + +``` + +```javascript +export default { + data() { + return { + showKeyboard: true + } + }, + + methods: { + onInput(value) { + Toast(value); + }, + onDelete() { + Toast('delete'); + } + } +} +``` +::: + + +### API + +| 参数 | 说明 | 类型 | 默认值 | 可选值 | +|-----------|-----------|-----------|-------------|-------------| +| show | 是否显示键盘 | `Boolean` | - | - | +| title | 键盘标题 | `String` | `安全输入键盘` | - | +| extraKey | 左下角按键内容 | `String` | `''` | - | +| zIndex | 键盘 z-index | `Number` | `100` | - | +| transition | 是否开启过场动画 | `Boolean` | `true` | - | +| showDeleteKey | 是否展示删除按钮 | `Boolean` | `true` | - | + +### Event + +| 事件名 | 说明 | 参数 | +|-----------|-----------|-----------| +| input | 点击按键时触发 | key: 按键内容 | +| delete | 点击删除键时触发 | - | +| blur | 点击非键盘区域时触发 | - | +| show | 键盘完全弹出时触发 | - | +| hide | 键盘完全收起时触发 | - | diff --git a/docs/src/doc.config.js b/docs/src/doc.config.js index 492844c19..a840d170d 100644 --- a/docs/src/doc.config.js +++ b/docs/src/doc.config.js @@ -143,6 +143,10 @@ module.exports = { "path": "/field", "title": "Field 输入框" }, + { + "path": "/number-keyboard", + "title": "NumberKeyboard 数字键盘" + }, { "path": "/radio", "title": "Radio 单选框" diff --git a/packages/index.js b/packages/index.js index 2129658e0..08d141822 100644 --- a/packages/index.js +++ b/packages/index.js @@ -24,6 +24,7 @@ import Lazyload from './lazyload'; import Loading from './loading'; import NavBar from './nav-bar'; import NoticeBar from './notice-bar'; +import NumberKeyboard from './number-keyboard'; import Panel from './panel'; import Picker from './picker'; import Popup from './popup'; @@ -74,6 +75,7 @@ const components = [ Loading, NavBar, NoticeBar, + NumberKeyboard, Panel, Picker, Popup, @@ -140,6 +142,7 @@ export { Loading, NavBar, NoticeBar, + NumberKeyboard, Panel, Picker, Popup, diff --git a/packages/number-keyboard/index.vue b/packages/number-keyboard/index.vue new file mode 100644 index 000000000..41ac7d1f1 --- /dev/null +++ b/packages/number-keyboard/index.vue @@ -0,0 +1,137 @@ + + + diff --git a/packages/vant-css/src/base.css b/packages/vant-css/src/base.css index 5cbf9bc24..9a148e573 100644 --- a/packages/vant-css/src/base.css +++ b/packages/vant-css/src/base.css @@ -5,4 +5,5 @@ @import "./common/var.css"; @import "./common/normalize.css"; @import "./common/hairline.css"; +@import "./common/animation.css"; diff --git a/packages/vant-css/src/common/animation.css b/packages/vant-css/src/common/animation.css new file mode 100644 index 000000000..070c8e1fc --- /dev/null +++ b/packages/vant-css/src/common/animation.css @@ -0,0 +1,21 @@ +@keyframes van-slide-bottom-enter { + from { + transform: translate3d(0, 100%, 0); + } +} + +@keyframes van-slide-bottom-leave { + to { + transform: translate3d(0, 100%, 0); + } +} + +.van-slide-bottom { + &-enter-active { + animation: van-slide-bottom-enter .3s both ease; + } + + &-leave-active { + animation: van-slide-bottom-leave .3s both ease; + } +} diff --git a/packages/vant-css/src/index.css b/packages/vant-css/src/index.css index 140c245e2..545716d92 100644 --- a/packages/vant-css/src/index.css +++ b/packages/vant-css/src/index.css @@ -34,6 +34,7 @@ @import './radio.css'; @import './switch.css'; @import './uploader.css'; +@import './number-keyboard.css'; /* action components */ @import './actionsheet.css'; diff --git a/packages/vant-css/src/number-keyboard.css b/packages/vant-css/src/number-keyboard.css new file mode 100644 index 000000000..fc2c4b39a --- /dev/null +++ b/packages/vant-css/src/number-keyboard.css @@ -0,0 +1,52 @@ +@import "./common/var.css"; + +.van-number-keyboard { + left: 0; + bottom: 0; + width: 100%; + position: fixed; + user-select: none; + background-color: $white; + animation-timing-function: ease-out; + + &__title { + font-weight: 400; + text-align: center; + color: $gray-dark; + font-size: 12px; + line-height: 25px; + } + + i { + width: calc(100%/3); + height: 54px; + font-size: 24px; + line-height: 54px; + font-style: normal; + text-align: center; + display: inline-block; + vertical-align: middle; + + &::after { + border-top-width: 1px; + } + + &:not(:nth-of-type(3n))::after { + border-right-width: 1px; + } + + &:nth-of-type(10), + &:nth-of-type(12) { + background-color: #F3F3F6; + } + } + + &__delete { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAeCAMAAABg6AyVAAAAbFBMVEUAAAAfHiIdHB4eHR8dHR4eHB4dHB4dHR8gICIdHB4dHB4dHB4dHB8eHh8hISEeHR8fHB8fHR8fHR8fHx8eHiArKyszMzMeHB8eHB8fHR8eHiAeHh4dHB4vLjDY2Nn////b29zKysq9vb28vLzkfBRpAAAAHHRSTlMAK/PW+I/llBv77N1kSCPwWlFAOTMGBb28hHlu08g5sgAAAMlJREFUOMuV1MsWgiAQgGHQyOx+s+sgYO//jnnMGIdDDfwbN99CYEDQFiVEKkolPUG7gl9VTWC31NKuDbVz+Fc1tRJtPDmxS2BS3p5ZC+XXnnbAVoz2WEBCH7uZAalzGoa06whGiznT6sG2xgX4QO2Aej1+KN7XBKL2FvGaMtTWBhbQhtoaYzVQrHKwuGf8hhAPSF5g3xPSt45sCHcouNWx436FGA+RHyQcD35EcUj54U8ff4WYvVi1zLjelUh/OG6XjOeLWv5hfAOI+HLwwOAqhAAAAABJRU5ErkJggg==") no-repeat center center; + background-size: auto 15px; + } + + i&--active { + background-color: $active-color!important; + } +} diff --git a/test/unit/specs/number-keyboard.spec.js b/test/unit/specs/number-keyboard.spec.js new file mode 100644 index 000000000..526a65885 --- /dev/null +++ b/test/unit/specs/number-keyboard.spec.js @@ -0,0 +1,146 @@ +import NumberKeyboard from 'packages/number-keyboard'; +import { mount } from 'avoriaz'; +import { triggerTouch } from '../utils'; + +function mockKeyDown(wrapper, keyIndex) { + const customEvent = document.createEvent('CustomEvent'); + customEvent.initCustomEvent('touchstart', true, true, {}); + Object.defineProperty(customEvent, 'target', { + value: { + dataset: { + key: keyIndex + } + } + }); + wrapper.element.dispatchEvent(customEvent); +} + +describe('NumberKeyboard', () => { + let wrapper; + afterEach(() => { + wrapper && wrapper.destroy(); + }); + + it('create a NumberKeyboard', () => { + wrapper = mount(NumberKeyboard, {}); + expect(wrapper.hasClass('van-number-keyboard')).to.be.true; + }); + + it('click a keyboard key', (done) => { + wrapper = mount(NumberKeyboard, {}); + + // just for coverage + wrapper.vm.handler(true); + + wrapper.vm.$on('input', value => { + expect(value).to.equal(1); + expect(wrapper.vm.active).to.equal(0); + + triggerTouch(wrapper, 'touchend'); + expect(wrapper.vm.active).to.equal(-1); + done(); + }); + + mockKeyDown(wrapper, 9); + mockKeyDown(wrapper, NaN); + mockKeyDown(wrapper, 0); + }); + + it('click delete key', (done) => { + wrapper = mount(NumberKeyboard, {}); + + const deleteSpy = sinon.spy(); + wrapper.vm.$on('delete', deleteSpy); + + mockKeyDown(wrapper, 11); + wrapper.vm.$nextTick(() => { + expect(deleteSpy.calledOnce).to.be.true; + done(); + }); + }); + + it('blur keyboard', (done) => { + wrapper = mount(NumberKeyboard, { + attachToDocument: true + }); + + const blur = sinon.spy(); + wrapper.vm.$on('blur', blur); + + triggerTouch(document.body, 'touchstart'); + wrapper.vm.$nextTick(() => { + expect(blur.calledOnce).to.be.true; + done(); + }); + }); + + it('listen to show event when has transtion', (done) => { + wrapper = mount(NumberKeyboard, { + attachToDocument: true + }); + + const show = sinon.spy(); + wrapper.vm.$on('show', show); + wrapper.vm.show = true; + + setTimeout(() => { + expect(show.calledOnce).to.be.true; + done(); + }, 800); + }); + + it('listen to show event when no transtion', (done) => { + wrapper = mount(NumberKeyboard, { + attachToDocument: true, + propsData: { + transition: false + } + }); + + const show = sinon.spy(); + wrapper.vm.$on('show', show); + wrapper.vm.show = true; + + wrapper.vm.$nextTick(() => { + expect(show.calledOnce).to.be.true; + done(); + }); + }); + + it('listen to hide event when has transtion', (done) => { + wrapper = mount(NumberKeyboard, { + attachToDocument: true, + propsData: { + show: true + } + }); + + const hide = sinon.spy(); + wrapper.vm.$on('hide', hide); + wrapper.vm.show = false; + + setTimeout(() => { + expect(hide.calledOnce).to.be.true; + done(); + }, 800); + }); + + it('listen to hide event when no transtion', (done) => { + wrapper = mount(NumberKeyboard, { + attachToDocument: true, + propsData: { + show: true, + transition: false + } + }); + + const hide = sinon.spy(); + wrapper.vm.$on('hide', hide); + wrapper.vm.show = false; + + wrapper.vm.$nextTick(() => { + expect(hide.calledOnce).to.be.true; + done(); + }); + }); +});