Skip to content

Commit ffd4e55

Browse files
committed
QA Feedback
1 parent ae83579 commit ffd4e55

6 files changed

Lines changed: 96 additions & 48 deletions

File tree

static/css/v3/forms.css

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@
2020
color: var(--color-text-primary);
2121
}
2222

23+
.field__label-row {
24+
display: flex;
25+
flex-direction: row;
26+
justify-content: space-between;
27+
height: 100%;
28+
width: 100%;
29+
}
30+
31+
.field__max-chars {
32+
color: var(--color-text-primary);
33+
padding-right: 10px;
34+
35+
font-family: var(--font-sans);
36+
font-size: var(--font-size-xs);
37+
font-weight: var(--font-weight-medium);
38+
line-height: var(--line-height-default);
39+
letter-spacing: var(--letter-spacing-tight);
40+
}
41+
2342
.field__control {
2443
display: flex;
2544
align-items: center;
@@ -311,7 +330,7 @@
311330
border-bottom-right-radius: 0;
312331
}
313332

314-
.dropdown--disabled .dropdown__trigger{
333+
.dropdown--disabled .dropdown__trigger {
315334
color: var(--color-text-tertiary);
316335
background: var(--color-surface-mid);
317336
cursor: not-allowed;

templates/v3/includes/_field_include.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010

1111
{% with type=bound_field.widget_type field=bound_field.field widget=bound_field.field.widget %}
1212
{% if type == 'email' %}
13-
{% include 'v3/includes/_field_text.html' with name=bound_field.name value=bound_field.value type='email' label=bound_field.label required=field.required disabled=field.disabled help_text=field.help_text placeholder=widget.attrs.placeholder only %}
13+
{% include 'v3/includes/_field_text.html' with name=bound_field.name value=bound_field.value type='email' label=bound_field.label required=field.required disabled=field.disabled help_text=field.help_text placeholder=widget.attrs.placeholder validate_type="email" only %}
1414
{% elif type == 'text' %}
15-
{% include 'v3/includes/_field_text.html' with name=bound_field.name value=bound_field.value type='text' label=bound_field.label required=field.required disabled=field.disabled help_text=field.help_text placeholder=widget.attrs.placeholder only %}
15+
{% include 'v3/includes/_field_text.html' with name=bound_field.name value=bound_field.value type='text' label=bound_field.label required=field.required disabled=field.disabled help_text=field.help_text placeholder=widget.attrs.placeholder max_chars=field.max_length display_max_chars=widget.attrs.display_max_chars only %}
1616
{% elif type == 'checkbox' %}
1717
{% include 'v3/includes/_field_checkbox.html' with name=bound_field.name label=bound_field.label required=field.required disabled=field.disabled help_text=field.help_text checked=bound_field.value only %}
1818
{% elif type == 'checkboxselectmultiple' %}

templates/v3/includes/_field_text.html

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
11
{% comment %}
22
V3 text input field.
33
Variables:
4-
name (required) — input name attribute
5-
label (optional) — label text
6-
placeholder (optional) — placeholder text
7-
value (optional) — pre-filled value; with submit_icon, also seeds the Alpine x-model state so it survives init
8-
type (optional) — input type, default "text"
9-
help_text (optional) — help text below the field
10-
error (optional) — static server-side error message, rendered on page load
11-
alpine_error (optional) — Alpine expression that resolves to an error string for dynamic
12-
server-driven errors without a page reload (e.g. "errors.title")
13-
validate_type (optional) — client-side validation before submit, currently supports "email"
14-
validate_error (optional) — error message shown when validate_type check fails
15-
icon_left (optional) — icon name for left slot (e.g. "search")
16-
icon_right (optional) — icon name for a decorative right-slot icon (non-interactive); mutually exclusive with submit_icon
17-
submit_icon (optional) — icon name for a submit button in the right slot (e.g. "arrow-right")
18-
submit_label (optional) — aria-label for the submit button, default "Submit"
19-
dispatch_field_change (optional) — when truthy (requires submit_icon), dispatches a debounced
20-
`field-change` event (`{ name, value }`) on each keystroke, matching the
21-
dropdown/combo pattern so parent filter components can react live.
22-
required (optional) — if truthy, adds required attribute
23-
disabled (optional) — if truthy, adds disabled attribute
24-
extra_class (optional) — additional classes on the wrapper
4+
name (required) — input name attribute
5+
label (optional) — label text
6+
placeholder (optional) — placeholder text
7+
value (optional) — pre-filled value; with submit_icon, also seeds the Alpine x-model state so it survives init
8+
type (optional) — input type, default "text"
9+
help_text (optional) — help text below the field
10+
error (optional) — static server-side error message, rendered on page load
11+
alpine_error (optional) — Alpine expression that resolves to an error string for dynamic
12+
server-driven errors without a page reload (e.g. "errors.title")
13+
validate_type (optional) — client-side validation before submit, currently supports "email"
14+
validate_error (optional) — error message shown when validate_type check fails
15+
icon_left (optional) — icon name for left slot (e.g. "search")
16+
icon_right (optional) — icon name for a decorative right-slot icon (non-interactive); mutually exclusive with submit_icon
17+
submit_icon (optional) — icon name for a submit button in the right slot (e.g. "arrow-right")
18+
submit_label (optional) — aria-label for the submit button, default "Submit"
19+
dispatch_field_change (optional) — when truthy (requires submit_icon), dispatches a debounced
20+
`field-change` event (`{ name, value }`) on each keystroke, matching the
21+
dropdown/combo pattern so parent filter components can react live.
22+
required (optional) — if truthy, adds required attribute
23+
disabled (optional) — if truthy, adds disabled attribute
24+
extra_class (optional) — additional classes on the wrapper
25+
display_max_chars (optional) — controls whether the max_char limit should be displayed. Requires the following argument as well.
26+
max_chars (optional) — if provided, will present a maximum length counter on this field. If JS is enabled, this
27+
field will dynamically show characters left
2528

2629
Error handling:
2730
error is the base layer for server-side validation and can be combined with either
@@ -56,9 +59,32 @@
5659
}"
5760
:class="{ 'field--error': isInvalid || isEmpty }"
5861
@validate="touched = true"
59-
{% elif alpine_error %}:class="{ 'field--error': {{ alpine_error }} }"{% endif %}>
62+
{% elif alpine_error %}:class="{ 'field--error': {{ alpine_error }} }"
63+
{% elif max_chars %}
64+
x-data="{
65+
value: '',
66+
maxChars: {{max_chars}},
67+
get charLeft() {
68+
return Number(this.maxChars) - this.value?.length || 0;
69+
},
70+
get isTooLong() {
71+
return this.charLeft < 0;
72+
}
73+
}"
74+
:class="{ 'field--error': isTooLong }"
75+
{% endif %}>
6076
{# djlint:on #}
77+
{% if label or display_max_chars %}
78+
<div class="field__label-row">
6179
{% if label %}<label class="field__label" for="field-{{ name }}">{{ label }}</label>{% endif %}
80+
{% if max_chars and display_max_chars %}
81+
<span
82+
class="field__max-chars" x-text="`${charLeft} left`">
83+
{{max_chars}} left
84+
</span>
85+
{% endif %}
86+
</div>
87+
{% endif %}
6288
<div class="field__control {% if disabled %}field__control--disabled{% endif %}"
6389
{% if submit_icon %}x-data="{ q: '{{ value|default:''|escapejs }}' }"{% endif %}>
6490
{% if icon_left %}
@@ -82,7 +108,7 @@
82108
{% elif help_text %}
83109
aria-describedby="field-{{ name }}-help"
84110
{% endif %}
85-
{% if validate_type == "email" %}x-model="value" @blur="touched = true"{% elif submit_icon %}x-model="q" {% if dispatch_field_change %}@input.debounce.150ms="$dispatch('field-change', { name: '{{ name }}', value: q })"{% endif %}{% endif %}>
111+
{% if validate_type == "email" %}x-model="value" @blur="touched = true"{% elif submit_icon %}x-model="q" {% if dispatch_field_change %}@input.debounce.150ms="$dispatch('field-change', { name: '{{ name }}', value: q })"{% endif %}{% else %}x-model="value"{% endif %}>
86112
{% if submit_icon %}
87113
<button type="submit"
88114
class="field__submit"
@@ -100,6 +126,9 @@
100126
<p class="field__error" x-cloak x-show="isInvalid" role="alert">{{ validate_error }}</p>
101127
<p class="field__error" x-cloak x-show="isEmpty" role="alert">This field is required.</p>
102128
{% endif %}
129+
{% if max_chars and display_max_chars %}
130+
<p class="field__error" x-cloak x-show="isTooLong" role="alert">This field is too long.</p>
131+
{% endif %}
103132
{% if alpine_error %}
104133
<p class="field__error"
105134
id="field-{{ name }}-error"

templates/v3/user_profile_edit.html

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
{% include 'v3/includes/_form_include.html' with form=form only %}
3838
{% endfor %}
3939
</div>
40+
<div class="field__help">All links will be displayed publicly on your profile for other users to see.</div>
4041
</div>
4142
<hr class="card__hr" aria-hidden="true" />
4243
<div class="card__column px-large">
@@ -47,9 +48,9 @@
4748
<div class="user-profile__default-space-col">
4849
<span class="field__label">Avatar</span>
4950
<div class="user-profile__avatar-row">
50-
<span id="user-profile-editable-avatar" class="avatar avatar--xxxl" aria-hidden="true"><img src="{{user_profile_form.avatar.value}}" class="avatar avatar--xxl avatar__img" alt="" loading="lazy" decoding="async" /></span>
51+
<span id="user-profile-editable-avatar" class="avatar avatar--xxxl" aria-hidden="true"><img src="{{ user_profile_form.avatar.value }}" class="avatar avatar--xxl avatar__img" alt="" loading="lazy" decoding="async" /></span>
5152
<input type="checkbox" id="user-profile__avatar-row-checkbox" name="avatar-row-checkbox" class="badge-button__radio" />
52-
{% include 'v3/includes/_field_file.html' with name=user_profile_form.avatar.name extra_class="user-profile__empty-avatar-input" accept='image/png,image/jpeg' only %}
53+
{% include 'v3/includes/_field_file.html' with name=user_profile_form.avatar.name extra_class='user-profile__empty-avatar-input' accept='image/png,image/jpeg' only %}
5354
<div class="user-profile__default-space-col user-profile__default-avatar-buttons">
5455
<label for="user-profile__avatar-row-checkbox" class="btn btn-primary">Replace Avatar<span class="btn-icon" aria-hidden>{% include 'includes/icon.html' with icon_name='pixel-pencil' icon_size=16 icon_viewbox='0 0 16 16' only %}</span></label>
5556
<input type="checkbox" id="user-profile__avatar-row-checkbox" name="{{ user_profile_form.delete_avatar.name }}" class="badge-button__radio" />
@@ -170,10 +171,6 @@
170171
{% include 'v3/includes/_field_include.html' with bound_field=user_profile_form.allow_notification_others_news_posted only %}
171172
</div>
172173
<hr class="card__hr" aria-hidden="true" />
173-
<div class="card__column px-large">
174-
{% include 'v3/includes/_field_include.html' with bound_field=user_profile_form.allow_notification_terms_updated only %}
175-
</div>
176-
<hr class="card__hr" aria-hidden="true" />
177174
<div class="card__cta_section">
178175
{% include 'v3/includes/_button.html' with style='primary' type='submit' label='Save Changes' only %}
179176
</div>
@@ -199,19 +196,24 @@
199196
})
200197

201198
const init = () => {
202-
const fileInput = document.querySelector("input[name='{{user_profile_form.avatar.name}}']")
199+
const fileInput = document.querySelectorAll("input[name='{{user_profile_form.avatar.name}}']")
203200
const avatarComponent = document.querySelector('#user-profile-editable-avatar')
204201
const avatarRowClose = document.querySelector('#avatar-row__close-button')
205202
const avatarImg = avatarComponent.querySelector('img')
206203
const avatarDelete = document.querySelector('#avatar-row__delete-avatar')
207204

208-
fileInput.addEventListener('change', (e) => {
209-
const imageFile = fileDataURL(e.target.files[0]).then((data) => {
210-
if (avatarImg !== null) avatarImg.src = data
205+
fileInput.forEach((el) => {
206+
el.addEventListener('change', (e) => {
207+
const imageFile = fileDataURL(e.target.files[0]).then((data) => {
208+
if (avatarImg !== null) avatarImg.src = data
209+
})
211210
})
212211
})
213212
avatarRowClose.addEventListener('click', () => {
214-
fileInput.value = ''
213+
fileInput.forEach((el)=> {
214+
el.value = '';
215+
Alpine.$data(el)['fileName'] = '';
216+
})
215217
if (avatarImg !== null) avatarImg.src = originalSrc
216218
})
217219
}

users/forms.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,9 @@ class V3ProfileLinkForm(forms.Form):
197197

198198

199199
class V3CommitEmailForm(forms.Form):
200-
email = forms.EmailField(max_length=80)
200+
email = forms.EmailField(
201+
max_length=80, widget=forms.EmailInput(attrs={"placeholder": "abc@example.com"})
202+
)
201203

202204

203205
V3CommitEmailFormSet = forms.formset_factory(V3CommitEmailForm, extra=0)
@@ -242,7 +244,9 @@ def __init__(self, *args, **kwargs):
242244
tagline = forms.CharField(
243245
max_length=70,
244246
help_text="This tagline is displayed next to your avatar on your profile & across the site",
245-
widget=forms.TextInput(attrs={"placeholder": "Placeholder"}),
247+
widget=forms.TextInput(
248+
attrs={"placeholder": "Placeholder", "display_max_chars": True}
249+
),
246250
)
247251
bio = forms.CharField(
248252
max_length=4000,
@@ -282,7 +286,9 @@ def __init__(self, *args, **kwargs):
282286
max_length=80, widget=forms.TextInput(attrs={"placeholder": "Placeholder"})
283287
)
284288
email = forms.EmailField(
285-
max_length=80, widget=forms.TextInput(attrs={"placeholder": "Placeholder"})
289+
max_length=80,
290+
widget=forms.TextInput(attrs={"placeholder": "Placeholder"}),
291+
disabled=True,
286292
)
287293
country = forms.ChoiceField(choices=[])
288294
indicate_last_login_method = forms.BooleanField(
@@ -320,7 +326,3 @@ def __init__(self, *args, **kwargs):
320326
label="Other users publish their news",
321327
required=False,
322328
)
323-
allow_notification_terms_updated = forms.BooleanField(
324-
label="The sites terms of use or privacy policy are changed",
325-
required=False,
326-
)

users/views.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,6 @@ def get_v3_context_data(self, **kwargs):
108108
if self.request.GET.get("edit", "").lower() == "true":
109109
ctx["user_profile_form"] = V3UserProfileForm(
110110
user_links={"website": "www.example.com"},
111-
commit_emails=[
112-
"example1@example.com",
113-
"example2@example.com",
114-
],
115111
initial={"avatar": self.request.user.avatar_url},
116112
)
117113
ctx["badge_tiers"] = [

0 commit comments

Comments
 (0)