@purpurds/text-field 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@purpurds/text-field",
3
+ "version": "3.0.0",
4
+ "license": "AGPL-3.0-only",
5
+ "main": "./dist/text-field.cjs.js",
6
+ "types": "./dist/text-field.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./dist/text-field.cjs.js",
10
+ "systemjs": "./dist/text-field.system.js",
11
+ "types": "./dist/text-field.d.ts",
12
+ "default": "./dist/text-field.es.js"
13
+ },
14
+ "./styles": "./dist/styles.css"
15
+ },
16
+ "source": "src/text-field.tsx",
17
+ "dependencies": {
18
+ "classnames": "~2.5.0",
19
+ "@purpurds/field-helper-text": "3.0.0",
20
+ "@purpurds/field-error-text": "3.0.0",
21
+ "@purpurds/icon": "3.0.0",
22
+ "@purpurds/label": "3.0.0",
23
+ "@purpurds/spinner": "3.0.0",
24
+ "@purpurds/tokens": "3.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@rushstack/eslint-patch": "~1.7.0",
28
+ "@storybook/blocks": "~7.6.0",
29
+ "@storybook/client-api": "~7.6.0",
30
+ "@storybook/react": "~7.6.0",
31
+ "@telia/base-rig": "~8.2.0",
32
+ "@telia/react-rig": "~3.2.0",
33
+ "@testing-library/dom": "~9.3.3",
34
+ "@testing-library/jest-dom": "~6.3.0",
35
+ "@testing-library/react": "~14.1.2",
36
+ "@types/node": "18",
37
+ "@types/react-dom": "~18.2.17",
38
+ "@types/react": "~18.2.42",
39
+ "eslint-plugin-testing-library": "~6.2.0",
40
+ "eslint": "~8.56.0",
41
+ "jsdom": "~22.1.0",
42
+ "lint-staged": "~10.5.3",
43
+ "prettier": "~2.8.8",
44
+ "react-dom": "~18.2.0",
45
+ "react": "~18.2.0",
46
+ "typescript": "~5.2.2",
47
+ "vite": "~5.0.6",
48
+ "vitest": "~1.2.0",
49
+ "@purpurds/component-rig": "1.0.0"
50
+ },
51
+ "scripts": {
52
+ "build:dev": "vite",
53
+ "build:watch": "vite build --watch",
54
+ "build": "rm -rf dist && vite build && vite build --mode systemjs",
55
+ "ci:build": "rushx build",
56
+ "coverage": "vitest run --coverage",
57
+ "lint:fix": "eslint . --fix",
58
+ "lint": "lint-staged --no-stash 2>&1",
59
+ "sbdev": "rush sbdev",
60
+ "test:unit": "vitest run --passWithNoTests",
61
+ "test:watch": "vitest --watch",
62
+ "test": "rushx test:unit",
63
+ "typecheck": "tsc -p ./tsconfig.check.json"
64
+ }
65
+ }
package/readme.mdx ADDED
@@ -0,0 +1,61 @@
1
+ import { Meta, Stories, ArgTypes, Primary, Subtitle } from "@storybook/blocks";
2
+
3
+ import * as TextFieldStories from "./src/text-field.stories";
4
+ import packageInfo from "./package.json";
5
+
6
+ <Meta name="Docs" title="Components/TextField" of={TextFieldStories} />
7
+
8
+ # TextField
9
+
10
+ <Subtitle>Version {packageInfo.version}</Subtitle>
11
+
12
+ ### Showcase
13
+
14
+ <Primary />
15
+
16
+ ### Properties
17
+
18
+ Except for the props below, [all "native" input attributs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) are also valid props. The only exceptions are:
19
+
20
+ - `type` - Restricted to `"email" | "number" | "password" | "search" | "tel" | "text"`
21
+ - `id` - Required
22
+
23
+ <ArgTypes />
24
+
25
+ ### Installation
26
+
27
+ #### Via NPM
28
+
29
+ Add the dependency to your consumer app like `"@purpurds/text-field": "x.y.z"`
30
+
31
+ #### From outside the monorepo (build-time)
32
+
33
+ To install this package, you need to setup access to the artifactory. [Click here to go to the guide on how to do that](https://github.com/telia-company/jfrog-documentation/blob/main/doc/JFrog/JFrog_Onboarding.md#getting-access-to-artifactory-and-other-jfrog-applications).
34
+
35
+ ---
36
+
37
+ In MyApp.tsx
38
+
39
+ ```tsx
40
+ import "@purpurds/tokens/index.css";
41
+ ```
42
+
43
+ and
44
+
45
+ ```tsx
46
+ import "@purpurds/text-field/styles";
47
+ ```
48
+
49
+ In MyComponent.tsx
50
+
51
+ ```tsx
52
+ import { TextField } from "@purpurds/text-field";
53
+
54
+ export const MyComponent = () => {
55
+ return (
56
+ <div>
57
+ <TextField {...someProps}>Some content</TextField>
58
+ </div>
59
+ );
60
+ };
61
+ ```
@@ -0,0 +1,4 @@
1
+ declare module "*.scss" {
2
+ const styles: { [className: string]: string };
3
+ export default styles;
4
+ }
@@ -0,0 +1,143 @@
1
+ .purpur-text-field {
2
+ $root: &;
3
+
4
+ position: relative;
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: var(--purpur-spacing-50);
8
+
9
+ &__label {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: var(--purpur-spacing-50);
13
+ width: fit-content;
14
+ }
15
+
16
+ &__label-text {
17
+ width: fit-content;
18
+ }
19
+
20
+ &__field-row {
21
+ display: flex;
22
+ width: 100%;
23
+ }
24
+
25
+ &__frame {
26
+ position: absolute;
27
+ inset: 0;
28
+ border-radius: var(--purpur-border-radius-sm);
29
+ border: var(--purpur-border-width-xs) solid var(--purpur-color-border-interactive-subtle);
30
+ pointer-events: none;
31
+ }
32
+
33
+ &__input-container {
34
+ position: relative;
35
+ display: flex;
36
+ align-items: center;
37
+ width: 100%;
38
+ border-radius: var(--purpur-border-radius-sm);
39
+ background: var(--purpur-color-background-primary);
40
+
41
+ &--disabled {
42
+ background: var(--purpur-color-background-interactive-disabled);
43
+ }
44
+
45
+ &--readonly {
46
+ background: var(--purpur-color-background-interactive-read-only);
47
+ }
48
+
49
+ &--end-adornment {
50
+ padding-right: var(--purpur-spacing-150);
51
+
52
+ #{$root}__input {
53
+ padding-right: var(--purpur-spacing-100);
54
+ }
55
+ }
56
+
57
+ &--start-adornment {
58
+ padding-left: var(--purpur-spacing-150);
59
+
60
+ #{$root}__input {
61
+ padding-left: var(--purpur-spacing-100);
62
+ }
63
+ }
64
+ }
65
+
66
+ &__input {
67
+ $inputRoot: &;
68
+
69
+ border-radius: var(--purpur-border-radius-sm);
70
+ padding: calc(var(--purpur-spacing-100) + var(--purpur-spacing-25)) var(--purpur-spacing-150);
71
+ border: none;
72
+ width: 100%;
73
+ box-sizing: border-box;
74
+ font-family: var(--purpur-typography-family-default), Helvetica, Arial, "Lucida Grande",
75
+ sans-serif;
76
+ font-size: var(--purpur-typography-scale-100);
77
+ line-height: 150%;
78
+ outline: none;
79
+ background: transparent;
80
+
81
+ &:hover {
82
+ ~ #{$root}__frame {
83
+ border-width: var(--purpur-border-width-sm);
84
+ border-color: var(--purpur-color-border-interactive-subtle-hover);
85
+ }
86
+ }
87
+
88
+ &:active:not(:disabled):not(:read-only),
89
+ &:focus:not(:disabled):not(:read-only) {
90
+ ~ #{$root}__frame {
91
+ outline: var(--purpur-border-width-sm) solid var(--purpur-color-border-interactive-focus);
92
+ outline-offset: calc(var(--purpur-spacing-10) * 2);
93
+ border-width: var(--purpur-border-width-xs);
94
+ border-color: var(--purpur-color-border-interactive-subtle-hover);
95
+ }
96
+ }
97
+
98
+ &:disabled {
99
+ color: var(--purpur-color-text-weak);
100
+
101
+ ~ #{$root}__frame {
102
+ border-width: var(--purpur-border-width-xs);
103
+ border-color: var(--purpur-color-border-medium);
104
+ }
105
+ }
106
+
107
+ &:read-only:not(:disabled) {
108
+ color: var(--purpur-color-text-default);
109
+
110
+ ~ #{$root}__frame {
111
+ border-width: var(--purpur-border-width-xs);
112
+ }
113
+
114
+ &:not(#{$inputRoot}--valid):not(#{$inputRoot}--error) {
115
+ ~ #{$root}__frame {
116
+ border-color: var(--purpur-color-border-medium);
117
+ }
118
+ }
119
+ }
120
+
121
+ &#{$inputRoot}--valid {
122
+ ~ #{$root}__frame {
123
+ border-color: var(--purpur-color-border-status-success);
124
+ }
125
+ }
126
+
127
+ &#{$inputRoot}--error:not(:hover) {
128
+ ~ #{$root}__frame {
129
+ border-color: var(--purpur-color-border-status-error);
130
+ }
131
+ }
132
+ }
133
+
134
+ &__adornment-container {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: var(--purpur-spacing-50);
138
+ }
139
+
140
+ &__valid-icon {
141
+ color: var(--purpur-color-text-status-success-medium);
142
+ }
143
+ }
@@ -0,0 +1,71 @@
1
+ import React from "react";
2
+ import { useArgs } from "@storybook/client-api";
3
+ import type { Meta, StoryObj } from "@storybook/react";
4
+
5
+ import "@purpurds/label/styles";
6
+ import "@purpurds/field-helper-text/styles";
7
+ import "@purpurds/field-error-text/styles";
8
+ import "@purpurds/icon/styles";
9
+ import "@purpurds/spinner/styles";
10
+ import { TextField } from "./text-field";
11
+
12
+ const meta: Meta<typeof TextField> = {
13
+ title: "Inputs/TextField",
14
+ component: TextField,
15
+ };
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof TextField>;
19
+
20
+ export const Showcase: Story = {
21
+ args: {
22
+ value: "Text field",
23
+ label: "Text field label",
24
+ helperText: "Helper text",
25
+ placeholder: "Enter text",
26
+ valid: false,
27
+ errorText: undefined,
28
+ loading: false,
29
+ disabled: false,
30
+ readOnly: false,
31
+ required: false,
32
+ type: "text",
33
+ },
34
+ argTypes: {
35
+ onChange: { action: "inputChange", table: { disable: true } },
36
+ type: { options: ["email", "number", "password", "search", "tel", "text"], control: "select" },
37
+ startAdornment: { table: { disable: true } },
38
+ endAdornment: { table: { disable: true } },
39
+ afterField: { table: { disable: true } },
40
+ },
41
+ parameters: {
42
+ design: [
43
+ {
44
+ name: "TextField",
45
+ type: "figma",
46
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?node-id=1528%3A1886",
47
+ },
48
+ ],
49
+ },
50
+ decorators: [
51
+ (Story) => (
52
+ <div style={{ maxWidth: "18.5rem" }}>
53
+ <Story />
54
+ </div>
55
+ ),
56
+ ],
57
+ render: ({ onChange, ...args }) => {
58
+ const [{ value }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
59
+ return (
60
+ <TextField
61
+ {...args}
62
+ id="playground-input"
63
+ value={value}
64
+ onChange={(e) => {
65
+ onChange?.(e);
66
+ updateArgs({ value: e.target.value });
67
+ }}
68
+ />
69
+ );
70
+ },
71
+ };
@@ -0,0 +1,218 @@
1
+ import React from "react";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+ import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+
6
+ import { TextField } from "./text-field";
7
+ const rootClassName = "purpur-text-field";
8
+
9
+ expect.extend(matchers);
10
+
11
+ describe("TextField", () => {
12
+ afterEach(cleanup);
13
+
14
+ it("should render plain", () => {
15
+ render(<TextField id="test" data-testid="test" />);
16
+
17
+ const input = screen.getByTestId("test-input");
18
+ expect(input).toBeInTheDocument();
19
+ expect(input.className).toBe(`${rootClassName}__input`);
20
+ expect(screen.queryByTestId("test-label")).not.toBeInTheDocument();
21
+ expect(screen.queryByTestId("test-helper-text")).not.toBeInTheDocument();
22
+ expect(screen.queryByTestId("test-start-adornments")).not.toBeInTheDocument();
23
+ expect(screen.queryByTestId("test-endrootClassName-adornments")).not.toBeInTheDocument();
24
+ });
25
+
26
+ it("should render with label", () => {
27
+ render(<TextField id="test" data-testid="test" label="Test label" />);
28
+
29
+ const input = screen.getByTestId("test-input");
30
+ expect(input).toBeInTheDocument();
31
+ expect(input.className).toBe(`${rootClassName}__input`);
32
+ expect(screen.getByTestId("test-label")).toHaveTextContent("Test label");
33
+ expect(screen.queryByTestId("test-helper-text")).not.toBeInTheDocument();
34
+ expect(screen.queryByTestId("test-start-adornments")).not.toBeInTheDocument();
35
+ expect(screen.queryByTestId("test-end-adornments")).not.toBeInTheDocument();
36
+ });
37
+
38
+ it("should render with helper-text", () => {
39
+ render(<TextField id="test" data-testid="test" label="Test label" helperText="Helper text" />);
40
+
41
+ const input = screen.getByTestId("test-input");
42
+ expect(input).toBeInTheDocument();
43
+ expect(input.className).toBe(`${rootClassName}__input`);
44
+ expect(screen.getByTestId("test-label")).toHaveTextContent("Test label");
45
+ expect(screen.getByTestId("test-helper-text")).toHaveTextContent("Helper text");
46
+ expect(screen.getByTestId("test-helper-text").querySelector("svg")).not.toBeInTheDocument();
47
+ expect(screen.queryByTestId("test-start-adornments")).not.toBeInTheDocument();
48
+ expect(screen.queryByTestId("test-end-adornments")).not.toBeInTheDocument();
49
+ });
50
+
51
+ it("should render with helper-text and error-text", () => {
52
+ render(
53
+ <TextField
54
+ errorText="Test error"
55
+ id="test"
56
+ data-testid="test"
57
+ label="Test label"
58
+ helperText="Helper text"
59
+ />
60
+ );
61
+
62
+ const input = screen.getByTestId("test-input");
63
+ expect(input).toBeInTheDocument();
64
+ expect(input).toHaveClass(`${rootClassName}__input--error`);
65
+ expect(input).not.toHaveClass(`${rootClassName}__input--valid`);
66
+ expect(screen.getByTestId("test-label")).toHaveTextContent("Test label");
67
+ expect(screen.getByTestId("test-helper-text")).toHaveTextContent("Helper text");
68
+ expect(screen.getByTestId("test-error-text")).toHaveTextContent("Test error");
69
+ expect(screen.getByTestId("test-error-text").querySelector("svg")).toBeInTheDocument();
70
+ expect(screen.queryByTestId("test-start-adornments")).not.toBeInTheDocument();
71
+ expect(screen.queryByTestId("test-end-adornments")).not.toBeInTheDocument();
72
+ });
73
+
74
+ it("should render valid", () => {
75
+ render(
76
+ <TextField valid id="test" data-testid="test" label="Test label" helperText="Helper text" />
77
+ );
78
+
79
+ const input = screen.getByTestId("test-input");
80
+ expect(input).toBeInTheDocument();
81
+ expect(input).not.toHaveClass(`${rootClassName}__input--error`);
82
+ expect(input).toHaveClass(`${rootClassName}__input--valid`);
83
+ expect(screen.getByTestId("test-label")).toHaveTextContent("Test label");
84
+ expect(screen.getByTestId("test-helper-text")).toHaveTextContent("Helper text");
85
+ expect(screen.getByTestId("test-helper-text").querySelector("svg")).not.toBeInTheDocument();
86
+ expect(screen.queryByTestId("test-start-adornments")).not.toBeInTheDocument();
87
+ expect(screen.getByTestId("test-end-adornments")).toBeInTheDocument();
88
+ expect(screen.getByTestId("test-valid-icon")).toBeInTheDocument();
89
+ });
90
+
91
+ it("should render loading", () => {
92
+ render(
93
+ <TextField loading id="test" data-testid="test" label="Test label" helperText="Helper text" />
94
+ );
95
+
96
+ const input = screen.getByTestId("test-input");
97
+ expect(input).toBeInTheDocument();
98
+ expect(input).not.toHaveClass(`${rootClassName}__input--error`);
99
+ expect(input).not.toHaveClass(`${rootClassName}__input--valid`);
100
+ expect(screen.getByTestId("test-label")).toHaveTextContent("Test label");
101
+ expect(screen.getByTestId("test-helper-text")).toHaveTextContent("Helper text");
102
+ expect(screen.getByTestId("test-helper-text").querySelector("svg")).not.toBeInTheDocument();
103
+ expect(screen.queryByTestId("test-start-adornments")).not.toBeInTheDocument();
104
+ expect(screen.getByTestId("test-end-adornments")).toBeInTheDocument();
105
+ expect(screen.getByTestId("test-spinner")).toBeInTheDocument();
106
+ });
107
+
108
+ it("should render with adornment", () => {
109
+ render(
110
+ <TextField
111
+ startAdornment={<span data-testid="start-adornment" />}
112
+ endAdornment={<span data-testid="end-adornment" />}
113
+ id="test"
114
+ data-testid="test"
115
+ label="Test label"
116
+ helperText="Helper text"
117
+ />
118
+ );
119
+
120
+ const input = screen.getByTestId("test-input");
121
+ expect(input).toBeInTheDocument();
122
+ expect(input).not.toHaveClass(`${rootClassName}__input--error`);
123
+ expect(input).not.toHaveClass(`${rootClassName}__input--valid`);
124
+ expect(screen.getByTestId("test-label")).toHaveTextContent("Test label");
125
+ expect(screen.getByTestId("test-helper-text")).toHaveTextContent("Helper text");
126
+ expect(screen.getByTestId("test-helper-text").querySelector("svg")).not.toBeInTheDocument();
127
+
128
+ const startAdornment = screen.getByTestId("test-start-adornments");
129
+ expect(startAdornment).toBeInTheDocument();
130
+ expect(within(startAdornment).getByTestId("start-adornment")).toBeInTheDocument();
131
+
132
+ const endAdornment = screen.getByTestId("test-end-adornments");
133
+ expect(endAdornment).toBeInTheDocument();
134
+ expect(within(endAdornment).getByTestId("end-adornment")).toBeInTheDocument();
135
+ });
136
+
137
+ it("should render required", () => {
138
+ render(
139
+ <TextField
140
+ required
141
+ id="test"
142
+ data-testid="test"
143
+ label="Test label"
144
+ helperText="Helper text"
145
+ />
146
+ );
147
+
148
+ const input = screen.getByTestId("test-input");
149
+ expect(input).toBeRequired();
150
+ expect(input).not.toHaveClass(`${rootClassName}__input--error`);
151
+ expect(input).not.toHaveClass(`${rootClassName}__input--valid`);
152
+ expect(screen.getByTestId("test-label")).toHaveTextContent("* Test label");
153
+ expect(screen.getByTestId("test-helper-text")).toHaveTextContent("Helper text");
154
+ expect(screen.queryByTestId("test-start-adornments")).not.toBeInTheDocument();
155
+ expect(screen.queryByTestId("test-end-adornments")).not.toBeInTheDocument();
156
+ });
157
+
158
+ it("should render disabled", () => {
159
+ render(
160
+ <TextField
161
+ disabled
162
+ id="test"
163
+ data-testid="test"
164
+ label="Test label"
165
+ helperText="Helper text"
166
+ />
167
+ );
168
+
169
+ const input = screen.getByTestId("test-input");
170
+ expect(input).toBeDisabled();
171
+ expect(input).not.toHaveClass(`${rootClassName}__input--error`);
172
+ expect(input).not.toHaveClass(`${rootClassName}__input--valid`);
173
+ expect(screen.getByTestId("test-label")).toHaveTextContent("Test label");
174
+ expect(screen.getByTestId("test-helper-text")).toHaveTextContent("Helper text");
175
+ expect(screen.queryByTestId("test-start-adornments")).not.toBeInTheDocument();
176
+ expect(screen.queryByTestId("test-end-adornments")).not.toBeInTheDocument();
177
+ });
178
+
179
+ it("should render readOnly", () => {
180
+ render(
181
+ <TextField
182
+ readOnly
183
+ id="test"
184
+ data-testid="test"
185
+ label="Test label"
186
+ helperText="Helper text"
187
+ />
188
+ );
189
+
190
+ const input = screen.getByTestId("test-input");
191
+ expect(input).toHaveAttribute("readOnly");
192
+ expect(input).not.toHaveClass(`${rootClassName}__input--error`);
193
+ expect(input).not.toHaveClass(`${rootClassName}__input--valid`);
194
+ expect(screen.getByTestId("test-label")).toHaveTextContent("Test label");
195
+ expect(screen.getByTestId("test-helper-text")).toHaveTextContent("Helper text");
196
+ expect(screen.queryByTestId("test-start-adornments")).not.toBeInTheDocument();
197
+ expect(screen.queryByTestId("test-end-adornments")).not.toBeInTheDocument();
198
+ });
199
+
200
+ it("should render with default value and onChange", () => {
201
+ const onChangeMock = vi.fn();
202
+ render(
203
+ <TextField
204
+ id="test"
205
+ data-testid="test"
206
+ onChange={onChangeMock}
207
+ defaultValue="Default value"
208
+ />
209
+ );
210
+ const input = screen.getByTestId("test-input");
211
+ expect(input).toHaveValue("Default value");
212
+
213
+ fireEvent.change(input, { target: { value: "Changed" } });
214
+
215
+ expect(input).toHaveValue("Changed");
216
+ expect(onChangeMock).toHaveBeenCalled();
217
+ });
218
+ });