diff --git a/src-docs/src/components/guide_page/_guide_page.scss b/src-docs/src/components/guide_page/_guide_page.scss index 578ef92e5df9..27e8150a5a14 100644 --- a/src-docs/src/components/guide_page/_guide_page.scss +++ b/src-docs/src/components/guide_page/_guide_page.scss @@ -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; } } diff --git a/src-docs/src/components/guide_page/guide_page_chrome.js b/src-docs/src/components/guide_page/guide_page_chrome.js index 65ab0b07d233..848298746d4b 100644 --- a/src-docs/src/components/guide_page/guide_page_chrome.js +++ b/src-docs/src/components/guide_page/guide_page_chrome.js @@ -41,6 +41,24 @@ export class GuidePageChrome extends Component { }); }; + renderSideNavBadge = ({ isBeta, isNew }) => { + if (isBeta) { + return ( + + BETA + + ); + } + if (isNew) { + return ( + + NEW + + ); + } + return undefined; + }; + scrollNavSectionIntoView = () => { // wait a bit for react to blow away and re-create the DOM // then scroll the selected nav section into view @@ -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}`; @@ -115,6 +133,7 @@ export class GuidePageChrome extends Component { : '', items: subItems, forceOpen: !!searchTerm || isCurrentlyOpenSubSection, + icon: this.renderSideNavBadge({ isBeta, isNew }), }; }); }; @@ -146,16 +165,6 @@ export class GuidePageChrome extends Component { const href = `#/${path}`; - const badge = isBeta ? ( - - BETA - - ) : isNew ? ( - - NEW - - ) : undefined; - let visibleName = name; if (searchTerm) { visibleName = ( @@ -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 }), }; }); diff --git a/src-docs/src/components/guide_section/guide_section.tsx b/src-docs/src/components/guide_section/guide_section.tsx index 4a989bb68293..0ecc4ab5e70d 100644 --- a/src-docs/src/components/guide_section/guide_section.tsx +++ b/src-docs/src/components/guide_section/guide_section.tsx @@ -35,6 +35,8 @@ export interface GuideSectionProps > { id?: string; title?: string; + isBeta?: boolean; + isNew?: boolean; text?: ReactNode; source?: any[]; demo?: ReactNode; @@ -83,6 +85,8 @@ export const GuideSectionCodeTypesMap = { export const GuideSection: FunctionComponent = ({ id, title, + isBeta, + isNew, text, demo, fullScreen, @@ -210,7 +214,13 @@ export const GuideSection: FunctionComponent = ({ className={classNames('guideSection', className)} > - + {text} diff --git a/src-docs/src/components/guide_section/guide_section_parts/guide_section_text.tsx b/src-docs/src/components/guide_section/guide_section_parts/guide_section_text.tsx index 5f50988eb5d8..75508a8d44aa 100644 --- a/src-docs/src/components/guide_section/guide_section_parts/guide_section_text.tsx +++ b/src-docs/src/components/guide_section/guide_section_parts/guide_section_text.tsx @@ -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) && ( + + ); + titleNode = ( <> -

{title}

+

+ {title} + {badge && <> {badge}} +

diff --git a/src-docs/src/views/guidelines/getting_started/getting_started.js b/src-docs/src/views/guidelines/getting_started/getting_started.js index 608abe7a5bd8..24610fd67178 100644 --- a/src-docs/src/views/guidelines/getting_started/getting_started.js +++ b/src-docs/src/views/guidelines/getting_started/getting_started.js @@ -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', @@ -268,5 +269,29 @@ import { findByTestSubject, render, screen } from '@elastic/eui/lib/test/rtl'; / ), }, + { + title: 'Customizing component defaults', + wrapText: false, + text: ( + <> + +

+ While all props can be individually customized via props, some + components can have their default props customized globally via{' '} + EuiProvider's{' '} + componentDefaults API.{' '} + + Read more in EuiProvider's documentation + + . +

+
+ + + {euiProviderComponentDefaultsSnippet} + + + ), + }, ], }; diff --git a/src-docs/src/views/provider/provider_component_defaults.tsx b/src-docs/src/views/provider/provider_component_defaults.tsx new file mode 100644 index 000000000000..f2561c5a6f1f --- /dev/null +++ b/src-docs/src/views/provider/provider_component_defaults.tsx @@ -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 = ` + + +`; diff --git a/src-docs/src/views/provider/provider_example.js b/src-docs/src/views/provider/provider_example.js index a6ac1a53ec6f..c01f8a9b5247 100644 --- a/src-docs/src/views/provider/provider_example.js +++ b/src-docs/src/views/provider/provider_example.js @@ -8,6 +8,7 @@ import { EuiCodeBlock, EuiLink, EuiSpacer, + EuiCallOut, } from '../../../../src/components'; import { GuideSectionPropsTable } from '../../components/guide_section/guide_section_parts/guide_section_props_table'; @@ -15,6 +16,10 @@ import { GuideSectionPropsTable } from '../../components/guide_section/guide_sec 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', @@ -135,6 +140,66 @@ export const ProviderExample = { ), }, + { + title: 'Component defaults', + isBeta: true, + text: ( + + +

+ 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{' '} + + discuss that request in EUI's GitHub repo + + . +

+
+ + +

+ All EUI components ship with a set of baseline defaults that can + usually be configured via props. For example,{' '} + + EuiFocusTrap + {' '} + defaults to crossFrame={'{false}'} - i.e., it + does not trap focus between iframes. If you wanted to change that + behavior in your app across all instances of{' '} + EuiFocusTrap, 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. +

+

+ EuiProvider allows overriding some component + defaults across all component usages globally via the{' '} + componentDefaults prop like so: +

+ + + {euiProviderComponentDefaultsSnippet} + + +

+ 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). +

+

+ The current list of supported components and the prop defaults they + accept are: +

+ +
+ ), + }, { title: 'Enforce usage', text: ( diff --git a/src/components/portal/portal.tsx b/src/components/portal/portal.tsx index 72cc1c52cb2c..07d91baea5e4 100644 --- a/src/components/portal/portal.tsx +++ b/src/components/portal/portal.tsx @@ -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; } diff --git a/src/components/provider/component_defaults/component_defaults.test.tsx b/src/components/provider/component_defaults/component_defaults.test.tsx new file mode 100644 index 000000000000..f982654c51cb --- /dev/null +++ b/src/components/provider/component_defaults/component_defaults.test.tsx @@ -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<{}>) => ( + + {children} + + ); + const { result } = renderHook(useEuiComponentDefaults, { wrapper }); + + expect(result.current).toMatchInlineSnapshot(` + Object { + "EuiPortal": Object { + "insert": Object { + "position": "before", + "sibling":
, + }, + }, + } + `); + }); + + // 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. +}); diff --git a/src/components/provider/component_defaults/component_defaults.tsx b/src/components/provider/component_defaults/component_defaults.tsx new file mode 100644 index 000000000000..954508b323c8 --- /dev/null +++ b/src/components/provider/component_defaults/component_defaults.tsx @@ -0,0 +1,55 @@ +/* + * 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, { createContext, useContext, FunctionComponent } from 'react'; + +import { EuiPortalProps } from '../../portal'; + +export type EuiComponentDefaults = { + /** + * Provide a global setting for EuiPortal's default insertion position. + */ + EuiPortal?: { insert: EuiPortalProps['insert'] }; + /** + * TODO + */ + EuiFocusTrap?: unknown; + /** + * TODO + */ + EuiPagination?: unknown; +}; + +// Declaring as a static const for reference integrity/reducing rerenders +const emptyDefaults = {}; + +/* + * Context + */ +export const EuiComponentDefaultsContext = + createContext(emptyDefaults); + +/* + * Component + */ +export const EuiComponentDefaultsProvider: FunctionComponent<{ + componentDefaults?: EuiComponentDefaults; +}> = ({ componentDefaults = emptyDefaults, children }) => { + return ( + + {children} + + ); +}; + +/* + * Hook + */ +export const useEuiComponentDefaults = () => { + return useContext(EuiComponentDefaultsContext); +}; diff --git a/src/components/provider/component_defaults/index.ts b/src/components/provider/component_defaults/index.ts new file mode 100644 index 000000000000..74e16dcac4af --- /dev/null +++ b/src/components/provider/component_defaults/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export * from './component_defaults'; diff --git a/upcoming_changelogs/6923.md b/upcoming_changelogs/6923.md new file mode 100644 index 000000000000..e78ffcbf2333 --- /dev/null +++ b/upcoming_changelogs/6923.md @@ -0,0 +1 @@ +- Added beta `componentDefaults` prop to `EuiProvider`, which will allow configuring certain default props globally. This list of components and defaults is still under consideration.