mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Dashboard] Presentation panel refactor (#207275)
Closes https://github.com/elastic/kibana/issues/206686 Closes https://github.com/elastic/kibana/issues/197897 Part of https://github.com/elastic/kibana/issues/207852 ## Summary This PR is a major refactor of the `PresentationPanel` component, including an overhaul of the hover action and panel title components. Some notable highlights include: - All styles in the `PresentationPanel` component were moved from SASS to Emotion - The over-complicated logic to combine hover actions when the panel shrinks was removed in favour of CSS, driven by a [container query](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries) Removing the `updateCombineHoverActions` function (which was defined in a React component and not memoized) also made a difference in performance when dragging: | Before | After | |--------|--------| |  |  | - The over-complicated logic defined in `usePresentationPanelTitleClickHandle`, which was meant to ignore the `onClick` that would trigger after a panel was dragged, was converted to 2 lines of CSS ### Small usability improvements This PR also includes a few small usability improvements, such as: - Ensuring that only the **first** row of hover actions overlaps with the Dashboard's sticky top navigation bar, and this only happens when the dashboard has no controls. This results in much better behaviour in most scenarios: | Before | After | |--------|--------| |  |  | - Adding a small delay for hiding the hover actions on mouse leave, which makes it a lot easier to grab the drag handle: | Before | After | |--------|--------| |  |  | - Preventing the resize handle from overlapping Dashboard's stick top navigation: | Before | After | |--------|--------| |  |  | ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
7f28ae63e3
commit
c35698bcf8
19 changed files with 455 additions and 639 deletions
|
@ -1,133 +0,0 @@
|
|||
.embPanel {
|
||||
z-index: auto;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
&-isLoading {
|
||||
// completely center the loading indicator
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// SASSTODO: The inheritence factor stemming from embeddables makes this class hard to change
|
||||
.embPanel__content {
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
z-index: 1;
|
||||
min-height: 0; // Absolute must for Firefox to scroll contents
|
||||
border-radius: $euiBorderRadius;
|
||||
overflow: hidden;
|
||||
|
||||
&[data-error] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.embPanel__content--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// SASSTODO: this MIGHT be fixing IE
|
||||
.embPanel__content--fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// HEADER
|
||||
|
||||
.embPanel__header {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
// ensure menu button is on the right even if the title doesn't exist
|
||||
justify-content: flex-end;
|
||||
height: $euiSizeL;
|
||||
}
|
||||
|
||||
.embPanel__header + .embPanel__content {
|
||||
border-radius: 0;
|
||||
border-bottom-left-radius: $euiBorderRadius;
|
||||
border-bottom-right-radius: $euiBorderRadius;
|
||||
}
|
||||
|
||||
.embPanel__title {
|
||||
@include euiTitle('xxs');
|
||||
overflow: hidden;
|
||||
line-height: 1.5;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
&:not(:empty) {
|
||||
line-height: $euiSizeL;
|
||||
padding-left: $euiSizeS;
|
||||
}
|
||||
|
||||
.embPanel__titleInner {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: $euiSizeS;
|
||||
}
|
||||
|
||||
.embPanel__titleTooltipAnchor {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.embPanel__titleText {
|
||||
@include euiTextTruncate;
|
||||
font-weight: $euiFontWeightSemiBold;
|
||||
}
|
||||
|
||||
.embPanel__placeholderTitleText {
|
||||
color: $euiColorMediumShade;
|
||||
font-weight: $euiFontWeightRegular;
|
||||
}
|
||||
}
|
||||
|
||||
.embPanel--dragHandle:not(.embPanel__title) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.embPanel__header--floater {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
* {
|
||||
z-index: $euiZLevel1; // apply high z-index to all children
|
||||
}
|
||||
}
|
||||
|
||||
// EDITING MODE
|
||||
.embPanel--editing {
|
||||
.embPanel--dragHandle {
|
||||
.embPanel--dragHandle:hover {
|
||||
background-color: transparentize($euiColorWarning, lightOrDarkTheme(.9, .7));
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LOADING and ERRORS
|
||||
|
||||
.embPanel__label {
|
||||
position: absolute;
|
||||
padding-left: $euiSizeS;
|
||||
z-index: $euiZLevel1;
|
||||
}
|
||||
|
||||
.embPanel--dragHandle {
|
||||
cursor: move;
|
||||
|
||||
img {
|
||||
pointer-events: all !important;
|
||||
}
|
||||
}
|
||||
|
||||
.embPanel__descriptionTooltipAnchor {
|
||||
padding: $euiSizeXS;
|
||||
}
|
|
@ -7,9 +7,10 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { transparentize, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { ViewMode } from '@kbn/presentation-publishing';
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
|
||||
import { PresentationPanelTitle } from './presentation_panel_title';
|
||||
import { usePresentationPanelHeaderActions } from './use_presentation_panel_header_actions';
|
||||
|
@ -38,6 +39,8 @@ export const PresentationPanelHeader = <
|
|||
showBadges = true,
|
||||
showNotifications = true,
|
||||
}: PresentationPanelHeaderProps<ApiType>) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const { notificationElements, badgeElements } = usePresentationPanelHeaderActions<ApiType>(
|
||||
showNotifications,
|
||||
showBadges,
|
||||
|
@ -53,29 +56,45 @@ export const PresentationPanelHeader = <
|
|||
[setDragHandle]
|
||||
);
|
||||
|
||||
const { captionStyles, headerStyles } = useMemo(() => {
|
||||
return {
|
||||
captionStyles: css`
|
||||
.dshLayout--editing &:hover {
|
||||
cursor: move;
|
||||
background-color: ${transparentize(euiTheme.colors.warning, 0.2)};
|
||||
}
|
||||
`,
|
||||
headerStyles: css`
|
||||
height: ${euiTheme.size.l};
|
||||
overflow: hidden;
|
||||
line-height: ${euiTheme.size.l};
|
||||
padding: 0px ${euiTheme.size.s};
|
||||
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-wrap: wrap;
|
||||
column-gap: ${euiTheme.size.s};
|
||||
align-items: center;
|
||||
`,
|
||||
};
|
||||
}, [euiTheme.colors.warning, euiTheme.size]);
|
||||
|
||||
const showPanelBar =
|
||||
(!hideTitle && panelTitle) || badgeElements.length > 0 || notificationElements.length > 0;
|
||||
|
||||
if (!showPanelBar) return null;
|
||||
|
||||
const headerClasses = classNames('embPanel__header', {
|
||||
'embPanel--dragHandle': viewMode === 'edit',
|
||||
'embPanel__header--floater': !showPanelBar,
|
||||
});
|
||||
|
||||
const titleClasses = classNames('embPanel__title', {
|
||||
'embPanel--dragHandle': viewMode === 'edit',
|
||||
});
|
||||
|
||||
return (
|
||||
<figcaption
|
||||
className={headerClasses}
|
||||
data-test-subj={`embeddablePanelHeading-${(panelTitle || '').replace(/\s/g, '')}`}
|
||||
className={'embPanel__header'}
|
||||
css={captionStyles}
|
||||
>
|
||||
<div
|
||||
className="embPanel__title"
|
||||
ref={memoizedSetDragHandle}
|
||||
data-test-subj="dashboardPanelTitle"
|
||||
className={titleClasses}
|
||||
css={headerStyles}
|
||||
>
|
||||
<PresentationPanelTitle
|
||||
api={api}
|
||||
|
|
|
@ -33,25 +33,25 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { ActionExecutionContext, buildContextMenuForActions } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import {
|
||||
apiCanLockHoverActions,
|
||||
EmbeddableApiContext,
|
||||
getViewModeSubject,
|
||||
useBatchedOptionalPublishingSubjects,
|
||||
ViewMode,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { css } from '@emotion/react';
|
||||
import { ActionWithContext } from '@kbn/ui-actions-plugin/public/context_menu/build_eui_context_menu_panels';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { uiActions } from '../../kibana_services';
|
||||
import {
|
||||
contextMenuTrigger,
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
panelNotificationTrigger,
|
||||
contextMenuTrigger,
|
||||
PANEL_NOTIFICATION_TRIGGER,
|
||||
panelNotificationTrigger,
|
||||
} from '../../panel_actions';
|
||||
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
|
||||
import { AnyApiAction } from '../../panel_actions/types';
|
||||
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
|
||||
import { useHoverActionStyles } from './use_hover_actions_styles';
|
||||
|
||||
const getContextMenuAriaLabel = (title?: string, index?: number) => {
|
||||
if (title) {
|
||||
|
@ -130,106 +130,26 @@ export const PresentationPanelHoverActions = ({
|
|||
const [showNotification, setShowNotification] = useState<boolean>(false);
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean>(false);
|
||||
const [notifications, setNotifications] = useState<AnyApiAction[]>([]);
|
||||
const hoverActionsRef = useRef<HTMLDivElement | null>(null);
|
||||
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
|
||||
const anchorRef = useRef<HTMLDivElement | null>(null);
|
||||
const rightHoverActionsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [combineHoverActions, setCombineHoverActions] = useState<boolean>(false);
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const EDIT_MODE_OUTLINE = `${euiTheme.border.width.thin} dashed ${euiTheme.colors.borderBaseFormsControl}`;
|
||||
const VIEW_MODE_OUTLINE = `${euiTheme.border.width.thin} solid ${euiTheme.colors.borderBasePlain}`;
|
||||
|
||||
const ALL_ROUNDED_CORNERS = `
|
||||
border-radius: ${euiTheme.border.radius.medium};
|
||||
`;
|
||||
const TOP_ROUNDED_CORNERS = `
|
||||
border-top-left-radius: ${euiTheme.border.radius.medium};
|
||||
border-top-right-radius: ${euiTheme.border.radius.medium};
|
||||
border-bottom: 0px;
|
||||
`;
|
||||
|
||||
const [borderStyles, setBorderStyles] = useState<string>(TOP_ROUNDED_CORNERS);
|
||||
|
||||
const updateCombineHoverActions = () => {
|
||||
if (!hoverActionsRef.current || !anchorRef.current) return;
|
||||
const anchorBox = anchorRef.current.getBoundingClientRect();
|
||||
const anchorLeft = anchorBox.left;
|
||||
const anchorTop = anchorBox.top;
|
||||
const anchorWidth = anchorRef.current.offsetWidth;
|
||||
const hoverActionsWidth =
|
||||
(rightHoverActionsRef.current?.offsetWidth ?? 0) +
|
||||
(dragHandleRef.current?.offsetWidth ?? 0) +
|
||||
parseInt(euiTheme.size.base, 10) * 2;
|
||||
const hoverActionsHeight = rightHoverActionsRef.current?.offsetHeight ?? 0;
|
||||
|
||||
// Left align hover actions when they would get cut off by the right edge of the window
|
||||
if (anchorLeft - (hoverActionsWidth - anchorWidth) <= parseInt(euiTheme.size.base, 10)) {
|
||||
dragHandleRef.current?.style.removeProperty('right');
|
||||
dragHandleRef.current?.style.setProperty('left', '0');
|
||||
} else {
|
||||
hoverActionsRef.current.style.removeProperty('left');
|
||||
hoverActionsRef.current.style.setProperty('right', '0');
|
||||
}
|
||||
|
||||
if (anchorRef.current && rightHoverActionsRef.current) {
|
||||
const shouldCombine = anchorWidth < hoverActionsWidth;
|
||||
const willGetCutOff = anchorTop < hoverActionsHeight;
|
||||
|
||||
if (shouldCombine !== combineHoverActions) {
|
||||
setCombineHoverActions(shouldCombine);
|
||||
}
|
||||
|
||||
if (willGetCutOff) {
|
||||
hoverActionsRef.current.style.setProperty('position', 'absolute');
|
||||
hoverActionsRef.current.style.setProperty('top', `-${euiTheme.size.s}`);
|
||||
} else if (shouldCombine) {
|
||||
hoverActionsRef.current.style.setProperty('top', `-${euiTheme.size.l}`);
|
||||
} else {
|
||||
hoverActionsRef.current.style.removeProperty('position');
|
||||
hoverActionsRef.current.style.removeProperty('top');
|
||||
}
|
||||
|
||||
if (shouldCombine || willGetCutOff) {
|
||||
setBorderStyles(ALL_ROUNDED_CORNERS);
|
||||
} else {
|
||||
setBorderStyles(TOP_ROUNDED_CORNERS);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const [
|
||||
defaultTitle,
|
||||
title,
|
||||
description,
|
||||
hidePanelTitle,
|
||||
hasLockedHoverActions,
|
||||
parentHideTitle,
|
||||
parentViewMode,
|
||||
] = useBatchedOptionalPublishingSubjects(
|
||||
api?.defaultTitle$,
|
||||
api?.title$,
|
||||
api?.description$,
|
||||
api?.hideTitle$,
|
||||
api?.hasLockedHoverActions$,
|
||||
api?.parentApi?.hideTitle$,
|
||||
/**
|
||||
* View mode changes often have the biggest influence over which actions will be compatible,
|
||||
* so we build and update all actions when the view mode changes. This is temporary, as these
|
||||
* actions should eventually all be Frequent Compatibility Change Actions which can track their
|
||||
* own dependencies.
|
||||
*/
|
||||
getViewModeSubject(api ?? undefined)
|
||||
);
|
||||
const [defaultTitle, title, description, hidePanelTitle, hasLockedHoverActions, parentHideTitle] =
|
||||
useBatchedOptionalPublishingSubjects(
|
||||
api?.defaultTitle$,
|
||||
api?.title$,
|
||||
api?.description$,
|
||||
api?.hideTitle$,
|
||||
api?.hasLockedHoverActions$,
|
||||
api?.parentApi?.hideTitle$
|
||||
);
|
||||
|
||||
const hideTitle = hidePanelTitle || parentHideTitle;
|
||||
const showDescription = description && (!title || hideTitle);
|
||||
|
||||
const quickActionIds = useMemo(
|
||||
() => QUICK_ACTION_IDS[parentViewMode === 'edit' ? 'edit' : 'view'],
|
||||
[parentViewMode]
|
||||
() => QUICK_ACTION_IDS[viewMode === 'edit' ? 'edit' : 'view'],
|
||||
[viewMode]
|
||||
);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
|
@ -369,15 +289,7 @@ export const PresentationPanelHoverActions = ({
|
|||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [
|
||||
actionPredicate,
|
||||
api,
|
||||
getActions,
|
||||
isContextMenuOpen,
|
||||
onClose,
|
||||
parentViewMode,
|
||||
quickActionIds,
|
||||
]);
|
||||
}, [actionPredicate, api, getActions, isContextMenuOpen, onClose, viewMode, quickActionIds]);
|
||||
|
||||
const quickActionElements = useMemo(() => {
|
||||
if (!api || quickActions.length < 1) return [];
|
||||
|
@ -472,6 +384,17 @@ export const PresentationPanelHoverActions = ({
|
|||
// memoize the drag handle to avoid calling `setDragHandle` unnecessarily
|
||||
() => (
|
||||
<button
|
||||
className={`embPanel--dragHandle`}
|
||||
css={css`
|
||||
cursor: move;
|
||||
visibility: hidden; // default for every mode **except** edit mode
|
||||
width: 0px;
|
||||
|
||||
.embPanel__hoverActionsAnchor--editMode & {
|
||||
width: auto;
|
||||
visibility: visible; // overwrite visibility in edit mode
|
||||
}
|
||||
`}
|
||||
ref={(ref) => {
|
||||
dragHandleRef.current = ref;
|
||||
setDragHandle('hoverActions', ref);
|
||||
|
@ -480,14 +403,13 @@ export const PresentationPanelHoverActions = ({
|
|||
<EuiIcon
|
||||
type="move"
|
||||
color="text"
|
||||
className={`embPanel--dragHandle`}
|
||||
aria-label={i18n.translate('presentationPanel.dragHandle', {
|
||||
defaultMessage: 'Move panel',
|
||||
})}
|
||||
data-test-subj="embeddablePanelDragHandle"
|
||||
css={css`
|
||||
margin: ${euiTheme.size.xs};
|
||||
`}
|
||||
data-test-subj="embeddablePanelDragHandle"
|
||||
aria-label={i18n.translate('presentationPanel.dragHandle', {
|
||||
defaultMessage: 'Move panel',
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
),
|
||||
|
@ -495,170 +417,90 @@ export const PresentationPanelHoverActions = ({
|
|||
);
|
||||
|
||||
const hasHoverActions = quickActionElements.length || contextMenuPanels.lastIndexOf.length;
|
||||
const { containerStyles, hoverActionStyles } = useHoverActionStyles(
|
||||
viewMode === 'edit',
|
||||
showBorder
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseOver={updateCombineHoverActions}
|
||||
onFocus={updateCombineHoverActions}
|
||||
ref={anchorRef}
|
||||
className={classNames('embPanel__hoverActionsAnchor', {
|
||||
'embPanel__hoverActionsAnchor--lockHoverActions': hasLockedHoverActions,
|
||||
'embPanel__hoverActionsAnchor--editMode': viewMode === 'edit',
|
||||
})}
|
||||
data-test-embeddable-id={api?.uuid}
|
||||
data-test-subj={`embeddablePanelHoverActions-${(title || defaultTitle || '').replace(
|
||||
/\s/g,
|
||||
''
|
||||
)}`}
|
||||
css={css`
|
||||
border-radius: ${euiTheme.border.radius.medium};
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.embPanel {
|
||||
${showBorder
|
||||
? `
|
||||
outline: ${viewMode === 'edit' ? EDIT_MODE_OUTLINE : VIEW_MODE_OUTLINE};
|
||||
`
|
||||
: ''}
|
||||
}
|
||||
|
||||
.embPanel__hoverActions {
|
||||
opacity: 0;
|
||||
padding: calc(${euiTheme.size.xs} - 1px);
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
background-color: ${euiTheme.colors.backgroundBasePlain};
|
||||
height: ${euiTheme.size.xl};
|
||||
|
||||
pointer-events: all; // Re-enable pointer-events for hover actions
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-within,
|
||||
&.embPanel__hoverActionsAnchor--lockHoverActions {
|
||||
.embPanel {
|
||||
outline: ${viewMode === 'edit' ? EDIT_MODE_OUTLINE : VIEW_MODE_OUTLINE};
|
||||
z-index: ${euiTheme.levels.menu};
|
||||
}
|
||||
.embPanel__hoverActionsWrapper {
|
||||
z-index: ${euiTheme.levels.toast};
|
||||
top: -${euiTheme.size.xl};
|
||||
|
||||
.embPanel__hoverActions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
css={containerStyles}
|
||||
>
|
||||
{children}
|
||||
{api ? (
|
||||
<div
|
||||
ref={hoverActionsRef}
|
||||
className="embPanel__hoverActionsWrapper"
|
||||
css={css`
|
||||
height: ${euiTheme.size.xl};
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 ${euiTheme.size.base};
|
||||
flex-wrap: nowrap;
|
||||
min-width: 100%;
|
||||
z-index: -1;
|
||||
pointer-events: none; // Prevent hover actions wrapper from blocking interactions with other panels
|
||||
`}
|
||||
>
|
||||
{viewMode === 'edit' && !combineHoverActions ? (
|
||||
<div
|
||||
data-test-subj="embPanel__hoverActions__left"
|
||||
className={classNames(
|
||||
'embPanel__hoverActions',
|
||||
'embPanel__hoverActionsLeft',
|
||||
className
|
||||
)}
|
||||
css={css`
|
||||
border: ${viewMode === 'edit' ? EDIT_MODE_OUTLINE : VIEW_MODE_OUTLINE};
|
||||
${borderStyles}
|
||||
`}
|
||||
>
|
||||
{dragHandle}
|
||||
</div>
|
||||
) : (
|
||||
<div /> // necessary for the right hover actions to align correctly when left hover actions are not present
|
||||
)}
|
||||
{hasHoverActions ? (
|
||||
<div
|
||||
ref={rightHoverActionsRef}
|
||||
data-test-subj="embPanel__hoverActions__right"
|
||||
className={classNames(
|
||||
'embPanel__hoverActions',
|
||||
'embPanel__hoverActionsRight',
|
||||
className
|
||||
)}
|
||||
css={css`
|
||||
border: ${viewMode === 'edit' ? EDIT_MODE_OUTLINE : VIEW_MODE_OUTLINE};
|
||||
${borderStyles}
|
||||
`}
|
||||
>
|
||||
{viewMode === 'edit' && combineHoverActions && dragHandle}
|
||||
{showNotifications && notificationElements}
|
||||
{showDescription && (
|
||||
<EuiIconTip
|
||||
title={!hideTitle ? title || undefined : undefined}
|
||||
content={description}
|
||||
delay="regular"
|
||||
position="top"
|
||||
anchorClassName="embPanel__descriptionTooltipAnchor"
|
||||
data-test-subj="embeddablePanelDescriptionTooltip"
|
||||
type="iInCircle"
|
||||
/>
|
||||
)}
|
||||
{quickActionElements.map(
|
||||
({ iconType, 'data-test-subj': dataTestSubj, onClick, name }, i) => (
|
||||
<EuiToolTip key={`main_action_${dataTestSubj}_${api?.uuid}`} content={name}>
|
||||
<EuiButtonIcon
|
||||
iconType={iconType}
|
||||
color="text"
|
||||
onClick={onClick as MouseEventHandler}
|
||||
data-test-subj={dataTestSubj}
|
||||
aria-label={name as string}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
)
|
||||
)}
|
||||
{contextMenuPanels.length ? (
|
||||
<EuiPopover
|
||||
repositionOnScroll
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
button={ContextMenuButton}
|
||||
isOpen={isContextMenuOpen}
|
||||
className={contextMenuClasses}
|
||||
closePopover={onClose}
|
||||
data-test-subj={
|
||||
isContextMenuOpen
|
||||
? 'embeddablePanelContextMenuOpen'
|
||||
: 'embeddablePanelContextMenuClosed'
|
||||
}
|
||||
focusTrapProps={{
|
||||
closeOnMouseup: true,
|
||||
clickOutsideDisables: false,
|
||||
onClickOutside: onClose,
|
||||
}}
|
||||
>
|
||||
<EuiContextMenu
|
||||
data-test-subj="presentationPanelContextMenuItems"
|
||||
initialPanelId={'mainMenu'}
|
||||
panels={contextMenuPanels}
|
||||
{api && hasHoverActions && (
|
||||
<div className={classNames('embPanel__hoverActions', className)} css={hoverActionStyles}>
|
||||
{dragHandle}
|
||||
{/* Wrapping all "right actions" in a span so that flex space-between works as expected */}
|
||||
<span>
|
||||
{showNotifications && notificationElements}
|
||||
{showDescription && (
|
||||
<EuiIconTip
|
||||
size="m"
|
||||
title={!hideTitle ? title || undefined : undefined}
|
||||
content={description}
|
||||
delay="regular"
|
||||
position="top"
|
||||
data-test-subj="embeddablePanelDescriptionTooltip"
|
||||
type="iInCircle"
|
||||
iconProps={{
|
||||
css: css`
|
||||
margin: ${euiTheme.size.xs};
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{quickActionElements.map(
|
||||
({ iconType, 'data-test-subj': dataTestSubj, onClick, name }, i) => (
|
||||
<EuiToolTip key={`main_action_${dataTestSubj}_${api?.uuid}`} content={name}>
|
||||
<EuiButtonIcon
|
||||
iconType={iconType}
|
||||
color="text"
|
||||
onClick={onClick as MouseEventHandler}
|
||||
data-test-subj={dataTestSubj}
|
||||
aria-label={name as string}
|
||||
/>
|
||||
</EuiPopover>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</EuiToolTip>
|
||||
)
|
||||
)}
|
||||
{contextMenuPanels.length ? (
|
||||
<EuiPopover
|
||||
repositionOnScroll
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
button={ContextMenuButton}
|
||||
isOpen={isContextMenuOpen}
|
||||
className={contextMenuClasses}
|
||||
closePopover={onClose}
|
||||
data-test-subj={
|
||||
isContextMenuOpen
|
||||
? 'embeddablePanelContextMenuOpen'
|
||||
: 'embeddablePanelContextMenuClosed'
|
||||
}
|
||||
focusTrapProps={{
|
||||
closeOnMouseup: true,
|
||||
clickOutsideDisables: false,
|
||||
onClickOutside: onClose,
|
||||
}}
|
||||
>
|
||||
<EuiContextMenu
|
||||
data-test-subj="presentationPanelContextMenuItems"
|
||||
initialPanelId={'mainMenu'}
|
||||
panels={contextMenuPanels}
|
||||
/>
|
||||
</EuiPopover>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { render, screen, fireEvent, renderHook } from '@testing-library/react';
|
||||
import { usePresentationPanelTitleClickHandler } from './presentation_panel_title';
|
||||
|
||||
describe('usePresentationPanelTitleClickHandler', () => {
|
||||
it('returns null when there is no element to attach listeners to', () => {
|
||||
const { result } = renderHook(usePresentationPanelTitleClickHandler);
|
||||
|
||||
expect(result.current).toBe(null);
|
||||
});
|
||||
|
||||
it('calls the click subscribe handler when the enter button is clicked on the provided element', async () => {
|
||||
const mockedClickHandler = jest.fn();
|
||||
|
||||
const TestComponent = ({ onClickHandler }: { onClickHandler: () => void }) => {
|
||||
const [$elm, setElm] = useState<HTMLElement | null>(null);
|
||||
const onClick$ = usePresentationPanelTitleClickHandler($elm);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = onClick$?.subscribe(onClickHandler);
|
||||
|
||||
return () => subscription?.unsubscribe();
|
||||
}, [onClick$, onClickHandler]);
|
||||
|
||||
return (
|
||||
<div data-test-subj="syntheticClick" ref={setElm}>
|
||||
Hello World
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render(<TestComponent onClickHandler={mockedClickHandler} />);
|
||||
|
||||
fireEvent.keyDown(await screen.findByTestId('syntheticClick'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
|
||||
expect(mockedClickHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -7,23 +7,17 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { EuiIcon, EuiLink, EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import { once } from 'lodash';
|
||||
import React, { useMemo, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
type Observable,
|
||||
fromEvent,
|
||||
map,
|
||||
race,
|
||||
mergeMap,
|
||||
takeUntil,
|
||||
takeLast,
|
||||
takeWhile,
|
||||
defaultIfEmpty,
|
||||
repeatWhen,
|
||||
} from 'rxjs';
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiScreenReaderOnly,
|
||||
EuiToolTip,
|
||||
euiTextTruncate,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ViewMode } from '@kbn/presentation-publishing';
|
||||
import {
|
||||
|
@ -32,66 +26,6 @@ import {
|
|||
} from '../../panel_actions/customize_panel_action';
|
||||
import { openCustomizePanelFlyout } from '../../panel_actions/customize_panel_action/open_customize_panel';
|
||||
|
||||
export const placeholderTitle = i18n.translate('presentationPanel.placeholderTitle', {
|
||||
defaultMessage: '[No Title]',
|
||||
});
|
||||
|
||||
const getAriaLabelForTitle = (title?: string) => {
|
||||
return title
|
||||
? i18n.translate('presentationPanel.enhancedAriaLabel', {
|
||||
defaultMessage: 'Panel: {title}',
|
||||
values: { title: title || placeholderTitle },
|
||||
})
|
||||
: i18n.translate('presentationPanel.ariaLabel', {
|
||||
defaultMessage: 'Panel',
|
||||
});
|
||||
};
|
||||
|
||||
const createDocumentMouseMoveListener = once(() => fromEvent<MouseEvent>(document, 'mousemove'));
|
||||
const createDocumentMouseUpListener = once(() => fromEvent<MouseEvent>(document, 'mouseup'));
|
||||
|
||||
export const usePresentationPanelTitleClickHandler = (titleElmRef: HTMLElement | null) => {
|
||||
const onClick = useRef<Observable<{ dragged: boolean }> | null>(null);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (titleElmRef) {
|
||||
const mouseup = createDocumentMouseUpListener();
|
||||
const mousemove = createDocumentMouseMoveListener();
|
||||
const mousedown = fromEvent<MouseEvent>(titleElmRef, 'mousedown');
|
||||
const keydown = fromEvent<KeyboardEvent>(titleElmRef, 'keydown');
|
||||
|
||||
const mousedragExclusiveClick$ = mousedown
|
||||
.pipe(
|
||||
mergeMap(function (md) {
|
||||
// create reference for when mouse is down
|
||||
const startX = md.offsetX;
|
||||
const startY = md.offsetY;
|
||||
|
||||
return mousemove
|
||||
.pipe(
|
||||
map(function (mm) {
|
||||
return { dragged: startX !== mm.clientX && startY !== mm.clientY };
|
||||
})
|
||||
)
|
||||
.pipe(takeUntil(mouseup), takeLast(1))
|
||||
.pipe(defaultIfEmpty({ dragged: false }));
|
||||
})
|
||||
)
|
||||
.pipe(repeatWhen(() => mousedown));
|
||||
|
||||
onClick.current = race(
|
||||
keydown.pipe(takeWhile((kd) => kd.key === 'Enter')).pipe(map(() => ({ dragged: false }))),
|
||||
mousedragExclusiveClick$
|
||||
);
|
||||
|
||||
setInitialized(true);
|
||||
}
|
||||
}, [titleElmRef]);
|
||||
|
||||
return initialized ? onClick.current : null;
|
||||
};
|
||||
|
||||
export const PresentationPanelTitle = ({
|
||||
api,
|
||||
headerId,
|
||||
|
@ -107,80 +41,92 @@ export const PresentationPanelTitle = ({
|
|||
panelDescription?: string;
|
||||
viewMode?: ViewMode;
|
||||
}) => {
|
||||
const [panelTitleElmRef, setPanelTitleElmRef] = useState<HTMLElement | null>(null);
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
openCustomizePanelFlyout({
|
||||
api: api as CustomizePanelActionApi,
|
||||
focusOnTitle: true,
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
const panelTitleElement = useMemo(() => {
|
||||
if (hideTitle) return null;
|
||||
const titleClassNames = classNames('embPanel__titleText', {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
embPanel__placeholderTitleText: !panelTitle,
|
||||
});
|
||||
|
||||
const titleStyles = css`
|
||||
${euiTextTruncate()};
|
||||
font-weight: ${euiTheme.font.weight.bold};
|
||||
|
||||
.kbnGridPanel--active & {
|
||||
pointer-events: none; // prevent drag event from triggering onClick
|
||||
}
|
||||
`;
|
||||
|
||||
if (viewMode !== 'edit' || !isApiCompatibleWithCustomizePanelAction(api)) {
|
||||
return <span className={titleClassNames}>{panelTitle}</span>;
|
||||
return (
|
||||
<span data-test-subj="embeddablePanelTitle" css={titleStyles}>
|
||||
{panelTitle}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiLink
|
||||
color="text"
|
||||
ref={setPanelTitleElmRef}
|
||||
className={titleClassNames}
|
||||
onClick={onClick}
|
||||
css={titleStyles}
|
||||
aria-label={i18n.translate('presentationPanel.header.titleAriaLabel', {
|
||||
defaultMessage: 'Click to edit title: {title}',
|
||||
values: { title: panelTitle ?? placeholderTitle },
|
||||
values: { title: panelTitle },
|
||||
})}
|
||||
data-test-subj={'embeddablePanelTitleLink'}
|
||||
data-test-subj="embeddablePanelTitle"
|
||||
>
|
||||
{panelTitle || placeholderTitle}
|
||||
{panelTitle}
|
||||
</EuiLink>
|
||||
);
|
||||
}, [setPanelTitleElmRef, hideTitle, panelTitle, viewMode, api]);
|
||||
|
||||
const onClick = usePresentationPanelTitleClickHandler(panelTitleElmRef);
|
||||
|
||||
useEffect(() => {
|
||||
const panelTitleClickSubscription = onClick?.subscribe(function onClickHandler({ dragged }) {
|
||||
if (!dragged) {
|
||||
openCustomizePanelFlyout({
|
||||
api: api as CustomizePanelActionApi,
|
||||
focusOnTitle: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => panelTitleClickSubscription?.unsubscribe();
|
||||
}, [api, onClick]);
|
||||
}, [onClick, hideTitle, panelTitle, viewMode, api, euiTheme]);
|
||||
|
||||
const describedPanelTitleElement = useMemo(() => {
|
||||
if (hideTitle) return null;
|
||||
if (!panelDescription) {
|
||||
return (
|
||||
<span data-test-subj="embeddablePanelTitleInner" className="embPanel__titleInner">
|
||||
{panelTitleElement}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const ariaLabel = getAriaLabelForTitle(panelTitle);
|
||||
const ariaLabelElement = (
|
||||
<EuiScreenReaderOnly>
|
||||
<span id={headerId}>{ariaLabel}</span>
|
||||
</EuiScreenReaderOnly>
|
||||
);
|
||||
if (!panelDescription) {
|
||||
return panelTitleElement;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
title={!hideTitle ? panelTitle || undefined : undefined}
|
||||
title={panelTitle}
|
||||
content={panelDescription}
|
||||
delay="regular"
|
||||
position="top"
|
||||
anchorClassName="embPanel__titleTooltipAnchor"
|
||||
anchorProps={{ 'data-test-subj': 'embeddablePanelTooltipAnchor' }}
|
||||
anchorProps={{
|
||||
'data-test-subj': 'embeddablePanelTooltipAnchor',
|
||||
css: css`
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
column-gap: ${euiTheme.size.xs};
|
||||
align-items: center;
|
||||
`,
|
||||
}}
|
||||
>
|
||||
<div data-test-subj="embeddablePanelTitleInner" className="embPanel__titleInner">
|
||||
{!hideTitle ? (
|
||||
<h2>
|
||||
{ariaLabelElement}
|
||||
<EuiScreenReaderOnly>
|
||||
<span id={headerId}>
|
||||
{panelTitle
|
||||
? i18n.translate('presentationPanel.ariaLabel', {
|
||||
defaultMessage: 'Panel: {title}',
|
||||
values: {
|
||||
title: panelTitle,
|
||||
},
|
||||
})
|
||||
: i18n.translate('presentationPanel.untitledPanelAriaLabel', {
|
||||
defaultMessage: 'Untitled panel',
|
||||
})}
|
||||
</span>
|
||||
</EuiScreenReaderOnly>
|
||||
{panelTitleElement}
|
||||
</h2>
|
||||
) : null}
|
||||
|
@ -193,7 +139,7 @@ export const PresentationPanelTitle = ({
|
|||
</div>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}, [hideTitle, panelDescription, panelTitle, panelTitleElement, headerId]);
|
||||
}, [hideTitle, panelDescription, panelTitle, panelTitleElement, headerId, euiTheme.size.xs]);
|
||||
|
||||
return describedPanelTitleElement;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useHoverActionStyles = (isEditMode: boolean, showBorder?: boolean) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const containerStyles = useMemo(() => {
|
||||
const editModeOutline = `${euiTheme.border.width.thin} dashed ${euiTheme.colors.borderBaseFormsControl}`;
|
||||
const viewModeOutline = `${euiTheme.border.width.thin} solid ${euiTheme.colors.borderBasePlain}`;
|
||||
|
||||
return css`
|
||||
// the border style can be overwritten by parents who define --hoverActionsBorderStyle; otherwise, default to either
|
||||
// editModeOutline or viewModeOutline depending on view mode
|
||||
--internalBorderStyle: var(
|
||||
--hoverActionsBorderStyle,
|
||||
${isEditMode ? editModeOutline : viewModeOutline}
|
||||
);
|
||||
|
||||
container: hoverActionsAnchor / inline-size;
|
||||
border-radius: ${euiTheme.border.radius.medium};
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
${showBorder
|
||||
? css`
|
||||
.embPanel {
|
||||
outline: var(--internalBorderStyle);
|
||||
}
|
||||
`
|
||||
: css`
|
||||
&:hover .embPanel {
|
||||
outline: var(--internalBorderStyle);
|
||||
z-index: ${euiTheme.levels.menu};
|
||||
}
|
||||
`}
|
||||
|
||||
.embPanel__hoverActions {
|
||||
position: absolute;
|
||||
top: -${euiTheme.size.xl};
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
// delay hiding hover actions to make grabbing the drag handle easier
|
||||
transition: ${euiTheme.animation.extraFast} opacity ease-in,
|
||||
${euiTheme.animation.extraFast} z-index linear,
|
||||
${euiTheme.animation.extraFast} visibility linear;
|
||||
transition-delay: ${euiTheme.animation.fast};
|
||||
}
|
||||
|
||||
&:hover .embPanel__hoverActions,
|
||||
&:has(:focus-visible) .embPanel__hoverActions,
|
||||
&.embPanel__hoverActionsAnchor--lockHoverActions .embPanel__hoverActions,
|
||||
.embPanel__hoverActions:hover,
|
||||
.embPanel__hoverActions:has(:focus-visible) {
|
||||
z-index: ${euiTheme.levels.menu};
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: none; // apply transition delay on hover out only
|
||||
}
|
||||
`;
|
||||
}, [euiTheme, showBorder, isEditMode]);
|
||||
|
||||
const hoverActionStyles = useMemo(() => {
|
||||
const singleWrapperStyles = css`
|
||||
width: fit-content;
|
||||
top: -${euiTheme.size.l} !important;
|
||||
right: ${euiTheme.size.xs};
|
||||
padding: var(--paddingAroundAction);
|
||||
|
||||
border-radius: ${euiTheme.border.radius.medium};
|
||||
border: var(--internalBorderStyle);
|
||||
background-color: ${euiTheme.colors.backgroundBasePlain};
|
||||
grid-template-columns: max-content;
|
||||
|
||||
& > * {
|
||||
// undo certain styles on all children so that parent takes precedence
|
||||
border: none !important;
|
||||
padding: 0px !important;
|
||||
border-radius: unset !important;
|
||||
background-color: transparent !important;
|
||||
height: unset !important;
|
||||
}
|
||||
`;
|
||||
|
||||
return css`
|
||||
--paddingAroundAction: calc(${euiTheme.size.xs} - 1px);
|
||||
|
||||
pointer-events: none; // prevent hover actions wrapper from blocking interactions with other panels
|
||||
|
||||
width: 100%;
|
||||
height: ${euiTheme.size.xl};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0px ${euiTheme.size.m};
|
||||
|
||||
& > * {
|
||||
// apply styles to all children
|
||||
display: flex;
|
||||
height: ${euiTheme.size.xl};
|
||||
flex: 0; // do not grow
|
||||
pointer-events: all; // re-enable pointer events for non-breakpoint children
|
||||
background-color: ${euiTheme.colors.backgroundBasePlain};
|
||||
border: var(--internalBorderStyle);
|
||||
border-bottom: 0px;
|
||||
padding: var(--paddingAroundAction);
|
||||
padding-bottom: 0px;
|
||||
border-top-left-radius: ${euiTheme.border.radius.medium};
|
||||
border-top-right-radius: ${euiTheme.border.radius.medium};
|
||||
}
|
||||
|
||||
// shrink down to single wrapped element with no breakpoint when panel gets small
|
||||
@container hoverActionsAnchor (width < 200px) {
|
||||
${singleWrapperStyles}
|
||||
}
|
||||
|
||||
// when Dashboard is in fullscreen mode, combine all floating actions on first row and nudge them down;
|
||||
// if the panel is **not** on the first row but it is expanded in fullscreen mode, do the same thing
|
||||
.dshDashboardViewportWrapper--isFullscreen .dshDashboardGrid__item[data-grid-row='0'] &,
|
||||
.dshDashboardViewportWrapper--isFullscreen .kbnGridPanel--expanded & {
|
||||
${singleWrapperStyles}
|
||||
top: -${euiTheme.size.s} !important;
|
||||
}
|
||||
`;
|
||||
}, [euiTheme]);
|
||||
|
||||
return { containerStyles, hoverActionStyles };
|
||||
};
|
|
@ -13,10 +13,10 @@ import { Subscription } from 'rxjs';
|
|||
|
||||
import { uiActions } from '../../kibana_services';
|
||||
import {
|
||||
panelBadgeTrigger,
|
||||
panelNotificationTrigger,
|
||||
PANEL_BADGE_TRIGGER,
|
||||
PANEL_NOTIFICATION_TRIGGER,
|
||||
panelBadgeTrigger,
|
||||
panelNotificationTrigger,
|
||||
} from '../../panel_actions';
|
||||
import { AnyApiAction } from '../../panel_actions/types';
|
||||
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
|
||||
|
@ -128,7 +128,6 @@ export const usePresentationPanelHeaderActions = <
|
|||
const badgeElement = (
|
||||
<EuiBadge
|
||||
key={badge.id}
|
||||
className="embPanel__headerBadge"
|
||||
iconType={badge.getIconType({ embeddable: api, trigger: panelBadgeTrigger })}
|
||||
onClick={() => badge.execute({ embeddable: api, trigger: panelBadgeTrigger })}
|
||||
onClickAriaLabel={badge.getDisplayName({ embeddable: api, trigger: panelBadgeTrigger })}
|
||||
|
|
|
@ -26,6 +26,8 @@ import {
|
|||
} from './types';
|
||||
|
||||
describe('Presentation panel', () => {
|
||||
const editPanelSpy = jest.spyOn(openCustomizePanel, 'openCustomizePanelFlyout');
|
||||
|
||||
const renderPresentationPanel = async ({
|
||||
props,
|
||||
api,
|
||||
|
@ -166,7 +168,7 @@ describe('Presentation panel', () => {
|
|||
};
|
||||
await renderPresentationPanel({ api });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('embeddablePanelTitleInner')).toHaveTextContent('SUPER TITLE');
|
||||
expect(screen.getByTestId('embeddablePanelTitle')).toHaveTextContent('SUPER TITLE');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -177,7 +179,7 @@ describe('Presentation panel', () => {
|
|||
};
|
||||
await renderPresentationPanel({ api });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('embeddablePanelTitleInner')).toHaveTextContent('SO Title');
|
||||
expect(screen.getByTestId('embeddablePanelTitle')).toHaveTextContent('SO Title');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -238,7 +240,7 @@ describe('Presentation panel', () => {
|
|||
});
|
||||
|
||||
it('opens customize panel flyout on title click when in edit mode', async () => {
|
||||
const spy = jest.spyOn(openCustomizePanel, 'openCustomizePanelFlyout');
|
||||
editPanelSpy.mockClear();
|
||||
|
||||
const api: DefaultPresentationPanelApi & PublishesDataViews & PublishesViewMode = {
|
||||
uuid: 'test',
|
||||
|
@ -248,15 +250,19 @@ describe('Presentation panel', () => {
|
|||
};
|
||||
await renderPresentationPanel({ api });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('embeddablePanelTitleInner')).toHaveTextContent('TITLE');
|
||||
expect(screen.getByTestId('embeddablePanelTitle')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByTestId('embeddablePanelTitleLink')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByTestId('embeddablePanelTitleLink'));
|
||||
const titleElement = screen.getByTestId('embeddablePanelTitle');
|
||||
expect(titleElement).toHaveTextContent('TITLE');
|
||||
expect(titleElement.nodeName).toBe('BUTTON');
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
await userEvent.click(titleElement);
|
||||
expect(editPanelSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not show title customize link in view mode', async () => {
|
||||
editPanelSpy.mockClear();
|
||||
|
||||
const api: DefaultPresentationPanelApi & PublishesDataViews & PublishesViewMode = {
|
||||
uuid: 'test',
|
||||
title$: new BehaviorSubject<string | undefined>('SUPER TITLE'),
|
||||
|
@ -265,9 +271,14 @@ describe('Presentation panel', () => {
|
|||
};
|
||||
await renderPresentationPanel({ api });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('embeddablePanelTitleInner')).toHaveTextContent('SUPER TITLE');
|
||||
expect(screen.getByTestId('embeddablePanelTitle')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByTestId('embeddablePanelTitleLink')).not.toBeInTheDocument();
|
||||
const titleElement = screen.getByTestId('embeddablePanelTitle');
|
||||
expect(titleElement).toHaveTextContent('SUPER TITLE');
|
||||
expect(titleElement.nodeName).toBe('SPAN');
|
||||
|
||||
await userEvent.click(titleElement);
|
||||
expect(editPanelSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hides title in view mode when API hide title option is true', async () => {
|
||||
|
|
|
@ -7,9 +7,8 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import './_presentation_panel.scss';
|
||||
|
||||
import { EuiErrorBoundary, EuiFlexGroup, EuiPanel, htmlIdGenerator } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { PanelLoader } from '@kbn/panel-loader';
|
||||
import {
|
||||
apiHasParentApi,
|
||||
|
@ -129,6 +128,7 @@ export const PresentationPanelInternal = <
|
|||
aria-labelledby={headerId}
|
||||
data-test-subj="embeddablePanel"
|
||||
{...contentAttrs}
|
||||
css={styles.embPanel}
|
||||
>
|
||||
{!hideHeader && api && (
|
||||
<PresentationPanelHeader
|
||||
|
@ -156,7 +156,10 @@ export const PresentationPanelInternal = <
|
|||
</EuiFlexGroup>
|
||||
)}
|
||||
{!initialLoadComplete && <PanelLoader />}
|
||||
<div className={blockingError ? 'embPanel__content--hidden' : 'embPanel__content'}>
|
||||
<div
|
||||
className={blockingError ? 'embPanel__content--hidden' : 'embPanel__content'}
|
||||
css={styles.embPanelContent}
|
||||
>
|
||||
<EuiErrorBoundary>
|
||||
<Component
|
||||
{...(componentProps as React.ComponentProps<typeof Component>)}
|
||||
|
@ -170,3 +173,31 @@ export const PresentationPanelInternal = <
|
|||
</PresentationPanelHoverActions>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* if there is no reliance on EUI theme, then it is more performant to store styles as minimizable objects
|
||||
* outside of the React component so that it is not parsed on every render
|
||||
*/
|
||||
const styles = {
|
||||
embPanel: css({
|
||||
zIndex: 'auto',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
}),
|
||||
embPanelContent: css({
|
||||
'&.embPanel__content': {
|
||||
display: 'flex',
|
||||
flex: '1 1 100%',
|
||||
zIndex: 1,
|
||||
minHeight: 0, // Absolute must for Firefox to scroll contents
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
'&.embPanel__content--hidden, &[data-error]': {
|
||||
display: 'none',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -16,15 +16,8 @@
|
|||
.dshDashboardGrid__item--expanded,
|
||||
.dshDashboardGrid__item--blurred,
|
||||
.dshDashboardGrid__item--focused {
|
||||
.embPanel--dragHandle:hover {
|
||||
background-color: unset;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.embPanel__hoverActions {
|
||||
.embPanel--dragHandle {
|
||||
visibility: hidden;
|
||||
}
|
||||
.embPanel--dragHandle {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,7 +25,7 @@
|
|||
.dshDashboardGrid__item--blurred,
|
||||
.dshDashboardGrid__item--focused {
|
||||
.embPanel__hoverActions {
|
||||
visibility: hidden;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,10 +10,12 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import { useAppFixedViewport } from '@kbn/core-rendering-browser';
|
||||
import { GridLayout, type GridLayoutData } from '@kbn/grid-layout';
|
||||
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { DashboardPanelState } from '../../../../common';
|
||||
import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../../common/content_management/constants';
|
||||
import { arePanelLayoutsEqual } from '../../../dashboard_api/are_panel_layouts_equal';
|
||||
|
@ -30,6 +32,7 @@ export const DashboardGrid = ({
|
|||
const dashboardApi = useDashboardApi();
|
||||
const layoutStyles = useLayoutStyles();
|
||||
const panelRefs = useRef<{ [panelId: string]: React.Ref<HTMLDivElement> }>({});
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const [expandedPanelId, panels, useMargins, viewMode] = useBatchedPublishingSubjects(
|
||||
dashboardApi.expandedPanelId$,
|
||||
|
@ -56,6 +59,11 @@ export const DashboardGrid = ({
|
|||
width: gridData.w,
|
||||
height: gridData.h,
|
||||
};
|
||||
// update `data-grid-row` attribute for all panels because it is used for some styling
|
||||
const panelRef = panelRefs.current[panelId];
|
||||
if (typeof panelRef !== 'function' && panelRef?.current) {
|
||||
panelRef.current.setAttribute('data-grid-row', `${gridData.y}`);
|
||||
}
|
||||
});
|
||||
|
||||
return [singleRow];
|
||||
|
@ -96,18 +104,17 @@ export const DashboardGrid = ({
|
|||
if (!panelRefs.current[id]) {
|
||||
panelRefs.current[id] = React.createRef();
|
||||
}
|
||||
|
||||
const type = currentPanels[id].type;
|
||||
return (
|
||||
<DashboardGridItem
|
||||
ref={panelRefs.current[id]}
|
||||
data-grid={currentPanels[id].gridData}
|
||||
key={id}
|
||||
id={id}
|
||||
type={type}
|
||||
setDragHandles={setDragHandles}
|
||||
appFixedViewport={appFixedViewport}
|
||||
dashboardContainerRef={dashboardContainerRef}
|
||||
data-grid-row={currentPanels[id].gridData.y} // initialize data-grid-row
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -141,12 +148,32 @@ export const DashboardGrid = ({
|
|||
viewMode,
|
||||
]);
|
||||
|
||||
const classes = classNames({
|
||||
'dshLayout-withoutMargins': !useMargins,
|
||||
'dshLayout--viewing': viewMode === 'view',
|
||||
'dshLayout--editing': viewMode !== 'view',
|
||||
'dshLayout-isMaximizedPanel': expandedPanelId !== undefined,
|
||||
});
|
||||
const { dashboardClasses, dashboardStyles } = useMemo(() => {
|
||||
return {
|
||||
dashboardClasses: classNames({
|
||||
'dshLayout-withoutMargins': !useMargins,
|
||||
'dshLayout--viewing': viewMode === 'view',
|
||||
'dshLayout--editing': viewMode !== 'view',
|
||||
'dshLayout-isMaximizedPanel': expandedPanelId !== undefined,
|
||||
}),
|
||||
dashboardStyles: css`
|
||||
// for dashboards with no controls, increase the z-index of the hover actions in the
|
||||
// top row so that they overlap the sticky nav in Dashboard
|
||||
.dshDashboardViewportWrapper:not(:has(.dshDashboardViewport-controls))
|
||||
&
|
||||
.dshDashboardGrid__item[data-grid-row='0']
|
||||
.embPanel__hoverActions {
|
||||
z-index: ${euiTheme.levels.toast};
|
||||
}
|
||||
|
||||
return <div className={classes}>{memoizedgridLayout}</div>;
|
||||
// when in fullscreen mode, combine all floating actions on first row and nudge them down
|
||||
`,
|
||||
};
|
||||
}, [useMargins, viewMode, expandedPanelId, euiTheme.levels.toast]);
|
||||
|
||||
return (
|
||||
<div className={dashboardClasses} css={dashboardStyles}>
|
||||
{memoizedgridLayout}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -30,6 +30,9 @@ export const useLayoutStyles = () => {
|
|||
* is resolved, we should swap these out for the drag-specific colour tokens
|
||||
*/
|
||||
return css`
|
||||
--dashboardActivePanelBorderStyle: ${euiTheme.border.width.thick} solid
|
||||
${euiTheme.colors.vis.euiColorVis0};
|
||||
|
||||
&.kbnGrid {
|
||||
// remove margin top + bottom on grid in favour of padding in row
|
||||
padding-bottom: 0px;
|
||||
|
@ -55,7 +58,7 @@ export const useLayoutStyles = () => {
|
|||
}
|
||||
|
||||
.kbnGridPanel--resizeHandle {
|
||||
z-index: ${euiTheme.levels.mask};
|
||||
z-index: ${euiTheme.levels.maskBelowHeader};
|
||||
|
||||
// applying mask via ::after allows for focus borders to show
|
||||
&:after {
|
||||
|
@ -79,12 +82,12 @@ export const useLayoutStyles = () => {
|
|||
}
|
||||
|
||||
.kbnGridPanel--active {
|
||||
.embPanel {
|
||||
outline: ${euiTheme.border.width.thick} solid ${euiTheme.colors.vis.euiColorVis0} !important;
|
||||
}
|
||||
// overwrite the border style on panels + hover actions for active panels
|
||||
--hoverActionsBorderStyle: var(--dashboardActivePanelBorderStyle);
|
||||
|
||||
// prevent the hover actions transition when active to prevent "blip" on resize
|
||||
.embPanel__hoverActions {
|
||||
border: ${euiTheme.border.width.thick} solid ${euiTheme.colors.vis.euiColorVis0} !important;
|
||||
border-bottom: 0px solid !important;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -101,6 +101,7 @@ export const DashboardViewport = ({
|
|||
<div
|
||||
className={classNames('dshDashboardViewportWrapper', {
|
||||
'dshDashboardViewportWrapper--defaultBg': !useMargins,
|
||||
'dshDashboardViewportWrapper--isFullscreen': fullScreenMode,
|
||||
})}
|
||||
>
|
||||
{viewMode !== 'print' ? (
|
||||
|
|
|
@ -287,7 +287,7 @@ export class DashboardPageObject extends FtrService {
|
|||
}
|
||||
// wait until the count of dashboard panels equals the count of drag handles
|
||||
await this.retry.waitFor('in edit mode', async () => {
|
||||
const panels = await this.find.allByCssSelector('.embPanel__hoverActionsWrapper');
|
||||
const panels = await this.find.allByCssSelector('[data-test-subj="embeddablePanel"]');
|
||||
const dragHandles = await this.find.allByCssSelector(
|
||||
'[data-test-subj="embeddablePanelDragHandle"]'
|
||||
);
|
||||
|
@ -643,7 +643,7 @@ export class DashboardPageObject extends FtrService {
|
|||
public async getPanelTitles() {
|
||||
this.log.debug('in getPanelTitles');
|
||||
const titleObjects = await this.find.allByCssSelector(
|
||||
'[data-test-subj="embeddablePanelTitleInner"] .embPanel__titleText'
|
||||
'[data-test-subj="embeddablePanelTitle"]'
|
||||
);
|
||||
return await Promise.all(titleObjects.map(async (title) => await title.getVisibleText()));
|
||||
}
|
||||
|
|
|
@ -176,7 +176,6 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
|
||||
async clickExpandPanelToggle() {
|
||||
this.log.debug(`clickExpandPanelToggle`);
|
||||
await this.openContextMenu();
|
||||
await this.clickPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
overflow: visible;
|
||||
}
|
||||
|
||||
.embPanel__hoverActionsLeft, .embPanel__dragHandle {
|
||||
.embPanel__dragHandle {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
@ -19,14 +19,10 @@
|
|||
}
|
||||
|
||||
.canvas__element--selected {
|
||||
.embPanel__hoverActionsAnchor {
|
||||
.embPanel__hoverActionsWrapper {
|
||||
z-index: $euiZLevel9;
|
||||
top: -$euiSizeXL;
|
||||
|
||||
.embPanel__hoverActions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.embPanel__hoverActions {
|
||||
z-index: $euiZLevel9;
|
||||
top: -$euiSizeXL;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
|
@ -6190,7 +6190,6 @@
|
|||
"presentationPanel.action.inspectPanel.displayName": "Inspecter",
|
||||
"presentationPanel.action.inspectPanel.untitledEmbeddableFilename": "[Aucun titre]",
|
||||
"presentationPanel.action.removePanel.displayName": "Supprimer",
|
||||
"presentationPanel.ariaLabel": "Panneau",
|
||||
"presentationPanel.badgeTrigger.description": "Des actions de badge apparaissent dans la barre de titre lorsqu'un élément incorporable est en cours de chargement dans un panneau.",
|
||||
"presentationPanel.badgeTrigger.title": "Badges du panneau",
|
||||
"presentationPanel.contextMenu.ariaLabel": "Options de panneau",
|
||||
|
@ -6199,7 +6198,6 @@
|
|||
"presentationPanel.contextMenuTrigger.description": "Une nouvelle action sera ajoutée au menu contextuel du panneau",
|
||||
"presentationPanel.contextMenuTrigger.title": "Menu contextuel",
|
||||
"presentationPanel.emptyErrorMessage": "Erreur",
|
||||
"presentationPanel.enhancedAriaLabel": "Panneau : {title}",
|
||||
"presentationPanel.error.editButton": "Modifier {value}",
|
||||
"presentationPanel.error.errorWhenLoadingPanel": "Une erreur s'est produite lors du chargement de ce panneau.",
|
||||
"presentationPanel.filters.filtersTitle": "Filtres",
|
||||
|
@ -6207,7 +6205,6 @@
|
|||
"presentationPanel.header.titleAriaLabel": "Cliquez pour modifier le titre : {title}",
|
||||
"presentationPanel.notificationTrigger.description": "Les actions de notification apparaissent dans l'angle supérieur droit des panneaux.",
|
||||
"presentationPanel.notificationTrigger.title": "Notifications du panneau",
|
||||
"presentationPanel.placeholderTitle": "[Aucun titre]",
|
||||
"presentationUtil.dashboardDrilldownConfig.components.openInNewTab": "Ouvrir le tableau de bord dans un nouvel onglet",
|
||||
"presentationUtil.dashboardDrilldownConfig.components.useCurrentDateRange": "Utiliser la plage de dates du tableau de bord d'origine",
|
||||
"presentationUtil.dashboardDrilldownConfig.components.useCurrentFiltersLabel": "Utiliser les filtres et la requête du tableau de bord d'origine",
|
||||
|
|
|
@ -6184,7 +6184,6 @@
|
|||
"presentationPanel.action.inspectPanel.displayName": "検査",
|
||||
"presentationPanel.action.inspectPanel.untitledEmbeddableFilename": "[タイトルなし]",
|
||||
"presentationPanel.action.removePanel.displayName": "削除",
|
||||
"presentationPanel.ariaLabel": "パネル",
|
||||
"presentationPanel.badgeTrigger.description": "パネルに埋め込み可能なファイルが読み込まれるときに、バッジアクションがタイトルバーに表示されます。",
|
||||
"presentationPanel.badgeTrigger.title": "パネルバッジ",
|
||||
"presentationPanel.contextMenu.ariaLabel": "パネルオプション",
|
||||
|
@ -6193,7 +6192,6 @@
|
|||
"presentationPanel.contextMenuTrigger.description": "新しいアクションがパネルのコンテキストメニューに追加されます",
|
||||
"presentationPanel.contextMenuTrigger.title": "コンテキストメニュー",
|
||||
"presentationPanel.emptyErrorMessage": "エラー",
|
||||
"presentationPanel.enhancedAriaLabel": "パネル:{title}",
|
||||
"presentationPanel.error.editButton": "{value} を編集",
|
||||
"presentationPanel.error.errorWhenLoadingPanel": "このパネルの読み込み中にエラーが発生しました。",
|
||||
"presentationPanel.filters.filtersTitle": "フィルター",
|
||||
|
@ -6201,7 +6199,6 @@
|
|||
"presentationPanel.header.titleAriaLabel": "クリックしてタイトルを編集:{title}",
|
||||
"presentationPanel.notificationTrigger.description": "パネルの右上に通知アクションが表示されます。",
|
||||
"presentationPanel.notificationTrigger.title": "パネル通知",
|
||||
"presentationPanel.placeholderTitle": "[タイトルなし]",
|
||||
"presentationUtil.dashboardDrilldownConfig.components.openInNewTab": "新しいタブでダッシュボードを開く",
|
||||
"presentationUtil.dashboardDrilldownConfig.components.useCurrentDateRange": "元のダッシュボードから日付範囲を使用",
|
||||
"presentationUtil.dashboardDrilldownConfig.components.useCurrentFiltersLabel": "元のダッシュボードからフィルターとクエリーを使用",
|
||||
|
|
|
@ -6141,7 +6141,6 @@
|
|||
"presentationPanel.action.inspectPanel.displayName": "检查",
|
||||
"presentationPanel.action.inspectPanel.untitledEmbeddableFilename": "[无标题]",
|
||||
"presentationPanel.action.removePanel.displayName": "移除",
|
||||
"presentationPanel.ariaLabel": "面板",
|
||||
"presentationPanel.badgeTrigger.description": "可嵌入对象在面板中加载后,徽章操作便显示在标题栏中。",
|
||||
"presentationPanel.badgeTrigger.title": "面板徽章",
|
||||
"presentationPanel.contextMenu.ariaLabel": "面板选项",
|
||||
|
@ -6150,7 +6149,6 @@
|
|||
"presentationPanel.contextMenuTrigger.description": "会将一个新操作添加到该面板的上下文菜单",
|
||||
"presentationPanel.contextMenuTrigger.title": "上下文菜单",
|
||||
"presentationPanel.emptyErrorMessage": "错误",
|
||||
"presentationPanel.enhancedAriaLabel": "面板:{title}",
|
||||
"presentationPanel.error.editButton": "编辑 {value}",
|
||||
"presentationPanel.error.errorWhenLoadingPanel": "加载此面板时出错。",
|
||||
"presentationPanel.filters.filtersTitle": "筛选",
|
||||
|
@ -6158,7 +6156,6 @@
|
|||
"presentationPanel.header.titleAriaLabel": "单击可编辑标题:{title}",
|
||||
"presentationPanel.notificationTrigger.description": "通知操作显示在面板右上角。",
|
||||
"presentationPanel.notificationTrigger.title": "面板通知",
|
||||
"presentationPanel.placeholderTitle": "[无标题]",
|
||||
"presentationUtil.dashboardDrilldownConfig.components.openInNewTab": "在新选项卡中打开仪表板",
|
||||
"presentationUtil.dashboardDrilldownConfig.components.useCurrentDateRange": "使用源仪表板的日期范围",
|
||||
"presentationUtil.dashboardDrilldownConfig.components.useCurrentFiltersLabel": "使用源仪表板的筛选和查询",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue