diff --git a/.github/actions/audit-accessibility/main.js b/.github/actions/audit-accessibility/main.js index 675f92dbbf..0906f4e229 100644 --- a/.github/actions/audit-accessibility/main.js +++ b/.github/actions/audit-accessibility/main.js @@ -1,7 +1,6 @@ // @ts-check const path = require('path'); const os = require('os'); -const _ = require('lodash'); const {execSync} = require('child_process'); const handler = require('serve-handler'); const http = require('http'); @@ -48,6 +47,7 @@ const startStorybook = () => { const port = 6006; const storybookServer = http.createServer((request, response) => { + // @ts-expect-error - type mismatch in response return handler(request, response, { public: 'public', cleanUrls: ['/'], diff --git a/.github/actions/utils/azure-storage.js b/.github/actions/utils/azure-storage.js index ea6c2eb0db..b2f1baafb4 100644 --- a/.github/actions/utils/azure-storage.js +++ b/.github/actions/utils/azure-storage.js @@ -7,8 +7,8 @@ const fs = require('fs'); const readFile = promisify(fs.readFile); const core = require('@actions/core'); -const ACCOUNT_NAME = core.getInput('azure-account-name') || process.env.INPUT_AZURE_ACCOUNT_NAME; -const ACCOUNT_KEY = core.getInput('azure-account-key') || process.env.INPUT_AZURE_ACCOUNT_KEY; +const ACCOUNT_NAME = core.getInput('azure-account-name') || process.env.INPUT_AZURE_ACCOUNT_NAME || ''; +const ACCOUNT_KEY = core.getInput('azure-account-key') || process.env.INPUT_AZURE_ACCOUNT_KEY || ''; const CONTAINER_NAME = 'mistica-web-' + Date.now(); diff --git a/package.json b/package.json index f4a418479f..67ad705878 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "jest-environment-puppeteer": "^6.1.1", "jimp": "^0.16.1", "lint-staged": "^12.3.7", + "lodash": "^4.17.21", "mini-css-extract-plugin": "^1.6.2", "node-fetch": "^2.6.7", "playroom": "^0.31.0", @@ -149,7 +150,6 @@ "@vanilla-extract/dynamic": "^2.0.3", "@vanilla-extract/sprinkles": "^1.5.1", "classnames": "^2.3.1", - "lodash": "^4.17.21", "moment": "^2.29.1", "react-autosuggest": "^10.1.0", "react-datetime": "^3.1.1", diff --git a/packages/generate-design-tokens/jsconfig.json b/packages/generate-design-tokens/jsconfig.json new file mode 100644 index 0000000000..519c874b15 --- /dev/null +++ b/packages/generate-design-tokens/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "es2020", + "checkJs": true + }, + "exclude": ["node_modules", ".yarn"] +} diff --git a/src/__tests__/sheet-test.tsx b/src/__tests__/sheet-test.tsx index 33f618e29b..28463e163c 100644 --- a/src/__tests__/sheet-test.tsx +++ b/src/__tests__/sheet-test.tsx @@ -95,7 +95,7 @@ test('RadioListSheet', async () => { await waitForElementToBeRemoved(sheet); expect(selectSpy).toHaveBeenCalledWith('1'); -}); +}, 15000); test('ActionsListSheet', async () => { const selectSpy = jest.fn(); diff --git a/src/button-layout.tsx b/src/button-layout.tsx index 8837e9d620..62340933ac 100644 --- a/src/button-layout.tsx +++ b/src/button-layout.tsx @@ -4,7 +4,7 @@ import {useIsomorphicLayoutEffect} from './hooks'; import {ButtonPrimary, ButtonSecondary, ButtonDanger} from './button'; import {BUTTON_MIN_WIDTH} from './button.css'; import classnames from 'classnames'; -import debounce from 'lodash/debounce'; +import {debounce} from './utils/helpers'; import {getPrefixedDataAttributes} from './utils/dom'; import * as styles from './button-layout.css'; diff --git a/src/fixed-footer-layout.tsx b/src/fixed-footer-layout.tsx index 05f0f641b6..ff905a3d8b 100644 --- a/src/fixed-footer-layout.tsx +++ b/src/fixed-footer-layout.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import classnames from 'classnames'; -import debounce from 'lodash/debounce'; +import {debounce} from './utils/helpers'; import {isRunningAcceptanceTest} from './utils/platform'; import { useElementDimensions, diff --git a/src/nestable-context.tsx b/src/nestable-context.tsx index 2e89d98364..a604e35f38 100644 --- a/src/nestable-context.tsx +++ b/src/nestable-context.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import isEqual from 'lodash/isEqual'; +import {isEqual} from './utils/helpers'; const useDeepCompareMemoize = (value: any) => { const ref = React.useRef(); diff --git a/src/switch-component.tsx b/src/switch-component.tsx index d23a8fc380..d47ec9f72e 100644 --- a/src/switch-component.tsx +++ b/src/switch-component.tsx @@ -4,7 +4,7 @@ this storybook bug: https://github.com/storybookjs/storybook/issues/11980 */ import * as React from 'react'; -import debounce from 'lodash/debounce'; +import {debounce} from './utils/helpers'; import {SPACE} from './utils/key-codes'; import {useControlProps} from './form-context'; import {Text3} from './text'; diff --git a/src/utils/__tests__/helpers-test.tsx b/src/utils/__tests__/helpers-test.tsx new file mode 100644 index 0000000000..73235f57a5 --- /dev/null +++ b/src/utils/__tests__/helpers-test.tsx @@ -0,0 +1,121 @@ +import {debounce, isEqual} from '../helpers'; + +beforeEach(() => { + jest.useFakeTimers(); +}); + +test('debounce happy case', () => { + const fn = jest.fn().mockImplementation((a) => a); + const debounced = debounce(fn, 5000); + + debounced(1); + jest.advanceTimersByTime(4500); + debounced(2); + jest.advanceTimersByTime(4500); + debounced(3); + expect(fn).not.toHaveBeenCalled(); + + jest.runAllTimers(); + + expect(fn).toHaveBeenCalledTimes(1); + expect(fn.mock.calls).toEqual([[3]]); +}); + +test('debounce with leading', () => { + const fn = jest.fn().mockImplementation((a) => a); + const debounced = debounce(fn, 5000, {leading: true}); + + debounced(1); + expect(fn.mock.calls).toEqual([[1]]); + + debounced(2); + debounced(3); + expect(fn.mock.calls).toEqual([[1]]); + + jest.runAllTimers(); + + expect(fn).toHaveBeenCalledTimes(2); + expect(fn.mock.calls).toEqual([[1], [3]]); +}); + +test('debounce with maxWait', () => { + const fn = jest.fn().mockImplementation((a) => a); + const debounced = debounce(fn, 2500, {maxWait: 3000}); + + debounced(1); + jest.advanceTimersByTime(2000); + + debounced(2); + debounced(3); + jest.advanceTimersByTime(2000); + expect(fn.mock.calls).toEqual([[3]]); + + debounced(4); + jest.runAllTimers(); + + expect(fn.mock.calls).toEqual([[3], [4]]); +}); + +test("debounce with leading and maxWait don't gets called twice", () => { + const fn = jest.fn().mockImplementation((a) => a); + const debounced = debounce(fn, 2500, {maxWait: 1000, leading: true}); + + debounced(1); + jest.runAllTimers(); + + expect(fn.mock.calls).toEqual([[1]]); +}); + +test('debounce cancel', () => { + const fn = jest.fn().mockImplementation((a) => a); + const debounced = debounce(fn, 5000); + + debounced(1); + debounced(2); + debounced(3); + + debounced.cancel(); + + jest.runAllTimers(); + + expect(fn).not.toHaveBeenCalled(); +}); + +test('isEqual happy case', () => { + const symbol = Symbol('abc'); + + const a = { + n: 123, + s: 'abc', + b: true, + nul: null, + und: undefined, + arr: [1, false, null, undefined, new Date(1234567890), {a: 1, b: 2, c: 3}], + date: new Date(1234567890), + obj: {a: 1, b: 2, c: 3}, + symbol, + }; + + const b = { + n: 123, + s: 'abc', + b: true, + nul: null, + und: undefined, + arr: [1, false, null, undefined, new Date(1234567890), {a: 1, b: 2, c: 3}], + date: new Date(1234567890), + obj: {a: 1, b: 2, c: 3}, + symbol, + }; + + expect(isEqual(a, b)).toBe(true); +}); + +test('isEqual with different primitives', () => { + expect(isEqual(1, 2)).toBe(false); + expect(isEqual('a', 'b')).toBe(false); + expect(isEqual(true, false)).toBe(false); + expect(isEqual(null, undefined)).toBe(false); + expect(isEqual(Symbol('abc'), Symbol('abc'))).toBe(false); + expect(isEqual(new Date(1234567890), new Date(1234567891))).toBe(false); +}); diff --git a/src/utils/helpers.tsx b/src/utils/helpers.tsx new file mode 100644 index 0000000000..3e17a7af76 --- /dev/null +++ b/src/utils/helpers.tsx @@ -0,0 +1,111 @@ +type Debounced = T & {cancel: () => void}; + +export const debounce = ) => any>( + func: T, + wait: number, + options: { + leading?: boolean; + maxWait?: number; + } = {} +): Debounced => { + let debounceTimeoutId: ReturnType | undefined; + let maxWaitTimeoutId: ReturnType | undefined; + let currentArgs: Parameters; + let isLeading = true; + + const debounced = (...args: Parameters) => { + if (debounceTimeoutId) { + clearTimeout(debounceTimeoutId); + } + + if (isLeading && options.leading) { + isLeading = false; + func(...args); + return; + } + + currentArgs = args; + + if (!maxWaitTimeoutId && options.maxWait) { + maxWaitTimeoutId = setTimeout(() => { + func(...currentArgs); + maxWaitTimeoutId = undefined; + clearTimeout(debounceTimeoutId); + }, options.maxWait); + } + + debounceTimeoutId = setTimeout(() => { + func(...args); + if (maxWaitTimeoutId) { + clearTimeout(maxWaitTimeoutId); + } + debounceTimeoutId = undefined; + maxWaitTimeoutId = undefined; + // eslint-disable-next-line testing-library/await-async-utils + }, wait); + }; + + debounced.cancel = () => { + if (debounceTimeoutId) { + clearTimeout(debounceTimeoutId); + debounceTimeoutId = undefined; + } + if (maxWaitTimeoutId) { + clearTimeout(maxWaitTimeoutId); + maxWaitTimeoutId = undefined; + } + }; + + return debounced as Debounced; +}; + +const isPrimitive = (v: unknown): v is string | number | undefined | null | boolean | symbol => { + if (v === null) { + return true; + } + if (typeof v === 'object' || typeof v === 'function') { + return false; + } + return true; +}; + +export const isEqual = (a: unknown, b: unknown): boolean => { + if (a === b) { + return true; + } + + if (isPrimitive(a) || isPrimitive(b)) { + return false; + } + + if (typeof a !== typeof b) { + return false; + } + + if (typeof a === 'function') { + // no need to check typeof b === 'function' because of the previous check + return false; + } + + if (Array.isArray(a) || Array.isArray(b)) { + if (Array.isArray(a) && Array.isArray(b)) { + return a.length === b.length && a.every((value, index) => isEqual(value, b[index])); + } + return false; + } + + if (a instanceof Date || b instanceof Date) { + if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime(); + } + return false; + } + + const keysA = Object.keys(a as any); + const keysB = Object.keys(b as any); + if (keysA.length === keysB.length) { + return keysA.every((key) => isEqual((a as any)[key], (b as any)[key])); + } + + return false; +}; diff --git a/vite.config.js b/vite.config.js index 4cc73ff8dd..4e43c04429 100644 --- a/vite.config.js +++ b/vite.config.js @@ -13,6 +13,13 @@ export default defineConfig({ fileNames: ({name}) => `${name.replace(/\.css$/, '.css-mistica')}.js`, }), ], + resolve: { + alias: { + // forbid lodash usage + lodash: '/dev/null', + 'lodash-es': '/dev/null', + }, + }, publicDir: false, build: { lib: {