diff --git a/.yarnrc.yml b/.yarnrc.yml index 3186f3f0795..4b49c5be129 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1 +1,7 @@ +httpRetry: 5 + +httpTimeout: 600000 + nodeLinker: node-modules + +npmRegistryServer: "https://registry.npmjs.org" diff --git a/cleanup.js b/cleanup.js new file mode 100644 index 00000000000..ef488fba6a9 --- /dev/null +++ b/cleanup.js @@ -0,0 +1,40 @@ +const fs = require('fs'); +const path = require('path'); + +function deleteFolderRecursive(pathToDelete) { + if (fs.existsSync(pathToDelete)) { + fs.readdirSync(pathToDelete).forEach((file) => { + const curPath = path.join(pathToDelete, file); + if (fs.lstatSync(curPath).isDirectory()) { + deleteFolderRecursive(curPath); + } else { + try { + fs.unlinkSync(curPath); + } catch (err) { + // console.error(`Error deleting file ${curPath}:`, err); + } + } + }); + try { + fs.rmdirSync(pathToDelete); + } catch (err) { + // console.error(`Error deleting directory ${pathToDelete}:`, err); + } + } +} + +['node_modules', '.yarn'].forEach((dir) => { + // console.log(`Removing ${dir}...`); + deleteFolderRecursive(dir); +}); + +['.pnp.cjs', '.pnp.loader.mjs'].forEach((file) => { + if (fs.existsSync(file)) { + try { + fs.unlinkSync(file); + // console.log(`Removed ${file}`); + } catch (err) { + // console.error(`Error removing ${file}:`, err); + } + } +}); diff --git a/eslint.config.mjs b/eslint.config.mjs index 567da2b099c..63ba6a9086d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,7 +1,19 @@ import { fixupPluginRules } from '@eslint/compat'; import js from '@eslint/js'; -import patternflyReact from 'eslint-plugin-patternfly-react'; -import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +// Try to import the workspace plugin by package name first; if Node's ESM resolver +// can't find it in this environment (Yarn v4 workspace on Windows) fall back to +// the local package build. Using top-level await is valid in an ESM config. +let patternflyReact; +try { + const mod = await import('eslint-plugin-patternfly-react'); + patternflyReact = mod && mod.default ? mod.default : mod; +} catch (e) { + // Fallback to the local lib build path. This keeps CI behavior unchanged while + // allowing local linting during development. + const local = await import('./packages/eslint-plugin-patternfly-react/lib/index.js'); + patternflyReact = local && local.default ? local.default : local; +} +import prettier from 'eslint-plugin-prettier'; import reactCompiler from 'eslint-plugin-react-compiler'; import reactHooks from 'eslint-plugin-react-hooks'; import react from 'eslint-plugin-react'; @@ -9,6 +21,25 @@ import testingLibrary from 'eslint-plugin-testing-library'; import globals from 'globals'; import tseslint from 'typescript-eslint'; +// The `recommended` export from some plugins (notably eslint-plugin-prettier) +// may include a legacy `extends` key which is invalid in flat config. To +// remain compatible we extract only the parts we can safely include here. +const prettierRecommended = (() => { + try { + const cfg = prettier && prettier.configs && prettier.configs.recommended; + if (cfg && typeof cfg === 'object') { + return { + rules: cfg.rules || {}, + settings: cfg.settings || {}, + languageOptions: cfg.languageOptions || {} + }; + } + } catch (e) { + // swallow — we'll just not include it + } + return null; +})(); + export default [ { ignores: [ @@ -27,10 +58,20 @@ export default [ ...tseslint.configs.recommended, react.configs.flat.recommended, react.configs.flat['jsx-runtime'], - eslintPluginPrettierRecommended, + // include only the safe parts of Prettier's recommended config (avoid legacy 'extends') + ...(prettierRecommended + ? [ + { + languageOptions: prettierRecommended.languageOptions, + settings: prettierRecommended.settings, + rules: prettierRecommended.rules + } + ] + : []), { plugins: { 'patternfly-react': fixupPluginRules(patternflyReact), + prettier: fixupPluginRules(prettier), 'react-hooks': fixupPluginRules(reactHooks), 'react-compiler': reactCompiler }, @@ -68,6 +109,7 @@ export default [ '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-misused-new': 'error', '@typescript-eslint/no-namespace': 'error', + 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { diff --git a/package.json b/package.json index 41033f69eef..9274d667a60 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "eslint": "^9.32.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-markdown": "^5.1.0", + "eslint-plugin-patternfly-react": "workspace:^", "eslint-plugin-prettier": "^5.2.6", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-compiler": "19.0.0-beta-ebf51a3-20250411", diff --git a/packages/react-core/src/components/Toolbar/ToolbarGroup.tsx b/packages/react-core/src/components/Toolbar/ToolbarGroup.tsx index ae2156b052d..65432b31a66 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarGroup.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarGroup.tsx @@ -166,6 +166,15 @@ export interface ToolbarGroupProps extends Omit, xl?: 'wrap' | 'nowrap'; '2xl'?: 'wrap' | 'nowrap'; }; + /** Indicates whether a flex grow modifier of 1 is applied at various breakpoints */ + flexGrow?: { + default?: 'flexGrow'; + sm?: 'flexGrow'; + md?: 'flexGrow'; + lg?: 'flexGrow'; + xl?: 'flexGrow'; + '2xl'?: 'flexGrow'; + }; /** Content to be rendered inside the data toolbar group */ children?: React.ReactNode; /** Flag that modifies the toolbar group to hide overflow and respond to available space. Used for horizontal navigation. */ @@ -185,6 +194,7 @@ class ToolbarGroupWithRef extends Component { columnGap, rowGap, rowWrap, + flexGrow, className, variant, children, @@ -221,6 +231,7 @@ class ToolbarGroupWithRef extends Component { alignSelf === 'center' && styles.modifiers.alignSelfCenter, alignSelf === 'baseline' && styles.modifiers.alignSelfBaseline, isOverflowContainer && styles.modifiers.overflowContainer, + formatBreakpointMods(flexGrow, styles, '', getBreakpoint(width)), className )} {...props} diff --git a/packages/react-core/src/components/Toolbar/ToolbarItem.tsx b/packages/react-core/src/components/Toolbar/ToolbarItem.tsx index 687654455ac..e9c9ed97c43 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarItem.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarItem.tsx @@ -1,6 +1,7 @@ import styles from '@patternfly/react-styles/css/components/Toolbar/toolbar'; import { css } from '@patternfly/react-styles'; -import { formatBreakpointMods, toCamel } from '../../helpers/util'; +import { formatBreakpointMods, setBreakpointCssVars, toCamel } from '../../helpers/util'; +import c_toolbar__item_Width from '@patternfly/react-tokens/dist/esm/c_toolbar__item_Width'; import { Divider } from '../Divider'; import { PageContext } from '../Page/PageContext'; @@ -17,6 +18,24 @@ export interface ToolbarItemProps extends React.HTMLProps { className?: string; /** A type modifier which modifies spacing specifically depending on the type of item */ variant?: ToolbarItemVariant | 'pagination' | 'label' | 'label-group' | 'separator' | 'expand-all'; + /** Width modifier at various breakpoints */ + widths?: { + default?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'; + sm?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'; + md?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'; + lg?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'; + xl?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'; + '2xl'?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'; + }; + /** Indicates whether a flex grow modifier of 1 is applied at various breakpoints */ + flexGrow?: { + default?: 'flexGrow'; + sm?: 'flexGrow'; + md?: 'flexGrow'; + lg?: 'flexGrow'; + xl?: 'flexGrow'; + '2xl'?: 'flexGrow'; + }; /** Visibility at various breakpoints. */ visibility?: { default?: 'hidden' | 'visible'; @@ -185,7 +204,10 @@ export const ToolbarItem: React.FunctionComponent = ({ children, isAllExpanded, isOverflowContainer, + widths, + flexGrow, role, + style, ...props }: ToolbarItemProps) => { if (variant === ToolbarItemVariant.separator) { @@ -210,20 +232,48 @@ export const ToolbarItem: React.FunctionComponent = ({ variant === ToolbarItemVariant['label-group'] && styles.modifiers.labelGroup, isAllExpanded && styles.modifiers.expanded, isOverflowContainer && styles.modifiers.overflowContainer, - formatBreakpointMods(visibility, styles, '', getBreakpoint(width)), - formatBreakpointMods(align, styles, '', getBreakpoint(width)), - formatBreakpointMods(gap, styles, '', getBreakpoint(width)), - formatBreakpointMods(columnGap, styles, '', getBreakpoint(width)), - formatBreakpointMods(rowGap, styles, '', getBreakpoint(width)), - formatBreakpointMods(rowWrap, styles, '', getBreakpoint(width)), alignItems === 'start' && styles.modifiers.alignItemsStart, alignItems === 'center' && styles.modifiers.alignItemsCenter, alignItems === 'baseline' && styles.modifiers.alignItemsBaseline, alignSelf === 'start' && styles.modifiers.alignSelfStart, alignSelf === 'center' && styles.modifiers.alignSelfCenter, alignSelf === 'baseline' && styles.modifiers.alignSelfBaseline, - className + formatBreakpointMods(visibility, styles, '', getBreakpoint(width)), + formatBreakpointMods(align, styles, '', getBreakpoint(width)), + formatBreakpointMods(gap, styles, '', getBreakpoint(width)), + formatBreakpointMods(columnGap, styles, '', getBreakpoint(width)), + formatBreakpointMods(rowGap, styles, '', getBreakpoint(width)), + formatBreakpointMods(rowWrap, styles, '', getBreakpoint(width)), + className, + formatBreakpointMods(flexGrow, styles, '', getBreakpoint(width)) )} + style={{ + ...style, + ...(widths + ? setBreakpointCssVars( + Object.entries(widths).reduce( + (acc, [bp, size]) => { + if (!size) { + return acc; + } + const cssVarValueMap: Record = { + sm: 'var(--pf-c-toolbar__item--m-w-sm--Width)', + md: 'var(--pf-c-toolbar__item--m-w-md--Width)', + lg: 'var(--pf-c-toolbar__item--m-w-lg--Width)', + xl: 'var(--pf-c-toolbar__item--m-w-xl--Width)', + '2xl': 'var(--pf-c-toolbar__item--m-w-2xl--Width)', + '3xl': 'var(--pf-c-toolbar__item--m-w-3xl--Width)', + '4xl': 'var(--pf-c-toolbar__item--m-w-4xl--Width)' + }; + const value = cssVarValueMap[size as keyof typeof cssVarValueMap]; + return value ? { ...acc, [bp]: value } : acc; + }, + {} as Record + ), + (c_toolbar__item_Width as any).name + ) + : undefined) + }} {...(variant === 'label' && { 'aria-hidden': true })} id={id} role={role} diff --git a/packages/react-core/src/components/Toolbar/__tests__/ToolbarGroup.test.tsx b/packages/react-core/src/components/Toolbar/__tests__/ToolbarGroup.test.tsx index 935a5480ee1..840387c27a6 100644 --- a/packages/react-core/src/components/Toolbar/__tests__/ToolbarGroup.test.tsx +++ b/packages/react-core/src/components/Toolbar/__tests__/ToolbarGroup.test.tsx @@ -11,6 +11,22 @@ describe('ToolbarGroup', () => { expect(screen.getByTestId('toolbargroup')).toHaveClass('pf-m-overflow-container'); }); + describe('ToolbarGroup flexGrow', () => { + const bps = ['default', 'sm', 'md', 'lg', 'xl', '2xl']; + + describe.each(bps)('flexGrow at various breakpoints', (bp) => { + it(`should render with pf-m-flex-grow when flexGrow is set at ${bp}`, () => { + render( + + Test + + ); + const bpFlexGrowClass = bp === 'default' ? 'pf-m-flex-grow' : `pf-m-flex-grow-on-${bp}`; + expect(screen.getByTestId('toolbargroup')).toHaveClass(bpFlexGrowClass); + }); + }); + }); + describe('ToobarGroup rowWrap', () => { const bps = ['default', 'sm', 'md', 'lg', 'xl', '2xl']; diff --git a/packages/react-core/src/components/Toolbar/__tests__/ToolbarItem.test.tsx b/packages/react-core/src/components/Toolbar/__tests__/ToolbarItem.test.tsx index 5fbd364a87a..b771e9d1291 100644 --- a/packages/react-core/src/components/Toolbar/__tests__/ToolbarItem.test.tsx +++ b/packages/react-core/src/components/Toolbar/__tests__/ToolbarItem.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; import { ToolbarItem } from '../ToolbarItem'; +import c_toolbar__item_Width from '@patternfly/react-tokens/dist/esm/c_toolbar__item_Width'; describe('ToolbarItem', () => { it('should render with pf-m-overflow-container when isOverflowContainer is set', () => { @@ -37,4 +38,38 @@ describe('ToolbarItem', () => { }); }); }); + + describe('ToolbarItem widths', () => { + const bps = ['default', 'sm', 'md', 'lg', 'xl', '2xl']; + const sizes = ['sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl']; + + describe.each(bps)('widths at various breakpoints', (bp) => { + it.each(sizes)(`applies width CSS var when widths is set to %s at ${bp}`, (size) => { + render( + + Test + + ); + const styleAttr = screen.getByTestId('toolbaritem').getAttribute('style') || ''; + const cssVarName = `${(c_toolbar__item_Width as any).name}${bp === 'default' ? '' : `-on-${bp}`}`; + expect(styleAttr).toContain(cssVarName); + }); + }); + }); + + describe('ToolbarItem flexGrow', () => { + const bps = ['default', 'sm', 'md', 'lg', 'xl', '2xl']; + + describe.each(bps)('flexGrow at various breakpoints', (bp) => { + it(`should render with pf-m-flex-grow when flexGrow is set at ${bp}`, () => { + render( + + Test + + ); + const bpFlexGrowClass = bp === 'default' ? 'pf-m-flex-grow' : `pf-m-flex-grow-on-${bp}`; + expect(screen.getByTestId('toolbaritem')).toHaveClass(bpFlexGrowClass); + }); + }); + }); }); diff --git a/packages/react-core/src/components/Toolbar/__tests__/__snapshots__/Toolbar.test.tsx.snap b/packages/react-core/src/components/Toolbar/__tests__/__snapshots__/Toolbar.test.tsx.snap index 9eb38733f4e..88f87e77471 100644 --- a/packages/react-core/src/components/Toolbar/__tests__/__snapshots__/Toolbar.test.tsx.snap +++ b/packages/react-core/src/components/Toolbar/__tests__/__snapshots__/Toolbar.test.tsx.snap @@ -17,11 +17,13 @@ exports[`Toolbar should render inset 1`] = ` >
Test
Test 2
@@ -30,6 +32,7 @@ exports[`Toolbar should render inset 1`] = ` />
Test 3
@@ -87,6 +90,7 @@ exports[`Toolbar should render with custom label content 1`] = ` >
test content
@@ -104,6 +108,7 @@ exports[`Toolbar should render with custom label content 1`] = ` >
2 filters applied