Skip to content

Commit

Permalink
[CSS-in-JS] Support extensions via overrides prop (#4547)
Browse files Browse the repository at this point in the history
* allow custom properties via overrides; type clean up

* add some tests

* update canopy

* overrides -> modify

* clean up

* Update src/services/theme/README.md

Co-authored-by: Caroline Horn <[email protected]>

* update readme

* Update src/components/common.ts

Co-authored-by: Chandler Prall <[email protected]>

Co-authored-by: Caroline Horn <[email protected]>
Co-authored-by: Chandler Prall <[email protected]>
  • Loading branch information
3 people authored Mar 1, 2021
1 parent 156f7fe commit 90740eb
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 98 deletions.
102 changes: 94 additions & 8 deletions src-docs/src/views/emotion/canopy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
computed,
euiThemeDefault,
buildTheme,
EuiThemeModifications,
} from '../../../../src/services';

const View = () => {
Expand Down Expand Up @@ -76,15 +77,15 @@ const View3 = () => {
const overrides = {
colors: {
light: { euiColorPrimary: '#8A07BD' },
dark: { euiColorPrimary: '#bd07a5' },
dark: { euiColorPrimary: '#BD07A5' },
},
};
return (
<>
<View />

<EuiSpacer />
<EuiThemeProvider overrides={overrides}>
<EuiThemeProvider modify={overrides}>
<em>Overriding primary</em>
<View />
</EuiThemeProvider>
Expand All @@ -98,16 +99,16 @@ const View2 = () => {
light: {
euiColorSecondary: computed(
['colors.euiColorPrimary'],
() => '#85e89d'
() => '#85E89d'
),
},
dark: { euiColorSecondary: '#f0fff4' },
dark: { euiColorSecondary: '#F0FFF4' },
},
};
return (
<>
<EuiSpacer />
<EuiThemeProvider overrides={overrides}>
<EuiThemeProvider modify={overrides}>
<em>Overriding secondary</em>
<View />
</EuiThemeProvider>
Expand Down Expand Up @@ -176,15 +177,95 @@ export default () => {
'CUSTOM'
);

// Difference is due to automatic colorMode reduction during value computation.
// Makes typing slightly inconvenient, but makes consuming values very convenient.
type ExtensionsUncomputed = {
colors: { light: { myColor: string }; dark: { myColor: string } };
custom: {
colors: {
light: { customColor: string };
dark: { customColor: string };
};
mySize: number;
};
};
type ExtensionsComputed = {
colors: { myColor: string };
custom: { colors: { customColor: string }; mySize: number };
};

// Type (EuiThemeModifications<ExtensionsUncomputed>) only necessary if you want IDE autocomplete support here
const extend: EuiThemeModifications<ExtensionsUncomputed> = {
colors: {
light: {
euiColorPrimary: '#F56407',
myColor: computed(['colors.euiColorPrimary'], ([primary]) => primary),
},
dark: {
euiColorPrimary: '#FA924F',
myColor: computed(['colors.euiColorPrimary'], ([primary]) => primary),
},
},
custom: {
colors: {
light: { customColor: '#080AEF' },
dark: { customColor: '#087EEF' },
},
mySize: 5,
},
};

const Extend = () => {
// Generic type (ExtensionsComputed) necessary if accessing extensions/custom properties
const [{ colors, custom }, colorMode] = useEuiTheme<ExtensionsComputed>();
return (
<div css={{ display: 'flex' }}>
<div>
{colorMode}
<pre>
<code>{JSON.stringify({ colors, custom }, null, 2)}</code>
</pre>
</div>
<div>
<h3>
<EuiIcon
aria-hidden="true"
type="stopFilled"
size="xxl"
css={{ color: colors.myColor }}
/>
</h3>
<h3>
<EuiIcon
aria-hidden="true"
type="stopFilled"
size="xxl"
css={{ color: colors.myColor }}
/>
</h3>
<h3>
<EuiIcon
aria-hidden="true"
type="stopFilled"
size="xxl"
css={{
color: custom.colors.customColor,
}}
/>
</h3>
</div>
</div>
);
};

return (
<>
<EuiThemeProvider
// theme={DefaultEuiTheme}
// colorMode={colorMode}
overrides={overrides}>
modify={overrides}>
<button type="button" onClick={toggleTheme}>
{/* @ts-ignore strike */}
<strike>Toggle Color Mode!</strike> Use global config
<del>Toggle Color Mode!</del> Use global config
</button>
<EuiSpacer />
<button type="button" onClick={lightColors}>
Expand All @@ -209,6 +290,11 @@ export default () => {
</EuiThemeProvider>
</EuiThemeProvider>
<EuiSpacer />
{/* Generic type is not necessary here. Note that it should be the uncomputed type */}
<EuiThemeProvider<ExtensionsUncomputed> modify={extend}>
<em>Extensions</em>
<Extend />
</EuiThemeProvider>
</>
);
};
13 changes: 11 additions & 2 deletions src/components/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ export type DistributivePick<T, K extends UnionKeys<T>> = T extends any
export type DistributiveOmit<T, K extends UnionKeys<T>> = T extends any
? Omit<T, Extract<keyof T, K>>
: never;
type RecursiveDistributiveOmit<T, K extends PropertyKey> = T extends any
? T extends object
? RecursiveOmit<T, K>
: T
: never;
export type RecursiveOmit<T, K extends PropertyKey> = Omit<
{ [P in keyof T]: RecursiveDistributiveOmit<T[P], K> },
K
>;

/*
TypeScript's discriminated unions are overly permissive: as long as one type of the union is satisfied
Expand Down Expand Up @@ -221,8 +230,8 @@ export type RecursivePartial<T> = {
? T[P]
: T[P] extends Array<infer U>
? Array<RecursivePartial<U>>
: T[P] extends ReadonlyArray<infer U> // eslint-disable-line @typescript-eslint/array-type
? ReadonlyArray<RecursivePartial<U>> // eslint-disable-line @typescript-eslint/array-type
: T[P] extends ReadonlyArray<infer U>
? ReadonlyArray<RecursivePartial<U>>
: T[P] extends Set<infer V> // checks for Sets
? Set<RecursivePartial<V>>
: T[P] extends Map<infer K, infer V> // checks for Maps
Expand Down
4 changes: 2 additions & 2 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export { useCombinedRefs, useDependentState } from './hooks';
export {
EuiSystemContext,
EuiThemeContext,
EuiOverrideContext,
EuiModificationsContext,
EuiColorModeContext,
useEuiTheme,
withEuiTheme,
Expand All @@ -142,7 +142,7 @@ export {
EuiThemeColor,
EuiThemeColorMode,
EuiThemeComputed,
EuiThemeOverrides,
EuiThemeModifications,
EuiThemeShape,
EuiThemeSystem,
} from './theme';
27 changes: 9 additions & 18 deletions src/services/theme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,14 @@ Returned from `getComputed`, in the shape of:
```js
getComputed(
EuiThemeDefault, // Theme system (Proxy)
{}, // Overrides object
{}, // Modifications object
'light' // Color mode
)
```

#### Overrides
#### Modifications

Compute-time value overrides for theme property values. Because a theme system is unchangeable, this mechanism allows for changing values at certain points during consumption.
The overrides object must match the partial shape of the theme system:

```js
{
sizes: {
euiSize: 4
}
}
```
Because the theme system (built theme) is immutable, modifications can only be made at compute time by providing overrides and extensions for theme property values. These modifications are passed to the `EuiThemeProvider` via the `modify` prop and should match the high-level object shape of the theme.

#### Color mode

Expand All @@ -84,25 +75,25 @@ colors: {

### EuiThemeProvider

Umbrella provider component that holds the various top-level theme configuration option providers: theme system, color mode, overrides; as well as the primary output provider: computed theme.
The actual computation for computed theme values takes place at this level, where the three inputs are known (theme system, color mode, overrides) and the output (computed theme) can be cached for consumption. Input changes are captured and the output is recomputed.
Umbrella provider component that holds the various top-level theme configuration option providers: theme system, color mode, modifications; as well as the primary output provider: computed theme.
The actual computation for computed theme values takes place at this level, where the three inputs are known (theme system, color mode, modifications) and the output (computed theme) can be cached for consumption. Input changes are captured and the output is recomputed.

```js
<EuiThemeProvider
theme={DefaultEuiTheme}
colorMode="light"
overrides={{}}
modify={{}}
/>
```

All three props are optional. The default values for EUI will be used in the event that no configuration is provided. Note, however that colorMode switching will require consumers to maintain that app state.

### useEuiTheme

A custom React hook that is returns the computed theme. This hook it little more than a wrapper around the `useContext` hook, accessing three of the top-level providers: computed theme, color mode, and overrides.
A custom React hook that returns the computed theme. This hook is little more than a wrapper around the `useContext` hook, accessing three of the top-level providers: computed theme, color mode, and modifications.

```js
const [theme, colorMode, overrides] = useEuiTheme();
const [theme, colorMode, modifications] = useEuiTheme();
```

The `theme` variable has TypeScript support, which will result in IDE autocomplete availability.
Expand Down Expand Up @@ -145,4 +136,4 @@ Snapshot testing ([as currently configured](https://emotion.sh/docs/testing#writ
+ }
+ }
>
```
```
4 changes: 2 additions & 2 deletions src/services/theme/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ import { createContext } from 'react';
import {
EuiThemeColorMode,
EuiThemeSystem,
EuiThemeOverrides,
EuiThemeModifications,
EuiThemeComputed,
} from './types';
import { EuiThemeDefault } from './theme';
import { DEFAULT_COLOR_MODE, getComputed } from './utils';

export const EuiSystemContext = createContext<EuiThemeSystem>(EuiThemeDefault);
export const EuiOverrideContext = createContext<EuiThemeOverrides>({});
export const EuiModificationsContext = createContext<EuiThemeModifications>({});
export const EuiColorModeContext = createContext<EuiThemeColorMode>(
DEFAULT_COLOR_MODE
);
Expand Down
10 changes: 5 additions & 5 deletions src/services/theme/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,28 @@ import React, { forwardRef, useContext } from 'react';

import {
EuiThemeContext,
EuiOverrideContext,
EuiModificationsContext,
EuiColorModeContext,
} from './context';
import {
EuiThemeColorMode,
EuiThemeOverrides,
EuiThemeModifications,
EuiThemeComputed,
} from './types';

export const useEuiTheme = <T extends {}>(): [
EuiThemeComputed<T>,
EuiThemeColorMode,
EuiThemeOverrides<T>
EuiThemeModifications<T>
] => {
const theme = useContext(EuiThemeContext);
const overrides = useContext(EuiOverrideContext);
const modifications = useContext(EuiModificationsContext);
const colorMode = useContext(EuiColorModeContext);

return [
theme as EuiThemeComputed<T>,
colorMode,
overrides as EuiThemeOverrides<T>,
modifications as EuiThemeModifications<T>,
];
};

Expand Down
4 changes: 2 additions & 2 deletions src/services/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
export {
EuiSystemContext,
EuiThemeContext,
EuiOverrideContext,
EuiModificationsContext,
EuiColorModeContext,
} from './context';
export { useEuiTheme, withEuiTheme } from './hooks';
Expand All @@ -40,7 +40,7 @@ export {
EuiThemeColor,
EuiThemeColorMode,
EuiThemeComputed,
EuiThemeOverrides,
EuiThemeModifications,
EuiThemeShape,
EuiThemeSystem,
} from './types';
Expand Down
Loading

0 comments on commit 90740eb

Please sign in to comment.