From: David Cho-Lerat <117642976+david-cho-lerat-sonarsource@users.noreply.github.com> Date: Mon, 13 Mar 2023 16:51:08 +0000 (+0100) Subject: Activate useful @typescript-eslint rules & fix warnings in `design-system/` and relat... X-Git-Tag: 10.0.0.68432~150 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=d4f7645c595a031cdad7d6dccf3ca9b0465590db;p=sonarqube.git Activate useful @typescript-eslint rules & fix warnings in `design-system/` and related components (#7759) --- diff --git a/server/sonar-web/.eslintrc b/server/sonar-web/.eslintrc index ca97fff5e6d..1e45ca41a4f 100644 --- a/server/sonar-web/.eslintrc +++ b/server/sonar-web/.eslintrc @@ -1,5 +1,5 @@ { - "extends": "sonarqube", + "extends": ["sonarqube", "./.eslintrc-typescript"], "rules": { "camelcase": "off", "promise/no-return-wrap": "warn", diff --git a/server/sonar-web/.eslintrc-typescript b/server/sonar-web/.eslintrc-typescript new file mode 100644 index 00000000000..22573c4bd22 --- /dev/null +++ b/server/sonar-web/.eslintrc-typescript @@ -0,0 +1,54 @@ +{ + "extends": [ + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:@typescript-eslint/strict" + ], + "parserOptions": { "project": "./tsconfig.json" }, + "rules": { + "@typescript-eslint/adjacent-overload-signatures": "warn", + "@typescript-eslint/array-type": ["warn", { "default": "array-simple" }], + "@typescript-eslint/await-thenable": "off", + "@typescript-eslint/ban-ts-comment": "warn", + "@typescript-eslint/ban-types": "warn", + "@typescript-eslint/consistent-indexed-object-style": "off", + "@typescript-eslint/default-param-last": "warn", + "@typescript-eslint/no-array-constructor": "warn", + "@typescript-eslint/no-confusing-non-null-assertion": "off", + "@typescript-eslint/no-confusing-void-expression": "warn", + "@typescript-eslint/no-empty-function": "warn", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-extra-non-null-assertion": "off", + "@typescript-eslint/no-floating-promises": "warn", + "@typescript-eslint/no-for-in-array": "warn", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/no-invalid-this": "warn", + "@typescript-eslint/no-loss-of-precision": "warn", + "@typescript-eslint/no-misused-new": "warn", + "@typescript-eslint/no-misused-promises": "warn", + "@typescript-eslint/no-namespace": "warn", + "@typescript-eslint/no-non-null-asserted-nullish-coalescing": "off", + "@typescript-eslint/no-non-null-asserted-optional-chain": "off", + "@typescript-eslint/no-redundant-type-constituents": "warn", + "@typescript-eslint/no-this-alias": "warn", + "@typescript-eslint/no-unnecessary-type-assertion": "off", + "@typescript-eslint/no-unnecessary-type-constraint": "warn", + "@typescript-eslint/no-unsafe-argument": "warn", + "@typescript-eslint/no-unsafe-assignment": "warn", + "@typescript-eslint/no-unsafe-call": "warn", + "@typescript-eslint/no-unsafe-member-access": "warn", + "@typescript-eslint/no-unsafe-return": "warn", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-var-requires": "warn", + "@typescript-eslint/non-nullable-type-assertion-style": "off", + "@typescript-eslint/prefer-as-const": "warn", + "@typescript-eslint/prefer-namespace-keyword": "warn", + "@typescript-eslint/require-await": "warn", + "@typescript-eslint/restrict-plus-operands": "warn", + "@typescript-eslint/restrict-template-expressions": "warn", + "@typescript-eslint/switch-exhaustiveness-check": "warn", + "@typescript-eslint/triple-slash-reference": "warn", + "@typescript-eslint/unbound-method": "off" + } +} diff --git a/server/sonar-web/design-system/.eslintrc b/server/sonar-web/design-system/.eslintrc index ead8e7410dc..181af256e97 100644 --- a/server/sonar-web/design-system/.eslintrc +++ b/server/sonar-web/design-system/.eslintrc @@ -1,5 +1,5 @@ { - "extends": ["sonarqube"], + "extends": ["sonarqube", "../.eslintrc-typescript"], "plugins": ["header", "typescript-sort-keys"], "rules": { // Custom SonarCloud config that differs from eslint-config-sonarqube diff --git a/server/sonar-web/design-system/package.json b/server/sonar-web/design-system/package.json index e4f4d91ea32..bbdc8c8535d 100644 --- a/server/sonar-web/design-system/package.json +++ b/server/sonar-web/design-system/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "yarn lint && yarn ts-check && vite build", "build-release": "yarn install --immutable && yarn build", - "lint": "eslint --ext js,ts,tsx,snap --quiet src", + "lint": "eslint --ext js,ts,tsx,snap src", "lint-report-ci": "yarn install --immutable && eslint --ext js,ts,tsx -f json -o eslint-report/eslint-report.json src || yarn lint", "test": "jest", "ts-check": "tsc --noEmit", diff --git a/server/sonar-web/design-system/src/components/Checkbox.tsx b/server/sonar-web/design-system/src/components/Checkbox.tsx index 7e352d04d3d..80bd53e02d5 100644 --- a/server/sonar-web/design-system/src/components/Checkbox.tsx +++ b/server/sonar-web/design-system/src/components/Checkbox.tsx @@ -66,7 +66,7 @@ export default function Checkbox({ ) => void }>; } export default function ClickEventBoundary({ children }: ClickEventBoundaryProps) { return React.cloneElement(children, { onClick: (e: React.SyntheticEvent) => { e.stopPropagation(); + if (typeof children.props.onClick === 'function') { children.props.onClick(e); } diff --git a/server/sonar-web/design-system/src/components/DeferredSpinner.tsx b/server/sonar-web/design-system/src/components/DeferredSpinner.tsx index 50df8bc2bf7..fad7efff8b0 100644 --- a/server/sonar-web/design-system/src/components/DeferredSpinner.tsx +++ b/server/sonar-web/design-system/src/components/DeferredSpinner.tsx @@ -46,7 +46,7 @@ export default class DeferredSpinner extends React.PureComponent { state: State = { showSpinner: false }; componentDidMount() { - if (this.props.loading == null || this.props.loading === true) { + if (this.props.loading == null || this.props.loading) { this.startTimer(); } } @@ -67,10 +67,9 @@ export default class DeferredSpinner extends React.PureComponent { } startTimer = () => { - this.timer = window.setTimeout( - () => this.setState({ showSpinner: true }), - this.props.timeout || DEFAULT_TIMEOUT - ); + this.timer = window.setTimeout(() => { + this.setState({ showSpinner: true }); + }, this.props.timeout ?? DEFAULT_TIMEOUT); }; stopTimer = () => { diff --git a/server/sonar-web/design-system/src/components/MainMenu.tsx b/server/sonar-web/design-system/src/components/MainMenu.tsx index e61964a77e3..68745af6f4c 100644 --- a/server/sonar-web/design-system/src/components/MainMenu.tsx +++ b/server/sonar-web/design-system/src/components/MainMenu.tsx @@ -25,6 +25,6 @@ const MainMenuUl = styled.ul` ${tw`sw-flex sw-gap-8 sw-items-center`} `; -export function MainMenu({ children }: React.PropsWithChildren<{}>) { +export function MainMenu({ children }: React.PropsWithChildren) { return {children}; } diff --git a/server/sonar-web/design-system/src/components/OutsideClickHandler.tsx b/server/sonar-web/design-system/src/components/OutsideClickHandler.tsx index 07de4f5422f..fb33549c1d1 100644 --- a/server/sonar-web/design-system/src/components/OutsideClickHandler.tsx +++ b/server/sonar-web/design-system/src/components/OutsideClickHandler.tsx @@ -56,7 +56,8 @@ export default class OutsideClickHandler extends React.Component { if (this.mounted) { // eslint-disable-next-line react/no-find-dom-node const node = findDOMNode(this); - if (!node || !node.contains(event.target as Node)) { + + if (!node?.contains(event.target as Node)) { this.props.onClickOutside(); } } diff --git a/server/sonar-web/design-system/src/components/Tooltip.tsx b/server/sonar-web/design-system/src/components/Tooltip.tsx index a298b7fdfd0..cb51f9dbb70 100644 --- a/server/sonar-web/design-system/src/components/Tooltip.tsx +++ b/server/sonar-web/design-system/src/components/Tooltip.tsx @@ -36,7 +36,7 @@ import { themeColor, themeContrast } from '../helpers/theme'; const MILLISECONDS_IN_A_SECOND = 1000; export interface TooltipProps { - children: React.ReactElement<{}>; + children: React.ReactElement; mouseEnterDelay?: number; mouseLeaveDelay?: number; onHide?: VoidFunction; @@ -88,16 +88,19 @@ export class TooltipInner extends React.Component { constructor(props: TooltipProps) { super(props); + this.state = { flipped: false, placement: props.placement, visible: props.visible !== undefined ? props.visible : false, }; + this.throttledPositionTooltip = throttle(this.positionTooltip, THROTTLE_SCROLL_DELAY); } componentDidMount() { this.mounted = true; + if (this.props.visible === true) { this.positionTooltip(); this.addEventListeners(); @@ -106,9 +109,9 @@ export class TooltipInner extends React.Component { componentDidUpdate(prevProps: TooltipProps, prevState: State) { if (this.props.placement !== prevProps.placement) { - this.setState({ placement: this.props.placement }, () => - this.onUpdatePlacement(this.hasVisibleChanged(prevState.visible, prevProps.visible)) - ); + this.setState({ placement: this.props.placement }, () => { + this.onUpdatePlacement(this.hasVisibleChanged(prevState.visible, prevProps.visible)); + }); } else if (this.hasVisibleChanged(prevState.visible, prevProps.visible)) { this.onUpdateVisible(); } else if (!this.state.flipped && this.needsFlipping(this.state)) { @@ -175,18 +178,15 @@ export class TooltipInner extends React.Component { hasVisibleChanged = (prevStateVisible: boolean, prevPropsVisible?: boolean) => { if (this.props.visible === undefined) { - return prevPropsVisible || this.state.visible !== prevStateVisible; + return prevPropsVisible ?? this.state.visible !== prevStateVisible; } + return this.props.visible !== prevPropsVisible; }; - isVisible = () => { - return this.props.visible ?? this.state.visible; - }; + isVisible = () => this.props.visible ?? this.state.visible; - getPlacement = (): PopupPlacement => { - return this.state.placement || PopupPlacement.Bottom; - }; + getPlacement = (): PopupPlacement => this.state.placement ?? PopupPlacement.Bottom; tooltipNodeRef = (node: HTMLElement | null) => { this.tooltipNode = node; @@ -217,6 +217,7 @@ export class TooltipInner extends React.Component { // eslint-disable-next-line react/no-find-dom-node const toggleNode = findDOMNode(this); + if (toggleNode && toggleNode instanceof Element && this.tooltipNode) { const { height, left, leftFix, top, topFix, width } = popupPositioning( toggleNode, @@ -262,7 +263,7 @@ export class TooltipInner extends React.Component { ) { this.setState({ visible: true }); } - }, (this.props.mouseEnterDelay || 0) * MILLISECONDS_IN_A_SECOND); + }, (this.props.mouseEnterDelay ?? 0) * MILLISECONDS_IN_A_SECOND); if (this.props.onShow) { this.props.onShow(); @@ -280,7 +281,7 @@ export class TooltipInner extends React.Component { if (this.mounted && this.props.visible === undefined && !this.mouseIn) { this.setState({ visible: false }); } - }, (this.props.mouseLeaveDelay || 0) * MILLISECONDS_IN_A_SECOND); + }, (this.props.mouseLeaveDelay ?? 0) * MILLISECONDS_IN_A_SECOND); if (this.props.onHide) { this.props.onHide(); @@ -301,8 +302,11 @@ export class TooltipInner extends React.Component { this.handlePointerEnter(); const { children } = this.props; - if (typeof children.props.onPointerEnter === 'function') { - children.props.onPointerEnter(); + + const props = children.props as { onPointerEnter?: VoidFunction }; + + if (typeof props.onPointerEnter === 'function') { + props.onPointerEnter(); } }; @@ -310,8 +314,11 @@ export class TooltipInner extends React.Component { this.handlePointerLeave(); const { children } = this.props; - if (typeof children.props.onPointerLeave === 'function') { - children.props.onPointerLeave(); + + const props = children.props as { onPointerLeave?: VoidFunction }; + + if (typeof props.onPointerLeave === 'function') { + props.onPointerLeave(); } }; @@ -378,7 +385,7 @@ export class TooltipInner extends React.Component { class TooltipPortal extends React.Component { el: HTMLElement; - constructor(props: {}) { + constructor(props: object) { super(props); this.el = document.createElement('div'); } diff --git a/server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx b/server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx index 1d9f6068e56..96d24f7f7bc 100644 --- a/server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx @@ -37,7 +37,7 @@ it('should show clear button only when there is a value', async () => { }); it('should attach ref', () => { - const ref = jest.fn(); + const ref = jest.fn() as jest.Mock; setupWithProps({ innerRef: ref }); expect(ref).toHaveBeenCalled(); expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLInputElement); diff --git a/server/sonar-web/design-system/src/components/__tests__/Link-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Link-test.tsx index fde843342b2..25163a7fd22 100644 --- a/server/sonar-web/design-system/src/components/__tests__/Link-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/Link-test.tsx @@ -26,7 +26,7 @@ import Link, { DiscreetLink } from '../Link'; beforeAll(() => { const { location } = window; - delete (window as any).location; + delete (window as { location?: Location }).location; window.location = { ...location, href: '' }; }); diff --git a/server/sonar-web/design-system/src/components/__tests__/NavLink-test.tsx b/server/sonar-web/design-system/src/components/__tests__/NavLink-test.tsx index 45f967d930b..650e5c73037 100644 --- a/server/sonar-web/design-system/src/components/__tests__/NavLink-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/NavLink-test.tsx @@ -26,7 +26,7 @@ import NavLink from '../NavLink'; beforeAll(() => { const { location } = window; - delete (window as any).location; + delete (window as { location?: Location }).location; window.location = { ...location, href: '' }; }); diff --git a/server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx index 8b448d17521..95ce51cc9b0 100644 --- a/server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx @@ -23,7 +23,7 @@ import { FCProps } from '../../types/misc'; import Tooltip, { TooltipInner } from '../Tooltip'; jest.mock('react-dom', () => { - const reactDom = jest.requireActual('react-dom'); + const reactDom = jest.requireActual('react-dom') as object; return { ...reactDom, findDOMNode: jest.fn().mockReturnValue(undefined) }; }); diff --git a/server/sonar-web/design-system/src/components/clipboard.tsx b/server/sonar-web/design-system/src/components/clipboard.tsx index ea05963f772..1e3763cb911 100644 --- a/server/sonar-web/design-system/src/components/clipboard.tsx +++ b/server/sonar-web/design-system/src/components/clipboard.tsx @@ -120,7 +120,7 @@ export function ClipboardButton({ icon={icon} innerRef={setCopyButton} > - {children || translate('copy')} + {children ?? translate('copy')} )} diff --git a/server/sonar-web/design-system/src/components/icons/Icon.tsx b/server/sonar-web/design-system/src/components/icons/Icon.tsx index 0603fe83cfe..0f5f743f4d4 100644 --- a/server/sonar-web/design-system/src/components/icons/Icon.tsx +++ b/server/sonar-web/design-system/src/components/icons/Icon.tsx @@ -81,6 +81,6 @@ export function OcticonHoc( ); } - IconWrapper.displayName = displayName || WrappedOcticon.displayName || WrappedOcticon.name; + IconWrapper.displayName = displayName ?? WrappedOcticon.displayName ?? WrappedOcticon.name; return IconWrapper; } diff --git a/server/sonar-web/design-system/src/components/popups.tsx b/server/sonar-web/design-system/src/components/popups.tsx index e517ceb7f7d..f2ed594ede7 100644 --- a/server/sonar-web/design-system/src/components/popups.tsx +++ b/server/sonar-web/design-system/src/components/popups.tsx @@ -52,7 +52,7 @@ function PopupBase(props: PopupProps, ref: React.Ref) { ` class PortalWrapper extends React.Component { el: HTMLElement; - constructor(props: {}) { + constructor(props: object) { super(props); this.el = document.createElement('div'); } diff --git a/server/sonar-web/design-system/src/helpers/__tests__/positioning-test.ts b/server/sonar-web/design-system/src/helpers/__tests__/positioning-test.ts index 7953d3531c9..9c90bbe92e3 100644 --- a/server/sonar-web/design-system/src/helpers/__tests__/positioning-test.ts +++ b/server/sonar-web/design-system/src/helpers/__tests__/positioning-test.ts @@ -26,14 +26,14 @@ const toggleRect = { width: 50, height: 20, }), -} as any; +} as Element & { getBoundingClientRect: jest.Mock }; const popupRect = { getBoundingClientRect: jest.fn().mockReturnValue({ width: 200, height: 100, }), -} as any; +} as Element & { getBoundingClientRect: jest.Mock }; beforeAll(() => { Object.defineProperties(document.documentElement, { diff --git a/server/sonar-web/design-system/src/helpers/positioning.ts b/server/sonar-web/design-system/src/helpers/positioning.ts index 09384e2299b..4cceaccc23d 100644 --- a/server/sonar-web/design-system/src/helpers/positioning.ts +++ b/server/sonar-web/design-system/src/helpers/positioning.ts @@ -32,6 +32,7 @@ * - TopLeft = above the block, horizontally left-aligned * - TopRight = above the block, horizontally right-aligned */ + export enum PopupPlacement { Bottom = 'bottom', BottomLeft = 'bottom-left', @@ -83,57 +84,52 @@ export function popupPositioning( const toggleRect = toggleNode.getBoundingClientRect(); const popupRect = popupNode.getBoundingClientRect(); - let left = 0; - let top = 0; + let { left, top } = toggleRect; switch (placement) { case PopupPlacement.Bottom: - left = toggleRect.left + toggleRect.width / 2 - popupRect.width / 2; - top = toggleRect.top + toggleRect.height; + left += toggleRect.width / 2 - popupRect.width / 2; + top += toggleRect.height; break; case PopupPlacement.BottomLeft: - left = toggleRect.left; - top = toggleRect.top + toggleRect.height; + top += toggleRect.height; break; case PopupPlacement.BottomRight: - left = toggleRect.left + toggleRect.width - popupRect.width; - top = toggleRect.top + toggleRect.height; + left += toggleRect.width - popupRect.width; + top += toggleRect.height; break; case PopupPlacement.Left: - left = toggleRect.left - popupRect.width; - top = toggleRect.top + toggleRect.height / 2 - popupRect.height / 2; + left -= popupRect.width; + top += toggleRect.height / 2 - popupRect.height / 2; break; case PopupPlacement.LeftTop: - left = toggleRect.left - popupRect.width; - top = toggleRect.top; + left -= popupRect.width; break; case PopupPlacement.LeftBottom: - left = toggleRect.left - popupRect.width; - top = toggleRect.top + toggleRect.height - popupRect.height; + left -= popupRect.width; + top += toggleRect.height - popupRect.height; break; case PopupPlacement.Right: - left = toggleRect.left + toggleRect.width; - top = toggleRect.top + toggleRect.height / 2 - popupRect.height / 2; + left += toggleRect.width; + top += toggleRect.height / 2 - popupRect.height / 2; break; case PopupPlacement.RightTop: - left = toggleRect.left + toggleRect.width; - top = toggleRect.top; + left += toggleRect.width; break; case PopupPlacement.RightBottom: - left = toggleRect.left + toggleRect.width; - top = toggleRect.top + toggleRect.height - popupRect.height; + left += toggleRect.width; + top += toggleRect.height - popupRect.height; break; case PopupPlacement.Top: - left = toggleRect.left + toggleRect.width / 2 - popupRect.width / 2; - top = toggleRect.top - popupRect.height; + left += toggleRect.width / 2 - popupRect.width / 2; + top -= popupRect.height; break; case PopupPlacement.TopLeft: - left = toggleRect.left; - top = toggleRect.top - popupRect.height; + top -= popupRect.height; break; case PopupPlacement.TopRight: - left = toggleRect.left + toggleRect.width - popupRect.width; - top = toggleRect.top - popupRect.height; + left += toggleRect.width - popupRect.width; + top -= popupRect.height; break; } @@ -141,6 +137,7 @@ export function popupPositioning( Math.max(left, getMinLeftPlacement(toggleRect)), getMaxLeftPlacement(toggleRect, popupRect) ); + const inBoundariesTop = Math.min( Math.max(top, getMinTopPlacement(toggleRect)), getMaxTopPlacement(toggleRect, popupRect) diff --git a/server/sonar-web/design-system/src/helpers/testUtils.tsx b/server/sonar-web/design-system/src/helpers/testUtils.tsx index 558906fe0dc..275881c194a 100644 --- a/server/sonar-web/design-system/src/helpers/testUtils.tsx +++ b/server/sonar-web/design-system/src/helpers/testUtils.tsx @@ -47,7 +47,9 @@ export function renderWithContext( return render(ui, { ...options, wrapper: getContextWrapper() }, userEventOptions); } -type RenderRouterOptions = { additionalRoutes?: ReactNode }; +interface RenderRouterOptions { + additionalRoutes?: ReactNode; +} export function renderWithRouter( ui: React.ReactElement, @@ -55,7 +57,7 @@ export function renderWithRouter( ) { const { additionalRoutes, userEventOptions, ...renderOptions } = options; - function RouterWrapper({ children }: React.PropsWithChildren<{}>) { + function RouterWrapper({ children }: React.PropsWithChildren) { return ( @@ -72,7 +74,7 @@ export function renderWithRouter( } function getContextWrapper() { - return function ContextWrapper({ children }: React.PropsWithChildren<{}>) { + return function ContextWrapper({ children }: React.PropsWithChildren) { return ( @@ -92,17 +94,25 @@ export function mockComponent(name: string, transformProps: (props: any) => any return MockedComponent; } -export const debounceTimer = jest.fn().mockImplementation((callback, timeout) => { - let timeoutId: number; - const debounced = jest.fn((...args) => { - window.clearTimeout(timeoutId); - timeoutId = window.setTimeout(() => callback(...args), timeout); - }); - (debounced as any).cancel = jest.fn(() => { - window.clearTimeout(timeoutId); +export const debounceTimer = jest + .fn() + .mockImplementation((callback: (...args: unknown[]) => void, timeout: number) => { + let timeoutId: number; + + const debounced = jest.fn((...args: unknown[]) => { + window.clearTimeout(timeoutId); + + timeoutId = window.setTimeout(() => { + callback(...args); + }, timeout); + }); + + (debounced as typeof debounced & { cancel: () => void }).cancel = jest.fn(() => { + window.clearTimeout(timeoutId); + }); + + return debounced; }); - return debounced; -}); export function flushPromises(usingFakeTime = false): Promise { return new Promise((resolve) => { diff --git a/server/sonar-web/design-system/src/helpers/theme.ts b/server/sonar-web/design-system/src/helpers/theme.ts index 6dab879cf4a..77048771012 100644 --- a/server/sonar-web/design-system/src/helpers/theme.ts +++ b/server/sonar-web/design-system/src/helpers/theme.ts @@ -101,20 +101,21 @@ function getColor( } // Is theme color overridden by a color name ? const color = colorOverride ? theme.colors[colorOverride as ThemeColors] : [r, g, b]; + if (typeof color === 'string') { return color as CSSColor; } - return getRGBAString(color, opacityOverride ?? color[3] ?? a); + return getRGBAString(color, opacityOverride ?? (color[3] as number | string | undefined) ?? a); } // Simplified version of getColor for contrast colors, fallback to colors if contrast isn't found function getContrast(theme: Theme, colorOverride: ThemeContrasts | ThemeColors | CSSColor) { // Custom CSS property or rgb(a) color, return it directly if ( - colorOverride?.startsWith('var(--') || - colorOverride?.startsWith('rgb(') || - colorOverride?.startsWith('rgba(') + colorOverride.startsWith('var(--') || + colorOverride.startsWith('rgb(') || + colorOverride.startsWith('rgba(') ) { return colorOverride as CSSColor; } diff --git a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx index 24b96e70a5d..182e8ae11dd 100644 --- a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx +++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx @@ -64,7 +64,7 @@ const MIN_SEARCH_QUERY_LENGTH = 2; export class GlobalSearch extends React.PureComponent { input?: HTMLInputElement | null; node?: HTMLElement | null; - nodes: Dict; + nodes: Dict; mounted = false; constructor(props: Props) { @@ -109,11 +109,9 @@ export class GlobalSearch extends React.PureComponent { handleFocus = () => { if (!this.state.open) { // simulate click to close any other dropdowns - const body = document.documentElement; - if (body) { - body.click(); - } + document.documentElement.click(); } + this.openSearch(); }; @@ -143,7 +141,7 @@ export class GlobalSearch extends React.PureComponent { getPlainComponentsList = (results: Results, more: More) => sortQualifiers(Object.keys(results)).reduce((components, qualifier) => { - const next = [...components, ...results[qualifier].map((component) => component.key)]; + const next = [...components, ...(results[qualifier] ?? []).map((component) => component.key)]; if (more[qualifier]) { next.push('qualifier###' + qualifier); } @@ -203,7 +201,7 @@ export class GlobalSearch extends React.PureComponent { more: { ...state.more, [qualifier]: 0 }, results: { ...state.results, - [qualifier]: uniqBy([...state.results[qualifier], ...moreResults], 'key'), + [qualifier]: uniqBy([...(state.results[qualifier] ?? []), ...moreResults], 'key'), }, selected: moreResults.length > 0 ? moreResults[0].key : state.selected, })); @@ -266,6 +264,7 @@ export class GlobalSearch extends React.PureComponent { scrollToSelected = () => { if (this.state.selected) { const node = this.nodes[this.state.selected]; + if (node && this.node) { scrollToElement(node, { topOffset: 30, diff --git a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx index 8e112b048e7..71779ed36cd 100644 --- a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx +++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx @@ -45,7 +45,9 @@ export default class GlobalSearchResult extends React.PureComponent { className={classNames('sw-flex sw-flex-col sw-items-start sw-space-y-1', { active: selected, })} - innerRef={(node: HTMLAnchorElement | null) => this.props.innerRef(component.key, node)} + innerRef={(node: HTMLAnchorElement | null) => { + this.props.innerRef(component.key, node); + }} key={component.key} onClick={this.props.onClose} onPointerEnter={this.doSelect} diff --git a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResults.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResults.tsx index 5ee4ca6f24c..fb1ef0017e3 100644 --- a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResults.tsx +++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResults.tsx @@ -29,7 +29,7 @@ export interface Props { more: More; onMoreClick: (qualifier: string) => void; onSelect: (componentKey: string) => void; - renderNoResults: () => React.ReactElement; + renderNoResults: () => React.ReactElement; renderResult: (component: ComponentResult) => React.ReactNode; results: Results; selected?: string; @@ -42,8 +42,10 @@ export default function GlobalSearchResults(props: Props): React.ReactElement { const components = props.results[qualifier]; - if (components.length > 0) { + + if (components?.length) { const more = props.more[qualifier]; + renderedComponents.push(
    • diff --git a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx index f16d54b2f57..ded1f414256 100644 --- a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx +++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx @@ -54,7 +54,9 @@ export default class GlobalSearchShowMore extends React.PureComponent { ) => this.handleMoreClick(e, qualifier)} + onClick={(e: React.MouseEvent) => { + this.handleMoreClick(e, qualifier); + }} onPointerEnter={() => { this.handleMouseEnter(qualifier); }} diff --git a/server/sonar-web/src/main/js/app/components/global-search/utils.ts b/server/sonar-web/src/main/js/app/components/global-search/utils.ts index 51f9fc636e5..4fbc1c4b1db 100644 --- a/server/sonar-web/src/main/js/app/components/global-search/utils.ts +++ b/server/sonar-web/src/main/js/app/components/global-search/utils.ts @@ -42,9 +42,9 @@ export interface ComponentResult { } export interface Results { - [qualifier: string]: ComponentResult[]; + [qualifier: string]: ComponentResult[] | undefined; } export interface More { - [qualifier: string]: number; + [qualifier: string]: number | undefined; } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx b/server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx index 597c031da3c..e7f0afadc52 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx @@ -37,6 +37,6 @@ function LogoWithAriaText() { ); } -export default function MainSonarQubeBar({ children }: React.PropsWithChildren<{}>) { +export default function MainSonarQubeBar({ children }: React.PropsWithChildren) { return {children}; } diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx b/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx index 6f383868948..234a5b9d70b 100644 --- a/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx +++ b/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx @@ -72,7 +72,12 @@ function renderEmbedDocsPopup() { -