From 6fdfd2f27f93e969a560751957f372810bdc222d Mon Sep 17 00:00:00 2001
From: Cee Chen <549407+cee-chen@users.noreply.github.com>
Date: Fri, 12 Jan 2024 10:26:08 -0800
Subject: [PATCH] [EuiResizableButton] Add new `indicator` style prop (#7455)
---
changelogs/upcoming/7455.md | 1 +
.../resizable_button_indicator.tsx | 51 ++++++
.../resizable_container_example.js | 33 +++-
.../flyout/flyout_resizable.styles.ts | 28 ++--
src/components/flyout/flyout_resizable.tsx | 5 +-
.../resizable_button.test.tsx.snap | 13 +-
.../resizable_container.test.tsx.snap | 18 +-
.../resizable_button.stories.tsx | 11 +-
.../resizable_button.styles.ts | 156 ++++++++++++------
.../resizable_button.test.tsx | 8 +
.../resizable_container/resizable_button.tsx | 99 ++++++-----
11 files changed, 309 insertions(+), 114 deletions(-)
create mode 100644 changelogs/upcoming/7455.md
create mode 100644 src-docs/src/views/resizable_container/resizable_button_indicator.tsx
diff --git a/changelogs/upcoming/7455.md b/changelogs/upcoming/7455.md
new file mode 100644
index 00000000000..556da2a60c7
--- /dev/null
+++ b/changelogs/upcoming/7455.md
@@ -0,0 +1 @@
+- Updated `EuiResizableButton` to allow customizing the `indicator` style with either `handle` (default) or `border`
diff --git a/src-docs/src/views/resizable_container/resizable_button_indicator.tsx b/src-docs/src/views/resizable_container/resizable_button_indicator.tsx
new file mode 100644
index 00000000000..32a39e91530
--- /dev/null
+++ b/src-docs/src/views/resizable_container/resizable_button_indicator.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { EuiText, EuiResizableContainer } from '../../../../src/components';
+import { faker } from '@faker-js/faker';
+
+const text = (
+ <>
+
@@ -504,6 +506,35 @@ export const ResizableContainerExample = {
demo: ,
snippet: collapsibleExtSnippet,
},
+ {
+ source: [
+ {
+ type: GuideSectionTypes.TSX,
+ code: ResizableButtonIndicatorSource,
+ },
+ ],
+ title: 'Resizable button indicator',
+ text: (
+ <>
+
+ By default, EuiResizableButton shows a grab handle
+ indicator as a UI hint. For use cases where the resize behavior is
+ "nice to have" but not a primary UX focus, or where there are many
+ other busy UI elements on the page, you can set{' '}
+ indicator="border" to display a subdued border
+ element instead, which only provides resize affordance on hover or
+ focus.
+
+ >
+ ),
+ demo: ,
+ demoPanelProps: { paddingSize: 'none' },
+ props: { EuiResizableButton },
+ snippet: ` `,
+ },
{
source: [
{
@@ -511,7 +542,7 @@ export const ResizableContainerExample = {
code: ResizableButtonSource,
},
],
- title: 'Custom resizable button',
+ title: 'Custom resize logic',
text: (
<>
diff --git a/src/components/flyout/flyout_resizable.styles.ts b/src/components/flyout/flyout_resizable.styles.ts
index abcda128fef..1257a6d6235 100644
--- a/src/components/flyout/flyout_resizable.styles.ts
+++ b/src/components/flyout/flyout_resizable.styles.ts
@@ -14,17 +14,21 @@ import { logicalCSS } from '../../global_styling';
export const euiFlyoutResizableButtonStyles = ({ euiTheme }: UseEuiTheme) => ({
euiFlyoutResizableButton: css`
position: absolute;
-
- /* Hide the default grab icon (although the hover/focus states should remain) */
- &::before,
- &::after {
- background-color: transparent;
- }
- `,
- left: css`
- ${logicalCSS('right', `-${euiTheme.border.width.thin}`)}
- `,
- right: css`
- ${logicalCSS('left', `-${euiTheme.border.width.thin}`)}
`,
+ overlay: {
+ left: css`
+ ${logicalCSS('right', 0)}
+ `,
+ right: css`
+ ${logicalCSS('left', 0)}
+ `,
+ },
+ push: {
+ left: css`
+ ${logicalCSS('right', `-${euiTheme.border.width.thin}`)}
+ `,
+ right: css`
+ ${logicalCSS('left', `-${euiTheme.border.width.thin}`)}
+ `,
+ },
});
diff --git a/src/components/flyout/flyout_resizable.tsx b/src/components/flyout/flyout_resizable.tsx
index 632d3af67c3..7005aafd69c 100644
--- a/src/components/flyout/flyout_resizable.tsx
+++ b/src/components/flyout/flyout_resizable.tsx
@@ -34,6 +34,7 @@ export const EuiFlyoutResizable = forwardRef(
maxWidth,
minWidth = 200,
side = 'right',
+ type = 'overlay',
children,
...rest
}: EuiFlyoutResizableProps,
@@ -41,7 +42,7 @@ export const EuiFlyoutResizable = forwardRef(
) => {
const euiTheme = useEuiTheme();
const styles = euiFlyoutResizableButtonStyles(euiTheme);
- const cssStyles = [styles.euiFlyoutResizableButton, styles[side]];
+ const cssStyles = [styles.euiFlyoutResizableButton, styles[type][side]];
const getFlyoutMinMaxWidth = useCallback(
(width: number) => {
@@ -141,10 +142,12 @@ export const EuiFlyoutResizable = forwardRef(
size={flyoutWidth || size}
maxWidth={maxWidth}
side={side}
+ type={type}
ref={setRefs}
>
`;
+
+exports[`EuiResizableButton renders different indicator styles and directions 1`] = `
+
+
+
+`;
diff --git a/src/components/resizable_container/__snapshots__/resizable_container.test.tsx.snap b/src/components/resizable_container/__snapshots__/resizable_container.test.tsx.snap
index 9b95ee197e1..ad3e6f48762 100644
--- a/src/components/resizable_container/__snapshots__/resizable_container.test.tsx.snap
+++ b/src/components/resizable_container/__snapshots__/resizable_container.test.tsx.snap
@@ -20,7 +20,7 @@ exports[`EuiResizableContainer can adjust panel props 1`] = `
= {
component: EuiResizableButton,
args: {
// Component defaults
+ indicator: 'handle',
alignIndicator: 'center',
disabled: false,
isHorizontal: false,
@@ -32,8 +33,14 @@ type Story = StoryObj;
export const Playground: Story = {
render: (args) => (
-
-
+
+
),
};
diff --git a/src/components/resizable_container/resizable_button.styles.ts b/src/components/resizable_container/resizable_button.styles.ts
index 340d249038b..a7436cc7554 100644
--- a/src/components/resizable_container/resizable_button.styles.ts
+++ b/src/components/resizable_container/resizable_button.styles.ts
@@ -22,38 +22,24 @@ export const euiResizableButtonStyles = (euiThemeContext: UseEuiTheme) => {
const transition = `${transitionSpeed} ease`;
return {
- // Mimics the "grab" icon with CSS psuedo-elements.
- // 1. The "grab" icon transforms into a thicker straight line on :hover and :focus
- // 2. Start/end aligned handles should have a slight margin offset that disappears on hover/focus
+ // Creates a resizable indicator (either a grab handle or a plain border) with CSS psuedo-elements.
+ // 1. The "grab" handle transforms into a thicker straight line on :hover and :focus
+ // 2. Start/end aligned grab handles should have a slight margin offset that disappears on hover/focus
// 3. CSS hack to smooth out/anti-alias the 1px wide handles at various zoom levels
euiResizableButton: css`
z-index: 1;
flex-shrink: 0;
display: flex;
justify-content: center;
- gap: ${mathWithUnits(grabHandleHeight, (x) => x * 2)};
&:disabled {
display: none;
}
- /* 1 */
- &:hover,
- &:focus {
- gap: 0;
- justify-content: center;
- }
-
- ${euiCanAnimate} {
- transition: gap ${transition}, justify-content ${transition};
- }
-
&::before,
&::after {
content: '';
display: block;
- background-color: ${euiTheme.colors.darkestShade};
- transform: translateZ(0); /* 3 */
${euiCanAnimate} {
transition: width ${transition}, height ${transition},
@@ -61,20 +47,15 @@ export const euiResizableButtonStyles = (euiThemeContext: UseEuiTheme) => {
}
}
- /* Lighten the "grab" icon on :hover */
+ /* Lighten color on :hover */
&:hover {
&::before,
&::after {
background-color: ${euiTheme.colors.mediumShade};
-
- /* Delay transition on hover so animation is not accidentally triggered on mouse over */
- ${euiCanAnimate} {
- transition-delay: ${transitionSpeed};
- }
}
}
- /* Add a transparent background to the container and emphasize the "grab" icon
+ /* Add a transparent background to the container and color the border primary
with primary color on :focus (NOTE - :active is needed for Safari) */
&:focus,
&:active {
@@ -84,7 +65,7 @@ export const euiResizableButtonStyles = (euiThemeContext: UseEuiTheme) => {
&::after {
background-color: ${euiTheme.colors.primary};
- /* Overrides default transition so that "grab" icon background-color doesn't animate */
+ /* Overrides default transition so that "grab" background-color doesn't animate */
${euiCanAnimate} {
transition: width ${transition}, height ${transition};
transition-delay: ${mathWithUnits(transitionSpeed, (x) => x / 2)};
@@ -97,25 +78,6 @@ export const euiResizableButtonStyles = (euiThemeContext: UseEuiTheme) => {
${logicalCSS('height', '100%')}
${logicalCSS('width', buttonSize)}
margin-inline: ${mathWithUnits(buttonSize, (x) => x / -2)};
-
- &::before,
- &::after {
- ${logicalCSS('width', grabHandleHeight)}
- ${logicalCSS('height', grabHandleWidth)}
- margin-block: ${euiTheme.size.base}; /* 2 */
- }
-
- /* 1 */
- &:hover,
- &:focus,
- &:active {
- &::before,
- &::after {
- ${logicalCSS('height', '100%')}
- margin-block: 0; /* 2 */
- transform: none; /* 3 */
- }
- }
`,
vertical: css`
flex-direction: column;
@@ -123,26 +85,120 @@ export const euiResizableButtonStyles = (euiThemeContext: UseEuiTheme) => {
${logicalCSS('width', '100%')}
${logicalCSS('height', buttonSize)}
margin-block: ${mathWithUnits(buttonSize, (x) => x / -2)};
+ `,
+ border: css`
&::before,
&::after {
- ${logicalCSS('height', grabHandleHeight)}
- ${logicalCSS('width', grabHandleWidth)}
- margin-inline: ${euiTheme.size.base}; /* 2 */
+ background-color: ${euiTheme.border.color};
}
+ `,
+ borderDirection: {
+ horizontal: css`
+ &::before {
+ ${logicalCSS('width', euiTheme.border.width.thin)}
+ ${logicalCSS('height', '100%')}
+ }
+
+ &:hover,
+ &:focus,
+ &:active {
+ &::after {
+ ${logicalCSS('width', euiTheme.border.width.thin)}
+ ${logicalCSS('height', '100%')}
+ }
+ }
+ `,
+ vertical: css`
+ &::before {
+ ${logicalCSS('height', euiTheme.border.width.thin)}
+ ${logicalCSS('width', '100%')}
+ }
+
+ &:hover,
+ &:focus,
+ &:active {
+ &::after {
+ ${logicalCSS('height', euiTheme.border.width.thin)}
+ ${logicalCSS('width', '100%')}
+ }
+ }
+ `,
+ },
+
+ handle: css`
+ gap: ${mathWithUnits(grabHandleHeight, (x) => x * 2)};
/* 1 */
&:hover,
&:focus,
&:active {
+ gap: 0;
+ }
+
+ ${euiCanAnimate} {
+ transition: gap ${transition};
+ }
+
+ &::before,
+ &::after {
+ background-color: ${euiTheme.colors.darkestShade};
+ transform: translateZ(0); /* 3 */
+ }
+
+ /* Lighten color on :hover */
+ &:hover {
&::before,
&::after {
- ${logicalCSS('width', '100%')}
- margin-inline: 0; /* 2 */
- transform: none; /* 3 */
+ /* Delay transition on hover so animation is not accidentally triggered on mouse over */
+ ${euiCanAnimate} {
+ transition-delay: ${transitionSpeed};
+ }
}
}
`,
+ handleDirection: {
+ horizontal: css`
+ &::before,
+ &::after {
+ ${logicalCSS('width', grabHandleHeight)}
+ ${logicalCSS('height', grabHandleWidth)}
+ margin-block: ${euiTheme.size.base}; /* 2 */
+ }
+
+ /* 1 */
+ &:hover,
+ &:focus,
+ &:active {
+ &::before,
+ &::after {
+ ${logicalCSS('height', '100%')}
+ margin-block: 0; /* 2 */
+ transform: none; /* 3 */
+ }
+ }
+ `,
+ vertical: css`
+ &::before,
+ &::after {
+ ${logicalCSS('height', grabHandleHeight)}
+ ${logicalCSS('width', grabHandleWidth)}
+ margin-inline: ${euiTheme.size.base}; /* 2 */
+ }
+
+ /* 1 */
+ &:hover,
+ &:focus,
+ &:active {
+ &::before,
+ &::after {
+ ${logicalCSS('width', '100%')}
+ margin-inline: 0; /* 2 */
+ transform: none; /* 3 */
+ }
+ }
+ `,
+ },
alignIndicator: {
center: css`
align-items: center;
diff --git a/src/components/resizable_container/resizable_button.test.tsx b/src/components/resizable_container/resizable_button.test.tsx
index baba1be332c..3ea4130eac8 100644
--- a/src/components/resizable_container/resizable_button.test.tsx
+++ b/src/components/resizable_container/resizable_button.test.tsx
@@ -28,6 +28,14 @@ describe('EuiResizableButton', () => {
expect(container).toMatchSnapshot();
});
+ it('renders different indicator styles and directions', () => {
+ const { container } = render(
+
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
it('renders as hidden if disabled', () => {
const { container } = render( );
diff --git a/src/components/resizable_container/resizable_button.tsx b/src/components/resizable_container/resizable_button.tsx
index 416ea0d1141..a25ca3c145b 100644
--- a/src/components/resizable_container/resizable_button.tsx
+++ b/src/components/resizable_container/resizable_button.tsx
@@ -37,8 +37,14 @@ export type EuiResizableButtonProps = ButtonHTMLAttributes &
*/
isHorizontal?: boolean;
/**
- * Specify the alignment of the initial resize indicator. Defaults to `center`,
- * but consider using `start` for extremely tall content that scrolls off-screen
+ * By default, EuiResizableButton will show a grab handle to indicate resizability.
+ * This indicator can be optionally hidden to show a plain border instead.
+ */
+ indicator?: 'handle' | 'border';
+ /**
+ * Allows customizing the alignment of grab `handle` indicators (does nothing for
+ * border indicators). Defaults to `center`, but consider using `start` for extremely
+ * tall content that scrolls off-screen
*/
alignIndicator?: 'center' | 'start' | 'end';
/**
@@ -54,44 +60,61 @@ export type EuiResizableButtonProps = ButtonHTMLAttributes &
export const EuiResizableButton = forwardRef<
HTMLButtonElement,
EuiResizableButtonProps
->(({ isHorizontal, alignIndicator = 'center', className, ...rest }, ref) => {
- const classes = classNames('euiResizableButton', className);
+>(
+ (
+ {
+ isHorizontal,
+ indicator = 'handle',
+ alignIndicator = 'center',
+ className,
+ ...rest
+ },
+ ref
+ ) => {
+ const classes = classNames('euiResizableButton', className);
- const euiTheme = useEuiTheme();
- const styles = euiResizableButtonStyles(euiTheme);
- const cssStyles = [
- styles.euiResizableButton,
- isHorizontal ? styles.horizontal : styles.vertical,
- styles.alignIndicator[alignIndicator],
- ];
+ const resizeDirection = isHorizontal ? 'horizontal' : 'vertical';
- return (
-
- {([horizontalResizerAriaLabel, verticalResizerAriaLabel]: string[]) => (
-
- )}
-
- );
-});
+ const euiTheme = useEuiTheme();
+ const styles = euiResizableButtonStyles(euiTheme);
+ const cssStyles = [
+ styles.euiResizableButton,
+ styles[indicator],
+ styles[`${indicator}Direction`][resizeDirection],
+ styles[resizeDirection],
+ indicator === 'handle' && styles.alignIndicator[alignIndicator],
+ ];
+
+ return (
+
+ {([horizontalResizerAriaLabel, verticalResizerAriaLabel]: string[]) => (
+
+ )}
+
+ );
+ }
+);
EuiResizableButton.displayName = 'EuiResizableButton';
/**