Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Fixed `EuiInMemoryTable`'s `onTableChange` callback not returning the correct `sort.field` value on pagination ([#5588](https://github.com/elastic/eui/pull/5588))

**Breaking changes**

- Upgraded TypeScript version to ~4.5.3 ([#5591](https://github.com/elastic/eui/pull/5591))

## [`46.2.0`](https://github.com/elastic/eui/tree/v46.2.0)

- Updated `EuiDataGrid`s with scrolling content to always have a border around the grid body and any scrollbars ([#5563](https://github.com/elastic/eui/pull/5563))
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
"@babel/plugin-transform-runtime": "^7.11.0",
"@babel/preset-env": "^7.11.0",
"@babel/preset-react": "^7.10.4",
"@babel/preset-typescript": "^7.12.1",
"@babel/preset-typescript": "^7.16.7",
"@cypress/code-coverage": "^3.9.12",
"@cypress/react": "^5.10.3",
"@cypress/webpack-dev-server": "^1.7.0",
Expand All @@ -132,8 +132,8 @@
"@types/tabbable": "^3.1.0",
"@types/url-parse": "^1.4.3",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
"argparse": "^2.0.1",
"autoprefixer": "^9.8.6",
"axe-core": "^4.1.1",
Expand Down Expand Up @@ -205,7 +205,7 @@
"puppeteer": "^5.5.0",
"raw-loader": "^4.0.1",
"react": "^16.12.0",
"react-docgen-typescript": "^1.20.5",
"react-docgen-typescript": "^2.2.2",
"react-dom": "^16.12.0",
"react-helmet": "^6.1.0",
"react-redux": "^7.2.1",
Expand All @@ -228,7 +228,7 @@
"start-server-and-test": "^1.11.3",
"style-loader": "^1.2.1",
"terser-webpack-plugin": "^4.1.0",
"typescript": "4.1.3",
"typescript": "4.5.3",
"uglifyjs-webpack-plugin": "^2.2.0",
"url-loader": "^4.1.0",
"webpack": "^4.46.0",
Expand All @@ -247,6 +247,6 @@
"prop-types": "^15.5.0",
"react": "^16.12",
"react-dom": "^16.12",
"typescript": "~4.1.3"
"typescript": "~4.5.3"
}
}
5 changes: 2 additions & 3 deletions scripts/babel/react-docgen-typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,6 @@ const intrinsicValuesRaw = [
* Replace ReactElement and ReactNode expanded types with ReactElement and ReactNode
*/
const reactElementTypeExpanded =
'ReactElement<any, string | ((props: any) => ReactElement<any, string | ... | (new (props: any) => Component<any, any, any>)>) | (new (props: any) => Component<any, any, any>)>';
'ReactElement<any, string | JSXElementConstructor<any>>';

const reactNodeTypeExpanded =
'string | number | boolean | {} | ReactElement<any, string | ((props: any) => ReactElement<any, string | ... | (new (props: any) => Component<any, any, any>)>) | (new (props: any) => Component<...>)> | ... 5 more ... | (ReactPortal & string)';
const reactNodeTypeExpanded = /(string \| number \| boolean \| {} \| ReactElement \| ReactNodeArray \| ReactPortal)( \| \({} & string\).+\(ReactPortal & string\))?/g;
2 changes: 1 addition & 1 deletion src-docs/src/components/guide_section/guide_section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export const GuideSection: FunctionComponent<GuideSection> = ({
const isPlaygroundUnsupported =
typeof window !== 'undefined' &&
typeof document !== 'undefined' &&
!!window.MSInputMethodContext &&
!!(window as any).MSInputMethodContext &&
// @ts-ignore doesn't exist?
!!document.documentMode;

Expand Down
2 changes: 1 addition & 1 deletion src/components/basic_table/basic_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export interface Criteria<T> {
*/
sort?: {
field: keyof T;
direction: Direction;
direction: 'asc' | 'desc';
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/components/basic_table/table_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { ReactElement, ReactNode, TdHTMLAttributes } from 'react';
import { Direction, HorizontalAlignment } from '../../services';
import { HorizontalAlignment } from '../../services';
import { Pagination } from './pagination_bar';
import { Action } from './action_types';
import { Primitive } from '../../services/sort/comparators';
Expand Down Expand Up @@ -147,7 +147,7 @@ export interface EuiTableSortingType<T> {
*/
sort?: {
field: keyof T;
direction: Direction;
direction: 'asc' | 'desc';
};
/**
* Enables/disables unsorting of table columns. Supported by EuiInMemoryTable.
Expand Down
160 changes: 82 additions & 78 deletions src/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,82 @@ export interface EuiButtonProps extends EuiButtonContentProps, CommonProps {
style?: CSSProperties;
}

export type EuiButtonPropsForAnchor = PropsForAnchor<
EuiButtonProps,
{
buttonRef?: Ref<HTMLAnchorElement>;
}
>;

export type EuiButtonPropsForButton = PropsForButton<
EuiButtonProps,
{
buttonRef?: Ref<HTMLButtonElement>;
}
>;

export type Props = ExclusiveUnion<
EuiButtonPropsForAnchor,
EuiButtonPropsForButton
>;

/**
* EuiButton is largely responsible for providing relevant props
* and the logic for element-specific attributes
*/
export const EuiButton: FunctionComponent<Props> = ({
isDisabled: _isDisabled,
disabled: _disabled,
href,
target,
rel,
type = 'button',
buttonRef,
...rest
}) => {
const isHrefValid = !href || validateHref(href);
const disabled = _disabled || !isHrefValid;
const isDisabled = _isDisabled || !isHrefValid;

const buttonIsDisabled = rest.isLoading || isDisabled || disabled;
const element = href && !isDisabled ? 'a' : 'button';

let elementProps = {};
// Props for all elements
elementProps = { ...elementProps, isDisabled: buttonIsDisabled };
// Element-specific attributes
if (element === 'button') {
elementProps = { ...elementProps, disabled: buttonIsDisabled };
}

const relObj: {
rel?: string;
href?: string;
type?: ButtonHTMLAttributes<HTMLButtonElement>['type'];
target?: string;
} = {};

if (href && !buttonIsDisabled) {
relObj.href = href;
relObj.rel = getSecureRelForTarget({ href, target, rel });
relObj.target = target;
} else {
relObj.type = type as ButtonHTMLAttributes<HTMLButtonElement>['type'];
}

return (
<EuiButtonDisplay
element={element}
baseClassName="euiButton"
ref={buttonRef}
{...elementProps}
{...relObj}
{...rest}
/>
);
};
EuiButton.displayName = 'EuiButton';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rundown on this "fix" for EuiButton's props tables - without dede73b, EuiButton's props table errors with docgenInfo is undefined:

This was the only component with this issue that I saw while I was checking every docs page. EuiButtonEmpty, EuiButtonIcon, and EuiButtonGroup did not have this issue. EuiSkipLink had almost the exact same typing (ExclusiveUnion for anchor or button refs) and did not have this issue - a heavy hint that the problem did not lie with the actual types of EuiButton.

Eventually, thanks to Chandler's SUPER helpful tips earlier on how to debug scripts/babel/react-docgen-typescript, I was able to figure out that docgenInfo was, for some reason, only being generated for the internal EuiButtonDisplay component also declared within the same file. So I experimented with temporarily moving it out of the file, and boom. EuiButton's props table started working again.

I could have moved EuiButtonDisplay out to its own button_display file (and still can, if people prefer) but opted for the fix with the least diffs. For whatever reason, if I moved EuiButtonDisplay down on the page and also gave EuiButton a displayName, that was enough for react-docgen-typescript to look at EuiButton first and ignore EuiButtonDisplay. 🙈

EDIT: Aha, after digging more through react-docgen-typescripts, it looks like this is a known issue on their end that was introduced in 2.1.0: styleguidist/react-docgen-typescript#395

@chandlerprall any thoughts on whether we should document this limitation for now until react-docgen-typescript has a fix?

Copy link
Contributor Author

@cee-chen cee-chen Feb 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated our component development wiki: 97a5cd8

With all that, I think this PR is ready to go if we're OK with all of the above type changes, and if someone else could take a quick second QA pass through this PR's staging server to check that there are no other obviously broken prop tables.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouch, that's a rough bug. Thanks for updating the wiki page, should be enough to help track this and point people to if/when needed.


export type EuiButtonDisplayProps = EuiButtonProps &
HTMLAttributes<HTMLElement> & {
/**
Expand All @@ -123,12 +199,13 @@ export type EuiButtonDisplayProps = EuiButtonProps &
};

/**
* *INTERNAL ONLY*
* Component for displaying any element as a button
* EuiButton is largely responsible for providing relevant props
* and the logic for element-specific attributes
* EuiButtonDisplay is an internal-only component used for displaying
* any element as a button.
* NOTE: This component *must* be below EuiButton in the file and
* EuiButton must also set a displayName for react-docgen-typescript
* to correctly set EuiButton's docgenInfo and display a props table.
*/
const EuiButtonDisplay = forwardRef<HTMLElement, EuiButtonDisplayProps>(
export const EuiButtonDisplay = forwardRef<HTMLElement, EuiButtonDisplayProps>(
(
{
element = 'button',
Expand Down Expand Up @@ -218,77 +295,4 @@ const EuiButtonDisplay = forwardRef<HTMLElement, EuiButtonDisplayProps>(
);
}
);

EuiButtonDisplay.displayName = 'EuiButtonDisplay';
export { EuiButtonDisplay };

export type EuiButtonPropsForAnchor = PropsForAnchor<
EuiButtonProps,
{
buttonRef?: Ref<HTMLAnchorElement>;
}
>;

export type EuiButtonPropsForButton = PropsForButton<
EuiButtonProps,
{
buttonRef?: Ref<HTMLButtonElement>;
}
>;

export type Props = ExclusiveUnion<
EuiButtonPropsForAnchor,
EuiButtonPropsForButton
>;

export const EuiButton: FunctionComponent<Props> = ({
isDisabled: _isDisabled,
disabled: _disabled,
href,
target,
rel,
type = 'button',
buttonRef,
...rest
}) => {
const isHrefValid = !href || validateHref(href);
const disabled = _disabled || !isHrefValid;
const isDisabled = _isDisabled || !isHrefValid;

const buttonIsDisabled = rest.isLoading || isDisabled || disabled;
const element = href && !isDisabled ? 'a' : 'button';

let elementProps = {};
// Props for all elements
elementProps = { ...elementProps, isDisabled: buttonIsDisabled };
// Element-specific attributes
if (element === 'button') {
elementProps = { ...elementProps, disabled: buttonIsDisabled };
}

const relObj: {
rel?: string;
href?: string;
type?: ButtonHTMLAttributes<HTMLButtonElement>['type'];
target?: string;
} = {};

if (href && !buttonIsDisabled) {
relObj.href = href;
relObj.rel = getSecureRelForTarget({ href, target, rel });
relObj.target = target;
} else {
relObj.type = type as ButtonHTMLAttributes<HTMLButtonElement>['type'];
}

return (
<EuiButtonDisplay
element={element}
baseClassName="euiButton"
ref={buttonRef}
{...elementProps}
{...relObj}
{...rest}
/>
);
};
7 changes: 3 additions & 4 deletions src/components/header/header_links/header_links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,15 @@ export const EuiHeaderLinks: FunctionComponent<EuiHeaderLinksProps> = ({
popoverProps,
...rest
}) => {
const { onClick: _onClick, iconType = 'apps', ...popoverButtonRest } = {
...popoverButtonProps,
};
const { onClick, iconType = 'apps', ...popoverButtonRest } =
popoverButtonProps || {};

const [mobileMenuIsOpen, setMobileMenuIsOpen] = useState(false);

const onMenuButtonClick: MouseEventHandler<
HTMLButtonElement & HTMLAnchorElement
> = (e) => {
_onClick && _onClick(e);
onClick?.(e);
setMobileMenuIsOpen(!mobileMenuIsOpen);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const EuiPinnableListGroup: FunctionComponent<EuiPinnableListGroupProps>
);

// Add the pinning action unless the item has it's own extra action
if (onPinClick && !itemProps.extraAction && pinnable) {
if (pinnable && !itemProps.extraAction) {
// Different displays for pinned vs unpinned
if (pinned) {
itemProps.extraAction = {
Expand Down
2 changes: 1 addition & 1 deletion src/components/markdown_editor/markdown_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export const EuiMarkdownEditor = forwardRef<
const parsed = parser.processSync(value);
return [parsed, null];
} catch (e) {
return [null, e];
return [null, e as EuiMarkdownParseError];
}
}, [parser, value]);

Expand Down
11 changes: 5 additions & 6 deletions src/components/overlay_mask/overlay_mask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,18 @@ export const EuiOverlayMask: FunctionComponent<EuiOverlayMaskProps> = ({
}, [className, headerZindexLocation]);

useEffect(() => {
if (!overlayMaskNode.current || !onClick) return;
const portalTarget = overlayMaskNode.current;
if (!portalTarget || !onClick) return;

const listener = (e: Event) => {
if (e.target === overlayMaskNode.current) {
if (e.target === portalTarget) {
onClick();
}
};
overlayMaskNode.current.addEventListener('click', listener);
portalTarget.addEventListener('click', listener);

return () => {
if (portalTarget && onClick) {
portalTarget.removeEventListener('click', listener);
}
portalTarget.removeEventListener('click', listener);
};
}, [onClick]);

Expand Down
6 changes: 3 additions & 3 deletions src/components/portal/portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ export const insertPositions: InsertPositionsMap = {
before: 'beforebegin',
};

type EuiPortalInsertPosition = keyof typeof insertPositions;

export const INSERT_POSITIONS: EuiPortalInsertPosition[] = keysOf(
insertPositions
);

type EuiPortalInsertPosition = keyof typeof insertPositions;

export interface EuiPortalProps {
/**
* ReactNode to render as this component's content
*/
children: ReactNode;
insert?: { sibling: HTMLElement; position: EuiPortalInsertPosition };
insert?: { sibling: HTMLElement; position: 'before' | 'after' };
portalRef?: (ref: HTMLDivElement | null) => void;
}

Expand Down
3 changes: 2 additions & 1 deletion src/components/search_bar/query/default_syntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,9 @@ const validateFieldValue = (
try {
schemaField.validate(value);
} catch (e) {
const message = e instanceof Error ? e.message : e;
error(
`Invalid value \`${expression}\` set for field \`${field}\` - ${e.message}`,
`Invalid value \`${expression}\` set for field \`${field}\` - ${message}`,
location
);
}
Expand Down
5 changes: 4 additions & 1 deletion src/components/search_bar/search_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ export class EuiSearchBar extends Component<EuiSearchBarProps, State> {
this.notifyControllingParent({ query, queryText, error: null });
this.setState({ query, queryText, error: null });
} catch (e) {
const error: Error = { name: e.name, message: e.message };
const error: Error =
e instanceof Error
? { name: e.name, message: e.message }
: { name: 'Unexpected error', message: String(e) };
Comment on lines +172 to +175
Copy link
Contributor Author

@cee-chen cee-chen Feb 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obligatory https://stackoverflow.com/a/68257472/4294462: In TS 4.4+ caught errors default to unknown instead of any. We could disable this via useUnknownInCatchVariables in tsconfig.json, but honestly I think it's a legit change and it's worth checking for a valid Error instance since something like throw 'foo' is an allowed error. LMK if you have other thoughts however!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe check for 'message' in e - possibly with a type guard to catch some non-error-but-still-compatible objects?

Copy link
Contributor Author

@cee-chen cee-chen Feb 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, I'd be more tempted to JSON.stringify() a custom error object with a message key - that way the user can always get the full error no matter what. This seems like it might be worth DRYing out a helper that checks for instanceof Error and tries to return a valid Error type if not. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably too engineery at that point, and leaving as-is makes more sense? 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough! We can keep it in our back pockets for later if we add more try/catches

this.notifyControllingParent({ query: null, queryText, error });
this.setState({ queryText, error });
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/suggest/suggest_item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export const EuiSuggestItem: FunctionComponent<EuiSuggestItemProps> = ({
<button
onClick={onClick}
className={classes}
{...(rest as PropsForButton)}
{...(rest as Omit<PropsForButton, 'onClick' | 'className'>)}
>
{innerContent}
</button>
Expand Down
Loading