Skip to content
Draft
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
7 changes: 7 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ export default tsEslint.config(
'line',
[' Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.', ' SPDX-License-Identifier: Apache-2.0'],
],
'no-restricted-syntax': [
'error',
{
selector: 'BinaryExpression[operator="in"][left.type="Literal"]',
message: 'Prefer a type guard function with `keyof` instead of raw `in` checks. See AWSUI-59006.',
},
],
'no-warning-comments': 'warn',
'simple-import-sort/imports': [
'error',
Expand Down
2 changes: 2 additions & 0 deletions src/app-layout/runtime-drawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,12 @@ function mapRuntimeHeaderActionsToHeaderActions(
return runtimeHeaderActions.map(runtimeHeaderAction => {
return {
...runtimeHeaderAction,
// eslint-disable-next-line no-restricted-syntax -- Runtime plugin API: property not in TS type
...('iconSvg' in runtimeHeaderAction &&
runtimeHeaderAction.iconSvg && {
iconSvg: convertRuntimeTriggerToReactNode(runtimeHeaderAction.iconSvg),
}),
// eslint-disable-next-line no-restricted-syntax -- Runtime plugin API: property not in TS type
...('pressedIconSvg' in runtimeHeaderAction &&
runtimeHeaderAction.pressedIconSvg && {
iconSvg: convertRuntimeTriggerToReactNode(runtimeHeaderAction.pressedIconSvg),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export const useAppLayout = (
return;
}

// eslint-disable-next-line no-restricted-syntax -- postMessage validation: runtime data shape check
if (!('payload' in message && 'id' in message.payload)) {
metrics.sendOpsMetricObject('awsui-widget-drawer-incorrect-payload', {
type: message.type,
Expand Down
6 changes: 3 additions & 3 deletions src/autosuggest/autosuggest-option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ const AutosuggestOption = (
ref: React.Ref<HTMLDivElement>
) => {
const baseProps = getBaseProps(rest);
const useEntered = 'type' in option && option.type === 'use-entered';
const isParent = 'type' in option && option.type === 'parent';
const isChild = 'type' in option && option.type === 'child';
const useEntered = option.type === 'use-entered';
const isParent = option.type === 'parent';
const isChild = option.type === 'child';
const { throughIndex, inGroupIndex, groupIndex } = getTestOptionIndexes(option) || {};

let optionContent;
Expand Down
3 changes: 2 additions & 1 deletion src/autosuggest/options-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,5 +179,6 @@ function createItems(options: Options) {
}

function isGroup(optionOrGroup: AutosuggestProps.Option): optionOrGroup is AutosuggestProps.OptionGroup {
return 'options' in optionOrGroup;
const key: keyof AutosuggestProps.OptionGroup = 'options';
return key in optionOrGroup;
}
2 changes: 1 addition & 1 deletion src/autosuggest/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AutosuggestItem } from '../interfaces';
type SearchableFields = 'value' | 'label' | 'description' | 'labelTag';
type SearchableTagFields = 'tags' | 'filteringTags';

const isGroup = (option: AutosuggestItem) => 'type' in option && option.type === 'parent';
const isGroup = (option: AutosuggestItem) => option.type === 'parent';

const popLastGroup = (options: AutosuggestItem[]) => {
if (options.length) {
Expand Down
10 changes: 6 additions & 4 deletions src/button-dropdown/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
GeneratedAnalyticsMetadataButtonDropdownCollapse,
GeneratedAnalyticsMetadataButtonDropdownExpand,
} from './analytics-metadata/interfaces.js';
import { ButtonDropdownProps, InternalButtonDropdownProps } from './interfaces';
import { ButtonDropdownProps, InternalButtonDropdownProps, InternalItem } from './interfaces';
import ItemsList from './items-list';
import { useButtonDropdown } from './utils/use-button-dropdown';
import { isLinkItem } from './utils/utils.js';
Expand Down Expand Up @@ -180,18 +180,20 @@ const InternalButtonDropdown = React.forwardRef(
const triggerId = useUniqueId('awsui-button-dropdown__trigger');

const triggerHasBadge = () => {
const groupKey: keyof ButtonDropdownProps.ItemGroup = 'items';
const flatItems = items.flatMap(item => {
if ('items' in item) {
if (groupKey in item) {
return item.items;
}
return item;
});

const badgeKey: keyof InternalItem = 'badge';
return (
variant === 'icon' &&
!!flatItems?.find(item => {
if ('badge' in item) {
return item.badge;
if (badgeKey in item) {
return (item as InternalItem).badge;
}
})
);
Expand Down
3 changes: 2 additions & 1 deletion src/button-group/item-element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ const ItemElement = forwardRef(
};

const onClickHandler = (event: CustomEvent<ButtonGroupProps.ItemClickDetails>) => {
const hasPopoverFeedback = 'popoverFeedback' in item && item.popoverFeedback;
const feedbackKey: keyof ButtonGroupProps.IconButton = 'popoverFeedback';
const hasPopoverFeedback = feedbackKey in item && item.popoverFeedback;

if (hasPopoverFeedback) {
setTooltip({ item: item.id, feedback: true });
Expand Down
12 changes: 5 additions & 7 deletions src/flashbar/collapsible-flashbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import {
getFlashTypeCount,
getItemColor,
getVisibleCollapsedItems,
isRefObject,
isStackableItem,
StackableItem,
} from './utils';

Expand Down Expand Up @@ -214,11 +216,11 @@ export default function CollapsibleFlashbar({ items, style, ...restProps }: Inte
// we need to use different, more custom and more controlled animations.
const hasEntered = (item: StackableItem | FlashbarProps.MessageDefinition) =>
enteringItems.some(_item => _item.id && _item.id === item.id);
const hasLeft = (item: StackableItem | FlashbarProps.MessageDefinition) => !('expandedIndex' in item);
const hasLeft = (item: StackableItem | FlashbarProps.MessageDefinition) => !isStackableItem(item);
const hasEnteredOrLeft = (item: StackableItem | FlashbarProps.MessageDefinition) => hasEntered(item) || hasLeft(item);

const showInnerContent = (item: StackableItem | FlashbarProps.MessageDefinition) =>
isFlashbarStackExpanded || hasLeft(item) || ('expandedIndex' in item && item.expandedIndex === 0);
isFlashbarStackExpanded || hasLeft(item) || (isStackableItem(item) && item.expandedIndex === 0);

const shouldUseStandardAnimation = (item: StackableItem, index: number) => index === 0 && hasEnteredOrLeft(item);

Expand Down Expand Up @@ -301,11 +303,7 @@ export default function CollapsibleFlashbar({ items, style, ...restProps }: Inte
if (shouldUseStandardAnimation(item, index) && transitionRootElement) {
if (typeof transitionRootElement === 'function') {
transitionRootElement(el);
} else if (
transitionRootElement &&
typeof transitionRootElement === 'object' &&
'current' in transitionRootElement
) {
} else if (isRefObject(transitionRootElement)) {
(transitionRootElement as React.MutableRefObject<HTMLDivElement | null>).current = el;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/flashbar/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function useFlashbar({
onItemsRemoved?: (items: FlashbarProps.MessageDefinition[]) => void;
onItemsChanged?: (options?: { allItemsHaveId?: boolean; isReducedMotion?: boolean }) => void;
}) {
// eslint-disable-next-line no-restricted-syntax -- Optional property existence check
const allItemsHaveId = useMemo(() => items.every(item => 'id' in item), [items]);
const baseProps = getBaseProps(restProps);
const ref = useRef<HTMLDivElement | null>(null);
Expand Down
7 changes: 2 additions & 5 deletions src/flashbar/non-collapsible-flashbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useFlashbar, useFlashbarVisibility } from './common';
import { TIMEOUT_FOR_ENTERING_ANIMATION } from './constant';
import { Flash } from './flash';
import { FlashbarProps, InternalFlashbarProps } from './interfaces';
import { isRefObject } from './utils';

import styles from './styles.css.js';

Expand Down Expand Up @@ -121,11 +122,7 @@ export default function NonCollapsibleFlashbar({ items, i18nStrings, style, ...r
// If there's a transition root element ref, update it too
if (transitionRootElement && typeof transitionRootElement === 'function') {
transitionRootElement(el);
} else if (
transitionRootElement &&
typeof transitionRootElement === 'object' &&
'current' in transitionRootElement
) {
} else if (transitionRootElement && isRefObject(transitionRootElement)) {
(transitionRootElement as React.MutableRefObject<HTMLDivElement | null>).current = el;
}
}}
Expand Down
12 changes: 12 additions & 0 deletions src/flashbar/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import { IconProps } from '../icon/interfaces';
import { FlashbarProps } from './interfaces';

Expand All @@ -13,6 +15,16 @@ export interface StackableItem extends FlashbarProps.MessageDefinition {
collapsedIndex?: number;
}

export function isStackableItem(item: StackableItem | FlashbarProps.MessageDefinition): item is StackableItem {
const key: keyof StackableItem = 'expandedIndex';
return key in item;
}

export function isRefObject<T>(ref: React.Ref<T>): ref is React.RefObject<T> {
const key: keyof React.RefObject<T> = 'current';
return ref !== null && typeof ref === 'object' && key in ref;
}

const typesToColors: Record<FlashbarProps.Type, string> = {
error: 'red',
info: 'blue',
Expand Down
1 change: 1 addition & 0 deletions src/internal/components/focus-lock/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const tabbables = [

/** Whether the element or any of its ancestors are not hidden. */
function isVisible(element: HTMLElement): boolean {
// eslint-disable-next-line no-restricted-syntax -- Browser API feature detection
if (!('checkVisibility' in element)) {
// checkVisibility isn't defined in JSDOM. It's safer to assume everything is visible.
return true;
Expand Down
6 changes: 4 additions & 2 deletions src/internal/components/option/utils/filter-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,7 @@ export const isInteractive = (option?: DropdownOption) => !!option && !option.di

export const isGroupInteractive = (option?: DropdownOption) => !!option && !option.disabled;

export const isGroup = (option?: OptionDefinition | OptionGroup): option is OptionGroup =>
!!option && 'options' in option && !!option.options;
export const isGroup = (option?: OptionDefinition | OptionGroup): option is OptionGroup => {
const key: keyof OptionGroup = 'options';
return !!option && key in option && !!option.options;
};
5 changes: 5 additions & 0 deletions src/internal/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,13 @@ export function isNode(target: unknown): target is Node {
target instanceof Node ||
(target !== null &&
typeof target === 'object' &&
// eslint-disable-next-line no-restricted-syntax -- Cross-window duck typing: instanceof fails across iframes
'nodeType' in target &&
typeof target.nodeType === 'number' &&
// eslint-disable-next-line no-restricted-syntax -- Cross-window duck typing: instanceof fails across iframes
'nodeName' in target &&
typeof target.nodeName === 'string' &&
// eslint-disable-next-line no-restricted-syntax -- Cross-window duck typing: instanceof fails across iframes
'parentNode' in target &&
typeof target.parentNode === 'object')
);
Expand All @@ -78,6 +81,7 @@ export function isHTMLElement(target: unknown): target is HTMLElement {
target instanceof HTMLElement ||
(isNode(target) &&
target.nodeType === Node.ELEMENT_NODE &&
// eslint-disable-next-line no-restricted-syntax -- Cross-window HTMLElement detection
'style' in target &&
typeof target.style === 'object' &&
typeof target.ownerDocument === 'object' &&
Expand All @@ -90,6 +94,7 @@ export function isSVGElement(target: unknown): target is SVGElement {
target instanceof SVGElement ||
(isNode(target) &&
target.nodeType === Node.ELEMENT_NODE &&
// eslint-disable-next-line no-restricted-syntax -- Cross-window SVGElement detection
'ownerSVGElement' in target &&
typeof target.ownerSVGElement === 'object')
);
Expand Down
1 change: 1 addition & 0 deletions src/link/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const InternalLink = React.forwardRef(

const fireClickEvent = (event: React.MouseEvent | React.KeyboardEvent) => {
const { altKey, ctrlKey, metaKey, shiftKey } = event;
// eslint-disable-next-line no-restricted-syntax -- MouseEvent vs KeyboardEvent discrimination
const button = 'button' in event ? event.button : 0;
// make onClick non-cancelable to prevent it from being used to block full page reload
// for navigation use `onFollow` event instead
Expand Down
6 changes: 4 additions & 2 deletions src/mixed-line-bar-chart/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,15 @@ export const getKeyValue = (key: ChartDataTypes) => (key instanceof Date ? key.g
export function isYThreshold<T>(
series: MixedLineBarChartProps.ChartSeries<T>
): series is MixedLineBarChartProps.YThresholdSeries {
return series.type === 'threshold' && 'y' in series;
const key: keyof MixedLineBarChartProps.YThresholdSeries = 'y';
return series.type === 'threshold' && key in series;
}

export function isXThreshold<T>(
series: MixedLineBarChartProps.ChartSeries<T>
): series is MixedLineBarChartProps.XThresholdSeries<T> {
return series.type === 'threshold' && 'x' in series;
const key: keyof MixedLineBarChartProps.XThresholdSeries<T> = 'x';
return series.type === 'threshold' && key in series;
}

export function isDataSeries<T>(
Expand Down
1 change: 1 addition & 0 deletions src/navigable-group/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const InternalNavigableGroup = forwardRef(
}

function isElementDisabled(element: HTMLElement) {
// eslint-disable-next-line no-restricted-syntax -- DOM element capability check
if ('disabled' in element) {
return element.disabled;
}
Expand Down
3 changes: 2 additions & 1 deletion src/property-filter/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
TokenGroup,
} from './interfaces';
import {
isInternalToken,
matchFilteringProperty,
matchOperator,
matchOperatorPrefix,
Expand All @@ -46,7 +47,7 @@ export const getQueryActions = ({
}) => {
const setQuery = (query: InternalQuery) => {
function transformToken(token: InternalToken | InternalTokenGroup): Token | TokenGroup {
if ('operator' in token) {
if (isInternalToken(token)) {
return matchTokenValue(token, filteringOptions);
}
return { ...token, tokens: token.tokens.map(transformToken) };
Expand Down
3 changes: 2 additions & 1 deletion src/property-filter/filter-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export function filterOptions(
}

function isGroup(optionOrGroup: AutosuggestProps.Option): optionOrGroup is AutosuggestProps.OptionGroup {
return 'options' in optionOrGroup;
const key: keyof AutosuggestProps.OptionGroup = 'options';
return key in optionOrGroup;
}

function matchSingleOption(option: OptionDefinition, searchText: string): boolean {
Expand Down
7 changes: 6 additions & 1 deletion src/property-filter/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,11 @@ const PropertyFilterInternal = React.forwardRef(
tokenOrGroup: Token | TokenGroup,
standaloneIndex?: number
): InternalToken | InternalTokenGroup {
return 'operation' in tokenOrGroup
const isTokenGroup = (t: Token | TokenGroup): t is TokenGroup => {
const key: keyof TokenGroup = 'operation';
return key in t;
};
return isTokenGroup(tokenOrGroup)
? {
operation: tokenOrGroup.operation,
tokens: tokenOrGroup.tokens.map(token => transformToken(token)),
Expand Down Expand Up @@ -316,6 +320,7 @@ const PropertyFilterInternal = React.forwardRef(
return;
}

// eslint-disable-next-line no-restricted-syntax -- Runtime property not in TS type
if (!('keepOpenOnSelect' in option)) {
createToken(value);
return;
Expand Down
12 changes: 8 additions & 4 deletions src/property-filter/token.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
LoadItemsDetail,
} from './interfaces';
import { TokenEditor } from './token-editor';
import { tokenGroupToTokens } from './utils';
import { isInternalToken, isInternalTokenGroup, tokenGroupToTokens } from './utils';

import analyticsSelectors from './analytics-metadata/styles.css.js';
import styles from './styles.css.js';
Expand Down Expand Up @@ -71,20 +71,24 @@ export const TokenButton = ({
}: TokenProps) => {
const tokenRef = useRef<FilteringTokenRef>(null);

const hasGroups = query.tokens.some(tokenOrGroup => 'operation' in tokenOrGroup);
const hasGroups = query.tokens.some(isInternalTokenGroup);
const first = tokenIndex === 0;

const tokenOrGroup = query.tokens[tokenIndex];
const tokens = tokenGroupToTokens<InternalToken>([tokenOrGroup]).map(t => ({ ...t, standaloneIndex: undefined }));
const operation = query.operation;
const groupOperation = 'operation' in tokenOrGroup ? tokenOrGroup.operation : operation === 'and' ? 'or' : 'and';
const groupOperation = isInternalTokenGroup(tokenOrGroup)
? tokenOrGroup.operation
: operation === 'and'
? 'or'
: 'and';

const [tempTokens, setTempTokens] = useState<InternalToken[]>(tokens);
const capturedTokenIndices = tempTokens.map(token => token.standaloneIndex).filter(index => index !== undefined);
const tokensToCapture: InternalToken[] = [];
for (let index = 0; index < query.tokens.length; index++) {
const token = query.tokens[index];
if ('operator' in token && token !== tokenOrGroup && !capturedTokenIndices.includes(index)) {
if (isInternalToken(token) && token !== tokenOrGroup && !capturedTokenIndices.includes(index)) {
tokensToCapture.push(token);
}
}
Expand Down
Loading
Loading