@sats-group/ui-lib 75.10.0 → 76.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sats-group/ui-lib",
3
- "version": "75.10.0",
3
+ "version": "76.0.0",
4
4
  "description": "SATS web user interface library",
5
5
  "engines": {
6
6
  "node": "^18 || ^20",
@@ -7,53 +7,72 @@
7
7
  .text-input {
8
8
  $block: &;
9
9
  $line-height: 1;
10
- $icon-width: 24px;
11
- $icon-spacing: spacing.$xs;
12
10
  $vertical-padding-xs: spacing.$xs;
13
11
  $vertical-padding-s: spacing.$s;
14
12
 
15
13
  &--variant-small {
16
- #{$block}__input {
14
+ #{$block}__input-wrapper {
17
15
  padding: $vertical-padding-xs spacing.$s;
18
- }
19
-
20
- #{$block}__icon {
21
- bottom: $vertical-padding-xs;
16
+ gap: spacing.$s;
22
17
  }
23
18
  }
24
19
 
25
20
  &--variant-large {
26
- #{$block}__input {
21
+ #{$block}__input-wrapper {
27
22
  padding: $vertical-padding-s spacing.$m;
28
- }
29
-
30
- #{$block}__icon {
31
- bottom: $vertical-padding-s;
23
+ gap: spacing.$m;
32
24
  }
33
25
  }
34
26
 
35
27
  &__wrapper {
36
28
  position: relative;
37
29
  display: flex;
38
- flex-direction: column-reverse;
30
+ flex-direction: column;
31
+ gap: spacing.$xxs;
39
32
  }
40
33
 
41
- &__input {
42
- @include font-sizes.normal(input);
34
+ &__length-counter {
35
+ color: light.$on-background-primary-alternate;
36
+ }
37
+
38
+ &__label-wrapper {
39
+ display: flex;
40
+ justify-content: space-between;
41
+ }
42
+
43
+ &__input-wrapper {
43
44
  border: 1px solid light.$ge-divider-default;
44
45
  border-radius: corner-radius.$s;
45
- line-height: $line-height;
46
+ display: flex;
47
+ align-items: center;
46
48
  background-color: light.$surface-primary-default;
47
49
  color: light.$on-surface-primary-default;
48
50
  width: 100%;
49
51
  box-sizing: border-box;
50
52
 
51
- #{$block}--icon & {
52
- padding-left: $icon-spacing * 2 + $icon-width;
53
+ &:focus-within {
54
+ border-color: light.$ge-border-focused;
55
+ outline: none;
53
56
  }
54
57
 
58
+ @media (hover: hover) {
59
+ &:hover {
60
+ background-color: light.$surface-primary-hover;
61
+ cursor: text;
62
+ }
63
+ }
64
+ }
65
+
66
+ &__input {
67
+ @include font-sizes.normal(input);
68
+ line-height: $line-height;
69
+ width: inherit;
70
+ border: none;
71
+ padding: 0;
72
+ background-color: transparent;
73
+
55
74
  &:focus {
56
- border-color: light.$ge-border-focused;
75
+ border-color: none;
57
76
  outline: none;
58
77
  }
59
78
 
@@ -88,96 +107,35 @@
88
107
  }
89
108
  }
90
109
 
91
- &__icon {
92
- color: light.$on-surface-primary-disabled;
93
- left: $icon-spacing;
94
- height: $icon-width;
95
- pointer-events: none;
96
- position: absolute;
97
- width: $icon-width;
98
-
99
- svg {
100
- display: block;
101
- height: 100%;
102
- width: 100%;
103
- }
104
- }
105
-
106
- &--moving-label {
107
- #{$block}__input {
108
- padding: spacing.$s spacing.$m;
109
- }
110
-
111
- #{$block}__label {
112
- position: absolute;
113
- z-index: 1;
114
- top: 0;
115
- margin: 0;
116
- padding: 0 6px;
117
- transform: translate(10px, spacing.$s);
118
- transition: transform 0.1s cubic-bezier(0.22, 0.57, 0.25, 1);
119
- transform-origin: left center;
120
- max-width: calc(100% - 25px);
121
- overflow: hidden;
122
- text-overflow: ellipsis;
123
- white-space: nowrap;
124
- line-height: $line-height;
125
-
126
- @media (prefers-reduced-motion) {
127
- transition: transform 0s;
128
- }
129
-
130
- &::before {
131
- content: '';
132
- position: absolute;
133
- z-index: -1;
134
- left: 0;
135
- bottom: 0;
136
- height: 50%;
137
- width: 100%;
138
- background-color: light.$surface-primary-default;
139
- }
140
- }
141
-
142
- &.text-input--moving-label {
143
- &.text-input--theme-dark {
144
- #{$block}__label {
145
- color: light.$on-fixed-surface-primary-default;
146
- }
147
-
148
- #{$block}__label {
149
- &::before {
150
- content: '';
151
- background-color: light.$fixed-surface-primary-default;
152
- }
153
- }
154
- }
155
- }
110
+ &__help,
111
+ &__error {
112
+ display: flex;
113
+ gap: spacing.$xs;
156
114
 
157
- & #{$block}__input:focus,
158
- & #{$block}__input:not(:placeholder-shown) {
159
- ~ #{$block}__label {
160
- transform: translate(10px, -50%) scale(0.8);
161
- }
115
+ > * {
116
+ align-self: flex-start;
162
117
  }
163
118
  }
164
119
 
165
120
  &__help {
166
- display: flex;
167
- align-items: center;
168
- gap: spacing.$xs;
169
- margin-top: spacing.$xxs;
170
- color: light.$on-background-primary-disabled;
121
+ color: light.$on-surface-primary-alternate;
171
122
  }
172
123
 
173
124
  &__error {
174
- display: flex;
175
- align-items: center;
176
- gap: spacing.$xs;
177
- margin-top: spacing.$xxs;
178
125
  color: light.$on-surface-error;
179
126
  }
180
127
 
128
+ &__help-icon,
129
+ &__error-icon,
130
+ &__icon {
131
+ flex-shrink: 0;
132
+ }
133
+
134
+ &__help-icon,
135
+ &__error-icon {
136
+ height: 16px; // Matches icon height. Otherwise, the auto height acts weirdly, by making the wrapper too tall.
137
+ }
138
+
181
139
  &__asterisk {
182
140
  color: light.$on-surface-featured;
183
141
  margin-left: spacing.$xs;
@@ -189,6 +147,28 @@
189
147
  #{$block}__help {
190
148
  color: light.$on-background-primary-disabled;
191
149
  }
150
+
151
+ #{$block}__input-wrapper {
152
+ background-color: light.$surface-primary-disabled;
153
+ border-color: light.$surface-primary-disabled;
154
+ cursor: auto;
155
+ }
156
+ }
157
+
158
+ &--error {
159
+ #{$block}__icon {
160
+ color: light.$on-surface-error;
161
+ }
162
+
163
+ #{$block}__input-wrapper {
164
+ outline: 2px solid light.$ge-signal-error;
165
+ outline-offset: -2px;
166
+
167
+ &:focus {
168
+ outline: 2px solid light.$ge-signal-error;
169
+ outline-offset: -2px;
170
+ }
171
+ }
192
172
  }
193
173
 
194
174
  &--theme-dark {
@@ -200,19 +180,25 @@
200
180
  color: light.$on-fixed-background-primary-default;
201
181
  }
202
182
 
183
+ #{$block}__length-counter,
203
184
  #{$block}__help {
204
- color: light.$on-fixed-background-primary-disabled;
185
+ color: light.$on-background-primary-alternate;
205
186
  }
206
187
 
207
188
  #{$block}__input {
189
+ background-color: transparent;
190
+ }
191
+
192
+ #{$block}__input-wrapper {
208
193
  background-color: light.$fixed-surface-secondary-default;
209
194
  border-color: light.$ge-border-default;
210
195
  color: light.$on-fixed-surface-primary-alternate;
211
196
 
212
- &:focus {
213
- background-color: light.$fixed-surface-primary-default;
197
+ &:focus-within {
198
+ background: light.$fixed-surface-primary-default;
214
199
  color: light.$on-fixed-surface-primary-default;
215
200
  outline: none;
201
+ border-color: light.$ge-border-focused;
216
202
 
217
203
  ~ #{$block}__icon {
218
204
  color: light.$on-fixed-surface-primary-default;
@@ -227,33 +213,39 @@
227
213
  &[disabled]::placeholder {
228
214
  color: light.$on-fixed-surface-primary-disabled;
229
215
  }
216
+
217
+ @media (hover: hover) {
218
+ &:hover {
219
+ background-color: light.$fixed-surface-primary-hover;
220
+ cursor: text;
221
+ }
222
+ }
223
+ }
224
+
225
+ #{$block}__input {
226
+ background-color: transparent;
227
+ color: light.$on-fixed-surface-primary-default;
230
228
  }
231
229
 
232
230
  &#{$block}--disabled {
233
231
  #{$block}__label,
234
232
  #{$block}__icon,
235
233
  #{$block}__help {
236
- color: light.$on-fixed-background-primary-disabled;
234
+ color: light.$on-fixed-surface-primary-disabled;
237
235
  }
238
236
 
239
237
  &#{$block}--error #{$block}__icon {
240
238
  color: light.$on-fixed-surface-error;
241
239
  }
242
- }
243
- }
244
240
 
245
- &--error {
246
- #{$block}__icon {
247
- color: light.$on-surface-error;
248
- }
249
-
250
- #{$block}__input {
251
- outline: 2px solid light.$ge-signal-error;
252
- outline-offset: -2px;
241
+ #{$block}__input-wrapper {
242
+ background-color: light.$fixed-surface-primary-selected;
243
+ border-color: light.$on-fixed-surface-primary-disabled;
244
+ cursor: auto;
245
+ }
253
246
 
254
- &:focus {
255
- outline: 2px solid light.$ge-signal-error;
256
- outline-offset: -2px;
247
+ #{$block}__input {
248
+ background-color: transparent;
257
249
  }
258
250
  }
259
251
  }
@@ -16,28 +16,40 @@ const RefTextInput = React.forwardRef<HTMLInputElement, Props>(
16
16
  defaultValue,
17
17
  disabled,
18
18
  hasError,
19
- hasMovingLabel,
20
19
  helpText,
21
- hiddenLabel,
22
- icon,
23
20
  label,
21
+ leadingIcon,
22
+ maxLength,
24
23
  name,
25
24
  onChange = () => {},
26
25
  placeholder,
27
26
  required,
28
27
  theme,
28
+ trailingIcon,
29
29
  type = 'text',
30
30
  variant = variants.large,
31
31
  ...restProps
32
32
  },
33
33
  ref,
34
34
  ) => {
35
+ const [count, setCount] = React.useState(0);
35
36
  const [isError, setIsError] = React.useState(hasError);
36
37
  const [validationOnChange, onInvalid, error] = useInputValidation(
37
38
  customErrorMessages,
38
39
  customErrorMessages ? customErrorMessages.defaultError : undefined,
39
40
  isError,
40
41
  );
42
+ const isMaxLengthValid = maxLength ? maxLength > 0 : undefined;
43
+
44
+ React.useEffect(() => {
45
+ if (defaultValue) {
46
+ if (typeof defaultValue === 'string') {
47
+ setCount(defaultValue.length);
48
+ } else if (typeof defaultValue === 'number') {
49
+ setCount(defaultValue);
50
+ }
51
+ }
52
+ }, [restProps]);
41
53
 
42
54
  useEffect(() => {
43
55
  setIsError(hasError);
@@ -50,38 +62,13 @@ const RefTextInput = React.forwardRef<HTMLInputElement, Props>(
50
62
  'text-input--theme-light': theme === themes.light,
51
63
  'text-input--disabled': disabled,
52
64
  'text-input--error': error || isError,
53
- 'text-input--moving-label': icon ? false : hasMovingLabel,
54
- 'text-input--icon': icon,
65
+ 'text-input--icon': leadingIcon || trailingIcon,
55
66
  'text-input--variant-small': variant === variants.small,
56
67
  'text-input--variant-large': variant === variants.large,
57
68
  })}
58
69
  >
59
70
  <div className="text-input__wrapper">
60
- <input
61
- {...restProps}
62
- className="text-input__input"
63
- defaultValue={defaultValue}
64
- disabled={disabled}
65
- required={required}
66
- name={name}
67
- onInvalid={e => onInvalid(e)}
68
- onChange={e => {
69
- onChange(e);
70
- validationOnChange(e);
71
- setIsError(false); // NOTE: We want to reset error state on change to not confuse users.
72
- }}
73
- // NOTE: Using " " as placeholder for moving label theme to enable using `:placeholder-shown` to determine when to move the label
74
- placeholder={
75
- icon ? placeholder : hasMovingLabel ? ' ' : placeholder
76
- }
77
- ref={ref}
78
- type={type}
79
- aria-label={icon || hiddenLabel ? label : undefined}
80
- />
81
-
82
- {icon ? (
83
- <div className="text-input__icon">{icon}</div>
84
- ) : hiddenLabel ? null : (
71
+ <div className="text-input__label-wrapper">
85
72
  <Text
86
73
  className="text-input__label"
87
74
  theme={Text.themes.emphasis}
@@ -94,22 +81,69 @@ const RefTextInput = React.forwardRef<HTMLInputElement, Props>(
94
81
  <span className="text-input__asterisk">*</span>
95
82
  ) : null}
96
83
  </Text>
97
- )}
98
- </div>
99
-
100
- {helpText ? (
101
- <div className="text-input__help">
102
- <SvgInfo />
103
- <Text size={Text.sizes.interface}>{helpText}</Text>
84
+ {isMaxLengthValid ? (
85
+ <div className="text-input__length-counter">
86
+ <Text
87
+ size={
88
+ variant === variants.small
89
+ ? Text.sizes.small
90
+ : Text.sizes.basic
91
+ }
92
+ >
93
+ {count}/{maxLength}
94
+ </Text>
95
+ </div>
96
+ ) : null}
104
97
  </div>
105
- ) : null}
106
- {/* NOTE: This is aria-hidden because reporting of validation errors is handled by the browser */}
107
- {error ? (
108
- <div aria-hidden="true" className="text-input__error">
109
- <SvgError />
110
- <Text size={Text.sizes.interface}>{error}</Text>
98
+
99
+ <div className="text-input__input-wrapper">
100
+ {leadingIcon ? (
101
+ <div className="text-input__icon">{leadingIcon}</div>
102
+ ) : null}
103
+ <input
104
+ className="text-input__input"
105
+ defaultValue={defaultValue}
106
+ disabled={disabled}
107
+ required={required}
108
+ name={name}
109
+ maxLength={isMaxLengthValid ? maxLength : undefined}
110
+ onInvalid={e => onInvalid(e)}
111
+ onChange={e => {
112
+ onChange(e);
113
+ validationOnChange(e);
114
+ setIsError(false); // NOTE: We want to reset error state on change to not confuse users.
115
+ if (isMaxLengthValid) {
116
+ setCount(e.target.value.length);
117
+ }
118
+ }}
119
+ placeholder={placeholder}
120
+ ref={ref}
121
+ type={type}
122
+ aria-label={label}
123
+ {...restProps}
124
+ />
125
+ {trailingIcon ? (
126
+ <div className="text-input__icon">{trailingIcon}</div>
127
+ ) : null}
111
128
  </div>
112
- ) : null}
129
+ {helpText ? (
130
+ <div className="text-input__help">
131
+ <div className="text-input__help-icon">
132
+ <SvgInfo />
133
+ </div>
134
+ <Text size={Text.sizes.interface}>{helpText}</Text>
135
+ </div>
136
+ ) : null}
137
+ {/* NOTE: This is aria-hidden because reporting of validation errors is handled by the browser */}
138
+ {error ? (
139
+ <div aria-hidden="true" className="text-input__error">
140
+ <div className="text-input__error-icon">
141
+ <SvgError />
142
+ </div>
143
+ <Text size={Text.sizes.interface}>{error}</Text>
144
+ </div>
145
+ ) : null}
146
+ </div>
113
147
  </label>
114
148
  );
115
149
  },
@@ -14,12 +14,11 @@ export const variants = {
14
14
  export type TextInput = {
15
15
  customErrorMessages?: Messages;
16
16
  hasError?: boolean;
17
- hasMovingLabel?: boolean;
18
17
  helpText?: string;
19
- hiddenLabel?: boolean;
20
- icon?: React.ReactNode;
21
18
  label: string;
19
+ leadingIcon?: React.ReactNode;
22
20
  name: string;
23
21
  theme?: ObjectValues<typeof themes>;
22
+ trailingIcon?: React.ReactNode;
24
23
  variant?: ObjectValues<typeof variants>;
25
24
  } & InputHtmlProps;