Skip to content

Instantly share code, notes, and snippets.

@HansKre
Last active October 21, 2022 06:49
Show Gist options
  • Save HansKre/36a9caf1c56f4332a0f0cfe5459bcf63 to your computer and use it in GitHub Desktop.
Save HansKre/36a9caf1c56f4332a0f0cfe5459bcf63 to your computer and use it in GitHub Desktop.
material-ui

jss class name compilation

Motivation: classNames-concept on prod & dev should not deviate from local development

  • MUIv4 forcefully removes all meaningful CSS-classnames during build process by compiling all jss-classnames to jss<id>
  • Not only is this a big deviation from what the developer, unit tests and CI/CD-pipeline see, it can also highly impact CSS-behavior if CSS-selectors are used.
  • This makes styling of 3rd-party components e.g. FOSS-plugins but even Backstage itself very hard or at least instable and brittle.
  • This PR disables jss-className compilation by re-implementing the name-generation algorithm in createGenerateClassName.ts which then enables styling the dark themed version of CatalogReactUserListPicker.

Implementation

// createGenerateClass.ts
/** This function is used to disable jss-className-compilation on prod. Motivation: classNames-concept on prod should not deviate from dev.
*
* pulled from https://github.com/mui/material-ui/blob/master/packages/mui-styles/src/createGenerateClassName/createGenerateClassName.js
========================================================== */

// pulled from @material-ui/core/styles/ThemeProvider/nested
const hasSymbol = typeof Symbol === 'function' && Symbol.for;

const nested = hasSymbol ? Symbol.for('mui.nested') : '__THEME_NESTED__';

/**
 * This is the list of the style rule name we use as drop in replacement for the built-in
 * pseudo classes (:checked, :disabled, :focused, etc.).
 *
 * Why do they exist in the first place?
 * These classes are used at a specificity of 2.
 * It allows them to override previously defined styles as well as
 * being untouched by simple user overrides.
 */
const stateClasses = [
  'checked',
  'disabled',
  'error',
  'focused',
  'focusVisible',
  'required',
  'expanded',
  'selected',
];

const getNextCounterId = (currentCounter: number): number => {
  const newCounter = currentCounter + 1;
  if (process.env.NODE_ENV !== 'production') {
    if (newCounter >= 1e10) {
      // eslint-disable-next-line no-console
      console.warn(
        [
          'MUI: You might have a memory leak.',
          'The ruleCounter is not supposed to grow that much.',
        ].join('')
      );
    }
  }
  return newCounter;
};

// Returns a function which generates unique class names based on counters.
// When new generator function is created, rule counter is reset.
// We need to reset the rule counter for SSR for each request.
//
// It's inspired by
// https://github.com/cssinjs/jss/blob/4e6a05dd3f7b6572fdd3ab216861d9e446c20331/src/utils/createGenerateClassName.js
export function createGenerateClassName(options: { seed?: string } = {}) {
  const { seed } = options;
  const seedPrefix = seed ? `${seed}-` : '';
  let ruleCounter = 0;
  return (rule: any, styleSheet: any) => {
    const name = styleSheet.options.name;

    if (name && name.indexOf('Mui') === 0 && !styleSheet.options.link) {
      if (stateClasses.indexOf(rule.key) !== -1) {
        return `Mui-${rule.key}`;
      }

      const prefix = `${seedPrefix}${name}-${rule.key}`;

      if (!styleSheet.options.theme[nested] || seed !== '') {
        return prefix;
      }

      ruleCounter = getNextCounterId(ruleCounter);
      return `${prefix}-${ruleCounter}`;
    }

    ruleCounter = getNextCounterId(ruleCounter);
    const suffix = `${rule.key}-${ruleCounter}`;

    if (styleSheet.options.classNamePrefix) {
      return `${seedPrefix}${styleSheet.options.classNamePrefix}-${suffix}`;
    }

    return `${seedPrefix}${suffix}`;
  };
}
// App.tsx
import { createGenerateClassName } from './createGenerateClassName';
import { StylesProvider } from '@material-ui/styles';

function App() {
  return (
    <StylesProvider generateClassName={createGenerateClassName()}>
      <App />
    </StylesProvider>
  );
}

References

Material UI

Media Queries

const up600 = useMediaQuery('(min-width:600px)');
const upXl = useMediaQuery<Theme>((theme) => theme.breakpoints.up('xl'));

Breakpoints

xs, extra-small: 0px sm, small: 600px md, medium: 900px lg, large: 1200px xl, extra-large: 1536px

Add custom breakpoints

const theme = createTheme({
  breakpoints: {
    values: {
      mobile: 0,
      tablet: 640,
      laptop: 1024,
      desktop: 1200,
    },
  },
});

// For TypeScript, [module augmentation](https://mui.com/material-ui/guides/typescript/#customization-of-theme) is needed for the theme to accept the above values
declare module '@mui/material/styles' {
  interface BreakpointOverrides {
    xs: false; // removes the `xs` breakpoint
    sm: false;
    md: false;
    lg: false;
    xl: false;
    mobile: true; // adds the `mobile` breakpoint
    tablet: true;
    laptop: true;
    desktop: true;
  }
}

Full Example with custom Breakpoints

import { useTheme } from '@material-ui/core';
import Grid from '@material-ui/core/Grid';
import {
  createTheme,
  ScopedCssBaseline,
  makeStyles,
  MuiThemeProvider,
} from '@material-ui/core/styles';
import { Breakpoints } from '@material-ui/core/styles/createBreakpoints';
import React from 'react';
import { ToolName } from '../../ToolName';

interface Props {
  name: string;
  children: React.ReactNode;
}

// rgba(0, 0, 0, 0.12) is background-color of <Divider /> component
const borderStyle = (style: 'dashed' | 'solid' = 'solid') =>
  `${style} rgba(0, 0, 0, 0.12) 1px`;

type SomeBreakpoints =
  | Partial<
      {
        unit: string;
        step: number;
      } & Breakpoints
    >
  | undefined;

const useStyles = makeStyles<any, { breakpoints: SomeBreakpoints }>(
  (theme) => ({
    rootContainer: ({ breakpoints }) => ({
      padding: 0,
      [theme.breakpoints.up(breakpoints?.values?.lg || 'lg')]: {
        // add left and right borders only to middle container
        '&:nth-of-type(2)': {
          borderLeft: borderStyle(),
          borderRight: borderStyle(),
        },
      },
      [theme.breakpoints.between(
        breakpoints?.values?.md || 'md',
        breakpoints?.values?.lg || 'lg'
      )]: {
        // if we give a left border to 2nd container and a right border to 3rd container, 1st and 3rd container will be off by 1px
        // instead, we add right boarders to 1st and 3rd to properly align them
        '&:nth-of-type(1), &:nth-of-type(3)': {
          borderRight: borderStyle(),
        },
      },
    }),
    toolNameContainer: {
      flex: '0 0 auto',
      width: '100%',
      // prevent scroll scrolling
      position: 'sticky',
      top: '0px',
      backgroundColor: 'white',
      borderTop: borderStyle(),
      borderBottom: borderStyle(),
    },
    contentContainer: {
      flex: '1 1 auto',
      // left-align with Dialog-Heading
      padding: `${theme.spacing(2)}px ${theme.spacing(2)}px ${theme.spacing(
        2
      )}px 40px`,
    },
  })
);

export function DialogContent({ name, children }: Props) {
  const baseTheme = useTheme();
  const breakpoints: SomeBreakpoints = {
    keys: ['xs', 'sm', 'md', 'lg', 'xl'],
    values: { xs: 0, sm: 568, md: 800, lg: 1050, xl: 1200 },
  };
  const customBreakpointsTheme = createTheme({
    ...baseTheme,
    breakpoints,
  });
  const classes = useStyles({ breakpoints });

  const logo = () => {
    switch (name) {
      case 'BlackDuck':
        return 'blackduck';
      case 'SonarQube':
        return 'sonar';
      case 'Harbor':
        return 'harbor';
      default:
        return undefined;
    }
  };
  return (
    <MuiThemeProvider theme={customBreakpointsTheme}>
      <ScopedCssBaseline>
        <Grid
          data-testid={`dialog-content-${logo()}`}
          container
          item
          xs={12}
          md={6}
          lg={4}
          direction='column'
          wrap='nowrap'
          className={classes.rootContainer}
        >
          <Grid item xs={12} className={classes.toolNameContainer}>
            <ToolName name={name} logo={logo()} large />
          </Grid>
          <Grid item xs={12} className={classes.contentContainer}>
            {children}
          </Grid>
        </Grid>
      </ScopedCssBaseline>
    </MuiThemeProvider>
  );
}

ThemeProvider to override Breakpoints

import { createTheme, MuiThemeProvider } from '@material-ui/core/styles';
import { Breakpoints } from '@material-ui/core/styles/createBreakpoints';
import useTheme from '@material-ui/core/styles/useTheme';
import React from 'react';

type Props = {
  children: React.ReactNode;
};

export type SomeBreakpoints =
  | Partial<
      {
        unit: string;
        step: number;
      } & Breakpoints
    >
  | undefined;

export const breakpoints: SomeBreakpoints = {
  keys: ['xs', 'sm', 'md', 'lg', 'xl'],
  values: { xs: 0, sm: 568, md: 800, lg: 1050, xl: 1200 },
};

/**
 * This ThemeProvider caters the needs of smallers screens and overwrites outer theme's breakpoints from:
 *  values: { xs: 0, sm: 600, md: 960, lg: 1280, xl: 1920 } to:
 *  values: { xs: 0, sm: 568, md: 800, lg: 1050, xl: 1200 }
 * The overwrite is scoped to the descendants only.
 */
export function ThemeProvider({ children }: Props) {
  const baseTheme = useTheme();
  const customBreakpointsTheme = createTheme({
    ...baseTheme,
    breakpoints,
  });
  return (
    <MuiThemeProvider theme={customBreakpointsTheme}>
      {children}
    </MuiThemeProvider>
  );
}

Animations

const useStyles = makeStyles((theme) => ({
  card: {
    '&[class*=MuiCard-root]': {
      height: '100%',
    },
    transition: 'box-shadow .3s, border-radius .6s',
    '&:hover': {
      boxShadow: '0 0 11px rgba(33,33,33,.2)',
      borderRadius: '10px',
    },
    '&:hover button#viewDetails': {
      animation: '$pulse 2s infinite',
    },
  },
  '@keyframes pulse': {
    '0%': {
      // rgb(3, 109, 193) equals to color --wb-blue-40: #036dc1
      boxShadow: '0 0 0 0 rgba(3, 109, 193, 0.6)',
    },
    '70%': {
      boxShadow: '0 0 0 10px rgba(3, 109, 193, 0.0)',
    },
    '100%': {
      boxShadow: '0 0 0 0 rgba(3, 109, 193, 0.0)',
    },
  },
}));

'&.foo' vs. '& .foo'

'&.foo' selects the foo-class on the component itself while '& .foo' selects the foo-class on a descendent. & >.foo is possible as well and would select only a direct child with the foo-class on it.

makeStyles with props

without theme

const useStyles = makeStyles<any, { shouldScroll: boolean }>({
  toggleScroll: ({ shouldScroll }) => ({
    // Enable wrapping
    '& [class*=MuiTabs-flexContainer]': {
      flexWrap: shouldScroll ? 'nowrap' : 'wrap',
    },
    // Remove default underlining since it doesn't work with multiple lines of tabs
    '& [class*=MuiTabs-indicator]': {
      display: shouldScroll ? 'inline-block' : 'none',
    },
    // Underline selected tab
    '& [class*=BackstageHeaderTabs][class*=selected]': {
      borderBottomStyle: 'solid',
      borderBottomWidth: 'initial',
      borderBottomColor: '#0078d6',
    },
  }),
});

export function ToggleableTabScroll() {
  const shouldScroll = useUserFeatureValue({ feature: 'tabscroll' });
  //...
}

with theme

const useStyles = makeStyles<any, { shouldScroll: boolean }>((theme) => ({
  toggleScroll: ({ shouldScroll }) => ({
    // Enable wrapping
    [theme.breakpoints.up('lg')]: {
      flexWrap: shouldScroll ? 'nowrap' : 'wrap',
    },
    // Remove default underlining since it doesn't work with multiple lines of tabs
    '& [class*=MuiTabs-indicator]': {
      display: shouldScroll ? 'inline-block' : 'none',
    },
    // Underline selected tab
    '& [class*=BackstageHeaderTabs][class*=selected]': {
      borderBottomStyle: 'solid',
      borderBottomWidth: 'initial',
      borderBottomColor: '#0078d6',
    },
  }),
}));

export function ToggleableTabScroll() {
  const shouldScroll = useUserFeatureValue({ feature: 'tabscroll' });
  //...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment