Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Material You] Add motion design tokens #35384

Merged
merged 6 commits into from Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 2 additions & 3 deletions packages/mui-material-next/src/Button/Button.tsx
Expand Up @@ -306,11 +306,10 @@ export const ButtonRoot = styled('button', {
padding: '10px 24px',
minWidth: 64,
letterSpacing,
// Taken from MD2, haven't really found a spec on transitions
transition: theme.transitions.create(
transition: theme.sys.motion.create(
['background-color', 'box-shadow', 'border-color', 'color'],
{
duration: theme.transitions.duration.short,
duration: tokens.sys.motion.duration.short3,
},
),
fontFamily: tokens.sys.typescale.label.large.family,
Expand Down
3 changes: 2 additions & 1 deletion packages/mui-material-next/src/styles/CssVarsProvider.tsx
Expand Up @@ -13,7 +13,8 @@ import defaultTheme from './defaultTheme';

const shouldSkipGeneratingVar = (keys: string[]) =>
!!keys[0].match(/(typography|mixins|breakpoints|direction|transitions)/) ||
(keys[0] === 'palette' && !!keys[1]?.match(/(mode|contrastThreshold|tonalOffset)/));
(keys[0] === 'palette' && !!keys[1]?.match(/(mode|contrastThreshold|tonalOffset)/)) ||
(keys[0] === 'motion' && !!keys[1]?.match(/(create|getAutoHeightDuration)/));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(keys[0] === 'motion' && !!keys[1]?.match(/(create|getAutoHeightDuration)/));

The function will be skipped by default, no need to put them here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I didn't know that, alright then I will just not add the motion key anywhere :)

mnajdova marked this conversation as resolved.
Show resolved Hide resolved

const { CssVarsProvider, useColorScheme, getInitColorSchemeScript } =
createCssVarsProvider<SupportedColorScheme>({
Expand Down
54 changes: 54 additions & 0 deletions packages/mui-material-next/src/styles/Theme.types.ts
Expand Up @@ -133,6 +133,57 @@ export interface Shapes {
borderRadius: number;
}

export interface MD3Easing {
linear: string;
standard: string;
standardAccelerate: string;
standardDecelerate: string;
emphasized: string;
emphasizedDecelerate: string;
emphasizedAccelerate: string;
legacy: string;
legacyDecelerate: string;
legacyAccelerate: string;
}
export interface MD3Duration {
short1: string;
short2: string;
short3: string;
short4: string;
medium1: string;
medium2: string;
medium3: string;
medium4: string;
long1: string;
long2: string;
long3: string;
long4: string;
extraLong1: string;
extraLong2: string;
extraLong3: string;
extraLong4: string;
}

export interface MotionOptions {
easing?: Partial<MD3Easing>;
duration?: Partial<MD3Duration>;
create?: (
props: string | string[],
options?: Partial<{ duration: number | string; easing: string; delay: number | string }>,
) => string;
getAutoHeightDuration?: (height: number) => number;
}

export interface Motion {
easing: MD3Easing;
duration: MD3Duration;
create: (
props: string | string[],
options?: Partial<{ duration: number | string; easing: string; delay: number | string }>,
) => string;
getAutoHeightDuration: (height: number) => number;
}

export interface MD3CssVarsThemeOptions extends Omit<MD2CssVarsThemeOptions, 'colorSchemes'> {
md3?: {
shape?: Partial<Shapes>;
Expand All @@ -144,6 +195,7 @@ export interface MD3CssVarsThemeOptions extends Omit<MD2CssVarsThemeOptions, 'co
typescale?: Partial<MD3Typescale>;
state?: Partial<MD3States>;
elevation?: string[];
motion?: MotionOptions;
};
}

Expand Down Expand Up @@ -175,6 +227,7 @@ export interface Theme extends Omit<MD2Theme, 'vars'> {
typescale: MD3Typescale;
state: MD3States;
elevation: string[];
motion: Motion;
};
md3: {
shape: Shapes;
Expand All @@ -191,6 +244,7 @@ export interface Theme extends Omit<MD2Theme, 'vars'> {
typescale: MD3Typescale;
state: MD3States;
elevation: string[];
motion: Omit<Motion, 'create' | 'getAutoHeightDuration'>;
};
md3: {
shape: Shapes;
Expand Down
20 changes: 14 additions & 6 deletions packages/mui-material-next/src/styles/extendTheme.ts
Expand Up @@ -25,6 +25,7 @@ import md3Typescale from './typescale';
import md3Typeface from './typeface';
import md3State from './states';
import { elevationLight, elevationDark } from './elevation';
import createMotions from './motion';

const defaultLightOverlays: Overlays = [...Array(25)].map(() => undefined) as Overlays;
const defaultDarkOverlays: Overlays = [...Array(25)].map((_, index) => {
Expand Down Expand Up @@ -56,6 +57,11 @@ export default function extendTheme(options: CssVarsThemeOptions = {}, ...args:
const md3LightColors = createMd3LightColorScheme(getCssVar, md3CommonPalette);
const md3DarkColors = createMd3DarkColorScheme(getCssVar, md3CommonPalette);

const motion = createMotions(input.sys?.motion);
const typescale = { ...md3Typescale, ...input.sys?.typescale };
const typeface = { ...md3Typeface, ...input.ref?.typeface };
const state = { ...md3State, ...input.sys?.state };

const {
palette: lightPalette,
// @ts-ignore - sys is md3 specific token
Expand All @@ -70,13 +76,14 @@ export default function extendTheme(options: CssVarsThemeOptions = {}, ...args:
useMaterialYou: true,
ref: {
...input.ref,
typeface: { ...md3Typeface, ...input.ref?.typeface },
typeface,
palette: deepmerge(md3CommonPalette, colorSchemesInput.light?.ref?.palette),
},
sys: {
...input.sys,
typescale: { ...md3Typescale, ...input.sys?.typescale },
state: { ...md3State, ...input.sys?.state },
typescale,
state,
motion,
color: { ...md3LightColors, ...colorSchemesInput.light?.sys?.color },
elevation: colorSchemesInput.light?.sys?.elevation ?? elevationLight,
},
Expand Down Expand Up @@ -105,13 +112,14 @@ export default function extendTheme(options: CssVarsThemeOptions = {}, ...args:
// @ts-ignore - it's fine, everything that is not supported will be spread
ref: {
...input.ref,
typeface: { ...md3Typeface, ...input.ref?.typeface },
typeface,
palette: deepmerge(md3CommonPalette, colorSchemesInput.dark?.ref?.palette),
},
sys: {
...input.sys,
typescale: { ...md3Typescale, ...input.sys?.typescale },
state: { ...md3State, ...input.sys?.state },
typescale,
state,
motion,
color: { ...md3DarkColors, ...colorSchemesInput.dark?.sys?.color },
elevation: colorSchemesInput.dark?.sys?.elevation ?? elevationDark,
},
Expand Down
156 changes: 156 additions & 0 deletions packages/mui-material-next/src/styles/motion.test.js
@@ -0,0 +1,156 @@
import { expect } from 'chai';
import { extendTheme } from '@mui/material-next/styles';
import createMotion, { easing, duration } from './motion';

describe('motion', () => {
const motion = createMotion({});
const create = motion.create;
const getAutoHeightDuration = motion.getAutoHeightDuration;

it('should allow to customize the default duration', () => {
const theme = extendTheme({
sys: {
motion: {
duration: {
medium1: '310ms',
},
},
},
});
expect(theme.sys.motion.create('color')).to.equal(`color 310ms ${easing.standard} 0ms`);
});

describe('create() function', () => {
describe('warnings', () => {
it('should warn when first argument is of bad type', () => {
expect(() => create(5554)).toErrorDev('MUI: Argument "props" must be a string or Array');
expect(() => create({})).toErrorDev('MUI: Argument "props" must be a string or Array');
});

it('should warn when bad "duration" option type', () => {
expect(() => create('font', { duration: null })).toErrorDev(
'MUI: Argument "duration" must be a number or a string but found null',
);
expect(() => create('font', { duration: {} })).toErrorDev(
'MUI: Argument "duration" must be a number or a string but found [object Object]',
);
});

it('should warn when bad "easing" option type', () => {
expect(() => create('transform', { easing: 123 })).toErrorDev(
'MUI: Argument "easing" must be a string',
);
expect(() => create('transform', { easing: {} })).toErrorDev(
'MUI: Argument "easing" must be a string',
);
});

it('should warn when bad "delay" option type', () => {
expect(() => create('size', { delay: null })).toErrorDev(
'MUI: Argument "delay" must be a number or a string',
);
expect(() => create('size', { delay: {} })).toErrorDev(
'MUI: Argument "delay" must be a number or a string',
);
});

it('should warn when passed unrecognized option', () => {
expect(() => create('size', { fffds: 'value' })).toErrorDev(
'MUI: Unrecognized argument(s) [fffds]',
);
});
});

it('should create default transition without arguments', () => {
const transition = create();
expect(transition).to.equal(`all ${duration.medium1} ${easing.standard} 0ms`);
});

it('should take string props as a first argument', () => {
const transition = create('color');
expect(transition).to.equal(`color ${duration.medium1} ${easing.standard} 0ms`);
});

it('should also take array of props as first argument', () => {
const options = { delay: 20 };
const multiple = create(['color', 'size'], options);
const single1 = create('color', options);
const single2 = create('size', options);
const expected = `${single1},${single2}`;
expect(multiple).to.equal(expected);
});

it('should optionally accept number "duration" option in second argument', () => {
const transition = create('font', { duration: 500 });
expect(transition).to.equal(`font 500ms ${easing.standard} 0ms`);
});

it('should optionally accept string "duration" option in second argument', () => {
const transition = create('font', { duration: '500ms' });
expect(transition).to.equal(`font 500ms ${easing.standard} 0ms`);
});

it('should round decimal digits of "duration" prop to whole numbers', () => {
const transition = create('font', { duration: 12.125 });
expect(transition).to.equal(`font 12ms ${easing.standard} 0ms`);
});

it('should optionally accept string "easing" option in second argument', () => {
const transition = create('transform', { easing: easing.linear });
expect(transition).to.equal(`transform ${duration.medium1} ${easing.linear} 0ms`);
});

it('should optionally accept number "delay" option in second argument', () => {
const transition = create('size', { delay: 150 });
expect(transition).to.equal(`size ${duration.medium1} ${easing.standard} 150ms`);
});

it('should optionally accept string "delay" option in second argument', () => {
const transition = create('size', { delay: '150ms' });
expect(transition).to.equal(`size ${duration.medium1} ${easing.standard} 150ms`);
});

it('should round decimal digits of "delay" prop to whole numbers', () => {
const transition = create('size', { delay: 1.547 });
expect(transition).to.equal(`size ${duration.medium1} ${easing.standard} 2ms`);
});

it('should return zero when not passed arguments', () => {
const zeroHeightDuration = getAutoHeightDuration();
expect(zeroHeightDuration).to.equal(0);
});

it('should return zero when passed undefined', () => {
const zeroHeightDuration = getAutoHeightDuration(undefined);
expect(zeroHeightDuration).to.equal(0);
});

it('should return zero when passed null', () => {
const zeroHeightDuration = getAutoHeightDuration(null);
expect(zeroHeightDuration).to.equal(0);
});

it('should return NaN when passed a negative number', () => {
const zeroHeightDurationNegativeOne = getAutoHeightDuration(-1);
// eslint-disable-next-line no-restricted-globals
expect(isNaN(zeroHeightDurationNegativeOne)).to.equal(true);
const zeroHeightDurationSmallNegative = getAutoHeightDuration(-0.000001);
// eslint-disable-next-line no-restricted-globals
expect(isNaN(zeroHeightDurationSmallNegative)).to.equal(true);
const zeroHeightDurationBigNegative = getAutoHeightDuration(-100000);
// eslint-disable-next-line no-restricted-globals
expect(isNaN(zeroHeightDurationBigNegative)).to.equal(true);
});

it('should return values for pre-calculated positive examples', () => {
let zeroHeightDuration = getAutoHeightDuration(14);
expect(zeroHeightDuration).to.equal(159);
zeroHeightDuration = getAutoHeightDuration(100);
expect(zeroHeightDuration).to.equal(239);
zeroHeightDuration = getAutoHeightDuration(0.0001);
expect(zeroHeightDuration).to.equal(46);
zeroHeightDuration = getAutoHeightDuration(100000);
expect(zeroHeightDuration).to.equal(6685);
});
});
});