-
Notifications
You must be signed in to change notification settings - Fork 890
[EuiIllustration] Add adaptive light-dark illustration variant #9797
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| - Updated `EuiIllustration` to render the color-mode-adaptive SVG when an asset provides one, setting `color-scheme` from the active theme so its colors resolve via CSS `light-dark()`. It falls back to the discrete `light`/`dark` markup otherwise. |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,6 +16,10 @@ import { hideAllStorybookControls } from '../../../.storybook/utils'; | |||||||||
| import { useEuiTheme } from '../../services'; | ||||||||||
| import { EuiButton } from '../button'; | ||||||||||
| import { EuiEmptyPrompt } from '../empty_prompt'; | ||||||||||
| import { EuiFlexGroup, EuiFlexItem } from '../flex'; | ||||||||||
| import { EuiPanel } from '../panel'; | ||||||||||
| import { EuiSpacer } from '../spacer'; | ||||||||||
| import { EuiText } from '../text'; | ||||||||||
| import { | ||||||||||
| EuiIllustration, | ||||||||||
| EuiIllustrationProps, | ||||||||||
|
|
@@ -59,7 +63,7 @@ export const Playground: Story = { | |||||||||
| if (fullWidth) props.push('fullWidth'); | ||||||||||
|
|
||||||||||
| return `import { ${type} } from '@elastic/eui-illustrations'; | ||||||||||
|
|
||||||||||
| <EuiIllustration ${props.join(' ')} />`; | ||||||||||
| }, | ||||||||||
| }, | ||||||||||
|
|
@@ -120,6 +124,52 @@ export const EmptyPrompt: Story = { | |||||||||
| ), | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| const ADAPTIVE_SNIPPET = `import { useEuiTheme } from '@elastic/eui'; | ||||||||||
| import { shoppingCart } from '@elastic/eui-illustrations'; | ||||||||||
|
|
||||||||||
| // One string. The ancestor \`color-scheme\` picks which \`light-dark()\` value | ||||||||||
| // applies. Pin it (\`light\`/\`dark\`), follow the OS \`prefers-color-scheme\` | ||||||||||
| // (\`light dark\`), or mirror the EUI theme (\`EuiProvider\`). | ||||||||||
| const PROVIDER_SCHEME = 'EuiProvider'; | ||||||||||
| const SYSTEM_SCHEME = 'system'; | ||||||||||
| const schemes = ['light', 'dark', PROVIDER_SCHEME, SYSTEM_SCHEME] as const; | ||||||||||
|
|
||||||||||
| const AdaptiveIllustrations = () => { | ||||||||||
| const { colorMode } = useEuiTheme(); | ||||||||||
| const providerScheme = colorMode === 'DARK' ? 'dark' : 'light'; | ||||||||||
|
|
||||||||||
| const resolveScheme = (scheme) => { | ||||||||||
| if (scheme === PROVIDER_SCHEME) return providerScheme; | ||||||||||
| if (scheme === SYSTEM_SCHEME) return 'light dark'; | ||||||||||
| return scheme; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| return schemes.map((scheme) => ( | ||||||||||
| <div | ||||||||||
| key={scheme} | ||||||||||
| style={{ colorScheme: resolveScheme(scheme) }} | ||||||||||
| dangerouslySetInnerHTML={{ __html: shoppingCart.adaptive ?? shoppingCart.light }} | ||||||||||
| /> | ||||||||||
| )); | ||||||||||
| };`; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Most assets ship a single \`adaptive\` SVG whose colors resolve via CSS | ||||||||||
| * \`light-dark()\`. \`EuiIllustration\` sets \`color-scheme\` from the EUI theme; | ||||||||||
| * this story sets it manually so the same string renders pinned \`light\`, | ||||||||||
| * pinned \`dark\`, following \`EuiProvider\`, and following the OS | ||||||||||
| * (\`light dark\`, via \`prefers-color-scheme\`) at once. | ||||||||||
| * \`aerospace\` has no \`adaptive\` variant and cannot adapt. | ||||||||||
| */ | ||||||||||
|
Comment on lines
+156
to
+163
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Do we even need this comment? It's verbose, the escaping makes it hard to read and the story is self-explanatory? |
||||||||||
| export const Adaptive: Story = { | ||||||||||
| parameters: { | ||||||||||
| vrt: { skip: true }, | ||||||||||
| codeSnippet: { snippet: ADAPTIVE_SNIPPET }, | ||||||||||
| ...hideAllStorybookControls, | ||||||||||
| }, | ||||||||||
| render: () => <AdaptiveExample />, | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * VRT only | ||||||||||
| */ | ||||||||||
|
|
@@ -146,6 +196,119 @@ export const SizingFullWidth: Story = { | |||||||||
| * Helpers | ||||||||||
| */ | ||||||||||
|
|
||||||||||
| // Sentinels resolved to real CSS in `AdaptiveExample`: `EuiProvider` to the | ||||||||||
| // live theme color mode (a module-level const can't read `useEuiTheme()`), and | ||||||||||
| // `system` to `light dark` (the value that follows `prefers-color-scheme`). | ||||||||||
|
Comment on lines
+199
to
+201
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I think this comment is redundant. It's normal we cannot use hooks at a module level, we see |
||||||||||
| const PROVIDER_SCHEME = 'EuiProvider'; | ||||||||||
| const SYSTEM_SCHEME = 'system'; | ||||||||||
|
|
||||||||||
| const ADAPTIVE_COLOR_SCHEMES = [ | ||||||||||
| { scheme: 'light', label: 'color-scheme: light' }, | ||||||||||
| { scheme: 'dark', label: 'color-scheme: dark' }, | ||||||||||
| { scheme: PROVIDER_SCHEME, label: 'color-scheme: EuiProvider' }, | ||||||||||
| { scheme: SYSTEM_SCHEME, label: 'color-scheme: system' }, | ||||||||||
| ] as const; | ||||||||||
|
|
||||||||||
| const AdaptiveCard = ({ | ||||||||||
| label, | ||||||||||
| illustration, | ||||||||||
| colorScheme, | ||||||||||
| }: { | ||||||||||
| label: string; | ||||||||||
| illustration: EuiIllustrationSource; | ||||||||||
| colorScheme: string; | ||||||||||
| }) => ( | ||||||||||
| <EuiPanel | ||||||||||
| hasBorder | ||||||||||
| paddingSize="m" | ||||||||||
| css={css` | ||||||||||
| color-scheme: ${colorScheme}; | ||||||||||
| `} | ||||||||||
| > | ||||||||||
| <EuiText size="xs" color="subdued"> | ||||||||||
| <code>{label}</code> | ||||||||||
| </EuiText> | ||||||||||
| <EuiSpacer size="s" /> | ||||||||||
| <div | ||||||||||
| css={css` | ||||||||||
| inline-size: 200px; | ||||||||||
| padding: 8px; | ||||||||||
| border-radius: 4px; | ||||||||||
|
Comment on lines
+235
to
+236
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion:
Suggested change
|
||||||||||
| /* Hardcoded so the surface follows color-scheme, not the EUI theme. */ | ||||||||||
| background: light-dark(#ffffff, #0b1628); | ||||||||||
| `} | ||||||||||
| dangerouslySetInnerHTML={{ | ||||||||||
| __html: illustration.adaptive ?? illustration.light, | ||||||||||
| }} | ||||||||||
| /> | ||||||||||
| </EuiPanel> | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| const AdaptiveExample = () => { | ||||||||||
| const { colorMode } = useEuiTheme(); | ||||||||||
| const providerScheme = colorMode === 'DARK' ? 'dark' : 'light'; | ||||||||||
|
|
||||||||||
| const resolveScheme = (scheme: string) => { | ||||||||||
| if (scheme === PROVIDER_SCHEME) return providerScheme; | ||||||||||
| if (scheme === SYSTEM_SCHEME) return 'light dark'; | ||||||||||
| return scheme; | ||||||||||
| }; | ||||||||||
| const resolveLabel = (scheme: string, label: string) => { | ||||||||||
| if (scheme === PROVIDER_SCHEME) return `${label} (${providerScheme})`; | ||||||||||
| if (scheme === SYSTEM_SCHEME) return `${label} (light dark)`; | ||||||||||
| return label; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| return ( | ||||||||||
| <EuiFlexGroup direction="column" gutterSize="l"> | ||||||||||
| <EuiFlexItem grow={false}> | ||||||||||
| <EuiText size="s"> | ||||||||||
| <p> | ||||||||||
| One <code>shopping-cart.adaptive</code> string, rendered under | ||||||||||
| several <code>color-scheme</code> values. No theme change or | ||||||||||
| re-render — CSS <code>light-dark()</code> does the work. The{' '} | ||||||||||
| <code>EuiProvider</code> card mirrors what{' '} | ||||||||||
| <strong>EuiIllustration</strong> does: it follows the EUI color mode | ||||||||||
| (toggle the theme in the Storybook toolbar). The <code>system</code>{' '} | ||||||||||
| card resolves to <code>color-scheme: light dark</code>, following | ||||||||||
| the OS/browser <code>prefers-color-scheme</code> instead, regardless | ||||||||||
| of the EUI theme. | ||||||||||
| </p> | ||||||||||
| </EuiText> | ||||||||||
| <EuiSpacer size="s" /> | ||||||||||
| <EuiFlexGroup gutterSize="m"> | ||||||||||
| {ADAPTIVE_COLOR_SCHEMES.map(({ scheme, label }) => ( | ||||||||||
| <EuiFlexItem key={label} grow={false}> | ||||||||||
| <AdaptiveCard | ||||||||||
| label={resolveLabel(scheme, label)} | ||||||||||
| illustration={illustrations.shoppingCart} | ||||||||||
| colorScheme={resolveScheme(scheme)} | ||||||||||
| /> | ||||||||||
| </EuiFlexItem> | ||||||||||
| ))} | ||||||||||
| </EuiFlexGroup> | ||||||||||
| </EuiFlexItem> | ||||||||||
|
|
||||||||||
| <EuiFlexItem grow={false}> | ||||||||||
| <EuiText size="s"> | ||||||||||
| <p> | ||||||||||
| <code>aerospace</code> has no <code>adaptive</code> variant, so it | ||||||||||
| falls back to the discrete <code>light</code> markup and does not | ||||||||||
| respond to <code>color-scheme</code> (shown following{' '} | ||||||||||
| <code>EuiProvider</code>). | ||||||||||
| </p> | ||||||||||
| </EuiText> | ||||||||||
| <EuiSpacer size="s" /> | ||||||||||
| <AdaptiveCard | ||||||||||
| label="aerospace.adaptive ?? light" | ||||||||||
| illustration={illustrations.aerospace} | ||||||||||
| colorScheme={providerScheme} | ||||||||||
| /> | ||||||||||
| </EuiFlexItem> | ||||||||||
| </EuiFlexGroup> | ||||||||||
| ); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Fixture SVG for VRT. Uses a fixed width smaller than the parent container | ||||||||||
| * so VRT snapshots can verify sizing without depending on `@elastic/eui-illustrations`. | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -33,6 +33,14 @@ export interface EuiIllustrationSource { | |||||||
| readonly light: string; | ||||||||
| /** Trusted SVG markup for the dark color mode. Inlined verbatim — see the interface's security note. */ | ||||||||
| readonly dark: string; | ||||||||
| /** | ||||||||
| * Trusted single-SVG markup whose colors resolve via CSS `light-dark()`, | ||||||||
| * driven by the `color-scheme` this component sets from the active | ||||||||
| * `colorMode`. Preferred when present and supported; otherwise the component | ||||||||
| * falls back to {@link light}/{@link dark}. Inlined verbatim — see the | ||||||||
| * interface's security note. | ||||||||
| */ | ||||||||
| readonly adaptive?: string; | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Little too verbose for my taste as well:
Suggested change
|
||||||||
| } | ||||||||
|
|
||||||||
| export type EuiIllustrationProps = Omit< | ||||||||
|
|
@@ -58,19 +66,37 @@ export type EuiIllustrationProps = Omit< | |||||||
| fullWidth?: boolean; | ||||||||
| }; | ||||||||
|
|
||||||||
| /** | ||||||||
| * Whether the runtime can resolve CSS `light-dark()`. Defaults to `true` when | ||||||||
| * `CSS` is unavailable (SSR) so the adaptive markup is chosen consistently on | ||||||||
| * the server and on modern clients, avoiding a hydration mismatch. | ||||||||
| */ | ||||||||
| const supportsLightDark = () => | ||||||||
| typeof CSS === 'undefined' || | ||||||||
| CSS.supports?.('color', 'light-dark(#000, #fff)') === true; | ||||||||
|
|
||||||||
| export const EuiIllustration: FunctionComponent<EuiIllustrationProps> = ({ | ||||||||
| type, | ||||||||
| alt, | ||||||||
| className, | ||||||||
| fullWidth = true, | ||||||||
| style, | ||||||||
| ...rest | ||||||||
| }) => { | ||||||||
| const { colorMode } = useEuiTheme(); | ||||||||
| const styles = useEuiMemoizedStyles(euiIllustrationStyles); | ||||||||
| const classes = classNames('euiIllustration', className); | ||||||||
| const cssStyles = [styles.euiIllustration, fullWidth && styles.fullWidth]; | ||||||||
|
|
||||||||
| const svg = colorMode === 'DARK' ? type.dark : type.light; | ||||||||
| const isDark = colorMode === 'DARK'; | ||||||||
| const useAdaptive = type.adaptive != null && supportsLightDark(); | ||||||||
| const svg = useAdaptive ? type.adaptive! : isDark ? type.dark : type.light; | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||||
|
|
||||||||
| // Pins `color-scheme` so the adaptive SVG's `light-dark()` colors follow the | ||||||||
| // EUI color mode rather than the OS preference. | ||||||||
| const inlineStyle = useAdaptive | ||||||||
| ? { colorScheme: isDark ? 'dark' : 'light', ...style } | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. non-blocking suggestion: Do we want to reverse to avoid consumers overriding it? Unlikely scenario.
Suggested change
|
||||||||
| : style; | ||||||||
|
|
||||||||
| const isDecorative = alt === ''; | ||||||||
| const a11yProps = isDecorative | ||||||||
|
|
@@ -81,6 +107,7 @@ export const EuiIllustration: FunctionComponent<EuiIllustrationProps> = ({ | |||||||
| <span | ||||||||
| className={classes} | ||||||||
| css={cssStyles} | ||||||||
| style={inlineStyle} | ||||||||
| {...a11yProps} | ||||||||
| {...rest} | ||||||||
| dangerouslySetInnerHTML={{ __html: svg }} | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| - Added an `adaptive` variant to illustration assets: a single SVG whose colors resolve at runtime via CSS `light-dark()`, generated by merging the `light`/`dark` pair. It is also emitted as `@elastic/eui-illustrations/svgs/<name>.adaptive.svg` for `<img>`/CSS consumers. Illustrations whose `light`/`dark` files are not structurally identical keep `light`/`dark` only. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
We should disable controls for Adaptive story. We don't need them there and they don't work anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
non-blocking question:
The point of stories for the components is to showcase the component usage. But here we are showcasing
@elastic/eui-illustrationusage. So maybe we can rely on the documentation website entry instead?