Skip to content

Commit

Permalink
[EuiProvider] Set up componentDefaults prop, context, & documentati…
Browse files Browse the repository at this point in the history
…on (#6923)

* Set up `EuiComponentDefaultsProvider`

+ light tests - actual defaults/override tests should be written per-component

* Add documentation

* Add support for beta/new badges to subitems in nav and subheadings

* changelog
  • Loading branch information
cee-chen committed Aug 1, 2023
1 parent 8787d0c commit 28cc355
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 24 deletions.
9 changes: 3 additions & 6 deletions src-docs/src/components/guide_page/_guide_page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,9 @@
}

.guideSideNav__itemBadge {
margin-inline: $euiSizeXS;
}

// Shift the margin on the badge when selected and the dropdown arrow no longer shows
.euiSideNavItemButton-isSelected .guideSideNav__itemBadge {
margin-right: 0;
margin-inline-start: $euiSizeXS;
// Decrease distance from right side to allow for longer titles and sub-items
margin-inline-end: -$euiSizeS;
}
}

Expand Down
33 changes: 21 additions & 12 deletions src-docs/src/components/guide_page/guide_page_chrome.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ export class GuidePageChrome extends Component {
});
};

renderSideNavBadge = ({ isBeta, isNew }) => {
if (isBeta) {
return (
<EuiBadge color="warning" className="guideSideNav__itemBadge">
BETA
</EuiBadge>
);
}
if (isNew) {
return (
<EuiBadge color="accent" className="guideSideNav__itemBadge">
NEW
</EuiBadge>
);
}
return undefined;
};

scrollNavSectionIntoView = () => {
// wait a bit for react to blow away and re-create the DOM
// then scroll the selected nav section into view
Expand Down Expand Up @@ -80,7 +98,7 @@ export class GuidePageChrome extends Component {
return;
}

return subSectionsWithTitles.map(({ title, sections }) => {
return subSectionsWithTitles.map(({ title, isBeta, isNew, sections }) => {
const id = slugify(title);

const subSectionHref = `${href}/${id}`;
Expand Down Expand Up @@ -115,6 +133,7 @@ export class GuidePageChrome extends Component {
: '',
items: subItems,
forceOpen: !!searchTerm || isCurrentlyOpenSubSection,
icon: this.renderSideNavBadge({ isBeta, isNew }),
};
});
};
Expand Down Expand Up @@ -146,16 +165,6 @@ export class GuidePageChrome extends Component {

const href = `#/${path}`;

const badge = isBeta ? (
<EuiBadge color="warning" className="guideSideNav__itemBadge">
BETA
</EuiBadge>
) : isNew ? (
<EuiBadge color="accent" className="guideSideNav__itemBadge">
NEW
</EuiBadge>
) : undefined;

let visibleName = name;
if (searchTerm) {
visibleName = (
Expand All @@ -176,7 +185,7 @@ export class GuidePageChrome extends Component {
isSelected: item.path === this.props.currentRoute.path,
forceOpen: !!(searchTerm && hasMatchingSubItem),
className: 'guideSideNav__item',
icon: badge,
icon: this.renderSideNavBadge({ isBeta, isNew }),
};
});

Expand Down
12 changes: 11 additions & 1 deletion src-docs/src/components/guide_section/guide_section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface GuideSectionProps
> {
id?: string;
title?: string;
isBeta?: boolean;
isNew?: boolean;
text?: ReactNode;
source?: any[];
demo?: ReactNode;
Expand Down Expand Up @@ -83,6 +85,8 @@ export const GuideSectionCodeTypesMap = {
export const GuideSection: FunctionComponent<GuideSectionProps> = ({
id,
title,
isBeta,
isNew,
text,
demo,
fullScreen,
Expand Down Expand Up @@ -210,7 +214,13 @@ export const GuideSection: FunctionComponent<GuideSectionProps> = ({
className={classNames('guideSection', className)}
>
<EuiSpacer size={(color || title) && isLargeBreakpoint ? 'xxl' : 'xs'} />
<GuideSectionExampleText title={title} id={id} wrapText={wrapText}>
<GuideSectionExampleText
title={title}
id={id}
isBeta={isBeta}
isNew={isNew}
wrapText={wrapText}
>
{text}
</GuideSectionExampleText>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,40 @@
import React, { FunctionComponent, ReactNode } from 'react';
import { EuiSpacer } from '../../../../../src/components/spacer';
import { EuiTitle } from '../../../../../src/components/title';
import { EuiText } from '../../../../../src/components/text';

import {
EuiSpacer,
EuiTitle,
EuiText,
EuiBetaBadge,
} from '../../../../../src/components';

export const LANGUAGES = ['javascript', 'html'] as const;

type GuideSectionExampleText = {
title?: ReactNode;
id?: string;
isBeta?: boolean;
isNew?: boolean;
children?: ReactNode;
wrapText?: boolean;
};

export const GuideSectionExampleText: FunctionComponent<
GuideSectionExampleText
> = ({ title, id, children, wrapText = true }) => {
> = ({ title, id, isBeta, isNew, children, wrapText = true }) => {
let titleNode;

if (title) {
const badge = (isBeta || isNew) && (
<EuiBetaBadge label={isBeta ? 'Beta' : 'New'} color="accent" size="s" />
);

titleNode = (
<>
<EuiTitle>
<h2 id={id}>{title}</h2>
<h2 id={id}>
{title}
{badge && <>&emsp;{badge}</>}
</h2>
</EuiTitle>
<EuiSpacer size="m" />
</>
Expand Down
25 changes: 25 additions & 0 deletions src-docs/src/views/guidelines/getting_started/getting_started.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AppSetup } from './_app_setup';
import { Tokens } from './_tokens';
import { Customizing } from './_customizing';
import { ThemeNotice } from '../../../views/theme/_components/_theme_notice.tsx';
import { euiProviderComponentDefaultsSnippet } from '../../provider/provider_component_defaults';

export const GettingStarted = {
title: 'Getting started',
Expand Down Expand Up @@ -268,5 +269,29 @@ import { findByTestSubject, render, screen } from '@elastic/eui/lib/test/rtl'; /
</>
),
},
{
title: 'Customizing component defaults',
wrapText: false,
text: (
<>
<EuiText grow={false}>
<p>
While all props can be individually customized via props, some
components can have their default props customized globally via{' '}
<strong>EuiProvider's</strong>{' '}
<EuiCode>componentDefaults</EuiCode> API.{' '}
<Link to="/utilities/provider#component-defaults">
Read more in EuiProvider's documentation
</Link>
.
</p>
</EuiText>
<EuiSpacer />
<EuiCodeBlock language="jsx" isCopyable fontSize="m">
{euiProviderComponentDefaultsSnippet}
</EuiCodeBlock>
</>
),
},
],
};
22 changes: 22 additions & 0 deletions src-docs/src/views/provider/provider_component_defaults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { FunctionComponent } from 'react';

import { EuiComponentDefaults } from '../../../../src/components/provider/component_defaults';

// Used to generate a "component" that is parsed for its types
// and used to generate a prop table
export const EuiComponentDefaultsProps: FunctionComponent<
EuiComponentDefaults
> = () => <></>;

// Used by both getting started and EuiProvider component documentation pages
// Exported in one place for DRYness
export const euiProviderComponentDefaultsSnippet = `<EuiProvider
componentDefaults={{
EuiTablePagination: { itemsPerPage: 20, },
EuiFocusTrap: { crossFrame: true },
EuiPortal: { insert },
}}
>
<App />
</EuiProvider>
`;
65 changes: 65 additions & 0 deletions src-docs/src/views/provider/provider_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ import {
EuiCodeBlock,
EuiLink,
EuiSpacer,
EuiCallOut,
} from '../../../../src/components';

import { GuideSectionPropsTable } from '../../components/guide_section/guide_section_parts/guide_section_props_table';

import Setup from './provider_setup';
import GlobalStyles from './provider_styles';
import Warnings from './provider_warning';
import {
EuiComponentDefaultsProps,
euiProviderComponentDefaultsSnippet,
} from './provider_component_defaults';

export const ProviderExample = {
title: 'Provider',
Expand Down Expand Up @@ -135,6 +140,66 @@ export const ProviderExample = {
</EuiText>
),
},
{
title: 'Component defaults',
isBeta: true,
text: (
<EuiText>
<EuiCallOut title="Beta status" iconType="beta">
<p>
This functionality is still currently in beta, and the list of
components as well as defaults that EUI will be supporting is
still under consideration. If you have a component you would like
to see added, feel free to{' '}
<EuiLink
href="https://github.com/elastic/eui/discussions/6922"
target="_blank"
>
discuss that request in EUI's GitHub repo
</EuiLink>
.
</p>
</EuiCallOut>
<EuiSpacer />

<p>
All EUI components ship with a set of baseline defaults that can
usually be configured via props. For example,{' '}
<Link to="/utilities/focus-trap">
<strong>EuiFocusTrap</strong>
</Link>{' '}
defaults to <EuiCode>crossFrame={'{false}'}</EuiCode> - i.e., it
does not trap focus between iframes. If you wanted to change that
behavior in your app across all instances of{' '}
<strong>EuiFocusTrap</strong>, you would be stuck manually passing
that prop over and over again, including in higher-level components
(like modals, popovers, and flyouts) that utilize focus traps.
</p>
<p>
<strong>EuiProvider</strong> allows overriding some component
defaults across all component usages globally via the{' '}
<EuiCode>componentDefaults</EuiCode> prop like so:
</p>

<EuiCodeBlock language="jsx" isCopyable fontSize="m">
{euiProviderComponentDefaultsSnippet}
</EuiCodeBlock>

<p>
The above example would override EUI's default table pagination size
(50) across all usages of EUI tables and data grids, all EUI focus
traps would trap focus even from iframes, and all EUI portals would
be inserted at a specified position (instead of the end of the
document body).
</p>
<p>
The current list of supported components and the prop defaults they
accept are:
</p>
<GuideSectionPropsTable component={EuiComponentDefaultsProps} />
</EuiText>
),
},
{
title: 'Enforce usage',
text: (
Expand Down
7 changes: 7 additions & 0 deletions src/components/portal/portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ export interface EuiPortalProps {
* ReactNode to render as this component's content
*/
children: ReactNode;
/**
* If not specified, `EuiPortal` will insert itself
* into the end of the `document.body` by default
*/
insert?: { sibling: HTMLElement; position: 'before' | 'after' };
/**
* Optional ref callback
*/
portalRef?: (ref: HTMLDivElement | null) => void;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React, { PropsWithChildren } from 'react';
import { renderHook } from '@testing-library/react-hooks';

import {
EuiComponentDefaultsProvider,
useEuiComponentDefaults,
} from './component_defaults';

describe('EuiComponentDefaultsProvider', () => {
it('sets up context that allows accessing the passed `componentDefaults` from anywhere', () => {
const wrapper = ({ children }: PropsWithChildren<{}>) => (
<EuiComponentDefaultsProvider
componentDefaults={{
EuiPortal: {
insert: {
sibling: document.createElement('div'),
position: 'before',
},
},
}}
>
{children}
</EuiComponentDefaultsProvider>
);
const { result } = renderHook(useEuiComponentDefaults, { wrapper });

expect(result.current).toMatchInlineSnapshot(`
Object {
"EuiPortal": Object {
"insert": Object {
"position": "before",
"sibling": <div />,
},
},
}
`);
});

// NOTE: Components are in charge of their own testing to ensure that the props
// coming from `useEuiComponentDefaults()` were properly applied. This file
// is simply a very light wrapper that carries prop data.
});
Loading

0 comments on commit 28cc355

Please sign in to comment.