mirror of
1
Fork 0

Simplify pronouns in user settings (#6835)

The main change here is to use `datalist` for pronouns This supports
(see also docs[1]):

* Displaying the value already set by the user (if any), otherwise
* Presenting a list of common options to the user, and
* Allowing them to freely enter any value

This setup requires no additional JS and resolves[2].

This is different from the previous flow which used, if JS was available:

* A menu for a default 'recognised' set of pronouns, and if the user
  wanted another value:
* An extra text div if the user wanted to enter custom pronouns

Without JS enabled both the menu and the custom text div would always be
displayed.

This change means there's no longer a distinction between 'custom' and
'recognised' pronouns (this difference looks to have only been made in
code, and not in any data models).

Link: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist [1]
Link: https://codeberg.org/forgejo/forgejo/issues/6774 [2]

Co-authored-by: Matthew Hughes <matthewhughes934@gmail.com>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6835
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: Otto <otto@codeberg.org>
Co-authored-by: mhughes9 <mhughes9@noreply.codeberg.org>
Co-committed-by: mhughes9 <mhughes9@noreply.codeberg.org>
This commit is contained in:
mhughes9 2025-02-26 18:46:51 +00:00 committed by Otto
parent 77a1af5ab8
commit 2024031a7a
6 changed files with 21 additions and 95 deletions

View File

@ -761,8 +761,6 @@ full_name = Full name
website = Website
location = Location
pronouns = Pronouns
pronouns_custom = Custom
pronouns_custom_label = Custom pronouns
pronouns_unspecified = Unspecified
update_theme = Change theme
update_profile = Update profile

View File

@ -12,7 +12,6 @@ import (
"net/http"
"os"
"path/filepath"
"slices"
"strings"
"time"
@ -42,8 +41,7 @@ const (
tplSettingsRepositories base.TplName = "user/settings/repos"
)
// must be kept in sync with `web_src/js/features/user-settings.js`
var recognisedPronouns = []string{"", "he/him", "she/her", "they/them", "it/its", "any pronouns"}
var commonPronouns = []string{"he/him", "she/her", "they/them", "it/its", "any pronouns"}
// Profile render user's profile page
func Profile(ctx *context.Context) {
@ -51,8 +49,8 @@ func Profile(ctx *context.Context) {
ctx.Data["PageIsSettingsProfile"] = true
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
ctx.Data["PronounsAreCustom"] = !slices.Contains(recognisedPronouns, ctx.Doer.Pronouns)
ctx.Data["CooldownPeriod"] = setting.Service.UsernameCooldownPeriod
ctx.Data["CommonPronouns"] = commonPronouns
ctx.HTML(http.StatusOK, tplSettingsProfile)
}
@ -63,8 +61,8 @@ func ProfilePost(ctx *context.Context) {
ctx.Data["PageIsSettingsProfile"] = true
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
ctx.Data["PronounsAreCustom"] = !slices.Contains(recognisedPronouns, ctx.Doer.Pronouns)
ctx.Data["CooldownPeriod"] = setting.Service.UsernameCooldownPeriod
ctx.Data["CommonPronouns"] = commonPronouns
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplSettingsProfile)

View File

@ -30,38 +30,14 @@
<input name="full_name" value="{{.SignedUser.FullName}}" maxlength="100">
</label>
<label id="label-pronouns" class="tw-hidden">
<label id="label-pronouns">
{{ctx.Locale.Tr "settings.pronouns"}}
<div id="pronouns-dropdown" class="ui selection dropdown" aria-labelledby="label-pronouns">
<input type="hidden" value="{{.SignedUser.Pronouns}}">
<div class="text">
{{if .PronounsAreCustom}}
{{ctx.Locale.Tr "settings.pronouns_custom"}}
{{else if eq "" .SignedUser.Pronouns}}
{{ctx.Locale.Tr "settings.pronouns_unspecified"}}
{{else}}
{{.SignedUser.Pronouns}}
{{end}}
</div>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="item{{if eq "" .SignedUser.Pronouns}} active selected{{end}}" data-value=""><p>{{ctx.Locale.Tr "settings.pronouns_unspecified"}}</p></div>
<div class="item{{if eq "he/him" .SignedUser.Pronouns}} active selected{{end}}" data-value="he/him">he/him</div>
<div class="item{{if eq "she/her" .SignedUser.Pronouns}} active selected{{end}}" data-value="she/her">she/her</div>
<div class="item{{if eq "they/them" .SignedUser.Pronouns}} active selected{{end}}" data-value="they/them">they/them</div>
<div class="item{{if eq "it/its" .SignedUser.Pronouns}} active selected{{end}}" data-value="it/its">it/its</div>
<div class="item{{if eq "any pronouns" .SignedUser.Pronouns}} active selected{{end}}" data-value="any pronouns">any pronouns</div>
{{if .PronounsAreCustom}}
<div class="item active selected" data-value="{{.SignedUser.Pronouns}}">{{ctx.Locale.Tr "settings.pronouns_custom"}}</div>
{{else}}
<div class="item" data-value="!"><i>{{ctx.Locale.Tr "settings.pronouns_custom"}}</i></div>
{{end}}
</div>
</div>
</label>
<label id="label-pronouns-custom">
{{ctx.Locale.Tr "settings.pronouns_custom_label"}}
<input name="pronouns" value="{{.SignedUser.Pronouns}}" maxlength="50">
<input name="pronouns" list="pronouns" placeholder="{{ctx.Locale.Tr "settings.pronouns_unspecified"}}" value="{{.SignedUser.Pronouns}}" maxlength="50">
<datalist id="pronouns">
{{range .CommonPronouns}}
<option value="{{.}}"></option>
{{end}}
</datalist>
</label>
<label {{if .Err_Biography}}class="field error"{{end}}>

View File

@ -16,8 +16,16 @@ test('User: Profile settings', async ({browser}, workerInfo) => {
await page.goto('/user/settings');
await page.getByLabel('Full name').fill('SecondUser');
await page.locator('#pronouns-dropdown').click();
await page.getByRole('option', {name: 'she/her'}).click();
const pronounsInput = page.locator('input[list="pronouns"]');
await expect(pronounsInput).toHaveAttribute('placeholder', 'Unspecified');
await pronounsInput.click();
const pronounsList = page.locator('datalist#pronouns');
const pronounsOptions = pronounsList.locator('option');
const pronounsValues = await pronounsOptions.evaluateAll((opts) => opts.map((opt) => opt.value));
expect(pronounsValues).toEqual(['he/him', 'she/her', 'they/them', 'it/its', 'any pronouns']);
await pronounsInput.fill('she/her');
await page.getByPlaceholder('Tell others a little bit').fill('I am a playwright test running for several seconds.');
await page.getByPlaceholder('Tell others a little bit').press('Tab');
await page.getByLabel('Website').fill('https://forgejo.org');
@ -44,9 +52,7 @@ test('User: Profile settings', async ({browser}, workerInfo) => {
await save_visual(page);
await page.goto('/user/settings');
await page.locator('#pronouns-dropdown').click();
await page.getByRole('option', {name: 'Custom'}).click();
await page.getByLabel('Custom pronouns').fill('rob/ot');
await page.locator('input[list="pronouns"]').fill('rob/ot');
await page.getByLabel('User visibility').click();
await page.getByLabel('Visible to everyone').click();
await page.getByLabel('Hide email address Your email').check();

View File

@ -1,50 +0,0 @@
import {hideElem, showElem} from '../utils/dom.js';
function onPronounsDropdownUpdate() {
const pronounsCustom = document.getElementById('label-pronouns-custom');
const pronounsCustomInput = pronounsCustom.querySelector('input');
const pronounsDropdown = document.getElementById('pronouns-dropdown');
const pronounsInput = pronounsDropdown.querySelector('input');
// must be kept in sync with `routers/web/user/setting/profile.go`
const isCustom = !(
pronounsInput.value === '' ||
pronounsInput.value === 'he/him' ||
pronounsInput.value === 'she/her' ||
pronounsInput.value === 'they/them' ||
pronounsInput.value === 'it/its' ||
pronounsInput.value === 'any pronouns'
);
if (isCustom) {
if (pronounsInput.value === '!') {
pronounsCustomInput.value = '';
} else {
pronounsCustomInput.value = pronounsInput.value;
}
showElem(pronounsCustom);
} else {
hideElem(pronounsCustom);
}
}
function onPronounsCustomUpdate() {
const pronounsCustomInput = document.querySelector('#label-pronouns-custom input');
const pronounsInput = document.querySelector('#pronouns-dropdown input');
pronounsInput.value = pronounsCustomInput.value;
}
export function initUserSettings() {
if (!document.querySelectorAll('.user.settings.profile').length) return;
const pronounsDropdown = document.getElementById('label-pronouns');
const pronounsCustomInput = document.querySelector('#label-pronouns-custom input');
const pronounsInput = pronounsDropdown.querySelector('input');
// If JS is disabled, the page will show the custom input, as the dropdown requires JS to work.
// JS progressively enhances the input by adding a dropdown, but it works regardless.
pronounsCustomInput.removeAttribute('name');
pronounsInput.setAttribute('name', 'pronouns');
showElem(pronounsDropdown);
onPronounsDropdownUpdate();
pronounsInput.addEventListener('change', onPronounsDropdownUpdate);
pronounsCustomInput.addEventListener('input', onPronounsCustomUpdate);
}

View File

@ -51,7 +51,6 @@ import {initAdminCommon} from './features/admin/common.js';
import {initRepoTemplateSearch} from './features/repo-template.js';
import {initRepoCodeView} from './features/repo-code.js';
import {initSshKeyFormParser} from './features/sshkey-helper.js';
import {initUserSettings} from './features/user-settings.js';
import {initRepoArchiveLinks} from './features/repo-common.js';
import {initRepoMigrationStatusChecker} from './features/repo-migrate.js';
import {
@ -185,7 +184,6 @@ onDomReady(() => {
initUserAuthOauth2();
initUserAuthWebAuthn();
initUserAuthWebAuthnRegister();
initUserSettings();
initRepoDiffView();
initPdfViewer();
initScopedAccessTokenCategories();