├── .eslintignore ├── .eslintrc.cjs ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── new-issue.md └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .node-version ├── .npmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── playwright.config.ts ├── src ├── app.d.ts ├── app.html ├── lib │ ├── CurrencyInput.svelte │ ├── constants.ts │ ├── index.ts │ └── types.ts └── routes │ └── +page.svelte ├── static └── favicon.png ├── svelte.config.js ├── tests └── svelte-currency-input.test.ts ├── tsconfig.json └── vite.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 5 | plugins: ['svelte3', '@typescript-eslint'], 6 | ignorePatterns: ['*.cjs'], 7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 8 | settings: { 9 | 'svelte3/typescript': () => require('typescript') 10 | }, 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020 14 | }, 15 | env: { 16 | browser: true, 17 | es2017: true, 18 | node: true 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New issue 3 | about: Create a report to help us improve the library 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | Briefly describe the issue or idea. 13 | 14 | ## Version Used 15 | 16 | Specify the version of `svelte-currency-input` you are using. 17 | 18 | ## Expected Behavior 19 | 20 | What should happen? 21 | 22 | ## Actual Behavior 23 | 24 | What actually happens? 25 | 26 | ## Props Used 27 | 28 | List down the props you used with the component, for example: 29 | 30 | ```svelte 31 | 32 | ``` 33 | 34 | ## Reproduction Steps 35 | 36 | 1. 37 | 2. 38 | 3. 39 | 40 | ## Additional Info 41 | 42 | - OS & Version: 43 | - Browser & Version: 44 | - Any other relevant details or potential solutions: 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Test, build & release 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | 7 | jobs: 8 | release: 9 | name: Publish 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: '20' 16 | 17 | - name: Install dependencies 18 | run: npm ci 19 | 20 | - name: Build package with SvelteKit 21 | run: npm run package 22 | 23 | - name: Run semantic release 24 | run: npx semantic-release --branches main 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test only 2 | 3 | on: 4 | pull_request: 5 | branches: [main, master] 6 | 7 | jobs: 8 | test: 9 | name: Playwright 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: '20' 16 | 17 | - name: Install dependencies 18 | run: npm ci 19 | 20 | - name: Install Playwright Browsers 21 | run: npx playwright install --with-deps 22 | 23 | - name: Run Playwright tests 24 | run: npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | /test-results 10 | /dist -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.9.0 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | README.md 10 | 11 | # Ignore files for PNPM, NPM and YARN 12 | pnpm-lock.yaml 13 | package-lock.json 14 | yarn.lock 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "pluginSearchDirs": ["."], 7 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Canutin 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 | # svelte-currency-input 2 | 3 | A masked form input that converts numbers to localized currency formats as you type 4 | 5 | [image](https://svelte.dev/repl/d8f7d22e5b384555b430f62b157ac503?version=3.50.1) 6 | 7 |

8 | 👩‍💻 Play with it on REPL  —  💵 See it in a real project! 9 |

10 | 11 | --- 12 | 13 | ## Features 14 | 15 | - Formats **positive** and **negative** values 16 | - Leverages [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) for **localizing** currency denominations and masking the input 17 | - Simple [API](#api) 18 | - Minimal default styling, easy to [customize](#styling) 19 | 20 | ## Usage 21 | 22 | ```bash 23 | npm install @canutin/svelte-currency-input --save 24 | ``` 25 | 26 | ```html 27 | 30 | 31 | 32 | ``` 33 | 34 | ## How it works 35 | 36 | When the form is submitted you get _unformatted_ or _formatted_ values from two ``'s. 37 | This is more or less what `` looks like under the hood: 38 | 39 | ```html 40 |
41 | 42 | 48 | 49 | 50 | 56 |
57 | ``` 58 | 59 | ## API 60 | 61 | Option | Type | Default | Description | 62 | ----------------- | --------------- | ----------- | ----------- | 63 | value | `number` | `undefined` | Initial value. If left `undefined` a formatted value of `0` is visible as a placeholder | 64 | locale | `string` | `en-US` | Overrides default locale. [Examples](https://gist.github.com/ncreated/9934896) | 65 | currency | `string` | `USD` | Overrides default currency. [Examples](https://www.xe.com/symbols/) | 66 | name | `string` | `total` | Applies the name to the [input fields](#how-it-works) for _unformatted_ (e.g `[name=total]`) and _formatted_ (e.g. `[name=formatted-total]`) values | 67 | id | `string` | `undefined` | Sets the `id` attribute on the input | 68 | required | `boolean` | `false` | Marks the input as required | 69 | disabled | `boolean` | `false` | Marks the input as disabled | 70 | placeholder | `string` `number` `null` | `0` | A `string` will override the default placeholder. A `number` will override it by formatting it to the set currency. Setting it to `null` will not show a placeholder | 71 | isZeroNullish | `boolean` | `false` | If `true` and when the value is `0`, it will override the default placeholder and render the formatted value in the field like any other value. _Note: this option might become the default in future versions_ | 72 | autocomplete | `string` | `undefined` | Sets the autocomplete attribute. Accepts any valid HTML [autocomplete attribute values](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#values) | 73 | isNegativeAllowed | `boolean` | `true` | If `false`, forces formatting only to positive values and ignores `--positive` and `--negative` styling modifiers | 74 | fractionDigits | `number` | `2` | Sets `maximumFractionDigits` in [`Intl.NumberFormat()` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#minimumfractiondigits) used for formatting the currency. Supported digits: `0` to `20` | 75 | inputClasses | `object` | [See below](#Styling) | Selectively overrides any class names passed | 76 | onValueChange | `Callback` | `undefined` | Runs a callback function after the value changes | 77 | 78 | ## Styling 79 | 80 | There are two ways of customizing the styling of the input: 81 | 1. Passing it your own CSS classes 82 | 2. Overriding the styles using the existing class names 83 | 84 | You can **override all of the class names** by passing an object to `inputClasses` that has **one or more** of these properties: 85 | 86 | ```typescript 87 | interface InputClasses { 88 | wrapper?: string; //
that contains the two elements 89 | unformatted?: string; // that contains the unformatted value 90 | formatted?: string; // that contains the formatted value 91 | formattedPositive?: string; // Class added when the formatted input is positive 92 | formattedNegative?: string; // Class added when the formatted input is negative 93 | formattedZero?: string; // Class added when the formatted input is zero 94 | } 95 | ``` 96 | 97 | Usage (with [Tailwind CSS](https://tailwindcss.com/) as an example): 98 | 99 | ```svelte 100 | 108 | ``` 109 | 110 | Alternatively you can **write your own CSS** by overriding the [default styles](https://github.com/canutin/svelte-currency-input/blob/main/src/lib/CurrencyInput.svelte) which use [BEM naming conventions](https://getbem.com/naming/). To do so apply your styles as shown below: 111 | 112 | ```svelte 113 |
114 | 115 |
116 | 117 | 136 | ``` 137 | 138 | ## Contributing 139 | 140 | Here's ways in which you can contribute: 141 | 142 | - Found a bug? Open a [new issue](https://github.com/canutin/svelte-currency-input/issues/new) 143 | - Comment or upvote [existing issues](https://github.com/canutin/svelte-currency-input/issues) 144 | - Submit a [pull request](https://github.com/canutin/svelte-currency-input/pulls) 145 | 146 | ## Developing 147 | 148 | This package was generated with [SvelteKit](https://kit.svelte.dev/). Install dependencies with `npm install`, then start a development server: 149 | 150 | ```bash 151 | npm run dev 152 | 153 | # or start the server and open the app in a new browser tab 154 | npm run dev -- --open 155 | ``` 156 | 157 | #### Integration tests 158 | 159 | The component is tested using [Playwright](https://playwright.dev/). 160 | You can find the tests in [`tests/svelte-currency-input.test.ts`](https://github.com/Canutin/svelte-currency-input/blob/main/tests/svelte-currency-input.test.ts) 161 | 162 | To run all tests on **Chromium**, **Firefox** and **Webkit**: 163 | ```bash 164 | npm run test 165 | ``` 166 | 167 | To run all tests on a specific browser (e.g. **Webkit**): 168 | ```bash 169 | npx playwright test --project=webkit 170 | ``` 171 | 172 | Additional debug commands can be found on [Playwright's documentation](https://playwright.dev/docs/test-cli). 173 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@canutin/svelte-currency-input", 3 | "version": "0.0.0-development", 4 | "scripts": { 5 | "dev": "vite dev", 6 | "build": "vite build", 7 | "preview": "vite preview", 8 | "package": "svelte-kit sync && svelte-package && publint", 9 | "test": "playwright test", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "lint": "prettier --plugin-search-dir . --check . && eslint .", 13 | "format": "prettier --plugin-search-dir . --write ." 14 | }, 15 | "exports": { 16 | ".": { 17 | "types": "./dist/index.d.ts", 18 | "svelte": "./dist/index.js" 19 | } 20 | }, 21 | "files": [ 22 | "dist", 23 | "!dist/**/*.test.*", 24 | "!dist/**/*.spec.*" 25 | ], 26 | "devDependencies": { 27 | "@playwright/test": "^1.39.0", 28 | "@sveltejs/adapter-auto": "^3.2.5", 29 | "@sveltejs/adapter-cloudflare": "^4.7.3", 30 | "@sveltejs/kit": "^2.7.1", 31 | "@sveltejs/package": "^2.2.2", 32 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 33 | "@types/node": "^20.8.7", 34 | "@typescript-eslint/eslint-plugin": "^6.0.0", 35 | "@typescript-eslint/parser": "^6.0.0", 36 | "date-fns": "^2.30.0", 37 | "eslint": "^8.28.0", 38 | "eslint-config-prettier": "^8.5.0", 39 | "eslint-plugin-svelte": "^2.30.0", 40 | "prettier": "^2.8.0", 41 | "prettier-plugin-svelte": "^2.10.1", 42 | "publint": "^0.2.5", 43 | "sass": "^1.69.0", 44 | "semantic-release": "^24.1.1", 45 | "svelte": "^4.2.19", 46 | "svelte-check": "^3.4.3", 47 | "tslib": "^2.4.1", 48 | "typescript": "^5.0.0", 49 | "vite": "^5.0.0" 50 | }, 51 | "optionalDependencies": { 52 | "@cloudflare/workerd-linux-64": "^1.20240909.0", 53 | "@rollup/rollup-linux-x64-gnu": "^4.21.3" 54 | }, 55 | "svelte": "./dist/index.js", 56 | "types": "./dist/index.d.ts", 57 | "type": "module", 58 | "description": "A form input that converts numbers to currencies as you type in localized formats", 59 | "keywords": [ 60 | "svelte", 61 | "currency", 62 | "money", 63 | "input", 64 | "i18n", 65 | "positive", 66 | "negative" 67 | ], 68 | "repository": { 69 | "type": "git", 70 | "url": "git+https://github.com/fmaclen/svelte-currency-input.git" 71 | }, 72 | "author": "Fernando Maclen ", 73 | "license": "MIT", 74 | "bugs": { 75 | "url": "https://github.com/fmaclen/svelte-currency-input/issues" 76 | }, 77 | "homepage": "https://svelte-currency-input.fernando.is" 78 | } 79 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { type PlaywrightTestConfig, devices } from '@playwright/test'; 2 | 3 | const isEnvCI = process.env.CI; 4 | 5 | const enableMultipleBrowsers = [ 6 | { 7 | name: 'chromium', 8 | use: { ...devices['Desktop Chrome'] } 9 | }, 10 | { 11 | name: 'firefox', 12 | use: { ...devices['Desktop Firefox'] } 13 | }, 14 | { 15 | name: 'webkit', 16 | use: { ...devices['Desktop Safari'] } 17 | } 18 | ]; 19 | 20 | const config: PlaywrightTestConfig = { 21 | webServer: { 22 | command: 'npm run dev', 23 | port: 5173 24 | }, 25 | workers: isEnvCI ? 1 : undefined, 26 | retries: isEnvCI ? 2 : 0, 27 | use: { 28 | trace: isEnvCI ? 'off' : 'retain-on-failure', 29 | screenshot: isEnvCI ? 'off' : 'only-on-failure' 30 | }, 31 | projects: enableMultipleBrowsers 32 | }; 33 | 34 | export default config; 35 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // See https://kit.svelte.dev/docs/types#app 4 | // for information about these interfaces 5 | // and what to do when importing types 6 | declare namespace App { 7 | // interface Locals {} 8 | // interface PageData {} 9 | // interface PageError {} 10 | // interface Platform {} 11 | } 12 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svelte-currency-input | Demo 5 | 6 | 7 | 8 | 9 | %sveltekit.head% 10 | 11 | 12 |
%sveltekit.body%
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/lib/CurrencyInput.svelte: -------------------------------------------------------------------------------- 1 | 202 | 203 |
204 | 211 | 0 ? 'decimal' : 'numeric'} 224 | name={`formatted-${name}`} 225 | required={required && !isZero} 226 | placeholder={handlePlaceholder(placeholder)} 227 | {autocomplete} 228 | {disabled} 229 | {id} 230 | bind:this={inputElement} 231 | bind:value={formattedValue} 232 | on:keydown={handleKeyDown} 233 | on:keyup={setUnformattedValue} 234 | on:blur={setFormattedValue} 235 | /> 236 |
237 | 238 | 264 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_LOCALE = 'en-US'; 2 | export const DEFAULT_CURRENCY = 'USD'; 3 | export const DEFAULT_NAME = 'total'; 4 | export const DEFAULT_VALUE = 0; 5 | export const DEFAULT_FRACTION_DIGITS = 2; 6 | 7 | export const DEFAULT_CLASS_WRAPPER = 'currencyInput'; 8 | export const DEFAULT_CLASS_UNFORMATTED = 'currencyInput__unformatted'; 9 | export const DEFAULT_CLASS_FORMATTED = 'currencyInput__formatted'; 10 | export const DEFAULT_CLASS_FORMATTED_POSITIVE = 'currencyInput__formatted--positive'; 11 | export const DEFAULT_CLASS_FORMATTED_NEGATIVE = 'currencyInput__formatted--negative'; 12 | export const DEFAULT_CLASS_FORMATTED_ZERO = 'currencyInput__formatted--zero'; 13 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import CurrencyInput from './CurrencyInput.svelte'; 2 | export default CurrencyInput; 3 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface InputClasses { 2 | wrapper?: string; 3 | unformatted?: string; 4 | formatted?: string; 5 | formattedPositive?: string; 6 | formattedNegative?: string; 7 | formattedZero?: string; 8 | } 9 | 10 | export type Callback = (value: number) => any; 11 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 | 49 | 50 |
51 |
52 | Stand-alone inputs 53 | 54 | 55 | 63 | 70 | 71 | 72 | 73 | 74 | 85 | { 94 | // Prevent alerting on initial load 95 | if (unchangedValue !== value) { 96 | unchangedValue = value; // Update the unchanged value 97 | if (browser) window.alert(`The value for ARS has changed to: ${value}`); // Alert the user 98 | } 99 | }} 100 | /> 101 | 102 | 110 | 118 |
119 | 120 |
121 | Chained inputs 122 | 129 | 130 | 137 | 145 |
146 |
147 | 148 | 155 |
156 | 157 | 303 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmaclen/svelte-currency-input/126dff57ff08917a8cd62c9dea9c262d48615c6d/static/favicon.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import cloudFlare from '@sveltejs/adapter-cloudflare'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | adapter: cloudFlare({ 12 | // REF https://github.com/sveltejs/kit/blob/fd6eb9b152001a537f4277a9e597aa24405a51af/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md#usage 13 | routes: { 14 | include: ['/*'], 15 | exclude: [''] 16 | } 17 | }), 18 | } 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /tests/svelte-currency-input.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test, type Page } from '@playwright/test'; 2 | 3 | const DELAY_FOR_FORMATTED_VALUE_IN_MS = 25; 4 | 5 | const isMacOs = process.platform === 'darwin'; 6 | const selectAll = async (page: Page) => { 7 | isMacOs ? await page.keyboard.press('Meta+A') : await page.keyboard.press('Control+A'); 8 | }; 9 | 10 | // HACK: 11 | // This is a workaround because Playwright starts running the assertions immediately 12 | // after the DOM loads but the component is updated a few milliseconds later. 13 | // This causes a race condition in some tests causing assertions to fail. 14 | // 15 | // The real solution would be to figure out why some fields are not already updated 16 | // when the component is mounted, or why is it triggering a re-render. 17 | // REF: https://github.com/fmaclen/svelte-currency-input/issues/62 18 | const waitForInitialLoad = async (page: Page) => { 19 | const DELAY_IN_MS = 100; 20 | await page.waitForTimeout(DELAY_IN_MS); 21 | } 22 | 23 | test.describe('CurrencyInput', () => { 24 | test.beforeEach(async ({ page }) => { 25 | await page.goto('/'); 26 | }); 27 | 28 | test('Default behavior is correct', async ({ page }) => { 29 | // Test field with "zero" value 30 | const colonUnformattedInput = page.locator('.currencyInput__unformatted[name=colon]'); 31 | const colonFormattedInput = page.locator('.currencyInput__formatted[name="formatted-colon"]'); 32 | await expect(colonUnformattedInput).not.toBeDisabled(); 33 | await expect(colonUnformattedInput).toHaveAttribute('type', 'hidden'); 34 | await expect(colonUnformattedInput).toHaveValue('0'); 35 | await expect(colonFormattedInput).not.toBeDisabled(); 36 | await expect(colonFormattedInput).toHaveValue(''); 37 | await expect(colonFormattedInput).toHaveAttribute('type', 'text'); 38 | await expect(colonFormattedInput).toHaveAttribute('placeholder', '₡0,00'); 39 | await expect(colonFormattedInput).not.toHaveClass(/currencyInput__formatted--positive/); 40 | await expect(colonFormattedInput).not.toHaveClass(/currencyInput__formatted--negative/); 41 | await expect(colonFormattedInput).toHaveClass(/currencyInput__formatted--zero/); 42 | 43 | // Test field with "positive" value 44 | const yenUnformattedInput = page.locator('.currencyInput__unformatted[name=yen]'); 45 | const yenFormattedInput = page.locator('.currencyInput__formatted[name="formatted-yen"]'); 46 | await expect(yenUnformattedInput).not.toBeDisabled(); 47 | await expect(yenUnformattedInput).toHaveAttribute('type', 'hidden'); 48 | await expect(yenUnformattedInput).toHaveValue('5678.9'); 49 | await expect(yenFormattedInput).not.toBeDisabled(); 50 | await expect(yenFormattedInput).toHaveValue('¥5,678.90'); 51 | await expect(yenFormattedInput).toHaveAttribute('type', 'text'); 52 | await expect(yenFormattedInput).toHaveAttribute('placeholder', '¥0.00'); 53 | await expect(yenFormattedInput).toHaveClass(/currencyInput__formatted--positive/); 54 | await expect(yenFormattedInput).not.toHaveClass(/currencyInput__formatted--negative/); 55 | await expect(yenFormattedInput).not.toHaveClass(/currencyInput__formatted--zero/); 56 | 57 | // Test field with "negative" value 58 | const defaultUnformattedInput = page.locator('.currencyInput__unformatted[name=default]'); 59 | const defaultFormattedInput = page.locator( 60 | '.currencyInput__formatted[name="formatted-default"]' 61 | ); 62 | await expect(yenUnformattedInput).not.toBeDisabled(); 63 | await expect(defaultUnformattedInput).toHaveAttribute('type', 'hidden'); 64 | await expect(defaultUnformattedInput).toHaveValue('-42069.69'); 65 | await expect(defaultFormattedInput).not.toBeDisabled(); 66 | await expect(defaultFormattedInput).toHaveValue('-$42,069.69'); 67 | await expect(defaultFormattedInput).toHaveAttribute('type', 'text'); 68 | await expect(defaultFormattedInput).toHaveAttribute('placeholder', '$0.00'); 69 | await expect(defaultFormattedInput).not.toHaveClass(/currencyInput__formatted--positive/); 70 | await expect(defaultFormattedInput).toHaveClass(/currencyInput__formatted--negative/); 71 | await expect(defaultFormattedInput).not.toHaveClass(/currencyInput__formatted--zero/); 72 | 73 | // Test field that is "disabled" 74 | const shekelUnformattedInput = page.locator('.currencyInput__unformatted[name=shekel]'); 75 | const shekelFormattedInput = page.locator('.currencyInput__formatted[name="formatted-shekel"]'); 76 | await expect(shekelUnformattedInput).toBeDisabled(); 77 | await expect(shekelUnformattedInput).toHaveAttribute('type', 'hidden'); 78 | await expect(shekelUnformattedInput).toHaveValue('97532.95'); 79 | await expect(shekelFormattedInput).toHaveValue('₪97,532.95'); 80 | await expect(shekelFormattedInput).toBeDisabled(); 81 | await expect(shekelFormattedInput).toHaveAttribute('type', 'text'); 82 | await expect(shekelFormattedInput).toHaveAttribute('placeholder', '₪0.00'); 83 | await expect(shekelFormattedInput).toHaveClass(/currencyInput__formatted--positive/); 84 | await expect(shekelFormattedInput).not.toHaveClass(/currencyInput__formatted--negative/); 85 | await expect(shekelFormattedInput).not.toHaveClass(/currencyInput__formatted--zero/); 86 | 87 | // Submitting a form returns the correct values 88 | const demoSubmitForm = page.locator('button#submit-form'); 89 | const demoOutput = page.locator('pre.demoForm__pre'); 90 | await demoSubmitForm.click(); 91 | expect(await demoOutput.textContent()).toMatch( 92 | JSON.stringify( 93 | { 94 | default: '-42069.69', 95 | 'formatted-default': '-$42,069.69', 96 | colon: '0', 97 | 'formatted-colon': '', 98 | pound: '1234.56', 99 | 'formatted-pound': '£1,234.56', 100 | bitcoin: '0.87654321', 101 | 'formatted-bitcoin': '฿0.87654321', 102 | yen: '5678.9', 103 | 'formatted-yen': '¥5,678.90', 104 | euro: '-42069.69', 105 | 'formatted-euro': '€ -42.069,69', 106 | won: '0', 107 | 'formatted-won': '', 108 | pesos: '999', 109 | 'formatted-pesos': '$ 999,00', 110 | rupees: '678', 111 | 'formatted-rupees': '₹678.000', 112 | soles: '0', 113 | 'formatted-soles': 'S/ 0.00', 114 | dinars: '0', 115 | 'formatted-dinars': '', 116 | 'chained-east-caribbean-dollar': '10000', 117 | 'formatted-chained-east-caribbean-dollar': 'EC$10,000.0000', 118 | 'chained-euros': '10000', 119 | 'formatted-chained-euros': '€ 10.000,00', 120 | 'chained-dollars': '10000', 121 | 'formatted-chained-dollars': '$10,000' 122 | }, 123 | null, 124 | 2 125 | ) 126 | ); 127 | }); 128 | 129 | test('Updating an input has the correct behavior', async ({ page }) => { 130 | await waitForInitialLoad(page); 131 | 132 | const colonUnformattedInput = page.locator('.currencyInput__unformatted[name=colon]'); 133 | const colonFormattedInput = page.locator('.currencyInput__formatted[name="formatted-colon"]'); 134 | 135 | // Check the there is no value in the input 136 | await expect(colonUnformattedInput).toHaveValue('0'); 137 | await expect(colonFormattedInput).toHaveValue(''); 138 | 139 | await colonFormattedInput.focus(); 140 | await page.keyboard.type('420,69', { delay: DELAY_FOR_FORMATTED_VALUE_IN_MS }); 141 | await expect(colonFormattedInput).toHaveValue('₡420,69'); 142 | await expect(colonUnformattedInput).toHaveValue('420.69'); 143 | await expect(colonFormattedInput).toHaveClass(/currencyInput__formatted--positive/); 144 | await expect(colonFormattedInput).not.toHaveClass(/currencyInput__formatted--negative/); 145 | await expect(colonFormattedInput).not.toHaveClass(/currencyInput__formatted--zero/); 146 | 147 | // Use arrow keys to go back to the first character 148 | for (let i = 0; i < '₡420,69'.length; i++) await page.keyboard.press('ArrowLeft'); 149 | await page.keyboard.type('-'); 150 | await expect(colonFormattedInput).toHaveValue('-₡420,69'); 151 | await expect(colonUnformattedInput).toHaveValue('-420.69'); 152 | await expect(colonFormattedInput).not.toHaveClass(/currencyInput__formatted--positive/); 153 | await expect(colonFormattedInput).toHaveClass(/currencyInput__formatted--negative/); 154 | await expect(colonFormattedInput).not.toHaveClass(/currencyInput__formatted--zero/); 155 | 156 | // Use right arrow keys to position cusror at the end of the input 157 | for (let i = 0; i < '₡420,69'.length; i++) await page.keyboard.press('ArrowRight'); 158 | // Delete the number but keep the currency symbol and sign 159 | for (let i = 1; i < '420,69'.length; i++) await page.keyboard.press('Backspace'); 160 | await page.waitForTimeout(DELAY_FOR_FORMATTED_VALUE_IN_MS); 161 | await expect(colonFormattedInput).toHaveValue('-₡'); 162 | // FIXME: at this point the hidden value should be set to 0 but without formatting `colonFormattedInput` 163 | await expect(colonUnformattedInput).toHaveValue('-4'); 164 | 165 | await page.keyboard.press('Backspace'); 166 | await page.waitForTimeout(DELAY_FOR_FORMATTED_VALUE_IN_MS); 167 | await expect(colonFormattedInput).toHaveValue('-'); 168 | // FIXME: at this point the hidden value should be set to 0 but without formatting `colonFormattedInput` 169 | await expect(colonUnformattedInput).toHaveValue('-4'); 170 | 171 | await page.keyboard.type('69,42', { delay: DELAY_FOR_FORMATTED_VALUE_IN_MS }); 172 | await expect(colonFormattedInput).toHaveValue('-₡69,42'); 173 | await expect(colonUnformattedInput).toHaveValue('-69.42'); 174 | 175 | for (let i = 0; i < '-₡69,42'.length; i++) await page.keyboard.press('Backspace'); 176 | await page.waitForTimeout(DELAY_FOR_FORMATTED_VALUE_IN_MS); 177 | await expect(colonUnformattedInput).toHaveValue('0'); 178 | }); 179 | 180 | test("Incorrect characters can't be entered", async ({ page }) => { 181 | const colonUnformattedInput = page.locator('.currencyInput__unformatted[name=colon]'); 182 | const colonFormattedInput = page.locator('.currencyInput__formatted[name="formatted-colon"]'); 183 | 184 | // Check the there is no value in the input 185 | await expect(colonUnformattedInput).toHaveValue('0'); 186 | await expect(colonFormattedInput).toHaveValue(''); 187 | 188 | // Check typing letters doesn't do anything 189 | await colonFormattedInput.focus(); 190 | await page.keyboard.type('abc'); 191 | await expect(colonUnformattedInput).toHaveValue('0'); 192 | await expect(colonFormattedInput).toHaveValue(''); 193 | 194 | // Check keyboard combinations don't do anything 195 | await page.keyboard.press('Shift+A'); 196 | await expect(colonFormattedInput).toHaveValue(''); 197 | 198 | // Check keyboard shortcuts are allowed 199 | await page.keyboard.type('420,69', { delay: DELAY_FOR_FORMATTED_VALUE_IN_MS }); 200 | await expect(colonFormattedInput).toHaveValue('₡420,69'); 201 | await expect(colonUnformattedInput).toHaveValue('420.69'); 202 | 203 | // Check "Backspace" works 204 | await selectAll(page); 205 | await page.keyboard.press('Backspace'); 206 | await page.waitForTimeout(DELAY_FOR_FORMATTED_VALUE_IN_MS); 207 | await expect(colonUnformattedInput).toHaveValue('0'); 208 | await expect(colonFormattedInput).toHaveValue(''); 209 | 210 | // Add data to the field again 211 | await page.keyboard.type('-420,69', { delay: DELAY_FOR_FORMATTED_VALUE_IN_MS }); 212 | await expect(colonFormattedInput).toHaveValue('-₡420,69'); 213 | await expect(colonUnformattedInput).toHaveValue('-420.69'); 214 | 215 | // Check "Delete" also works 216 | await selectAll(page); 217 | await page.keyboard.press('Delete'); 218 | await expect(colonUnformattedInput).toHaveValue('0'); 219 | await expect(colonFormattedInput).toHaveValue(''); 220 | }); 221 | 222 | test('Placeholders can be overriden', async ({ page }) => { 223 | // Default placeholder 224 | const defaultFormattedInput = page.locator( 225 | '.currencyInput__formatted[name="formatted-default"]' 226 | ); 227 | await expect(defaultFormattedInput).toHaveAttribute('placeholder', '$0.00'); 228 | 229 | // Null placeholder 230 | const poundFormattedInput = page.locator('.currencyInput__formatted[name="formatted-pound"]'); 231 | await expect(poundFormattedInput).toHaveAttribute('placeholder', ''); 232 | 233 | // Overriden placeholder 234 | const wonFormattedInput = page.locator('.currencyInput__formatted[name="formatted-won"]'); 235 | await expect(wonFormattedInput).toHaveAttribute('placeholder', '₩1,234.56'); 236 | }); 237 | 238 | test('Fraction digits can be overriden', async ({ page }) => { 239 | await waitForInitialLoad(page); 240 | 241 | const bitcoinUnformattedInput = page.locator('.currencyInput__unformatted[name=bitcoin]'); 242 | const bitcoinFormattedInput = page.locator( 243 | '.currencyInput__formatted[name="formatted-bitcoin"]' 244 | ); 245 | 246 | await expect(bitcoinUnformattedInput).toHaveValue('0.87654321'); 247 | await expect(bitcoinFormattedInput).toHaveValue('฿0.87654321'); 248 | await expect(bitcoinFormattedInput).toHaveAttribute('placeholder', '฿0.00000000'); 249 | 250 | await bitcoinFormattedInput.focus(); 251 | await selectAll(page); 252 | await page.keyboard.press('Backspace'); 253 | await page.waitForTimeout(DELAY_FOR_FORMATTED_VALUE_IN_MS); 254 | await expect(bitcoinUnformattedInput).toHaveValue('0'); 255 | await expect(bitcoinFormattedInput).toHaveValue(''); 256 | 257 | // Decimals beyond the maximum allowed are rounded 258 | await page.keyboard.type('-0.987654329', { delay: DELAY_FOR_FORMATTED_VALUE_IN_MS }); 259 | await expect(bitcoinUnformattedInput).toHaveValue('-0.98765433'); 260 | await expect(bitcoinFormattedInput).toHaveValue('-฿0.98765433'); 261 | }); 262 | 263 | test.describe('Pressing the comma or period keys have the correct behavior', async () => { 264 | test('Pressing "." gets converted to ","', async ({ page }) => { 265 | await waitForInitialLoad(page); 266 | 267 | const euroFormattedInput = page.locator('.currencyInput__formatted[name="formatted-euro"]'); 268 | const euroUnformattedInput = page.locator('.currencyInput__unformatted[name=euro]'); 269 | await euroFormattedInput.focus(); 270 | 271 | await selectAll(page); 272 | await page.keyboard.press('Backspace'); 273 | await page.waitForTimeout(DELAY_FOR_FORMATTED_VALUE_IN_MS); 274 | await expect(euroUnformattedInput).toHaveValue('0'); 275 | 276 | await page.keyboard.type('-111222.33', { delay: DELAY_FOR_FORMATTED_VALUE_IN_MS }); 277 | await expect(euroFormattedInput).toHaveValue('€ -111.222,33'); 278 | await expect(euroUnformattedInput).toHaveValue('-111222.33'); 279 | }); 280 | 281 | test('Pressing "," gets converted to "."', async ({ page }) => { 282 | await waitForInitialLoad(page); 283 | 284 | const bitcoinUnformattedInput = page.locator('.currencyInput__unformatted[name=bitcoin]'); 285 | const bitcoinFormattedInput = page.locator( 286 | '.currencyInput__formatted[name="formatted-bitcoin"]' 287 | ); 288 | 289 | await bitcoinFormattedInput.focus(); 290 | await selectAll(page); 291 | await page.keyboard.press('Backspace'); 292 | await page.waitForTimeout(DELAY_FOR_FORMATTED_VALUE_IN_MS); 293 | await expect(bitcoinUnformattedInput).toHaveValue('0'); 294 | 295 | await page.keyboard.type('444555,66', { delay: DELAY_FOR_FORMATTED_VALUE_IN_MS }); 296 | await expect(bitcoinFormattedInput).toHaveValue('฿444,555.66'); 297 | await expect(bitcoinUnformattedInput).toHaveValue('444555.66'); 298 | }); 299 | }); 300 | 301 | test('Formatting is applied on:blur', async ({ page }) => { 302 | const euroFormattedInput = page.locator('.currencyInput__formatted[name="formatted-euro"]'); 303 | const euroUnformattedInput = page.locator('.currencyInput__unformatted[name=euro]'); 304 | const colonFormattedInput = page.locator('.currencyInput__formatted[name="formatted-colon"]'); 305 | 306 | // The old value should remain because `-` doesn't override it 307 | await euroFormattedInput.focus(); 308 | await selectAll(page); 309 | await page.keyboard.type('-'); 310 | await colonFormattedInput.focus(); 311 | await expect(euroFormattedInput).toHaveValue('€ -42.069,69'); 312 | await expect(euroUnformattedInput).toHaveValue('-42069.69'); 313 | 314 | // The value is reset to 0 because Backspace overrides it 315 | await euroFormattedInput.focus(); 316 | await selectAll(page); 317 | await page.keyboard.press('Backspace'); 318 | await page.waitForTimeout(DELAY_FOR_FORMATTED_VALUE_IN_MS); 319 | await page.keyboard.type('-'); 320 | await colonFormattedInput.focus(); 321 | await expect(euroFormattedInput).toHaveValue(''); 322 | await expect(euroUnformattedInput).toHaveValue('0'); 323 | }); 324 | 325 | test('Pressing Tab has the correct behavior', async ({ page }, testInfo) => { 326 | // Tabbing in Webkit is broken: https://github.com/Canutin/svelte-currency-input/issues/40 327 | if (testInfo.project.name !== 'webkit') { 328 | const formattedInputs = page.locator('.currencyInput__formatted'); 329 | expect(await formattedInputs.count()).toBe(15); 330 | 331 | await formattedInputs.first().focus(); 332 | await expect(formattedInputs.nth(0)).toBeFocused(); 333 | 334 | await page.keyboard.press('Tab'); 335 | await expect(formattedInputs.nth(1)).toBeFocused(); 336 | 337 | await page.keyboard.press('Tab'); 338 | await expect(formattedInputs.nth(2)).toBeFocused(); 339 | 340 | await page.keyboard.press('Tab'); 341 | await expect(formattedInputs.nth(3)).toBeFocused(); 342 | 343 | await page.keyboard.press('Tab'); 344 | await expect(formattedInputs.nth(4)).toBeFocused(); 345 | 346 | await page.keyboard.press('Tab'); 347 | await expect(formattedInputs.nth(5)).not.toBeFocused(); // The fifth input is disabled 348 | await expect(formattedInputs.nth(6)).toBeFocused(); 349 | 350 | await page.keyboard.press('Tab'); 351 | await expect(formattedInputs.nth(7)).toBeFocused(); 352 | 353 | await page.keyboard.press('Tab'); 354 | await expect(formattedInputs.nth(8)).toBeFocused(); 355 | } 356 | }); 357 | 358 | test('Class names can be overwritten', async ({ page }) => { 359 | const customWrapperClass = page.locator('.custom-wrapper-class'); 360 | const customUnformattedClass = page.locator('.custom-unformatted-class'); 361 | 362 | await expect(customWrapperClass).toBeVisible(); 363 | await expect(customWrapperClass).toHaveClass(/currencyInput/); // We don't override the default class 364 | await expect(customUnformattedClass).toHaveValue('0'); 365 | await expect(customUnformattedClass).not.toHaveClass(/currencyInput__unformatted/); // We override the default class 366 | }); 367 | 368 | test('A callback function is fired when value changes', async ({ page }) => { 369 | const pesosFormattedInput = page.locator('.currencyInput__formatted[name="formatted-pesos"]'); 370 | 371 | // Prepare to assert and accept dialog 372 | page.on('dialog', (dialog) => { 373 | expect(dialog.message()).not.toMatch('The value for ARS has changed to: 999'); 374 | expect(dialog.message()).toMatch('The value for ARS has changed to: 99'); 375 | dialog.accept(); 376 | }); 377 | 378 | await expect(pesosFormattedInput).toBeVisible(); 379 | await pesosFormattedInput.focus(); 380 | await page.keyboard.press('Backspace'); 381 | }); 382 | 383 | test('Autocomplete attribute can be set', async ({ page }) => { 384 | const pesosFormattedInput = page.locator('.currencyInput__formatted[name="formatted-pesos"]'); 385 | await expect(pesosFormattedInput).toHaveAttribute('autocomplete', 'off'); 386 | }); 387 | 388 | test('A value with zero cents and more than 1 fraction digits gets formatted on blur', async ({ page }) => { 389 | const rupeesFormattedInput = page.locator('.currencyInput__formatted[name="formatted-rupees"]'); 390 | const rupeesUnformattedInput = page.locator('.currencyInput__unformatted[name="rupees"]'); 391 | await expect(rupeesFormattedInput).toHaveValue('₹678.000'); 392 | await expect(rupeesUnformattedInput).toHaveValue('678'); 393 | 394 | await rupeesFormattedInput.focus(); 395 | await selectAll(page); 396 | await page.keyboard.press('Backspace'); 397 | await page.keyboard.type('123'); 398 | await expect(rupeesFormattedInput).toHaveValue('₹123'); 399 | await expect(rupeesFormattedInput).not.toHaveValue('₹123.000'); 400 | 401 | await page.locator('body').click(); // Click outside the input to trigger formatting 402 | await expect(rupeesFormattedInput).toHaveValue('₹123.000'); 403 | await expect(rupeesUnformattedInput).toHaveValue('123'); 404 | }); 405 | 406 | test("isZeroNullish doesn't render placeholder when the value is 0", async ({ page }) => { 407 | const solesUnformattedInput = page.locator('.currencyInput__unformatted[name="soles"]'); 408 | const solesFormattedInput = page.locator('.currencyInput__formatted[name="formatted-soles"]'); 409 | await expect(solesUnformattedInput).toHaveValue('0'); 410 | await expect(solesFormattedInput).toHaveValue('S/ 0.00'); 411 | await expect(solesFormattedInput).toHaveAttribute('placeholder', ''); 412 | 413 | const colonUnformattedInput = page.locator('.currencyInput__unformatted[name=colon]'); 414 | const colonFormattedInput = page.locator('.currencyInput__formatted[name="formatted-colon"]'); 415 | await expect(colonUnformattedInput).toHaveValue('0'); 416 | await expect(colonFormattedInput).not.toHaveValue('₡0,00'); 417 | await expect(colonFormattedInput).toHaveAttribute('placeholder', '₡0,00'); 418 | }); 419 | 420 | test("A custom placeholder can be set", async ({ page }) => { 421 | const dinarsUnformattedInput = page.locator('.currencyInput__unformatted[name="dinars"]'); 422 | const dinarsFormattedInput = page.locator('.currencyInput__formatted[name="formatted-dinars"]'); 423 | await expect(dinarsUnformattedInput).toHaveValue('0'); 424 | await expect(dinarsFormattedInput).toHaveValue(''); 425 | await expect(dinarsFormattedInput).toHaveAttribute('placeholder', 'How many Dinars?'); 426 | }); 427 | 428 | test('Prevent duplicated decimal points', async ({ page }) => { 429 | // Periods as decimals 430 | const poundUnformattedInput = page.locator('.currencyInput__unformatted[name=pound]'); 431 | const poundFormattedInput = page.locator('.currencyInput__formatted[name="formatted-pound"]'); 432 | await expect(poundUnformattedInput).toHaveValue('1234.56'); 433 | await expect(poundFormattedInput).toHaveValue('£1,234.56'); 434 | 435 | await poundFormattedInput.focus(); 436 | await page.keyboard.type('....'); 437 | await expect(poundUnformattedInput).toHaveValue('1234.56'); 438 | await expect(poundFormattedInput).toHaveValue('£1,234.56'); 439 | 440 | // Commas as decimals 441 | const colonUnformattedInput = page.locator('.currencyInput__unformatted[name=colon]'); 442 | const colonFormattedInput = page.locator('.currencyInput__formatted[name="formatted-colon"]'); 443 | await expect(colonUnformattedInput).toHaveValue('0'); 444 | await expect(colonFormattedInput).toHaveValue(''); 445 | 446 | await colonFormattedInput.focus(); 447 | await page.keyboard.type('123,,,,,'); 448 | await expect(colonUnformattedInput).toHaveValue('123'); 449 | await expect(colonFormattedInput).toHaveValue('₡123,'); 450 | 451 | // Pressing multiple commas when locale for decimals is a period 452 | const dinarsUnformattedInput = page.locator('.currencyInput__unformatted[name="dinars"]'); 453 | const dinarsFormattedInput = page.locator('.currencyInput__formatted[name="formatted-dinars"]'); 454 | await expect(dinarsUnformattedInput).toHaveValue('0'); 455 | await expect(dinarsFormattedInput).toHaveValue(''); 456 | 457 | await dinarsFormattedInput.focus(); 458 | await page.keyboard.type('123,,,,,'); 459 | await expect(dinarsUnformattedInput).toHaveValue('123'); 460 | await expect(dinarsFormattedInput).toHaveValue('RSD 123.'); 461 | }); 462 | 463 | test("inputmode is set correctly based on fractionDigits", async ({ page }) => { 464 | // fractionDigits == undefined (defaults to 2) 465 | const solesFormattedInput = page.locator('.currencyInput__formatted[name="formatted-soles"]'); 466 | await expect(solesFormattedInput).toHaveAttribute('inputmode', 'decimal'); 467 | 468 | // fractionDigits == 3 469 | const rupeesFormattedInput = page.locator('.currencyInput__formatted[name="formatted-rupees"]'); 470 | await expect(rupeesFormattedInput).toHaveAttribute('inputmode', 'decimal'); 471 | 472 | // fractionDigits == 0 473 | const dinarsFormattedInput = page.locator('.currencyInput__formatted[name="formatted-dinars"]'); 474 | await expect(dinarsFormattedInput).toHaveAttribute('inputmode', 'numeric'); 475 | }); 476 | 477 | test('Updating chained inputs have the correct behavior', async ({ page }) => { 478 | const chainedDollarsUnformattedInput = page.locator( 479 | '.currencyInput__unformatted[name="chained-dollars"]' 480 | ); 481 | const chainedDollarsFormattedInput = page.locator( 482 | '.currencyInput__formatted[name="formatted-chained-dollars"]' 483 | ); 484 | const chainedEurosUnformattedInput = page.locator( 485 | '.currencyInput__unformatted[name="chained-euros"]' 486 | ); 487 | const chainedEurosFormattedInput = page.locator( 488 | '.currencyInput__formatted[name="formatted-chained-euros"]' 489 | ); 490 | const chainedEastCaribbeanDollarUnformattedInput = page.locator( 491 | '.currencyInput__unformatted[name="chained-east-caribbean-dollar"]' 492 | ); 493 | const chainedEastCaribbeanDollarFormattedInput = page.locator( 494 | '.currencyInput__formatted[name="formatted-chained-east-caribbean-dollar"]' 495 | ); 496 | const chainedValueButton = page.locator('button#set-chained-value'); 497 | 498 | // The default chained value is `9999.99` but because `chainedDollars` has 499 | // fraction digits set to `0` it gets rounded to `10_000` onMount(), 500 | // thus updating the other chained inputs to `10_000` as well. 501 | await expect(chainedDollarsUnformattedInput).toHaveValue('10000'); 502 | await expect(chainedDollarsFormattedInput).toHaveValue('$10,000'); 503 | await expect(chainedEurosUnformattedInput).toHaveValue('10000'); 504 | await expect(chainedEurosFormattedInput).toHaveValue('€ 10.000,00'); 505 | await expect(chainedEastCaribbeanDollarUnformattedInput).toHaveValue('10000'); 506 | await expect(chainedEastCaribbeanDollarFormattedInput).toHaveValue('EC$10,000.0000'); 507 | 508 | // Set a new chained value by clicking a button 509 | await chainedValueButton.click(); 510 | // USD input has fraction digits is 0 511 | await expect(chainedDollarsUnformattedInput).toHaveValue('421'); 512 | await expect(chainedDollarsFormattedInput).toHaveValue('$421'); 513 | // EUR input has fraction digits is 2 514 | await expect(chainedEurosFormattedInput).toHaveValue('€ 420,69'); 515 | await expect(chainedEurosUnformattedInput).toHaveValue('420.69'); 516 | // XCD input has fraction digits is 4 517 | await expect(chainedEastCaribbeanDollarUnformattedInput).toHaveValue('420.69'); 518 | await expect(chainedEastCaribbeanDollarFormattedInput).toHaveValue('EC$420.6900'); 519 | 520 | // Set a new chained value by deleting the value in the USD input 521 | await chainedDollarsFormattedInput.focus(); 522 | await selectAll(page); 523 | await page.keyboard.press('Backspace'); 524 | await expect(chainedDollarsUnformattedInput).toHaveValue('0'); 525 | await expect(chainedDollarsFormattedInput).toHaveValue(''); 526 | await expect(chainedEurosUnformattedInput).toHaveValue('0'); 527 | await expect(chainedEurosFormattedInput).toHaveValue(''); 528 | await expect(chainedEastCaribbeanDollarUnformattedInput).toHaveValue('0'); 529 | await expect(chainedEastCaribbeanDollarFormattedInput).toHaveValue(''); 530 | }); 531 | 532 | test('an id can be set', async ({ page }) => { 533 | const fourTwentySixNineInput = page.locator('#four-twenty-six-nine'); 534 | await expect(fourTwentySixNineInput).toBeVisible(); 535 | await expect(fourTwentySixNineInput).toHaveValue('-$42,069.69'); 536 | }); 537 | 538 | test('pressing enter submits the form', async ({ page }) => { 539 | const preTag = page.locator('.demoForm__pre'); 540 | await expect(preTag).toContainText('Submit form to see a JSON output of the values'); 541 | await expect(preTag).not.toContainText('bitcoin'); 542 | 543 | const allInputs = page.locator('.currencyInput__formatted'); 544 | await allInputs.first().focus(); 545 | await expect(allInputs.first()).toHaveValue('-$42,069.69'); 546 | 547 | await page.keyboard.press('Enter'); 548 | await expect(preTag).not.toContainText('Submit form to see a JSON output of the values'); 549 | await expect(preTag).toContainText('bitcoin'); 550 | }); 551 | }); 552 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import type { UserConfig } from 'vite'; 3 | 4 | const config: UserConfig = { 5 | plugins: [sveltekit()] 6 | }; 7 | 8 | export default config; 9 | --------------------------------------------------------------------------------