+ {{$statusUnread := 1}}{{$statusRead := 2}}{{$statusPinned := 3}} {{$notificationUnreadCount := call .PageGlobalData.GetNotificationUnreadCount}} -
+ {{$pageTypeIsRead := eq $.PageType "read"}} +
- {{if and (eq .Status 1)}} + {{if and (not $pageTypeIsRead) $notificationUnreadCount}}
{{$.CsrfTokenHtml}} -
- -
+
{{end}}
-
-
- {{if not .Notifications}} -
- {{svg "octicon-inbox" 56 "tw-mb-4"}} - {{if eq .Status 1}} - {{ctx.Locale.Tr "notification.no_unread"}} +
+ {{range $one := .Notifications}} +
+
+ {{if $one.Issue}} + {{template "shared/issueicon" $one.Issue}} {{else}} - {{ctx.Locale.Tr "notification.no_read"}} + {{svg "octicon-repo" 16 "text grey"}} {{end}}
- {{else}} - {{range $notification := .Notifications}} -
-
- {{if .Issue}} - {{template "shared/issueicon" .Issue}} - {{else}} - {{svg "octicon-repo" 16 "text grey"}} - {{end}} -
- -
- {{.Repository.FullName}} {{if .Issue}}#{{.Issue.Index}}{{end}} - {{if eq .Status 3}} - {{svg "octicon-pin" 13 "text blue tw-mt-0.5 tw-ml-1"}} - {{end}} -
-
- - {{if .Issue}} - {{.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}} - {{else}} - {{.Repository.FullName}} - {{end}} - -
-
-
- {{if .Issue}} - {{DateUtils.TimeSince .Issue.UpdatedUnix}} - {{else}} - {{DateUtils.TimeSince .UpdatedUnix}} - {{end}} -
-
- {{if ne .Status 3}} -
- {{$.CsrfTokenHtml}} - - - -
- {{end}} - {{if or (eq .Status 1) (eq .Status 3)}} -
- {{$.CsrfTokenHtml}} - - - - -
- {{else if eq .Status 2}} -
- {{$.CsrfTokenHtml}} - - - - -
- {{end}} -
+ +
+ {{$one.Repository.FullName}} {{if $one.Issue}}#{{$one.Issue.Index}}{{end}} + {{if eq $one.Status $statusPinned}} + {{svg "octicon-pin" 13 "text blue"}} + {{end}}
+
+ {{if $one.Issue}} + {{$one.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}} + {{else}} + {{$one.Repository.FullName}} + {{end}} +
+
+
+ {{if $one.Issue}} + {{DateUtils.TimeSince $one.Issue.UpdatedUnix}} + {{else}} + {{DateUtils.TimeSince $one.UpdatedUnix}} + {{end}} +
+
+ {{$.CsrfTokenHtml}} + + {{if ne $one.Status $statusPinned}} + + {{end}} + {{if or (eq $one.Status $statusUnread) (eq $one.Status $statusPinned)}} + + {{else if eq $one.Status $statusRead}} + + {{end}} +
+
+ {{else}} +
+ {{svg "octicon-inbox" 56 "tw-mb-4"}} + {{if $pageTypeIsRead}} + {{ctx.Locale.Tr "notification.no_read"}} + {{else}} + {{ctx.Locale.Tr "notification.no_unread"}} {{end}} - {{end}} -
+
+ {{end}}
{{template "base/paginate" .}}
diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl index 2c01c88d47..7dbac0ebd4 100644 --- a/templates/user/settings/account.tmpl +++ b/templates/user/settings/account.tmpl @@ -35,37 +35,12 @@ {{end}}
- {{if not (and ($.UserDisabledFeatures.Contains "manage_credentials") (not $.EnableNotifyMail))}} + {{if not ($.UserDisabledFeatures.Contains "manage_credentials")}}

{{ctx.Locale.Tr "settings.manage_emails"}}

- {{if $.EnableNotifyMail}} -
-
- {{$.CsrfTokenHtml}} - -
- - -
-
- -
-
-
- {{end}} {{if not ($.UserDisabledFeatures.Contains "manage_credentials")}} {{range .Emails}}
diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl index c6c15512ab..34e089a68a 100644 --- a/templates/user/settings/navbar.tmpl +++ b/templates/user/settings/navbar.tmpl @@ -4,11 +4,16 @@ {{ctx.Locale.Tr "settings.profile"}} - {{if not (and ($.UserDisabledFeatures.Contains "manage_credentials" "deletion") (not $.EnableNotifyMail))}} + {{if not ($.UserDisabledFeatures.Contains "manage_credentials" "deletion")}} {{ctx.Locale.Tr "settings.account"}} {{end}} + {{if $.EnableNotifyMail}} + + {{ctx.Locale.Tr "notifications"}} + + {{end}} {{ctx.Locale.Tr "settings.appearance"}} diff --git a/templates/user/settings/notifications.tmpl b/templates/user/settings/notifications.tmpl new file mode 100644 index 0000000000..4694bbb30a --- /dev/null +++ b/templates/user/settings/notifications.tmpl @@ -0,0 +1,34 @@ +{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings")}} +
+

+ {{ctx.Locale.Tr "notifications"}} +

+
+
+
+
+ {{$.CsrfTokenHtml}} +
+ + +
+
+ +
+
+
+
+
+
+ +{{template "user/settings/layout_footer" .}} diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index f8bc33c32a..a2247801f7 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -9,9 +9,11 @@ import ( "fmt" "io" "net/http" + "net/http/httptest" "strings" "testing" + asymkey_model "code.gitea.io/gitea/models/asymkey" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" @@ -20,9 +22,13 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/oauth2_provider" "code.gitea.io/gitea/tests" + "github.com/markbates/goth" + "github.com/markbates/goth/gothic" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -931,3 +937,107 @@ func testOAuth2WellKnown(t *testing.T) { defer test.MockVariableValue(&setting.OAuth2.Enabled, false)() MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound) } + +func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) { + cfg.Provider = util.IfZero(cfg.Provider, "gitea") + err := auth_model.CreateSource(db.DefaultContext, &auth_model.Source{ + Type: auth_model.OAuth2, + Name: authName, + IsActive: true, + Cfg: &cfg, + }) + require.NoError(t, err) +} + +func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + var mockServer *httptest.Server + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/.well-known/openid-configuration": + _, _ = w.Write([]byte(`{ + "issuer": "` + mockServer.URL + `", + "authorization_endpoint": "` + mockServer.URL + `/authorize", + "token_endpoint": "` + mockServer.URL + `/token", + "userinfo_endpoint": "` + mockServer.URL + `/userinfo" + }`)) + default: + http.NotFound(w, r) + } + })) + defer mockServer.Close() + + ctx := t.Context() + oauth2Source := oauth2.Source{ + Provider: "openidConnect", + ClientID: "test-client-id", + SSHPublicKeyClaimName: "sshpubkey", + FullNameClaimName: "name", + OpenIDConnectAutoDiscoveryURL: mockServer.URL + "/.well-known/openid-configuration", + } + addOAuth2Source(t, "test-oidc-source", oauth2Source) + authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(ctx, "test-oidc-source") + require.NoError(t, err) + + sshKey1 := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf" + sshKey2 := "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIE7kM1R02+4ertDKGKEDcKG0s+2vyDDcIvceJ0Gqv5f1AAAABHNzaDo=" + sshKey3 := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEHjnNEfE88W1pvBLdV3otv28x760gdmPao3lVD5uAt9" + cases := []struct { + testName string + mockFullName string + mockRawData map[string]any + expectedSSHPubKeys []string + }{ + { + testName: "Login1", + mockFullName: "FullName1", + mockRawData: map[string]any{"sshpubkey": []any{sshKey1 + " any-comment"}}, + expectedSSHPubKeys: []string{sshKey1}, + }, + { + testName: "Login2", + mockFullName: "FullName2", + mockRawData: map[string]any{"sshpubkey": []any{sshKey2 + " any-comment", sshKey3}}, + expectedSSHPubKeys: []string{sshKey2, sshKey3}, + }, + { + testName: "Login3", + mockFullName: "FullName3", + mockRawData: map[string]any{}, + expectedSSHPubKeys: []string{}, + }, + } + + session := emptyTestSession(t) + for _, c := range cases { + t.Run(c.testName, func(t *testing.T) { + defer test.MockVariableValue(&setting.OAuth2Client.Username, "")() + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + return goth.User{ + Provider: authSource.Cfg.(*oauth2.Source).Provider, + UserID: "oidc-userid", + Email: "oidc-email@example.com", + RawData: c.mockRawData, + Name: c.mockFullName, + }, nil + })() + req := NewRequest(t, "GET", "/user/oauth2/test-oidc-source/callback?code=XYZ&state=XYZ") + session.MakeRequest(t, req, http.StatusSeeOther) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "oidc-userid"}) + keys, _, err := db.FindAndCount[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{ + ListOptions: db.ListOptionsAll, + OwnerID: user.ID, + LoginSourceID: authSource.ID, + }) + require.NoError(t, err) + var sshPubKeys []string + for _, key := range keys { + sshPubKeys = append(sshPubKeys, key.Content) + } + assert.ElementsMatch(t, c.expectedSSHPubKeys, sshPubKeys) + assert.Equal(t, c.mockFullName, user.FullName) + }) + } +} diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go index 0097a7f62e..b8f086e2b1 100644 --- a/tests/integration/repo_commits_test.go +++ b/tests/integration/repo_commits_test.go @@ -24,39 +24,59 @@ import ( func TestRepoCommits(t *testing.T) { defer tests.PrepareTestEnv(t)() - session := loginUser(t, "user2") - // Request repository commits page - req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master") - resp := session.MakeRequest(t, req, http.StatusOK) + t.Run("CommitList", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo16/commits/branch/master") + resp := session.MakeRequest(t, req, http.StatusOK) - doc := NewHTMLParser(t, resp.Body) - commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") - assert.True(t, exists) - assert.NotEmpty(t, commitURL) -} - -func Test_ReposGitCommitListNotMaster(t *testing.T) { - defer tests.PrepareTestEnv(t)() - session := loginUser(t, "user2") - req := NewRequest(t, "GET", "/user2/repo16/commits/branch/master") - resp := session.MakeRequest(t, req, http.StatusOK) - - doc := NewHTMLParser(t, resp.Body) - var commits []string - doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) { - commitURL, _ := s.Attr("href") - commits = append(commits, path.Base(commitURL)) + var commits, userHrefs []string + doc := NewHTMLParser(t, resp.Body) + doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) { + commits = append(commits, path.Base(s.AttrOr("href", ""))) + }) + doc.doc.Find("#commits-table .author-wrapper").Each(func(i int, s *goquery.Selection) { + userHrefs = append(userHrefs, s.AttrOr("href", "")) + }) + assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits) + assert.Equal(t, []string{"/user2", "/user21", "/user2"}, userHrefs) }) - assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits) - var userHrefs []string - doc.doc.Find("#commits-table .author-wrapper").Each(func(i int, s *goquery.Selection) { - userHref, _ := s.Attr("href") - userHrefs = append(userHrefs, userHref) + t.Run("LastCommit", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo16") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "") + authorHref := doc.doc.Find(".latest-commit .author-wrapper").AttrOr("href", "") + assert.Equal(t, "/user2/repo16/commit/69554a64c1e6030f051e5c3f94bfbd773cd6a324", commitHref) + assert.Equal(t, "/user2", authorHref) + }) + + t.Run("CommitListNonExistingCommiter", func(t *testing.T) { + // check the commit list for a repository with no gitea user + // * commit 985f0301dba5e7b34be866819cd15ad3d8f508ee (branch2) + // * Author: 6543 <6543@obermui.de> + req := NewRequest(t, "GET", "/user2/repo1/commits/branch/branch2") + resp := session.MakeRequest(t, req, http.StatusOK) + + doc := NewHTMLParser(t, resp.Body) + commitHref := doc.doc.Find("#commits-table tr:first-child .commit-id-short").AttrOr("href", "") + assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref) + authorElem := doc.doc.Find("#commits-table tr:first-child .author-wrapper") + assert.Equal(t, "6543", authorElem.Text()) + assert.Equal(t, "span", authorElem.Nodes[0].Data) + }) + + t.Run("LastCommitNonExistingCommiter", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo1/src/branch/branch2") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "") + assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref) + authorElem := doc.doc.Find(".latest-commit .author-wrapper") + assert.Equal(t, "6543", authorElem.Text()) + assert.Equal(t, "span", authorElem.Nodes[0].Data) }) - assert.Equal(t, []string{"/user2", "/user21", "/user2"}, userHrefs) } func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index 67af5b5877..aa1571c163 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -17,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers" + "code.gitea.io/gitea/routers/web/auth" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/tests" @@ -103,8 +105,9 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) { defer tests.PrepareTestEnv(t)() mockLinkAccount := func(ctx *context.Context) { + authSource := auth_model.Source{ID: 1} gothUser := goth.User{Email: "invalid-email", Name: "."} - _ = ctx.Session.Set("linkAccountGothUser", gothUser) + _ = ctx.Session.Set("linkAccountData", auth.LinkAccountData{AuthSource: authSource, GothUser: gothUser}) } t.Run("EnablePasswordSignInForm=false", func(t *testing.T) { diff --git a/web_src/css/repo/home-file-list.css b/web_src/css/repo/home-file-list.css index f2ab052a54..6aa9e4bca3 100644 --- a/web_src/css/repo/home-file-list.css +++ b/web_src/css/repo/home-file-list.css @@ -71,7 +71,7 @@ #repo-files-table .repo-file-cell.name .entry-name { flex-shrink: 1; - min-width: 3em; + min-width: 1ch; /* leave about one letter space when shrinking, need to fine tune the "shrinks" in this grid in the future */ } @media (max-width: 767.98px) { diff --git a/web_src/css/user.css b/web_src/css/user.css index caabf1834c..d42e8688fb 100644 --- a/web_src/css/user.css +++ b/web_src/css/user.css @@ -114,6 +114,14 @@ border-radius: var(--border-radius); } +.notifications-item { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5em; + padding: 0.5em 1em; +} + .notifications-item:hover { background: var(--color-hover); } @@ -129,6 +137,9 @@ .notifications-item:hover .notifications-buttons { display: flex; + align-items: center; + justify-content: end; + gap: 0.25em; } .notifications-item:hover .notifications-updated { diff --git a/web_src/js/features/admin/common.ts b/web_src/js/features/admin/common.ts index 4ed5d62eee..dd5b1f464d 100644 --- a/web_src/js/features/admin/common.ts +++ b/web_src/js/features/admin/common.ts @@ -102,6 +102,9 @@ function initAdminAuthentication() { break; } } + + const supportSshPublicKey = document.querySelector(`#${provider}_SupportSSHPublicKey`)?.value === 'true'; + toggleElem('.field.oauth2_ssh_public_key_claim_name', supportSshPublicKey); onOAuth2UseCustomURLChange(applyDefaultValues); } diff --git a/web_src/js/features/notification.ts b/web_src/js/features/notification.ts index dc0acb0244..4a1aa3ede9 100644 --- a/web_src/js/features/notification.ts +++ b/web_src/js/features/notification.ts @@ -1,40 +1,13 @@ import {GET} from '../modules/fetch.ts'; -import {toggleElem, type DOMEvent, createElementFromHTML} from '../utils/dom.ts'; +import {toggleElem, createElementFromHTML} from '../utils/dom.ts'; import {logoutFromWorker} from '../modules/worker.ts'; const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config; let notificationSequenceNumber = 0; -export function initNotificationsTable() { - const table = document.querySelector('#notification_table'); - if (!table) return; - - // when page restores from bfcache, delete previously clicked items - window.addEventListener('pageshow', (e) => { - if (e.persisted) { // page was restored from bfcache - const table = document.querySelector('#notification_table'); - const unreadCountEl = document.querySelector('.notifications-unread-count'); - let unreadCount = parseInt(unreadCountEl.textContent); - for (const item of table.querySelectorAll('.notifications-item[data-remove="true"]')) { - item.remove(); - unreadCount -= 1; - } - unreadCountEl.textContent = String(unreadCount); - } - }); - - // mark clicked unread links for deletion on bfcache restore - for (const link of table.querySelectorAll('.notifications-item[data-status="1"] .notifications-link')) { - link.addEventListener('click', (e: DOMEvent) => { - e.target.closest('.notifications-item').setAttribute('data-remove', 'true'); - }); - } -} - -async function receiveUpdateCount(event: MessageEvent) { +async function receiveUpdateCount(event: MessageEvent<{type: string, data: string}>) { try { - const data = JSON.parse(event.data); - + const data = JSON.parse(event.data.data); for (const count of document.querySelectorAll('.notification_count')) { count.classList.toggle('tw-hidden', data.Count === 0); count.textContent = `${data.Count}`; @@ -71,7 +44,7 @@ export function initNotificationCount() { type: 'start', url: `${window.location.origin}${appSubUrl}/user/events`, }); - worker.port.addEventListener('message', (event: MessageEvent) => { + worker.port.addEventListener('message', (event: MessageEvent<{type: string, data: string}>) => { if (!event.data || !event.data.type) { console.error('unknown worker message event', event); return; @@ -144,11 +117,11 @@ async function updateNotificationCountWithCallback(callback: (timeout: number, n } async function updateNotificationTable() { - const notificationDiv = document.querySelector('#notification_div'); + let notificationDiv = document.querySelector('#notification_div'); if (notificationDiv) { try { const params = new URLSearchParams(window.location.search); - params.set('div-only', String(true)); + params.set('div-only', 'true'); params.set('sequence-number', String(++notificationSequenceNumber)); const response = await GET(`${appSubUrl}/notifications?${params.toString()}`); @@ -160,7 +133,8 @@ async function updateNotificationTable() { const el = createElementFromHTML(data); if (parseInt(el.getAttribute('data-sequence-number')) === notificationSequenceNumber) { notificationDiv.outerHTML = data; - initNotificationsTable(); + notificationDiv = document.querySelector('#notification_div'); + window.htmx.process(notificationDiv); // when using htmx, we must always remember to process the new content changed by us } } catch (error) { console.error(error); diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index ca18d1e828..770c7fc00c 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -15,7 +15,7 @@ import {initTableSort} from './features/tablesort.ts'; import {initAdminUserListSearchForm} from './features/admin/users.ts'; import {initAdminConfigs} from './features/admin/config.ts'; import {initMarkupAnchors} from './markup/anchors.ts'; -import {initNotificationCount, initNotificationsTable} from './features/notification.ts'; +import {initNotificationCount} from './features/notification.ts'; import {initRepoIssueContentHistory} from './features/repo-issue-content.ts'; import {initStopwatch} from './features/stopwatch.ts'; import {initFindFileInRepo} from './features/repo-findfile.ts'; @@ -117,7 +117,6 @@ const initPerformanceTracer = callInitFunctions([ initDashboardRepoList, initNotificationCount, - initNotificationsTable, initOrgTeam, diff --git a/web_src/js/modules/toast.ts b/web_src/js/modules/toast.ts index ed807a4977..087103cbd8 100644 --- a/web_src/js/modules/toast.ts +++ b/web_src/js/modules/toast.ts @@ -44,7 +44,7 @@ type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast }; // See https://github.com/apvarun/toastify-js#api for options function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast { - const body = useHtmlBody ? String(message) : htmlEscape(message); + const body = useHtmlBody ? message : htmlEscape(message); const parent = document.querySelector('.ui.dimmer.active') ?? document.body; const duplicateKey = preventDuplicates ? (preventDuplicates === true ? `${level}-${body}` : preventDuplicates) : ''; diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 9cdf6f5005..6d6a3735da 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -283,7 +283,7 @@ export function isElemVisible(el: HTMLElement): boolean { // This function DOESN'T account for all possible visibility scenarios, its behavior is covered by the tests of "querySingleVisibleElem" if (!el) return false; // checking el.style.display is not necessary for browsers, but it is required by some tests with happy-dom because happy-dom doesn't really do layout - return !el.classList.contains('tw-hidden') && Boolean((el.offsetWidth || el.offsetHeight || el.getClientRects().length) && el.style.display !== 'none'); + return !el.classList.contains('tw-hidden') && (el.offsetWidth || el.offsetHeight || el.getClientRects().length) && el.style.display !== 'none'; } // replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this