mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +01:00 
			
		
		
		
	Fix autofocus behavior (#34397)
The "autofocus" was abused or misbehaved:
1. When users visit a page but they are not going to change a field,
then the field shouldn't get "autofocus"
* the "auth" / "user" page: in most cases, users do not want to change
the names
    * see also the GitHub's "settings" page behavior.
2. There shouldn't be duplicate "autofocus" inputs in most cases, only
the first one focuses
3. When a panel is shown, the "autofocus" should get focus
    * "add ssh key" panel
This PR fixes all these problems and by the way remove duplicate
"isElemHidden" function.
			
			
This commit is contained in:
		| @@ -1,5 +1,5 @@ | ||||
| import {POST} from '../modules/fetch.ts'; | ||||
| import {addDelegatedEventListener, hideElem, showElem, toggleElem} from '../utils/dom.ts'; | ||||
| import {addDelegatedEventListener, hideElem, isElemVisible, showElem, toggleElem} from '../utils/dom.ts'; | ||||
| import {fomanticQuery} from '../modules/fomantic/base.ts'; | ||||
| import {camelize} from 'vue'; | ||||
|  | ||||
| @@ -79,10 +79,11 @@ function onShowPanelClick(el: HTMLElement, e: MouseEvent) { | ||||
|   // if it has "toggle" class, it toggles the panel | ||||
|   e.preventDefault(); | ||||
|   const sel = el.getAttribute('data-panel'); | ||||
|   if (el.classList.contains('toggle')) { | ||||
|     toggleElem(sel); | ||||
|   } else { | ||||
|     showElem(sel); | ||||
|   const elems = el.classList.contains('toggle') ? toggleElem(sel) : showElem(sel); | ||||
|   for (const elem of elems) { | ||||
|     if (isElemVisible(elem as HTMLElement)) { | ||||
|       elem.querySelector<HTMLElement>('[autofocus]')?.focus(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import {isElemHidden, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.ts'; | ||||
| import {isElemVisible, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.ts'; | ||||
| import {GET} from '../modules/fetch.ts'; | ||||
|  | ||||
| const {appSubUrl} = window.config; | ||||
| @@ -28,7 +28,7 @@ export function parseIssueListQuickGotoLink(repoLink: string, searchText: string | ||||
| } | ||||
|  | ||||
| export function initCommonIssueListQuickGoto() { | ||||
|   const goto = document.querySelector('#issue-list-quick-goto'); | ||||
|   const goto = document.querySelector<HTMLElement>('#issue-list-quick-goto'); | ||||
|   if (!goto) return; | ||||
|  | ||||
|   const form = goto.closest('form'); | ||||
| @@ -37,7 +37,7 @@ export function initCommonIssueListQuickGoto() { | ||||
|  | ||||
|   form.addEventListener('submit', (e) => { | ||||
|     // if there is no goto button, or the form is submitted by non-quick-goto elements, submit the form directly | ||||
|     let doQuickGoto = !isElemHidden(goto); | ||||
|     let doQuickGoto = isElemVisible(goto); | ||||
|     const submitter = submitEventSubmitter(e); | ||||
|     if (submitter !== form && submitter !== input && submitter !== goto) doQuickGoto = false; | ||||
|     if (!doQuickGoto) return; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import {updateIssuesMeta} from './repo-common.ts'; | ||||
| import {toggleElem, isElemHidden, queryElems} from '../utils/dom.ts'; | ||||
| import {toggleElem, queryElems, isElemVisible} from '../utils/dom.ts'; | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {confirmModal} from './comp/ConfirmModal.ts'; | ||||
| import {showErrorToast} from '../modules/toast.ts'; | ||||
| @@ -33,8 +33,8 @@ function initRepoIssueListCheckboxes() { | ||||
|     toggleElem('#issue-filters', !anyChecked); | ||||
|     toggleElem('#issue-actions', anyChecked); | ||||
|     // there are two panels but only one select-all checkbox, so move the checkbox to the visible panel | ||||
|     const panels = document.querySelectorAll('#issue-filters, #issue-actions'); | ||||
|     const visiblePanel = Array.from(panels).find((el) => !isElemHidden(el)); | ||||
|     const panels = document.querySelectorAll<HTMLElement>('#issue-filters, #issue-actions'); | ||||
|     const visiblePanel = Array.from(panels).find((el) => isElemVisible(el)); | ||||
|     const toolbarLeft = visiblePanel.querySelector('.issue-list-toolbar-left'); | ||||
|     toolbarLeft.prepend(issueSelectAll); | ||||
|   }; | ||||
|   | ||||
| @@ -25,10 +25,14 @@ test('createElementFromAttrs', () => { | ||||
| }); | ||||
|  | ||||
| test('querySingleVisibleElem', () => { | ||||
|   let el = createElementFromHTML('<div><span>foo</span></div>'); | ||||
|   let el = createElementFromHTML('<div></div>'); | ||||
|   expect(querySingleVisibleElem(el, 'span')).toBeNull(); | ||||
|   el = createElementFromHTML('<div><span>foo</span></div>'); | ||||
|   expect(querySingleVisibleElem(el, 'span').textContent).toEqual('foo'); | ||||
|   el = createElementFromHTML('<div><span style="display: none;">foo</span><span>bar</span></div>'); | ||||
|   expect(querySingleVisibleElem(el, 'span').textContent).toEqual('bar'); | ||||
|   el = createElementFromHTML('<div><span class="some-class tw-hidden">foo</span><span>bar</span></div>'); | ||||
|   expect(querySingleVisibleElem(el, 'span').textContent).toEqual('bar'); | ||||
|   el = createElementFromHTML('<div><span>foo</span><span>bar</span></div>'); | ||||
|   expect(() => querySingleVisibleElem(el, 'span')).toThrowError('Expected exactly one visible element'); | ||||
| }); | ||||
|   | ||||
| @@ -9,24 +9,24 @@ type ElementsCallback<T extends Element> = (el: T) => Promisable<any>; | ||||
| type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>; | ||||
| export type DOMEvent<E extends Event, T extends Element = HTMLElement> = E & { target: Partial<T>; }; | ||||
|  | ||||
| function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) { | ||||
| function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]): ArrayLikeIterable<Element> { | ||||
|   if (typeof el === 'string' || el instanceof String) { | ||||
|     el = document.querySelectorAll(el as string); | ||||
|   } | ||||
|   if (el instanceof Node) { | ||||
|     func(el, ...args); | ||||
|     return [el]; | ||||
|   } else if (el.length !== undefined) { | ||||
|     // this works for: NodeList, HTMLCollection, Array, jQuery | ||||
|     for (const e of (el as ArrayLikeIterable<Element>)) { | ||||
|       func(e, ...args); | ||||
|     } | ||||
|   } else { | ||||
|     throw new Error('invalid argument to be shown/hidden'); | ||||
|     const elems = el as ArrayLikeIterable<Element>; | ||||
|     for (const elem of elems) func(elem, ...args); | ||||
|     return elems; | ||||
|   } | ||||
|   throw new Error('invalid argument to be shown/hidden'); | ||||
| } | ||||
|  | ||||
| export function toggleClass(el: ElementArg, className: string, force?: boolean) { | ||||
|   elementsCall(el, (e: Element) => { | ||||
| export function toggleClass(el: ElementArg, className: string, force?: boolean): ArrayLikeIterable<Element> { | ||||
|   return elementsCall(el, (e: Element) => { | ||||
|     if (force === true) { | ||||
|       e.classList.add(className); | ||||
|     } else if (force === false) { | ||||
| @@ -43,23 +43,16 @@ export function toggleClass(el: ElementArg, className: string, force?: boolean) | ||||
|  * @param el ElementArg | ||||
|  * @param force force=true to show or force=false to hide, undefined to toggle | ||||
|  */ | ||||
| export function toggleElem(el: ElementArg, force?: boolean) { | ||||
|   toggleClass(el, 'tw-hidden', force === undefined ? force : !force); | ||||
| export function toggleElem(el: ElementArg, force?: boolean): ArrayLikeIterable<Element> { | ||||
|   return toggleClass(el, 'tw-hidden', force === undefined ? force : !force); | ||||
| } | ||||
|  | ||||
| export function showElem(el: ElementArg) { | ||||
|   toggleElem(el, true); | ||||
| export function showElem(el: ElementArg): ArrayLikeIterable<Element> { | ||||
|   return toggleElem(el, true); | ||||
| } | ||||
|  | ||||
| export function hideElem(el: ElementArg) { | ||||
|   toggleElem(el, false); | ||||
| } | ||||
|  | ||||
| export function isElemHidden(el: ElementArg) { | ||||
|   const res: boolean[] = []; | ||||
|   elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden'))); | ||||
|   if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`); | ||||
|   return res[0]; | ||||
| export function hideElem(el: ElementArg): ArrayLikeIterable<Element> { | ||||
|   return toggleElem(el, false); | ||||
| } | ||||
|  | ||||
| function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback<T>): ArrayLikeIterable<T> { | ||||
| @@ -275,14 +268,12 @@ export function initSubmitEventPolyfill() { | ||||
|   document.body.addEventListener('focus', submitEventPolyfillListener); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Check if an element is visible, equivalent to jQuery's `:visible` pseudo. | ||||
|  * Note: This function doesn't account for all possible visibility scenarios. | ||||
|  */ | ||||
| export function isElemVisible(element: HTMLElement): boolean { | ||||
|   if (!element) return false; | ||||
|   // checking element.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 Boolean((element.offsetWidth || element.offsetHeight || element.getClientRects().length) && element.style.display !== 'none'); | ||||
| export function isElemVisible(el: HTMLElement): boolean { | ||||
|   // Check if an element is visible, equivalent to jQuery's `:visible` pseudo. | ||||
|   // 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'); | ||||
| } | ||||
|  | ||||
| // replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this | ||||
|   | ||||
		Reference in New Issue
	
	Block a user