Skip to content

Commit d4c0f88

Browse files
authored
Merge pull request #300 from silinternational/develop
2 parents 981eadd + e6e698a commit d4c0f88

File tree

9 files changed

+270
-5
lines changed

9 files changed

+270
-5
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
# Changelog
2+
All notable changes to this project will be documented in this file.
3+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), enforced with [semantic-release](https://github.com/semantic-release/semantic-release).
4+
5+
## [11.8.0](https://github.com/silinternational/ui-components/compare/v11.7.0...v11.8.0) (2025-04-03)
6+
7+
8+
### Added
9+
10+
* **components:** Add NumberInput component and story (minor release) ([31a0a44](https://github.com/silinternational/ui-components/commit/31a0a4486ec670d0e5c07ee45faec3977e8b0d1c))
11+
112
# Changelog
213

314
All notable changes to this project will be documented in this file.
+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<!-- https://github.com/material-components/material-components-web/tree/master/packages/mdc-textfield -->
2+
<script>
3+
/** A Svelte component that represents a text input for Number values. */
4+
import { addOrRemoveInvalidClass, getDecimalPlacesLength } from './helpers'
5+
import { generateRandomID } from '../../../random'
6+
import { MDCTextField } from '@material/textfield'
7+
import { afterUpdate, onMount } from 'svelte'
8+
9+
/** @type {string} The label for the input. */
10+
export let label = ''
11+
/** @type {number} The value of the input. */
12+
export let value = 0
13+
/** @type {number} The step value for the input. */
14+
export let step = 1
15+
/** @type {string} The placeholder for the input. */
16+
export let placeholder = ''
17+
/** @type {string} The name of the input. */
18+
export let name = ''
19+
/** @type {number} The maximum value allowed for the input. */
20+
export let maxValue = undefined
21+
/** @type {number} The minimum value allowed for the input. */
22+
export let minValue = undefined
23+
/** @type {boolean} If true, the input will be focused on mount. */
24+
export let autofocus = false
25+
/** @type {boolean} If true, the input will be disabled. */
26+
export let disabled = false
27+
/** @type {boolean} If true, the input will be required. */
28+
export let required = false
29+
/** @type {string} The description to display below the input. */
30+
export let description = ''
31+
/** @type {boolean} lets the component know to use error class. */
32+
export let showError = false
33+
/** @type {boolean} lets the component know to use warn class. */
34+
export let showWarn = false
35+
36+
const labelID = generateRandomID('text-label-')
37+
38+
let maxlength = 524288 /* default */
39+
let element = {}
40+
let mdcTextField = {}
41+
let width = ''
42+
let hasFocused = false
43+
let hasBlurred = false
44+
45+
$: valueLength = value?.toString()?.length
46+
$: hasExceededMaxLength = maxlength && valueLength > maxlength
47+
$: hasExceededMaxValue = maxValue && internalValue > maxValue
48+
$: isLowerThanMinValue = minValue && internalValue < minValue
49+
$: showErrorIcon = hasExceededMaxValue || isLowerThanMinValue || hasExceededMaxLength || valueNotDivisibleByStep
50+
$: error = showErrorIcon || (hasFocused && hasBlurred && required && !internalValue)
51+
$: showCounter = maxlength && valueLength / maxlength > 0.85
52+
$: valueHasTooManyDecPlaces = getDecimalPlacesLength(internalValue) > getDecimalPlacesLength(step)
53+
$: valueNotDivisibleByStep =
54+
(internalValue && (internalValue / Number(step)).toFixed(2) % 1 !== 0) || valueHasTooManyDecPlaces
55+
$: internalValue = Number(value) || 0
56+
$: warn = showWarn
57+
$: value, addOrRemoveInvalidClass(error, element)
58+
$: addOrRemoveInvalidClass(showError || showWarn, element)
59+
60+
onMount(() => {
61+
mdcTextField = new MDCTextField(element)
62+
return () => mdcTextField.destroy()
63+
})
64+
65+
afterUpdate(() => (width = `${element?.offsetWidth}px`))
66+
67+
const focus = (node) => autofocus && node.focus()
68+
</script>
69+
70+
<label
71+
class="mdc-text-field mdc-text-field--outlined mdc-text-field--with-leading-icon {$$props.class ||
72+
''} textfield-radius"
73+
class:mdc-text-field--no-label={!label}
74+
class:mdc-text-field--disabled={disabled}
75+
class:warn
76+
class:showError
77+
bind:this={element}
78+
>
79+
<span class="mdc-notched-outline">
80+
<span class="mdc-notched-outline__leading" />
81+
{#if label}
82+
<span class="mdc-notched-outline__notch">
83+
<span class="mdc-floating-label" class:error id={labelID}>
84+
{label}
85+
</span>
86+
</span>
87+
{/if}
88+
<span class="mdc-notched-outline__trailing" />
89+
</span>
90+
<i class="material-icons mdc-text-field__icon mdc-text-field__icon--leading" class:error aria-hidden="true"> </i>
91+
<input
92+
data-1p-ignore
93+
{step}
94+
type="number"
95+
min={minValue}
96+
max={maxValue}
97+
class="mdc-text-field__input"
98+
aria-labelledby={labelID}
99+
aria-controls="{labelID}-helper-id"
100+
aria-describedby="{labelID}-helper-id"
101+
bind:value
102+
use:focus
103+
on:focus={() => (hasFocused = true)}
104+
on:blur
105+
on:blur={() => (hasBlurred = true)}
106+
on:keydown
107+
on:keypress
108+
on:keyup
109+
{disabled}
110+
{maxlength}
111+
{name}
112+
{placeholder}
113+
{required}
114+
/>
115+
{#if showErrorIcon}
116+
<i class="material-icons mdc-text-field__icon mdc-text-field__icon--trailing error" aria-hidden="true"> error</i>
117+
{/if}
118+
</label>
119+
<div class="mdc-text-field-helper-line" style="width: {width};">
120+
<div
121+
class="mdc-text-field-helper-text
122+
mdc-text-field-helper-text--{error ? 'validation-msg' : 'persistent'}"
123+
id="{labelID}-helper-id"
124+
aria-hidden="true"
125+
>
126+
{#if !error && description}
127+
{description}
128+
{:else if required && !internalValue}
129+
✴Required
130+
{:else if hasExceededMaxValue}
131+
Maximum value allowed is {maxValue}
132+
{:else if isLowerThanMinValue}
133+
Minimun value allowed is ({minValue})
134+
{:else if valueNotDivisibleByStep}
135+
{internalValue} is not divisible by {step}
136+
{:else if hasExceededMaxLength}
137+
Maximum {maxlength} characters
138+
{/if}
139+
</div>
140+
{#if showCounter}
141+
<div class="mdc-text-field-character-counter" class:error>
142+
{valueLength} / {maxlength}
143+
</div>
144+
{/if}
145+
</div>

components/mdc/TextInput/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ import './_index.scss'
22
import TextArea from './TextArea.svelte'
33
import TextField from './TextField.svelte'
44
import MoneyInput from './MoneyInput.svelte'
5+
import NumberInput from './NumberInput.svelte'
56

6-
export { TextArea, TextField, MoneyInput }
7+
export { TextArea, TextField, MoneyInput, NumberInput }

components/mdc/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import Select from './Select'
2323
import Snackbar from './Snackbar'
2424
import Switch from './Switch'
2525
import TabBar from './TabBar'
26-
import { TextArea, TextField, MoneyInput } from './TextInput'
26+
import { TextArea, TextField, NumberInput, MoneyInput } from './TextInput'
2727
import Tooltip from './Tooltip'
2828
import TopAppBar from './TopAppBar'
2929
import Tour from './Tour'
@@ -45,6 +45,7 @@ export {
4545
List,
4646
Menu,
4747
MoneyInput,
48+
NumberInput,
4849
Page,
4950
Progress,
5051
TabBar,

index.d.ts

+18
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,24 @@ declare module '@silintl/ui-components' {
221221
}
222222
export class MoneyInput extends SvelteComponent<MoneyInputProps> {}
223223

224+
interface NumberInputProps {
225+
label?: string
226+
value?: number | string
227+
name?: string
228+
placeholder?: string
229+
autofocus?: boolean
230+
disabled?: boolean
231+
required?: boolean
232+
minValue?: number | string
233+
maxValue?: number | string
234+
step?: number | string
235+
description?: string
236+
class?: string
237+
showWarn?: boolean
238+
showError?: boolean
239+
}
240+
export class NumberInput extends SvelteComponent<NumberInputProps> {}
241+
224242
export namespace Progress {
225243
type CircularProps = {}
226244
export class Circular extends SvelteComponent<CircularProps> {}

index.mjs

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
List,
1515
Menu,
1616
MoneyInput,
17+
NumberInput,
1718
Page,
1819
Progress,
1920
Select,
@@ -48,6 +49,7 @@ export {
4849
List,
4950
Menu,
5051
MoneyInput,
52+
NumberInput,
5153
Progress,
5254
Select,
5355
SearchableSelect,

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@silintl/ui-components",
3-
"version": "11.7.0",
3+
"version": "11.8.0",
44
"description": "Reusable Svelte components for some internal applications",
55
"main": "index.mjs",
66
"module": "index.mjs",

stories/NumberInput.stories.svelte

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<script>
2+
import { NumberInput } from '../components/mdc'
3+
import { copyAndModifyArgs } from './helpers.js'
4+
import { Meta, Template, Story } from '@storybook/addon-svelte-csf'
5+
import { getDecimalPlacesLength } from '../components/mdc/TextInput/helpers'
6+
7+
let arrayOfValues = []
8+
let dynamicValue
9+
let lastKey = ''
10+
let title = 'NumberInput'
11+
12+
const args = {
13+
label: title,
14+
'on:keydown': (event) => console.log('keydown', event),
15+
'on:keypress': (event) => (lastKey = event.key),
16+
'on:keyup': (event) => console.log('keyup', event),
17+
class: '', //will only work with global class
18+
step: '1',
19+
}
20+
$: arrayOfValues.forEach((v) =>
21+
setTimeout(() => {
22+
dynamicValue = v
23+
}, 100),
24+
)
25+
26+
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#sequence_generator_range
27+
function range(start, stop, step) {
28+
const numberOfDecToFix = getDecimalPlacesLength(step)
29+
return Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + (i * step).toFixed(numberOfDecToFix))
30+
}
31+
32+
function setValues(max, step) {
33+
arrayOfValues = range(0, max, step)
34+
}
35+
</script>
36+
37+
<style>
38+
.d-none {
39+
display: none;
40+
}
41+
</style>
42+
43+
<Meta title="Atoms/NumberInput" component={NumberInput} />
44+
45+
<Template let:args>
46+
{#if !args.label}
47+
<div class="d-none">
48+
{setValues(args.maxValue, args.step)}
49+
</div>
50+
{/if}
51+
<NumberInput
52+
value={!args.label && dynamicValue}
53+
{...args}
54+
on:keydown={args['on:keydown']}
55+
on:keypress={args['on:keypress']}
56+
on:keyup={args['on:keyup']}
57+
/>
58+
{#if lastKey}
59+
<p>Last key pressed: {lastKey}</p>
60+
{/if}
61+
</Template>
62+
63+
<Story name="Default" {args} />
64+
65+
<Story name="Required" args={copyAndModifyArgs(args, { required: true })} />
66+
67+
<Story name="Placeholder" args={copyAndModifyArgs(args, { placeholder: 'Placeholder' })} />
68+
69+
<Story name="Label" args={copyAndModifyArgs(args, { label: 'label' })} />
70+
71+
<Story name="MinValue" args={copyAndModifyArgs(args, { minValue: '0' })} />
72+
73+
<Story name="MaxValue" args={copyAndModifyArgs(args, { maxValue: '10' })} />
74+
75+
<Story name="Step" args={copyAndModifyArgs(args, { step: '.5' })} />
76+
77+
<Story name="Autofocus" args={copyAndModifyArgs(args, { autofocus: true })} />
78+
79+
<Story name="Disabled" args={copyAndModifyArgs(args, { disabled: true })} />
80+
81+
<Story name="Description" args={copyAndModifyArgs(args, { description: 'a description' })} />
82+
83+
<Story name="Test step" args={{ ...args, label: '' }} />
84+
85+
<Story name="showError" args={copyAndModifyArgs(args, { showError: true })} />
86+
87+
<Story name="showWarn" args={copyAndModifyArgs(args, { showWarn: true })} />

0 commit comments

Comments
 (0)