diff --git a/.yarn/patches/react-live-npm-4.1.7-7b41625faa.patch b/.yarn/patches/react-live-npm-4.1.7-7b41625faa.patch new file mode 100644 index 00000000000..b761b9d8550 --- /dev/null +++ b/.yarn/patches/react-live-npm-4.1.7-7b41625faa.patch @@ -0,0 +1,26 @@ +diff --git a/dist/index.js b/dist/index.js +index 7bcfee33d9cfaa6c5f9d7da40335ec2551f932b6..4c29438cb94412d25aed193f9c86f20ccebb9009 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -168,7 +168,7 @@ var import_sucrase = require("sucrase"); + var defaultTransforms = ["jsx", "imports"]; + function transform(opts = {}) { + const transforms = Array.isArray(opts.transforms) ? opts.transforms.filter(Boolean) : defaultTransforms; +- return (code) => (0, import_sucrase.transform)(code, { transforms }).code; ++ return (code) => (0, import_sucrase.transform)(code, { transforms, jsxPragma: 'jsx' }).code; + } + + // src/utils/transpile/errorBoundary.tsx +diff --git a/dist/index.mjs b/dist/index.mjs +index 44a3fbed4fc0545b75235add7369a262e8ee5220..6cfad17b4ce81553bac1243b7421f5f75ade43fc 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -127,7 +127,7 @@ import { transform as _transform } from "sucrase"; + var defaultTransforms = ["jsx", "imports"]; + function transform(opts = {}) { + const transforms = Array.isArray(opts.transforms) ? opts.transforms.filter(Boolean) : defaultTransforms; +- return (code) => _transform(code, { transforms }).code; ++ return (code) => _transform(code, { transforms, jsxPragma: 'jsx' }).code; + } + + // src/utils/transpile/errorBoundary.tsx diff --git a/packages/docusaurus-theme/package.json b/packages/docusaurus-theme/package.json index 3f7a8e12b4e..8a0c34fd66e 100644 --- a/packages/docusaurus-theme/package.json +++ b/packages/docusaurus-theme/package.json @@ -39,7 +39,7 @@ "moment": "^2.30.1", "prism-react-renderer": "^2.3.1", "react-is": "^18.3.1", - "react-live": "^4.1.7" + "react-live": "patch:react-live@npm%3A4.1.7#~/.yarn/patches/react-live-npm-4.1.7-7b41625faa.patch" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/docusaurus-theme/src/components/demo/context.ts b/packages/docusaurus-theme/src/components/demo/context.ts index 2e4de44707c..7ed16e30379 100644 --- a/packages/docusaurus-theme/src/components/demo/context.ts +++ b/packages/docusaurus-theme/src/components/demo/context.ts @@ -2,7 +2,15 @@ import { createContext, useContext } from 'react'; import { DemoSourceMeta } from './demo'; export interface DemoContextObject { + /** + * Array of all available sources for this demo instance + */ sources: DemoSourceMeta[]; + + /** + * Add source to the list of available sources + * This should only be used internally when initializing the component! + */ addSource(source: DemoSourceMeta): void; } diff --git a/packages/docusaurus-theme/src/components/demo/demo.tsx b/packages/docusaurus-theme/src/components/demo/demo.tsx index 7159a26a421..aae631f5d56 100644 --- a/packages/docusaurus-theme/src/components/demo/demo.tsx +++ b/packages/docusaurus-theme/src/components/demo/demo.tsx @@ -21,6 +21,7 @@ import { DemoSource } from './source'; import { demoDefaultScope } from './scope'; import { DemoActionsBar } from './actions_bar'; import { demoCodeTransformer } from './code_transformer'; +import { DemoPreviewProps } from './preview/preview'; export interface DemoSourceMeta { code: string; @@ -40,6 +41,7 @@ export interface DemoProps extends PropsWithChildren { * The default scope exposes all React and EUI exports. */ scope?: Record; + previewPadding?: DemoPreviewProps['padding']; } const getDemoStyles = (euiTheme: UseEuiTheme) => ({ @@ -56,12 +58,13 @@ const getDemoStyles = (euiTheme: UseEuiTheme) => ({ export const Demo = ({ children, scope, - isSourceOpen: _isSourceOpen = true + isSourceOpen: _isSourceOpen = false, + previewPadding, }: DemoProps) => { const styles = useEuiMemoizedStyles(getDemoStyles); const [sources, setSources] = useState([]); const [isSourceOpen, setIsSourceOpen] = useState(_isSourceOpen); - const activeSource = sources[0]; + const activeSource = sources[0] || null; // liveProviderKey restarts the demo to its initial state const [liveProviderKey, setLiveProviderKey] = useState(0); @@ -101,7 +104,7 @@ export const Demo = ({ theme={prismThemes.dracula} scope={finalScope} > - + ({ editor: css` diff --git a/packages/docusaurus-theme/src/components/demo/preview/preview.tsx b/packages/docusaurus-theme/src/components/demo/preview/preview.tsx index ca3995f0156..e6a4bee426e 100644 --- a/packages/docusaurus-theme/src/components/demo/preview/preview.tsx +++ b/packages/docusaurus-theme/src/components/demo/preview/preview.tsx @@ -3,11 +3,16 @@ import { LivePreview } from 'react-live'; import BrowserOnly from '@docusaurus/BrowserOnly'; import ErrorBoundary from '@docusaurus/ErrorBoundary'; import { ErrorBoundaryErrorMessageFallback } from '@docusaurus/theme-common'; -import { UseEuiTheme, EuiFlexGroup, useEuiTheme } from '@elastic/eui'; +import { UseEuiTheme, useEuiTheme, EuiPaddingSize, euiPaddingSize } from '@elastic/eui'; +import { CSSProperties } from 'react'; + +export interface DemoPreviewProps { + padding?: EuiPaddingSize; +} const getPreviewStyles = (euiTheme: UseEuiTheme) => ({ previewWrapper: css` - padding: ${euiTheme.euiTheme.size.l}; + padding: var(--docs-demo-preview-padding); border-radius: var(--docs-demo-border-radius); `, }); @@ -21,16 +26,21 @@ const PreviewLoader = () => (
Loading...
); -export const DemoPreview = () => { +export const DemoPreview = ({ padding = 'l' }: DemoPreviewProps) => { const euiTheme = useEuiTheme(); const styles = getPreviewStyles(euiTheme); + const paddingSize = euiPaddingSize(euiTheme, padding); + + const style = { + '--docs-demo-preview-padding': paddingSize, + } as CSSProperties; return ( }> {() => ( <> }> -
+
diff --git a/packages/docusaurus-theme/src/components/demo/scope.ts b/packages/docusaurus-theme/src/components/demo/scope.ts index 1a09137b908..8c8ab52fd75 100644 --- a/packages/docusaurus-theme/src/components/demo/scope.ts +++ b/packages/docusaurus-theme/src/components/demo/scope.ts @@ -1,5 +1,14 @@ import React from 'react'; import * as EUI from '@elastic/eui'; +import * as EmotionReact from '@emotion/react'; + +/** + * A custom client-side require() alternative to inform users it's not available + * in our demo environment + */ +const clientSideRequire = () => { + throw new Error('require() is not accessible in the interactive demo environment! All EUI and React exports are available in the global scope for you to use without the need to import them.'); +} export const demoDefaultScope: Record = { // React @@ -8,4 +17,9 @@ export const demoDefaultScope: Record = { // EUI exports ...EUI, + + // Emotion + ...EmotionReact, + + require: clientSideRequire, }; diff --git a/packages/docusaurus-theme/src/components/demo/source/get_source_from_children.ts b/packages/docusaurus-theme/src/components/demo/source/get_source_from_children.ts index 6dff6b4da38..15c04eb03e1 100644 --- a/packages/docusaurus-theme/src/components/demo/source/get_source_from_children.ts +++ b/packages/docusaurus-theme/src/components/demo/source/get_source_from_children.ts @@ -12,6 +12,11 @@ export interface SourceMeta { * Get source string from given children. */ export const getSourceFromChildren = (children: ReactNode): string | null => { + // Direct (non-MDX) usage almost always passes a string + if (typeof children === 'string') { + return children; + } + if (Children.count(children) !== 1 || !isElement(children)) { // This should never happen return null; diff --git a/packages/docusaurus-theme/src/theme/CodeBlock/index.tsx b/packages/docusaurus-theme/src/theme/CodeBlock/index.tsx new file mode 100644 index 00000000000..89df9db28e4 --- /dev/null +++ b/packages/docusaurus-theme/src/theme/CodeBlock/index.tsx @@ -0,0 +1,44 @@ +import React, { isValidElement, type ReactNode } from 'react'; +import { EuiCodeBlock } from '@elastic/eui'; +import type { Props } from '@theme/CodeBlock'; +import { Demo } from '../../components/demo'; + +/** + * Best attempt to make the children a plain string so it is copyable. If there + * are react elements, we will not be able to copy the content, and it will + * return `children` as-is; otherwise, it concatenates the string children + * together. + */ +function maybeStringifyChildren(children: ReactNode): ReactNode { + if (React.Children.toArray(children).some((el) => isValidElement(el))) { + return children; + } + // The children is now guaranteed to be one/more plain strings + return Array.isArray(children) ? children.join('') : (children as string); +} + +export default function CodeBlock({ + children: rawChildren, + metastring, + className, + ...props +}: Props): JSX.Element { + const children = maybeStringifyChildren(rawChildren); + const language = className?.replace('language-', '') || undefined; + + if (metastring?.startsWith('interactive')) { + return {children}; + } + + return ( + + {children} + + ); +} diff --git a/packages/docusaurus-theme/src/theme/MDXComponents/Code.tsx b/packages/docusaurus-theme/src/theme/MDXComponents/Code.tsx index 56b15d04f18..84b74232e5f 100644 --- a/packages/docusaurus-theme/src/theme/MDXComponents/Code.tsx +++ b/packages/docusaurus-theme/src/theme/MDXComponents/Code.tsx @@ -1,13 +1,9 @@ import React from 'react'; import type CodeType from '@theme-init/MDXComponents/Code'; +import CodeBlock from '@theme/CodeBlock'; import type { WrapperProps } from '@docusaurus/types'; import { css } from '@emotion/react'; -import { - EuiCode, - EuiCodeBlock, - useEuiMemoizedStyles, - UseEuiTheme, -} from '@elastic/eui'; +import { EuiCode, useEuiMemoizedStyles, UseEuiTheme } from '@elastic/eui'; type Props = WrapperProps; @@ -33,15 +29,24 @@ const Code = ({ children, className, ...rest }: Props): JSX.Element => { ) : false; - return isInlineCode ? ( - - {children} - - ) : ( - + if (isInlineCode) { + return ( + + {children} + + ); + } + + return ( + {children} - - ); + + ) }; export default Code; diff --git a/packages/docusaurus-theme/src/theme/theme.d.ts b/packages/docusaurus-theme/src/theme/theme.d.ts index 7cf34e65f34..2625fbd5a7d 100644 --- a/packages/docusaurus-theme/src/theme/theme.d.ts +++ b/packages/docusaurus-theme/src/theme/theme.d.ts @@ -118,6 +118,31 @@ declare module '@theme-original/EditThisPage' { export default function EditThisPage(props: Props): JSX.Element; } +// original: https://github.com/facebook/docusaurus/blob/fa743c81defd24e22eae45c81bd79eb8ec2c4ef0/packages/docusaurus-theme-classic/src/theme-classic.d.ts#L364 +declare module '@theme/CodeBlock' { + import type { ReactNode } from 'react'; + + export interface Props { + readonly children: ReactNode; + readonly className?: string; + readonly metastring?: string; + readonly title?: string; + readonly language?: string; + readonly showLineNumbers?: boolean; + } + + export default function CodeBlock(props: Props): JSX.Element; +} + +// original: https://github.com/facebook/docusaurus/blob/8b877d27d4b1bcd5c2ee13dde8332407a1c26447/packages/docusaurus-theme-classic/src/theme-classic.d.ts#L510 +declare module '@theme/MDXComponents/Code' { + import type {ComponentProps} from 'react'; + + export interface Props extends ComponentProps<'code'> {} + + export default function MDXCode(props: Props): JSX.Element; +} + // original: https://github.com/facebook/docusaurus/blob/fa743c81defd24e22eae45c81bd79eb8ec2c4ef0/packages/docusaurus-theme-classic/src/theme-classic.d.ts#L563 declare module '@theme-original/DocSidebar' { import type { PropSidebarItem } from '@docusaurus/plugin-content-docs'; diff --git a/packages/website/docs/02_components/navigation/button/overview.mdx b/packages/website/docs/02_components/navigation/button/overview.mdx index 72664cb9049..bb807c9c9a8 100644 --- a/packages/website/docs/02_components/navigation/button/overview.mdx +++ b/packages/website/docs/02_components/navigation/button/overview.mdx @@ -20,421 +20,413 @@ The most standard button component is **EuiButton** which comes in two styles an When using colors other than `primary`, be sure that either the words or an icon also represents the status. For instance, don't rely on color alone to represent dangerous actions but use words like "Delete" not "Confirm". The `text` and `accent` colors should be used sparingly as they can easily be confused with other states like disabled and danger. - - ```tsx - import React from 'react'; - import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; - - const buttons = [ - 'primary', - 'success', - 'warning', - 'danger', - 'text', - 'accent', - 'disabled', - ]; +```tsx interactive +import React from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; + +const buttons = [ + 'primary', + 'success', + 'warning', + 'danger', + 'text', + 'accent', + 'disabled', +]; + +export default () => ( +
+ {buttons.map((value) => ( + <> + + + {}} + > + {value.charAt(0).toUpperCase() + value.slice(1)} + + - export default () => ( -
- {buttons.map((value) => ( - <> - - - {}} - > - {value.charAt(0).toUpperCase() + value.slice(1)} - - - - - {}} - > - Filled - - - - - {}} - > - Small - - - - - {}} - > - Small and filled - - - - - {}} - > - Full width - - - - - - ))} -
- ); - ``` - + + {}} + > + Filled + + + + + {}} + > + Small + + + + + {}} + > + Small and filled + + + + + {}} + > + Full width + + +
+ + + ))} +
+); +``` ## Empty button Use **EuiButtonEmpty** when you want to reduce the importance of the button, but still want to align it to the rest of the buttons. It is also the only button component that supports down to size `xs`. - - ```tsx - import React from 'react'; - import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - } from '@elastic/eui'; - - const buttons = ['primary', 'success', 'warning', 'danger', 'text', 'disabled']; - - export default () => ( -
- {buttons.map((value) => ( - <> - - - {}} - > - {value.charAt(0).toUpperCase() + value.slice(1)} - - - - - {}} - > - Small - - - - - {}} - > - Extra small - - - - - - ))} -
- ); - ``` -
+```tsx interactive +import React from 'react'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; + +const buttons = ['primary', 'success', 'warning', 'danger', 'text', 'disabled']; + +export default () => ( +
+ {buttons.map((value) => ( + <> + + + {}} + > + {value.charAt(0).toUpperCase() + value.slice(1)} + + + + + {}} + > + Small + + + + + {}} + > + Extra small + + + + + + ))} +
+); +``` ## Flush empty button When aligning **EuiButtonEmpty** components to the left or the right, you should make sure they’re flush with the edge of their container, so that they’re horizontally aligned with the other content in the container. - - ```tsx - import React from 'react'; - import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +```tsx interactive +import React from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - export default () => ( - - - Flush left - +export default () => ( + + + Flush left + - - Flush right - + + Flush right + - - Flush both - - - ); - ``` - + + Flush both + + +); +``` ## Buttons with icons All button components accept an `iconType` which must be an acceptable [**EuiIcon**](#/display/icons) type. Multi-color icons like app icons will be converted to single color. Icons can be displayed on the opposite side by passing `iconSide="right"`. - - ```tsx - import React from 'react'; - import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiSpacer, - } from '@elastic/eui'; - - export default () => ( -
- - - {}} iconType="heart"> - Primary - - +```tsx interactive +import React from 'react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSpacer, +} from '@elastic/eui'; - - {}}> - Filled - - +export default () => ( +
+ + + {}} iconType="heart"> + Primary + + - - {}}> - Small - - + + {}}> + Filled + + - - {}}> - Small and filled - - + + {}}> + Small + + - - {}}> - Full width - - - + + {}}> + Small and filled + + - + + {}}> + Full width + + + - - - {}} iconType="lensApp"> - Empty button - - + - - {}} iconType="lensApp" size="s"> - Small empty - - + + + {}} iconType="lensApp"> + Empty button + + - - {}} iconType="lensApp" size="xs"> - Extra small empty - - - + + {}} iconType="lensApp" size="s"> + Small empty + + - + + {}} iconType="lensApp" size="xs"> + Extra small empty + + + - - - {}} iconType="arrowDown"> - Icon right - - + - - {}} - > - Filled - - + + + {}} iconType="arrowDown"> + Icon right + + - - {}} - > - Small - - + + {}} + > + Filled + + - - {}} - > - Small and filled - - + + {}} + > + Small + + - - {}} - > - Full width - - - + + {}} + > + Small and filled + + - + + {}} + > + Full width + + + - - - {}} - iconType="arrowDown" - > - Icon right - - + - - {}} - iconType="arrowDown" - size="s" - > - Small empty - - + + + {}} + iconType="arrowDown" + > + Icon right + + - - {}} - iconType="arrowDown" - size="xs" - > - Extra small empty - - - + + {}} + iconType="arrowDown" + size="s" + > + Small empty + + - + + {}} + iconType="arrowDown" + size="xs" + > + Extra small empty + + + - - - {}} iconType="heart" isDisabled> - Disabled - - + - - {}} isDisabled> - Filled - - + + + {}} iconType="heart" isDisabled> + Disabled + + - - {}} isDisabled> - Small - - + + {}} isDisabled> + Filled + + - - {}} - isDisabled - > - Small and filled - - + + {}} isDisabled> + Small + + - - {}} isDisabled> - Full width - - - + + {}} + isDisabled + > + Small and filled + + - + + {}} isDisabled> + Full width + + + - - - {}} - iconType="dashboardApp" - > - Disabled app icon - - + - - {}} - iconType="arrowDown" - iconSide="right" - size="xs" - > - Disabled icon right - - - -
- ); - ``` - + + + {}} + iconType="dashboardApp" + > + Disabled app icon + + + + + {}} + iconType="arrowDown" + iconSide="right" + size="xs" + > + Disabled icon right + + + +
+); +``` ## Icon buttons @@ -446,142 +438,140 @@ An **EuiButtonIcon** is a button that only contains an icon (no text). Use the ` **EuiButtonIcon** requires an `aria-label` to express the meaning to screen readers. -::: - - - ```tsx - import React from 'react'; - import { - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTitle, - EuiCode, - } from '@elastic/eui'; - - const colors = ['primary', 'success', 'warning', 'danger', 'text', 'accent']; - - export default () => ( - <> - - {colors.map((color) => ( - - {}} - iconType="help" - aria-label="Help" - /> - - ))} - - - -

- Display (empty, base,{' '} - fill) -

-
- - - - - - - - - - - - - - -

Disabled

-
- - - - - - - - - - - - - - -

- Size (xs, s, m) -

-
- - - - - - - - - - - - - - -

All icons types inherit button color

-
- - - - - - - - - +::: + +```tsx interactive +import React from 'react'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, + EuiCode, +} from '@elastic/eui'; + +const colors = ['primary', 'success', 'warning', 'danger', 'text', 'accent']; + +export default () => ( + <> + + {colors.map((color) => ( + {}} + iconType="help" + aria-label="Help" /> - - - - - - ); - ``` -
+ ))} + + + +

+ Display (empty, base,{' '} + fill) +

+
+ + + + + + + + + + + + + + +

Disabled

+
+ + + + + + + + + + + + + + +

+ Size (xs, s, m) +

+
+ + + + + + + + + + + + + + +

All icons types inherit button color

+
+ + + + + + + + + + + + + + + + +); +``` ## Buttons as links @@ -593,117 +583,113 @@ it is not usually recommended. For more specific information on how to integrate If you are creating a purely text-based link, like the one in the previous paragraph, use [**EuiLink**](#/navigation/link) instead. - - ```tsx - import React, { Fragment } from 'react'; - import { - EuiButton, - EuiButtonEmpty, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, +```tsx interactive +import React, { Fragment } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, } from '@elastic/eui'; - export default () => ( - - - - Link to elastic.co - - - - - Link to elastic.co - - - - - - - - - - - - - - Disabled link - - - - - - Disabled empty link - - - - - - - - - ); - ``` - - -## Loading state - -Setting the `isLoading` prop to true will add the loading spinner or swap the existing icon for the loading spinner -and set the button to disabled. It is good practice to also rename the button to "Loading…". - - - ```tsx - import React from 'react'; - import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - } from '@elastic/eui'; - - export default () => ( +export default () => ( + - Loading… + Link to elastic.co - - Loading… - + + Link to elastic.co + + + + + + + + + - - Loading… + + Disabled link - {}} isLoading> - Loading… + + Disabled empty link - {}} isLoading iconSide="right"> - Loading… - + - - ); - ``` - + + +); +``` + +## Loading state + +Setting the `isLoading` prop to true will add the loading spinner or swap the existing icon for the loading spinner +and set the button to disabled. It is good practice to also rename the button to "Loading…". + +```tsx interactive +import React from 'react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; + +export default () => ( + + + Loading… + + + + + Loading… + + + + + + Loading… + + + + + {}} isLoading> + Loading… + + + + + {}} isLoading iconSide="right"> + Loading… + + + +); +``` ## Split buttons @@ -711,80 +697,78 @@ EUI [does not support](https://github.com/elastic/eui/issues/4171) split buttons we recommend using separate buttons for the main and overflow actions. You can achieve this by simply using the `display` and `size` props **EuiButtonIcon** to match that of the primary action button. - - ```tsx - import React, { useState } from 'react'; - import { - EuiButton, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiContextMenuPanel, - EuiContextMenuItem, - EuiPopover, - useGeneratedHtmlId, - } from '@elastic/eui'; - - export default () => { - const [isPopoverOpen, setPopover] = useState(false); - const splitButtonPopoverId = useGeneratedHtmlId({ - prefix: 'splitButtonPopover', - }); - - const onButtonClick = () => { - setPopover(!isPopoverOpen); - }; +```tsx interactive +import React, { useState } from 'react'; +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiPopover, + useGeneratedHtmlId, +} from '@elastic/eui'; - const closePopover = () => { - setPopover(false); - }; +export default () => { + const [isPopoverOpen, setPopover] = useState(false); + const splitButtonPopoverId = useGeneratedHtmlId({ + prefix: 'splitButtonPopover', + }); - const items = [ - - Copy - , - - Edit - , - - Share - , - ]; - - return ( - <> - - - - Last 15 min - - - - - } - isOpen={isPopoverOpen} - closePopover={closePopover} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - - - - - ); + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); }; - ``` - + + const items = [ + + Copy + , + + Edit + , + + Share + , + ]; + + return ( + <> + + + + Last 15 min + + + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + + + + ); +}; +``` ## Toggle buttons @@ -795,38 +779,36 @@ Though there are two **exclusive** situations to consider. 1. If your button changes its readable **text**, via children or `aria-label`, then there is no additional accessibility concern. - - ```tsx - import React, { useState } from 'react'; - import { EuiButton, EuiButtonIcon } from '@elastic/eui'; +```tsx interactive +import React, { useState } from 'react'; +import { EuiButton, EuiButtonIcon } from '@elastic/eui'; - export default () => { - const [toggle0On, setToggle0On] = useState(false); - const [toggle1On, setToggle1On] = useState(true); +export default () => { + const [toggle0On, setToggle0On] = useState(false); + const [toggle1On, setToggle1On] = useState(true); - return ( - <> - { - setToggle0On((isOn) => !isOn); - }} - > - {toggle0On ? 'Hey there good lookin' : 'Toggle me'} - -   - { - setToggle1On((isOn) => !isOn); - }} - /> - - ); - }; - ``` - + return ( + <> + { + setToggle0On((isOn) => !isOn); + }} + > + {toggle0On ? 'Hey there good lookin' : 'Toggle me'} + +   + { + setToggle1On((isOn) => !isOn); + }} + /> + + ); +}; +``` 2. If your button only changes the **visual** appearance, you must add `aria-pressed` passing a boolean for the on and off states. All EUI button types provide a helper prop for this called `isSelected`. @@ -837,45 +819,43 @@ Do not add `aria-pressed` or `isSelected` if you also change the readable text. ::: - - ```tsx - import React, { useState } from 'react'; - import { EuiButton, EuiButtonIcon } from '@elastic/eui'; +```tsx interactive +import React, { useState } from 'react'; +import { EuiButton, EuiButtonIcon } from '@elastic/eui'; - export default () => { - const [toggle2On, setToggle2On] = useState(true); - const [toggle3On, setToggle3On] = useState(false); +export default () => { + const [toggle2On, setToggle2On] = useState(true); + const [toggle3On, setToggle3On] = useState(false); - return ( - <> - { - setToggle2On((isOn) => !isOn); - }} - > - Toggle me - -   - { - setToggle3On((isOn) => !isOn); - }} - /> - - ); - }; - ``` - + return ( + <> + { + setToggle2On((isOn) => !isOn); + }} + > + Toggle me + +   + { + setToggle3On((isOn) => !isOn); + }} + /> + + ); +}; +``` ## Button groups @@ -889,245 +869,241 @@ This is only for accessibility, however, so it will be visibly hidden. ::: - - ```tsx - import React, { useState, Fragment } from 'react'; - import { - EuiButtonGroup, - EuiSpacer, - EuiTitle, - useGeneratedHtmlId, - } from '@elastic/eui'; - - export default () => { - const basicButtonGroupPrefix = useGeneratedHtmlId({ - prefix: 'basicButtonGroup', - }); - const multiSelectButtonGroupPrefix = useGeneratedHtmlId({ - prefix: 'multiSelectButtonGroup', - }); - const disabledButtonGroupPrefix = useGeneratedHtmlId({ - prefix: 'disabledButtonGroup', - }); - - const toggleButtons = [ - { - id: `${basicButtonGroupPrefix}__0`, - label: 'Option one', - }, - { - id: `${basicButtonGroupPrefix}__1`, - label: 'Option two is selected by default', - }, - { - id: `${basicButtonGroupPrefix}__2`, - label: 'Option three', - }, - ]; +```tsx interactive +import React, { useState, Fragment } from 'react'; +import { + EuiButtonGroup, + EuiSpacer, + EuiTitle, + useGeneratedHtmlId, +} from '@elastic/eui'; - const toggleButtonsDisabled = [ - { - id: `${disabledButtonGroupPrefix}__0`, - label: 'Option one', - }, - { - id: `${disabledButtonGroupPrefix}__1`, - label: 'Option two is selected by default', - }, - { - id: `${disabledButtonGroupPrefix}__2`, - label: 'Option three', - }, - ]; +export default () => { + const basicButtonGroupPrefix = useGeneratedHtmlId({ + prefix: 'basicButtonGroup', + }); + const multiSelectButtonGroupPrefix = useGeneratedHtmlId({ + prefix: 'multiSelectButtonGroup', + }); + const disabledButtonGroupPrefix = useGeneratedHtmlId({ + prefix: 'disabledButtonGroup', + }); + + const toggleButtons = [ + { + id: `${basicButtonGroupPrefix}__0`, + label: 'Option one', + }, + { + id: `${basicButtonGroupPrefix}__1`, + label: 'Option two is selected by default', + }, + { + id: `${basicButtonGroupPrefix}__2`, + label: 'Option three', + }, + ]; - const toggleButtonsMulti = [ - { - id: `${multiSelectButtonGroupPrefix}__0`, - label: 'Option 1', - }, - { - id: `${multiSelectButtonGroupPrefix}__1`, - label: 'Option 2 is selected by default', - }, - { - id: `${multiSelectButtonGroupPrefix}__2`, - label: 'Option 3', - }, - ]; - - const [toggleIdSelected, setToggleIdSelected] = useState( - `${basicButtonGroupPrefix}__1` - ); - const [toggleIdDisabled, setToggleIdDisabled] = useState( - `${disabledButtonGroupPrefix}__1` - ); - const [toggleIdToSelectedMap, setToggleIdToSelectedMap] = useState({ - [`${multiSelectButtonGroupPrefix}__1`]: true, - }); - - const onChange = (optionId) => { - setToggleIdSelected(optionId); - }; + const toggleButtonsDisabled = [ + { + id: `${disabledButtonGroupPrefix}__0`, + label: 'Option one', + }, + { + id: `${disabledButtonGroupPrefix}__1`, + label: 'Option two is selected by default', + }, + { + id: `${disabledButtonGroupPrefix}__2`, + label: 'Option three', + }, + ]; - const onChangeDisabled = (optionId) => { - setToggleIdDisabled(optionId); - }; + const toggleButtonsMulti = [ + { + id: `${multiSelectButtonGroupPrefix}__0`, + label: 'Option 1', + }, + { + id: `${multiSelectButtonGroupPrefix}__1`, + label: 'Option 2 is selected by default', + }, + { + id: `${multiSelectButtonGroupPrefix}__2`, + label: 'Option 3', + }, + ]; - const onChangeMulti = (optionId) => { - const newToggleIdToSelectedMap = { - ...toggleIdToSelectedMap, - ...{ - [optionId]: !toggleIdToSelectedMap[optionId], - }, - }; - setToggleIdToSelectedMap(newToggleIdToSelectedMap); - }; + const [toggleIdSelected, setToggleIdSelected] = useState( + `${basicButtonGroupPrefix}__1` + ); + const [toggleIdDisabled, setToggleIdDisabled] = useState( + `${disabledButtonGroupPrefix}__1` + ); + const [toggleIdToSelectedMap, setToggleIdToSelectedMap] = useState({ + [`${multiSelectButtonGroupPrefix}__1`]: true, + }); - return ( - - onChange(id)} - /> - - -

Primary & multi select

-
- - onChangeMulti(id)} - color="primary" - type="multi" - /> - - -

Disabled & full width

-
- - onChangeDisabled(id)} - buttonSize="m" - isDisabled - isFullWidth - /> -
- ); + const onChange = (optionId) => { + setToggleIdSelected(optionId); }; - ``` -
-### Icon only button groups + const onChangeDisabled = (optionId) => { + setToggleIdDisabled(optionId); + }; -If you're just displaying a group of icons, add the prop `isIconOnly`. + const onChangeMulti = (optionId) => { + const newToggleIdToSelectedMap = { + ...toggleIdToSelectedMap, + ...{ + [optionId]: !toggleIdToSelectedMap[optionId], + }, + }; + setToggleIdToSelectedMap(newToggleIdToSelectedMap); + }; + + return ( + + onChange(id)} + /> + + +

Primary & multi select

+
+ + onChangeMulti(id)} + color="primary" + type="multi" + /> + + +

Disabled & full width

+
+ + onChangeDisabled(id)} + buttonSize="m" + isDisabled + isFullWidth + /> +
+ ); +}; +``` - - ```tsx - import React, { useState, Fragment } from 'react'; - import { EuiButtonGroup, htmlIdGenerator } from '@elastic/eui'; +### Icon only button groups - const idPrefix3 = htmlIdGenerator()(); +If you're just displaying a group of icons, add the prop `isIconOnly`. - export default () => { - const toggleButtonsIcons = [ - { - id: `${idPrefix3}0`, - label: 'Align left', - iconType: 'editorAlignLeft', - }, - { - id: `${idPrefix3}1`, - label: 'Align center', - iconType: 'editorAlignCenter', - }, - { - id: `${idPrefix3}2`, - label: 'Align right', - iconType: 'editorAlignRight', - isDisabled: true, - }, - ]; - - const toggleButtonsIconsMulti = [ - { - id: `${idPrefix3}3`, - label: 'Bold', - name: 'bold', - iconType: 'editorBold', - }, - { - id: `${idPrefix3}4`, - label: 'Italic', - name: 'italic', - iconType: 'editorItalic', - isDisabled: true, - }, - { - id: `${idPrefix3}5`, - label: 'Underline', - name: 'underline', - iconType: 'editorUnderline', - }, - { - id: `${idPrefix3}6`, - label: 'Strikethrough', - name: 'strikethrough', - iconType: 'editorStrike', - }, - ]; +```tsx interactive +import React, { useState, Fragment } from 'react'; +import { EuiButtonGroup, htmlIdGenerator } from '@elastic/eui'; + +const idPrefix3 = htmlIdGenerator()(); + +export default () => { + const toggleButtonsIcons = [ + { + id: `${idPrefix3}0`, + label: 'Align left', + iconType: 'editorAlignLeft', + }, + { + id: `${idPrefix3}1`, + label: 'Align center', + iconType: 'editorAlignCenter', + }, + { + id: `${idPrefix3}2`, + label: 'Align right', + iconType: 'editorAlignRight', + isDisabled: true, + }, + ]; - const [toggleIconIdSelected, setToggleIconIdSelected] = useState( - `${idPrefix3}1` - ); - const [toggleIconIdToSelectedMap, setToggleIconIdToSelectedMap] = useState( - {} - ); + const toggleButtonsIconsMulti = [ + { + id: `${idPrefix3}3`, + label: 'Bold', + name: 'bold', + iconType: 'editorBold', + }, + { + id: `${idPrefix3}4`, + label: 'Italic', + name: 'italic', + iconType: 'editorItalic', + isDisabled: true, + }, + { + id: `${idPrefix3}5`, + label: 'Underline', + name: 'underline', + iconType: 'editorUnderline', + }, + { + id: `${idPrefix3}6`, + label: 'Strikethrough', + name: 'strikethrough', + iconType: 'editorStrike', + }, + ]; - const onChangeIcons = (optionId) => { - setToggleIconIdSelected(optionId); - }; + const [toggleIconIdSelected, setToggleIconIdSelected] = useState( + `${idPrefix3}1` + ); + const [toggleIconIdToSelectedMap, setToggleIconIdToSelectedMap] = useState( + {} + ); - const onChangeIconsMulti = (optionId) => { - const newToggleIconIdToSelectedMap = { - ...toggleIconIdToSelectedMap, - ...{ - [optionId]: !toggleIconIdToSelectedMap[optionId], - }, - }; + const onChangeIcons = (optionId) => { + setToggleIconIdSelected(optionId); + }; - setToggleIconIdToSelectedMap(newToggleIconIdToSelectedMap); + const onChangeIconsMulti = (optionId) => { + const newToggleIconIdToSelectedMap = { + ...toggleIconIdToSelectedMap, + ...{ + [optionId]: !toggleIconIdToSelectedMap[optionId], + }, }; - return ( - - onChangeIcons(id)} - isIconOnly - /> -    - onChangeIconsMulti(id)} - type="multi" - isIconOnly - /> - - ); + setToggleIconIdToSelectedMap(newToggleIconIdToSelectedMap); }; - ``` - + + return ( + + onChangeIcons(id)} + isIconOnly + /> +    + onChangeIconsMulti(id)} + type="multi" + isIconOnly + /> + + ); +}; +``` ### Button groups in forms @@ -1137,112 +1113,110 @@ Compressed groups should always be `fullWidth` so they line up nicely in their s For a more detailed example of how to integrate with forms, see the ["Complex example"](#/forms/compressed-forms#complex-example) on the compressed forms page. - - ```tsx - import React, { useState } from 'react'; - import { - EuiButtonGroup, - EuiSpacer, - EuiPanel, - useGeneratedHtmlId, - } from '@elastic/eui'; - - export default () => { - const compressedToggleButtonGroupPrefix = useGeneratedHtmlId({ - prefix: 'compressedToggleButtonGroup', - }); - const multiSelectButtonGroupPrefix = useGeneratedHtmlId({ - prefix: 'multiSelectButtonGroup', - }); - - const toggleButtonsCompressed = [ - { - id: `${compressedToggleButtonGroupPrefix}__0`, - label: 'fine', - }, - { - id: `${compressedToggleButtonGroupPrefix}__1`, - label: 'rough', - }, - { - id: `${compressedToggleButtonGroupPrefix}__2`, - label: 'coarse', - }, - ]; - - const toggleButtonsIconsMulti = [ - { - id: `${multiSelectButtonGroupPrefix}__0`, - label: 'Bold', - name: 'bold', - iconType: 'editorBold', - }, - { - id: `${multiSelectButtonGroupPrefix}__1`, - label: 'Italic', - name: 'italic', - iconType: 'editorItalic', - isDisabled: true, - }, - { - id: `${multiSelectButtonGroupPrefix}__2`, - label: 'Underline', - name: 'underline', - iconType: 'editorUnderline', - }, - { - id: `${multiSelectButtonGroupPrefix}__3`, - label: 'Strikethrough', - name: 'strikethrough', - iconType: 'editorStrike', - }, - ]; +```tsx interactive +import React, { useState } from 'react'; +import { + EuiButtonGroup, + EuiSpacer, + EuiPanel, + useGeneratedHtmlId, +} from '@elastic/eui'; - const [toggleIconIdToSelectedMapIcon, setToggleIconIdToSelectedMapIcon] = - useState({}); - const [toggleCompressedIdSelected, setToggleCompressedIdSelected] = useState( - `${compressedToggleButtonGroupPrefix}__1` - ); +export default () => { + const compressedToggleButtonGroupPrefix = useGeneratedHtmlId({ + prefix: 'compressedToggleButtonGroup', + }); + const multiSelectButtonGroupPrefix = useGeneratedHtmlId({ + prefix: 'multiSelectButtonGroup', + }); + + const toggleButtonsCompressed = [ + { + id: `${compressedToggleButtonGroupPrefix}__0`, + label: 'fine', + }, + { + id: `${compressedToggleButtonGroupPrefix}__1`, + label: 'rough', + }, + { + id: `${compressedToggleButtonGroupPrefix}__2`, + label: 'coarse', + }, + ]; - const onChangeCompressed = (optionId) => { - setToggleCompressedIdSelected(optionId); - }; + const toggleButtonsIconsMulti = [ + { + id: `${multiSelectButtonGroupPrefix}__0`, + label: 'Bold', + name: 'bold', + iconType: 'editorBold', + }, + { + id: `${multiSelectButtonGroupPrefix}__1`, + label: 'Italic', + name: 'italic', + iconType: 'editorItalic', + isDisabled: true, + }, + { + id: `${multiSelectButtonGroupPrefix}__2`, + label: 'Underline', + name: 'underline', + iconType: 'editorUnderline', + }, + { + id: `${multiSelectButtonGroupPrefix}__3`, + label: 'Strikethrough', + name: 'strikethrough', + iconType: 'editorStrike', + }, + ]; - const onChangeIconsMultiIcons = (optionId) => { - const newToggleIconIdToSelectedMapIcon = { - ...toggleIconIdToSelectedMapIcon, - ...{ - [optionId]: !toggleIconIdToSelectedMapIcon[optionId], - }, - }; + const [toggleIconIdToSelectedMapIcon, setToggleIconIdToSelectedMapIcon] = + useState({}); + const [toggleCompressedIdSelected, setToggleCompressedIdSelected] = useState( + `${compressedToggleButtonGroupPrefix}__1` + ); + + const onChangeCompressed = (optionId) => { + setToggleCompressedIdSelected(optionId); + }; - setToggleIconIdToSelectedMapIcon(newToggleIconIdToSelectedMapIcon); + const onChangeIconsMultiIcons = (optionId) => { + const newToggleIconIdToSelectedMapIcon = { + ...toggleIconIdToSelectedMapIcon, + ...{ + [optionId]: !toggleIconIdToSelectedMapIcon[optionId], + }, }; - return ( - - onChangeCompressed(id)} - buttonSize="compressed" - isFullWidth - /> - - onChangeIconsMultiIcons(id)} - type="multi" - buttonSize="compressed" - isIconOnly - /> - - ); + setToggleIconIdToSelectedMapIcon(newToggleIconIdToSelectedMapIcon); }; - ``` - + + return ( + + onChangeCompressed(id)} + buttonSize="compressed" + isFullWidth + /> + + onChangeIconsMultiIcons(id)} + type="multi" + buttonSize="compressed" + isIconOnly + /> + + ); +}; +``` diff --git a/yarn.lock b/yarn.lock index c9cc6259bef..2531716dd68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5703,7 +5703,7 @@ __metadata: moment: "npm:^2.30.1" prism-react-renderer: "npm:^2.3.1" react-is: "npm:^18.3.1" - react-live: "npm:^4.1.7" + react-live: "patch:react-live@npm%3A4.1.7#~/.yarn/patches/react-live-npm-4.1.7-7b41625faa.patch" typescript: "npm:~5.4.5" peerDependencies: react: ^18.0.0 @@ -31278,7 +31278,7 @@ __metadata: languageName: node linkType: hard -"react-live@npm:^4.1.7": +"react-live@npm:4.1.7": version: 4.1.7 resolution: "react-live@npm:4.1.7" dependencies: @@ -31292,6 +31292,20 @@ __metadata: languageName: node linkType: hard +"react-live@patch:react-live@npm%3A4.1.7#~/.yarn/patches/react-live-npm-4.1.7-7b41625faa.patch": + version: 4.1.7 + resolution: "react-live@patch:react-live@npm%3A4.1.7#~/.yarn/patches/react-live-npm-4.1.7-7b41625faa.patch::version=4.1.7&hash=33c21c" + dependencies: + prism-react-renderer: "npm:^2.0.6" + sucrase: "npm:^3.31.0" + use-editable: "npm:^2.3.3" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/f26c208a9d2e885a1c0a0a3eae8126ac6e772e37507a53d36f802bc993449f8765dff621d51cfba681c68de4da9aad5f01c2fb1c2df716d27c96ccd94cacf188 + languageName: node + linkType: hard + "react-loadable-ssr-addon-v5-slorber@npm:^1.0.1": version: 1.0.1 resolution: "react-loadable-ssr-addon-v5-slorber@npm:1.0.1"