Skip to content

Commit 32075d2

Browse files
authored
Add types to various low-level functions (#31781)
Adds types to various low-level modules. All changes are type-only, no runtime changes. `tsc` now reports 38 less errors. One problem was that `@types/sortablejs` does not accept promise return in its functions which triggered the linter, so I disabled the rules on those line.
1 parent 9633f33 commit 32075d2

13 files changed

+123
-77
lines changed

package-lock.json

+18-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
"stylelint-declaration-strict-value": "1.10.6",
108108
"stylelint-value-no-unknown-custom-properties": "6.0.1",
109109
"svgo": "3.3.2",
110+
"type-fest": "4.23.0",
110111
"updates": "16.3.7",
111112
"vite-string-plugin": "1.3.4",
112113
"vitest": "2.0.5"

types.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ declare module '*.svg' {
33
export default value;
44
}
55

6+
declare module '*.css' {
7+
const value: string;
8+
export default value;
9+
}
10+
611
declare let __webpack_public_path__: string;
712

813
interface Window {
@@ -20,3 +25,7 @@ declare module 'htmx.org/dist/htmx.esm.js' {
2025
const value = await import('htmx.org');
2126
export default value;
2227
}
28+
29+
interface Element {
30+
_tippy: import('tippy.js').Instance;
31+
}

web_src/js/features/dropzone.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function addCopyLink(file) {
5252
copyLinkEl.addEventListener('click', async (e) => {
5353
e.preventDefault();
5454
const success = await clippie(generateMarkdownLinkForAttachment(file));
55-
showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error);
55+
showTemporaryTooltip(e.target as Element, success ? i18n.copy_success : i18n.copy_error);
5656
});
5757
file.previewTemplate.append(copyLinkEl);
5858
}

web_src/js/features/repo-issue-list.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ async function initIssuePinSort() {
196196

197197
createSortable(pinDiv, {
198198
group: 'shared',
199-
onEnd: pinMoveEnd,
199+
onEnd: pinMoveEnd, // eslint-disable-line @typescript-eslint/no-misused-promises
200200
});
201201
}
202202

web_src/js/features/repo-projects.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ async function initRepoProjectSortable() {
6060
handle: '.project-column-header',
6161
delayOnTouchOnly: true,
6262
delay: 500,
63-
onSort: async () => {
63+
onSort: async () => { // eslint-disable-line @typescript-eslint/no-misused-promises
6464
boardColumns = mainBoard.querySelectorAll('.project-column');
6565

6666
const columnSorting = {
@@ -84,8 +84,8 @@ async function initRepoProjectSortable() {
8484
const boardCardList = boardColumn.querySelectorAll('.cards')[0];
8585
createSortable(boardCardList, {
8686
group: 'shared',
87-
onAdd: moveIssue,
88-
onUpdate: moveIssue,
87+
onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
88+
onUpdate: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
8989
delayOnTouchOnly: true,
9090
delay: 500,
9191
});

web_src/js/features/stopwatch.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function initStopwatch() {
2727
stopwatchEl.removeAttribute('href'); // intended for noscript mode only
2828

2929
createTippy(stopwatchEl, {
30-
content: stopwatchPopup.cloneNode(true),
30+
content: stopwatchPopup.cloneNode(true) as Element,
3131
placement: 'bottom-end',
3232
trigger: 'click',
3333
maxWidth: 'none',

web_src/js/modules/dirauto.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
22

3+
type DirElement = HTMLInputElement | HTMLTextAreaElement;
4+
35
// for performance considerations, it only uses performant syntax
4-
function attachDirAuto(el) {
6+
function attachDirAuto(el: DirElement) {
57
if (el.type !== 'hidden' &&
68
el.type !== 'checkbox' &&
79
el.type !== 'radio' &&
@@ -18,10 +20,12 @@ export function initDirAuto() {
1820
const mutation = mutationList[i];
1921
const len = mutation.addedNodes.length;
2022
for (let i = 0; i < len; i++) {
21-
const addedNode = mutation.addedNodes[i];
23+
const addedNode = mutation.addedNodes[i] as HTMLElement;
2224
if (!isDocumentFragmentOrElementNode(addedNode)) continue;
23-
if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') attachDirAuto(addedNode);
24-
const children = addedNode.querySelectorAll('input, textarea');
25+
if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') {
26+
attachDirAuto(addedNode as DirElement);
27+
}
28+
const children = addedNode.querySelectorAll<DirElement>('input, textarea');
2529
const len = children.length;
2630
for (let childIdx = 0; childIdx < len; childIdx++) {
2731
attachDirAuto(children[childIdx]);
@@ -30,7 +34,7 @@ export function initDirAuto() {
3034
}
3135
});
3236

33-
const docNodes = document.querySelectorAll('input, textarea');
37+
const docNodes = document.querySelectorAll<DirElement>('input, textarea');
3438
const len = docNodes.length;
3539
for (let i = 0; i < len; i++) {
3640
attachDirAuto(docNodes[i]);

web_src/js/modules/sortable.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export async function createSortable(el, opts = {}) {
1+
import type {SortableOptions} from 'sortablejs';
2+
3+
export async function createSortable(el, opts: {handle?: string} & SortableOptions = {}) {
24
const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs');
35

46
return new Sortable(el, {
@@ -15,5 +17,5 @@ export async function createSortable(el, opts = {}) {
1517
opts.onUnchoose?.(e);
1618
},
1719
...opts,
18-
});
20+
} satisfies SortableOptions);
1921
}

web_src/js/modules/stores.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {reactive} from 'vue';
2+
import type {Reactive} from 'vue';
23

3-
let diffTreeStoreReactive;
4+
let diffTreeStoreReactive: Reactive<Record<string, any>>;
45
export function diffTreeStore() {
56
if (!diffTreeStoreReactive) {
67
diffTreeStoreReactive = reactive(window.config.pageData.diffFileInfo);

web_src/js/modules/tippy.ts

+24-23
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,38 @@
11
import tippy, {followCursor} from 'tippy.js';
22
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
33
import {formatDatetime} from '../utils/time.ts';
4+
import type {Content, Instance, Props} from 'tippy.js';
45

5-
const visibleInstances = new Set();
6+
type TippyOpts = {
7+
role?: string,
8+
theme?: 'default' | 'tooltip' | 'menu' | 'box-with-header' | 'bare',
9+
} & Partial<Props>;
10+
11+
const visibleInstances = new Set<Instance>();
612
const arrowSvg = `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`;
713

8-
export function createTippy(target, opts = {}) {
14+
export function createTippy(target: Element, opts: TippyOpts = {}) {
915
// the callback functions should be destructured from opts,
1016
// because we should use our own wrapper functions to handle them, do not let the user override them
1117
const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
1218

13-
const instance = tippy(target, {
19+
const instance: Instance = tippy(target, {
1420
appendTo: document.body,
1521
animation: false,
1622
allowHTML: false,
1723
hideOnClick: false,
1824
interactiveBorder: 20,
1925
ignoreAttributes: true,
2026
maxWidth: 500, // increase over default 350px
21-
onHide: (instance) => {
27+
onHide: (instance: Instance) => {
2228
visibleInstances.delete(instance);
2329
return onHide?.(instance);
2430
},
25-
onDestroy: (instance) => {
31+
onDestroy: (instance: Instance) => {
2632
visibleInstances.delete(instance);
2733
return onDestroy?.(instance);
2834
},
29-
onShow: (instance) => {
35+
onShow: (instance: Instance) => {
3036
// hide other tooltip instances so only one tooltip shows at a time
3137
for (const visibleInstance of visibleInstances) {
3238
if (visibleInstance.props.role === 'tooltip') {
@@ -43,7 +49,7 @@ export function createTippy(target, opts = {}) {
4349
theme: theme || role || 'default',
4450
plugins: [followCursor],
4551
...other,
46-
});
52+
} satisfies Partial<Props>);
4753

4854
if (role === 'menu') {
4955
target.setAttribute('aria-haspopup', 'true');
@@ -58,12 +64,8 @@ export function createTippy(target, opts = {}) {
5864
* If the target element has no content, then no tooltip will be attached, and it returns null.
5965
*
6066
* Note: "tooltip" doesn't equal to "tippy". "tooltip" means a auto-popup content, it just uses tippy as the implementation.
61-
*
62-
* @param target {HTMLElement}
63-
* @param content {null|string}
64-
* @returns {null|tippy}
6567
*/
66-
function attachTooltip(target, content = null) {
68+
function attachTooltip(target: Element, content: Content = null) {
6769
switchTitleToTooltip(target);
6870

6971
content = content ?? target.getAttribute('data-tooltip-content');
@@ -84,7 +86,7 @@ function attachTooltip(target, content = null) {
8486
placement: target.getAttribute('data-tooltip-placement') || 'top-start',
8587
followCursor: target.getAttribute('data-tooltip-follow-cursor') || false,
8688
...(target.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true, aria: {content: 'describedby', expanded: false}} : {}),
87-
};
89+
} as TippyOpts;
8890

8991
if (!target._tippy) {
9092
createTippy(target, props);
@@ -94,7 +96,7 @@ function attachTooltip(target, content = null) {
9496
return target._tippy;
9597
}
9698

97-
function switchTitleToTooltip(target) {
99+
function switchTitleToTooltip(target: Element) {
98100
let title = target.getAttribute('title');
99101
if (title) {
100102
// apply custom formatting to relative-time's tooltips
@@ -118,16 +120,15 @@ function switchTitleToTooltip(target) {
118120
* According to https://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order , mouseover event is fired before mouseenter event
119121
* Some browsers like PaleMoon don't support "addEventListener('mouseenter', capture)"
120122
* The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy
121-
* @param e {Event}
122123
*/
123-
function lazyTooltipOnMouseHover(e) {
124+
function lazyTooltipOnMouseHover(e: MouseEvent) {
124125
e.target.removeEventListener('mouseover', lazyTooltipOnMouseHover, true);
125126
attachTooltip(this);
126127
}
127128

128129
// Activate the tooltip for current element.
129130
// If the element has no aria-label, use the tooltip content as aria-label.
130-
function attachLazyTooltip(el) {
131+
function attachLazyTooltip(el: Element) {
131132
el.addEventListener('mouseover', lazyTooltipOnMouseHover, {capture: true});
132133

133134
// meanwhile, if the element has no aria-label, use the tooltip content as aria-label
@@ -140,15 +141,15 @@ function attachLazyTooltip(el) {
140141
}
141142

142143
// Activate the tooltip for all children elements.
143-
function attachChildrenLazyTooltip(target) {
144-
for (const el of target.querySelectorAll('[data-tooltip-content]')) {
144+
function attachChildrenLazyTooltip(target: Element) {
145+
for (const el of target.querySelectorAll<Element>('[data-tooltip-content]')) {
145146
attachLazyTooltip(el);
146147
}
147148
}
148149

149150
export function initGlobalTooltips() {
150151
// use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed
151-
const observerConnect = (observer) => observer.observe(document, {
152+
const observerConnect = (observer: MutationObserver) => observer.observe(document, {
152153
subtree: true,
153154
childList: true,
154155
attributeFilter: ['data-tooltip-content', 'title'],
@@ -159,15 +160,15 @@ export function initGlobalTooltips() {
159160
for (const mutation of [...mutationList, ...pending]) {
160161
if (mutation.type === 'childList') {
161162
// mainly for Vue components and AJAX rendered elements
162-
for (const el of mutation.addedNodes) {
163+
for (const el of mutation.addedNodes as NodeListOf<Element>) {
163164
if (!isDocumentFragmentOrElementNode(el)) continue;
164165
attachChildrenLazyTooltip(el);
165166
if (el.hasAttribute('data-tooltip-content')) {
166167
attachLazyTooltip(el);
167168
}
168169
}
169170
} else if (mutation.type === 'attributes') {
170-
attachTooltip(mutation.target);
171+
attachTooltip(mutation.target as Element);
171172
}
172173
}
173174
observerConnect(observer);
@@ -177,7 +178,7 @@ export function initGlobalTooltips() {
177178
attachChildrenLazyTooltip(document.documentElement);
178179
}
179180

180-
export function showTemporaryTooltip(target, content) {
181+
export function showTemporaryTooltip(target: Element, content: Content) {
181182
// if the target is inside a dropdown, don't show the tooltip because when the dropdown
182183
// closes, the tippy would be pushed unsightly to the top-left of the screen like seen
183184
// on the issue comment menu.

web_src/js/utils/color.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
import tinycolor from 'tinycolor2';
2+
import type {ColorInput} from 'tinycolor2';
23

34
// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
45
// Keep this in sync with modules/util/color.go
5-
function getRelativeLuminance(color) {
6+
function getRelativeLuminance(color: ColorInput) {
67
const {r, g, b} = tinycolor(color).toRgb();
78
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
89
}
910

10-
function useLightText(backgroundColor) {
11+
function useLightText(backgroundColor: ColorInput) {
1112
return getRelativeLuminance(backgroundColor) < 0.453;
1213
}
1314

1415
// Given a background color, returns a black or white foreground color that the highest
1516
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
1617
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
17-
export function contrastColor(backgroundColor) {
18+
export function contrastColor(backgroundColor: ColorInput) {
1819
return useLightText(backgroundColor) ? '#fff' : '#000';
1920
}
2021

21-
function resolveColors(obj) {
22+
function resolveColors(obj: Record<string, string>) {
2223
const styles = window.getComputedStyle(document.documentElement);
23-
const getColor = (name) => styles.getPropertyValue(name).trim();
24+
const getColor = (name: string) => styles.getPropertyValue(name).trim();
2425
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)]));
2526
}
2627

0 commit comments

Comments
 (0)