Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Popup): displaying SVG icons #2946

Merged
merged 17 commits into from
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions packages/react-ui/components/Hint/Hint.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@
<Hint text="Подсказка">Базовая</Hint>
```

Пример подсказки с иконкой.

```jsx harmony
<Hint text="Редактирование">
<svg width="16" height="16" viewBox="0 0 16 16">
<path
fillRule="evenodd"
d="M13 12V12.998H8V12H13ZM3 13V11L9 4.99999L11 6.99999L5 13H3ZM13 5L11.5 6.5L9.5 4.5L11 3L13 5Z"
clipRule="evenodd"
/>
</svg>
</Hint>
```

Пример подсказки, всегда всплывающей слева.

```jsx harmony
Expand Down
2 changes: 1 addition & 1 deletion packages/react-ui/components/Hint/Hint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export class Hint extends React.PureComponent<HintProps, HintState> implements I
);
}

public getAnchorElement = (): Nullable<HTMLElement> => {
public getAnchorElement = (): Nullable<Element> => {
return this.popupRef.current?.anchorElement;
};

Expand Down
47 changes: 47 additions & 0 deletions packages/react-ui/components/Hint/__stories__/Hint.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,53 @@ SetManualAndOpenedPropOnClick.parameters = {
},
};

export const WithSVGIcon: Story = () => {
return (
<Hint text="hint">
<svg data-tid="icon" width="16" height="16" viewBox="0 0 16 16">
<path d="M9 3H7V7H3V9H7V13H9V9H13V7H9V3Z" />
</svg>
</Hint>
);
};

WithSVGIcon.parameters = {
creevey: {
skip: [
{
in: [
'chromeDark',
'chrome8px',
'firefox8px',
'firefox',
'firefoxFlat8px',
'firefoxDark',
'ie118px',
'ie11',
'ie11Dark',
],
reason: 'internal logic being tested and not something UI related',
},
],
tests: {
async idle() {
await this.expect(await this.takeScreenshot()).to.matchImage('idle');
},
async hover() {
await this.browser
.actions()
.move({
origin: this.browser.findElement({ css: '[data-tid="icon"]' }),
})
.perform();
await delay(1000);

await this.expect(await this.browser.takeScreenshot()).to.matchImage('open');
},
},
},
};

@rootNode
class CustomClassComponent extends React.Component<{}, {}> {
private setRootNode!: TSetRootNode;
Expand Down
54 changes: 36 additions & 18 deletions packages/react-ui/components/Hint/__tests__/Hint-test.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,54 @@
import { mount } from 'enzyme';
import React from 'react';
import React, { useState } from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';

import { Hint } from '../Hint';

describe('Hint', () => {
it('render without crash', () => {
const wrapper = mount<Hint>(<Hint text="world">Hello</Hint>);
it('should render without crash', () => {
const hintChildrenText = 'Hello';
render(<Hint text="world">{hintChildrenText}</Hint>);

expect(wrapper).toHaveLength(1);
const hintChildren = screen.getByText(hintChildrenText);
expect(hintChildren).toBeInTheDocument();
});

it('controlled opening only if manual prop passed', () => {
const wrapper = mount<Hint>(
<Hint opened text="world">
it('should not open be controlled manually without `manual` prop passed', () => {
const hintText = 'world';
render(
<Hint opened text={hintText}>
Hello
</Hint>,
);

expect(wrapper.state('opened')).toBe(false);
const hintContent = screen.queryByText(hintText);
expect(hintContent).not.toBeInTheDocument();
});

it('manual opening', () => {
const wrapper = mount<Hint>(
<Hint manual text="world">
Hello
</Hint>,
);
it('should open hint manually', () => {
const hintText = 'world';
const Component = () => {
const [isOpen, setIsOpen] = useState(false);

return (
<>
<Hint opened={isOpen} manual text="world">
Hello
</Hint>
<button onClick={() => setIsOpen(true)}>open manually</button>
</>
);
};

render(<Component />);

expect(wrapper.state('opened')).toBe(false);
const hintContent = screen.queryByText(hintText);
expect(hintContent).not.toBeInTheDocument();

wrapper.setProps({ opened: true });
const openButton = screen.getByRole('button');
userEvent.click(openButton);

expect(wrapper.state('opened')).toBe(true);
const hintContentUpdated = screen.getByText(hintText);
expect(hintContentUpdated).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ export const getScrollSizeParams = (inner: HTMLElement, axis: 'x' | 'y') => {
};
};

export const getScrollYOffset = (element: HTMLElement, container: HTMLElement) => {
const elementOffset = element.offsetTop;
export const getScrollYOffset = (element: Element, container: Element) => {
// @ts-expect-error: needs to be replaced with getDOMRect. See IF-647
const elementTopOffset = element.offsetTop;

if (container.scrollTop > elementOffset) {
return elementOffset;
if (container.scrollTop > elementTopOffset) {
return elementTopOffset;
}

const offset = elementOffset + element.scrollHeight - container.offsetHeight;
// @ts-expect-error: needs to be replaced with getDOMRect. See IF-647
const offset = elementTopOffset + element.scrollHeight - container.offsetHeight;
if (container.scrollTop < offset) {
return offset;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,14 @@ export class ScrollContainer extends React.Component<ScrollContainerProps> {

/**
* @public
* @param {HTMLElement} element
* @param {Element} element
*/
public scrollTo(element: Nullable<HTMLElement>) {
public scrollTo(element: Nullable<Element>) {
if (!element || !this.inner) {
return;
}

// @ts-expect-error: needs to be replaced with getDOMRect. See IF-647
zhzz marked this conversation as resolved.
Show resolved Hide resolved
this.inner.scrollLeft = element.offsetLeft;
this.inner.scrollTop = getScrollYOffset(element, this.inner);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react-ui/components/Toast/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export class Toast extends React.Component<ToastProps, ToastState> {
);
}

private setRootRef = (element: Nullable<HTMLElement>) => {
private setRootRef = (element: Nullable<Element>) => {
this.setRootNode(element);
// @ts-ignore
this.rootRef.current = element;
Expand Down
4 changes: 2 additions & 2 deletions packages/react-ui/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ export class Tooltip extends React.PureComponent<TooltipProps, TooltipState> imp
);
}

public getAnchorElement = (): Nullable<HTMLElement> => {
public getAnchorElement = (): Nullable<Element> => {
return this.popupRef.current?.anchorElement;
};

Expand Down Expand Up @@ -494,7 +494,7 @@ export class Tooltip extends React.PureComponent<TooltipProps, TooltipState> imp

if (
(this.props.trigger === 'hover&focus' && this.state.focused) ||
(this.props.trigger === 'hover' && event.relatedTarget === this.contentElement)
(this.props.trigger === 'hover' && event instanceof MouseEvent && event.relatedTarget === this.contentElement)
) {
return;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react-ui/internal/CommonWrapper/CommonWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface CommonProps {
}

interface CommonPropsRootNodeRef {
rootNodeRef?: (instance: Nullable<HTMLElement>) => void;
rootNodeRef?: (instance: Nullable<Element>) => void;
}

export type NotCommonProps<P> = Omit<P, keyof CommonProps>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface DropdownContainerPosition {

export interface DropdownContainerProps {
align?: 'left' | 'right';
getParent: () => Nullable<HTMLElement>;
getParent: () => Nullable<Element>;
children?: React.ReactNode;
disablePortal?: boolean;
offsetY?: number;
Expand Down
4 changes: 2 additions & 2 deletions packages/react-ui/internal/InternalMenu/InternalMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export class InternalMenu extends React.PureComponent<MenuProps, MenuState> {
};

private focusOnRootElement = (): void => {
getRootNode(this)?.focus();
((getRootNode(this) as HTMLElement) || SVGElement)?.focus();
zhzz marked this conversation as resolved.
Show resolved Hide resolved
};

private shouldRecalculateMaxHeight = (prevProps: MenuProps): boolean => {
Expand Down Expand Up @@ -314,7 +314,7 @@ export class InternalMenu extends React.PureComponent<MenuProps, MenuState> {

private highlightItem = (index: number): void => {
this.setState({ highlightedIndex: index });
getRootNode(this)?.focus();
((getRootNode(this) as HTMLElement) || SVGElement)?.focus();
};

private unhighlight = () => {
Expand Down
33 changes: 16 additions & 17 deletions packages/react-ui/internal/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Transition } from 'react-transition-group';
import raf from 'raf';
import warning from 'warning';

import { getDOMRect } from '../../lib/dom/getDOMRect';
import { Nullable } from '../../typings/utility-types';
import * as LayoutEvents from '../../lib/LayoutEvents';
import { ZIndex } from '../ZIndex';
Expand All @@ -13,7 +14,7 @@ import { isFunction, isNonNullable, isNullable, isRefableElement } from '../../l
import { isIE11, isEdge, isSafari } from '../../lib/client';
import { ThemeContext } from '../../lib/theming/ThemeContext';
import { Theme } from '../../lib/theming/Theme';
import { isHTMLElement, safePropTypesInstanceOf } from '../../lib/SSRSafe';
import { isElement, safePropTypesInstanceOf } from '../../lib/SSRSafe';
import { isTestEnv } from '../../lib/currentEnvironment';
import { CommonProps, CommonWrapper } from '../CommonWrapper';
import { cx } from '../../lib/theming/Emotion';
Expand Down Expand Up @@ -203,12 +204,12 @@ export class Popup extends React.Component<PopupProps, PopupState> {
private theme!: Theme;
private layoutEventsToken: Nullable<ReturnType<typeof LayoutEvents.addListener>>;
private locationUpdateId: Nullable<number> = null;
private lastPopupElement: Nullable<HTMLElement>;
private lastPopupElement: Nullable<Element>;
private isMobileLayout!: boolean;
private setRootNode!: TSetRootNode;
private refForTransition = React.createRef<HTMLDivElement>();

public anchorElement: Nullable<HTMLElement> = null;
public anchorElement: Nullable<Element> = null;

public componentDidMount() {
this.updateLocation();
Expand Down Expand Up @@ -288,7 +289,7 @@ export class Popup extends React.Component<PopupProps, PopupState> {
const { anchorElement, useWrapper } = this.props;

let anchor: Nullable<React.ReactNode> = null;
if (isHTMLElement(anchorElement)) {
if (isElement(anchorElement)) {
this.updateAnchorElement(anchorElement);
} else if (React.isValidElement(anchorElement)) {
anchor = useWrapper ? <span>{anchorElement}</span> : anchorElement;
Expand All @@ -313,7 +314,7 @@ export class Popup extends React.Component<PopupProps, PopupState> {
// which should be called within updateAnchorElement
// in the case when the anchor is not refable

const canGetAnchorNode = !!anchorWithRef || isHTMLElement(anchorElement);
const canGetAnchorNode = !!anchorWithRef || isElement(anchorElement);

return (
<RenderContainer anchor={anchorWithRef || anchor} ref={canGetAnchorNode ? null : this.updateAnchorElement}>
Expand All @@ -335,8 +336,8 @@ export class Popup extends React.Component<PopupProps, PopupState> {
}
};

private addEventListeners(element: Nullable<HTMLElement>) {
if (element && isHTMLElement(element)) {
private addEventListeners(element: Nullable<Element>) {
if (element && isElement(element)) {
element.addEventListener('mouseenter', this.handleMouseEnter);
element.addEventListener('mouseleave', this.handleMouseLeave);
element.addEventListener('click', this.handleClick);
Expand All @@ -345,8 +346,8 @@ export class Popup extends React.Component<PopupProps, PopupState> {
}
}

private removeEventListeners(element: Nullable<HTMLElement>) {
if (element && isHTMLElement(element)) {
private removeEventListeners(element: Nullable<Element>) {
if (element && isElement(element)) {
element.removeEventListener('mouseenter', this.handleMouseEnter);
element.removeEventListener('mouseleave', this.handleMouseLeave);
element.removeEventListener('click', this.handleClick);
Expand Down Expand Up @@ -387,7 +388,8 @@ export class Popup extends React.Component<PopupProps, PopupState> {

private calculateWidth = (width: PopupProps['width']) => {
if (typeof width === 'string' && width.includes('%')) {
return this.anchorElement ? (this.anchorElement.offsetWidth * parseFloat(width)) / 100 : 0;
const anchorWidth = Math.floor(getDOMRect(this.anchorElement).width);
return this.anchorElement ? (anchorWidth * parseFloat(width)) / 100 : 0;
}
return width;
};
Expand Down Expand Up @@ -471,7 +473,7 @@ export class Popup extends React.Component<PopupProps, PopupState> {
return isFunction(this.props.children) ? this.props.children() : this.props.children;
}

private refPopupElement = (element: Nullable<HTMLElement>) => {
private refPopupElement = (element: Nullable<Element>) => {
this.lastPopupElement = element;
};

Expand Down Expand Up @@ -561,16 +563,13 @@ export class Popup extends React.Component<PopupProps, PopupState> {
);
}

private getLocation(popupElement: HTMLElement, location?: Nullable<PopupLocation>) {
private getLocation(popupElement: Element, location?: Nullable<PopupLocation>) {
const { positions, tryPreserveFirstRenderedPosition } = this.props;
const anchorElement = this.anchorElement;

warning(
anchorElement && isHTMLElement(anchorElement),
'Anchor element is not defined or not instance of HTMLElement',
);
warning(anchorElement && isElement(anchorElement), 'Anchor element is not defined or not instance of Element');

if (!(anchorElement && isHTMLElement(anchorElement))) {
if (!(anchorElement && isElement(anchorElement))) {
return location;
}

Expand Down