diff --git a/hocs/with-class-names/README.md b/hocs/with-class-names/README.md new file mode 100644 index 00000000..5280da83 --- /dev/null +++ b/hocs/with-class-names/README.md @@ -0,0 +1,65 @@ +# `@byndyusoft-ui/with-class-names` + +--- + +> High Order Component for css styles injection ... + +## Installation + +``` +npm i @byndyusoft-ui/with-class-names +``` + +## Method "mergeClassNames" +Use this method for merge component styles with custom styles +```ts + mergeClassNames(targetStyles, sourceStyles, options) +``` + +### Option "withReplace" ("true" by default) + + +## Usage example + +*Checkbox.tsx (Byndyusoft-UI component from "@byndyusoft-ui/Checkbox")* +```tsx + // ... Some imports + import { IClassNames, TClassNamesRecord } from '@byndyusoft-ui/with-class-names'; + import styles from './Checkbox.module.css'; + + // List of classNames for check TypeScript + const styleClassNames = ['container', 'disabled', 'input', 'field', 'label', 'trigger'] as const; + + export type TCheckboxClassNames = TClassNamesRecord<(typeof styleClassNames)[number]>; + export const checkboxClassNames = styles as TCheckboxClassNames; + +export interface ICheckboxProps extends InputHTMLAttributes, IClassNames { + // ... some props +} + +const Checkbox = forwardRef( + ( + { + classNames = checkboxClassNames, + // ... other props + }, + forwardedRef + ) => { + // ... component body + } +); +``` + +*Checkbox.tsx (Project component with custom styles)* +```tsx + import withClassNames, { mergeClassNames } from '@byndyusoft-ui/with-class-names'; + import Checkbox, { TCheckboxClassNames, checkboxClassNames } from '@byndyusoft-ui/Checkbox'; + import styles from './CheckboxWithReplacedStyles.module.css'; + + const Checkbox = withClassNames( + Checkbox, + mergeClassNames(checkboxClassNames, styles as TCheckboxClassNames) + ); + + export default Checkbox; +``` diff --git a/hocs/with-class-names/package.json b/hocs/with-class-names/package.json new file mode 100644 index 00000000..a4731352 --- /dev/null +++ b/hocs/with-class-names/package.json @@ -0,0 +1,32 @@ +{ + "name": "@byndyusoft-ui/with-class-names", + "version": "0.0.0", + "description": "Byndyusoft UI React High Order Component for css styles injection", + "keywords": [ + "byndyusoft", + "byndyusoft-ui", + "react", + "hoc" + ], + "author": "Byndyusoft Frontend Stream ", + "homepage": "https://github.com/Byndyusoft/ui/tree/master/hocs/with-class-names#readme", + "license": "Apache-2.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/Byndyusoft/ui.git" + }, + "scripts": { + "build": "tsc", + "clean": "rimraf dist", + "lint": "eslint src --config ../../eslint.config.js", + "test": "jest --config ../../jest.config.js --roots ./hocs/with-class-names/src" + }, + "bugs": { + "url": "https://github.com/Byndyusoft/ui/issues" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/hocs/with-class-names/src/index.ts b/hocs/with-class-names/src/index.ts new file mode 100644 index 00000000..786cdaab --- /dev/null +++ b/hocs/with-class-names/src/index.ts @@ -0,0 +1,3 @@ +export type { IClassNames, TClassNamesRecord } from './withClassNames'; +export { default as mergeClassNames } from './mergeClassNames'; +export { default } from './withClassNames'; diff --git a/hocs/with-class-names/src/mergeClassNames.tests.ts b/hocs/with-class-names/src/mergeClassNames.tests.ts new file mode 100644 index 00000000..2fc07878 --- /dev/null +++ b/hocs/with-class-names/src/mergeClassNames.tests.ts @@ -0,0 +1,23 @@ +import mergeClassNames from './mergeClassNames'; + +describe('hocs / withClassNames / mergeClassNames', () => { + test('should return different classes', () => { + expect(mergeClassNames({}, { b: '2' })).toEqual({ b: '2' }); + expect(mergeClassNames({ a: '1' }, {})).toEqual({ a: '1' }); + expect(mergeClassNames({ a: '1' }, { b: '2' })).toEqual({ a: '1', b: '2' }); + }); + + test('should return replaced classes', () => { + expect(mergeClassNames({ a: '1' }, { a: '' })).toEqual({ a: '' }); + expect(mergeClassNames({ a: '1' }, { a: undefined })).toEqual({ a: undefined }); + expect(mergeClassNames({ a: '1' }, { a: '2', b: '3' })).toEqual({ a: '2', b: '3' }); + }); + + test('should return merged classes', () => { + expect(mergeClassNames({ a: '1', b: '3' }, { a: '2' }, { withReplace: false })).toEqual({ a: '1 2', b: '3' }); + expect(mergeClassNames({ a: '1', b: '' }, { a: '', b: '3' }, { withReplace: false })).toEqual({ + a: '1', + b: '3' + }); + }); +}); diff --git a/hocs/with-class-names/src/mergeClassNames.ts b/hocs/with-class-names/src/mergeClassNames.ts new file mode 100644 index 00000000..cfa64ccd --- /dev/null +++ b/hocs/with-class-names/src/mergeClassNames.ts @@ -0,0 +1,37 @@ +interface IMergeOptions { + withReplace?: boolean; +} + +const getDefaultOptions = (): IMergeOptions => ({ + withReplace: true +}); + +const joinClassNames = (target: CN, source: CN): CN => { + const result = Object.assign({}, target); + + for (const key in source) { + if (!source[key]) { + continue; + } + + if (result[key]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + result[key] = [result[key], source[key]].join(' '); + } else { + result[key] = source[key]; + } + } + + return result; +}; + +export default function mergeClassNames(target: CN, source: CN, options: IMergeOptions = {}): CN { + const mergedOptions = Object.assign(getDefaultOptions(), options); + + if (mergedOptions.withReplace) { + return Object.assign({}, target, source); + } + + return joinClassNames(target, source); +} diff --git a/hocs/with-class-names/src/withClassNames.tsx b/hocs/with-class-names/src/withClassNames.tsx new file mode 100644 index 00000000..f9f5b2d5 --- /dev/null +++ b/hocs/with-class-names/src/withClassNames.tsx @@ -0,0 +1,15 @@ +import React, { ComponentType, FunctionComponent, PropsWithChildren } from 'react'; + +export interface IClassNames { + classNames?: CN; +} + +export type TClassNamesRecord = Partial>; + +export default function withClassNames( + Component: ComponentType

>, + classNames: CN +): FunctionComponent> { + // eslint-disable-next-line react/display-name + return (props: PropsWithChildren

) => ; +} diff --git a/hocs/with-class-names/tsconfig.json b/hocs/with-class-names/tsconfig.json new file mode 100644 index 00000000..094a8f9a --- /dev/null +++ b/hocs/with-class-names/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationDir": "dist", + "outDir": "dist", + "module": "commonjs", + "target": "es6" + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules", + "src/*.tests.ts" + ] +} diff --git a/jest.config.js b/jest.config.js index 9c7f8d3c..175e18f2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - roots: ['/components', '/hooks', '/services'], + roots: ['/components', '/hocs', '/hooks', '/services'], setupFilesAfterEnv: ['/.jest/setup.ts'], transform: { '^.+\\.tsx?$': 'ts-jest' diff --git a/lerna.json b/lerna.json index 73d6f367..b640a922 100644 --- a/lerna.json +++ b/lerna.json @@ -1,11 +1,5 @@ { - "packages": [ - "components/*", - "hooks/*", - "packages/*", - "services/*", - "styles/*" - ], + "packages": ["components/*", "hocs/*", "hooks/*", "packages/*", "services/*", "styles/*"], "useWorkspaces": true, "version": "independent", "npmClient": "npm", diff --git a/package-lock.json b/package-lock.json index 344ab9f4..9081093d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "Apache-2.0", "workspaces": [ "components/*", + "hocs/*", "hooks/*", "packages/*", "services/*", @@ -67,6 +68,10 @@ "version": "0.1.0", "license": "ISC" }, + "hocs/with-class-names": { + "version": "0.0.0", + "license": "Apache-2.0" + }, "hooks/use-click-outside": { "name": "@byndyusoft-ui/use-click-outside", "version": "0.1.0", @@ -2166,6 +2171,10 @@ "resolved": "hooks/use-window-size", "link": true }, + "node_modules/@byndyusoft-ui/with-class-names": { + "resolved": "hocs/with-class-names", + "link": true + }, "node_modules/@byndyusoft/eslint-config": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@byndyusoft/eslint-config/-/eslint-config-2.0.0.tgz", @@ -34439,6 +34448,9 @@ "@byndyusoft-ui/use-latest-ref": "*" } }, + "@byndyusoft-ui/with-class-names": { + "version": "file:hocs/with-class-names" + }, "@byndyusoft/eslint-config": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@byndyusoft/eslint-config/-/eslint-config-2.0.0.tgz", diff --git a/package.json b/package.json index 0a79ca58..3670bb10 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "workspaces": [ "components/*", + "hocs/*", "hooks/*", "packages/*", "services/*",