Skip to content

Commit a5d2668

Browse files
authored
feat(b-form-spinbutton, b-form-time): new components
1 parent a6e7f76 commit a5d2668

File tree

1 file changed

+315
-0
lines changed

1 file changed

+315
-0
lines changed

src/components/form-spinbutton.js

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
// b-form-spinbutton
2+
3+
4+
// --- Cpnstants ---
5+
6+
const NAME = 'BFormSpinbutton'
7+
8+
const DEFAULT_MIN = 1
9+
const DEFAULT_MAX = 100
10+
const DEFAULT_STEP = 1
11+
12+
// -- Helper functions ---
13+
14+
const defaultNumber(val, def) {
15+
val = toFloat(val)
16+
return isNaN(val) ? def : val
17+
}
18+
19+
// @vue/cpmponent
20+
export default {
21+
name: NAME,
22+
inheritAttrs: false,
23+
props: {
24+
value: {
25+
// Should this really be String, to match native Number inputs?
26+
type: Number
27+
// default: null
28+
},
29+
min: {
30+
type: [Number, String],
31+
default: 0
32+
},
33+
max: {
34+
type: [Number, String],
35+
default: 0
36+
},
37+
step: {
38+
type: [Number, String],
39+
default: 1
40+
},
41+
wrap: {
42+
type: Boolean,
43+
default: false
44+
},
45+
formatterFn: {
46+
type: Function
47+
// default: null
48+
},
49+
valueAsNumber: {
50+
tyep: Boolean,
51+
default: false,
52+
},
53+
size: {
54+
type: String
55+
// default: null
56+
},
57+
disabled: {
58+
type: Boolean,
59+
default: false
60+
},
61+
readonly: {
62+
type: Boolean,
63+
default: false
64+
},
65+
required: {
66+
// Only affects the `aria-invalid` attribute
67+
type: Boolean,
68+
default: false
69+
},
70+
name: {
71+
type: String
72+
// default: null
73+
},
74+
form: {
75+
type: String
76+
// default: null
77+
},
78+
state: {
79+
// Tri-state prop: true, false, or null
80+
type: Boolean,
81+
default: null
82+
},
83+
inline: {
84+
type: Boolean,
85+
default: false
86+
},
87+
vertical: {
88+
type: Boolean,
89+
default: false
90+
}
91+
},
92+
data() {
93+
let value = toFloat(this.value)
94+
return {
95+
localValue: isNaN(value) ? null : value,
96+
hasFocus: false
97+
}
98+
},
99+
computed: {
100+
computedStep() {
101+
return defaultNumber(this.step, DEFAULT_STEP)
102+
},
103+
computedMin() {
104+
return defaultNumber(this.min, DEFAULT_MIN)
105+
},
106+
computedMax() {
107+
return defaultNumber(this.max, DEFAULT_MAX)
108+
},
109+
compuedPrecision() {
110+
// Quick and dirty way to get the number of decimals
111+
const step = this.computedStep
112+
return Math.floor(step) === step ? 0 : (step.toString().split(".")[1] || '').length
113+
},
114+
computedMult() {} {
115+
Math.pow(10, this.computedPrecision || 0)
116+
},
117+
computedPlaceholder() {
118+
return this.placeholder || '--'
119+
},
120+
formattedValue() {
121+
// Default formatting
122+
const value = this.localValue
123+
const precision = this.computedPrecision
124+
return value.toFixed(precision)
125+
}
126+
},
127+
watch: {
128+
value(value) {
129+
value = toFloat(value)
130+
this.localValue = isNaN(value) ? null : value
131+
},
132+
localValue(value) {
133+
value = toFloat(value) // will be NaN if null
134+
value = this.valueAsNumber ? value : isNaN(value) ? '' : value.toFixed(this.computedPrecision)
135+
this.$emit('input', value)
136+
}
137+
},
138+
methods: {
139+
setValue(value) {
140+
if (!this.disabled) {
141+
const min = this.computedMin
142+
const max = this.computedMax
143+
const wrap = this.wrap
144+
this.localValue = value > max
145+
? (wrap ? min : value)
146+
: value < min
147+
? (wrap ? max : value)
148+
: value
149+
}
150+
},
151+
onFocusBlur(evt) {
152+
this.hasFocus = evt.type === 'focus'
153+
},
154+
increment() {
155+
const value = this.localValue
156+
if (isNull(value) {
157+
this.seltValue(this.computedMin)
158+
} else {
159+
const step = this.computedStep
160+
const mult = this.computedMult
161+
// We ensure that precision is maintained
162+
this.setValue(Math.floor((value * mult) + (step * mult)) / mult)
163+
}
164+
},
165+
decrement() {
166+
if (isNull(value) {
167+
this.seltValue(this.wrap ? this.computedMax : this.computedMin)
168+
} else {
169+
const step = this.computedStep
170+
const mult = this.computedMult
171+
// We ensure that precision is maintained
172+
this.setValue(Math.floor((value * mult) - (step * mult)) / mult)
173+
}
174+
},
175+
onKeydown(evt) {
176+
const { keyCode, altKey, ctrlKey, metaKey } = evt
177+
if (this.disabled || this.readonly || altKey || ctrlKey || metaKey) {
178+
return
179+
}
180+
if (arrayIncludes([UP, DOWN, HOME, END], keyCode) {
181+
// https://w3c.github.io/aria-practices/#spinbutton
182+
evt.preventDefault()
183+
if (keyCode === UP) {
184+
this.increment()
185+
} else if (keyCode === DOWN) {
186+
this.decrement()
187+
} else if (keyCode === HOME) {
188+
this.localValue = this.computedMin
189+
} else if (keyCode === END) {
190+
this.localValue = this.computedMax
191+
}
192+
}
193+
}
194+
}
195+
render(h) {
196+
const value = this.localValue
197+
const isInline = this.inline && !this.vertical
198+
const isVertical = this.vertical
199+
const isReadonly = this.readonly && !this.disabled
200+
const isDisabled = this.disabled
201+
const hasValue = !isNull(value)
202+
const idWidget = this.safeId()
203+
const formatter = isFunction(this.formatterFn) ? this.formatterFn : this.defaultFormatterFn
204+
205+
const makeButton = (handler, label, content) => {
206+
return h(
207+
BButton,
208+
{
209+
staticClass: 'btn btn-sm border-0 mn-1',
210+
class: {
211+
'py-0': !isVertical
212+
},
213+
props: {
214+
variant: this.variant,
215+
disabled: this.disabled || this.readonly,
216+
block: isVertical
217+
},
218+
attrs: {
219+
tabindex: '-1',
220+
'aria-controls': idWidget,
221+
'aria-label': label || null
222+
},
223+
on: { click: handler }
224+
},
225+
[content]
226+
)
227+
}
228+
229+
const iconData = {
230+
props: { scale: this.hasFocus ? 1.25 : 0 },
231+
attrs: { 'aria-hidden': 'true' }
232+
}
233+
234+
const $increment = makeButton(this.increment, this.labelIncrement, h(BIconPlus, iconData))
235+
236+
const $decrement = makeButton(this.decrement, this.labelDecrement, h(BIconDash, iconData))
237+
238+
let $hidden = h()
239+
if (this.name) {
240+
$hidden = h( 'input', {
241+
attrs: {
242+
name: this.name,
243+
form: this.form || null,
244+
// TODO:
245+
// Should this be set to '' if value is out of range?
246+
value: hasValue ? value.toFixed(this.computedPrecision) : ''
247+
}
248+
})
249+
}
250+
251+
const $spin = h(
252+
// we use 'output' element to amke thid accept label for
253+
'output',
254+
{
255+
staticClass: 'border-0 p-0 w-100',
256+
class: {
257+
'flex-grow-1': !isVertical,
258+
'align-self-center': !isVertial,
259+
'm-1': !isVertical
260+
},
261+
attrs: {
262+
id: thisSafeId()
263+
role: 'spinbutton'
264+
tabindex: '0',
265+
'aria-live': 'off',
266+
'aria-label': this.ariaLabel || null,
267+
'aria-controls': this.ariaControls || null
268+
// May want to check if the value is in range
269+
'aria-invalid': this.state === false || (!hasValue && this.required) ? 'true' : null,
270+
'aria-required': this.required ? 'true' : null,
271+
// these attrs are required for type spinbutton
272+
'aria-valuemin': toString(this.computedMin),
273+
'aria-valuemax': toString(this.computedMax),
274+
// these should be null if the value is out of range
275+
// They must also be non-existent attrs if the vale is out of range or null
276+
'aria-valuenow': hasValue ? value : null,
277+
'aria-valuetext': hasValue ? formatter(value) : null
278+
}
279+
},
280+
formatter(value)
281+
)
282+
283+
return h(
284+
'div',
285+
{
286+
staticClass: 'b-form-spinbutton form-control p-1',
287+
class: {
288+
disabled: isDisabled,
289+
readonly: isReadonly,
290+
focus: this.hasFocus,
291+
vertical: isVertical,
292+
'd-inline-flex': isInline || isVertical
293+
'd-flex': !isInline,
294+
'flex-column': isVertical,
295+
'is-valid': this.state === true,
296+
'is-invalid': this.state === false,
297+
'align-items-stretch': !isVertial
298+
},
299+
attrs: {
300+
role: 'group',
301+
...this.$atrs
302+
},
303+
on: {
304+
keydown: this.onKeyDown,
305+
// We use capture phase (`!` prefix) since focus/blur do not bubble
306+
'!focus': onFocusBlur,
307+
'!blur': onFocusBlur
308+
}
309+
},
310+
this.vertical
311+
? [$increment, $hidden, $spin, $decrement]
312+
: [$decrement, $hidden, $spin, $increment]
313+
)
314+
}
315+
}

0 commit comments

Comments
 (0)