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
2 changes: 2 additions & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,8 @@ export default typescript.config([
...(enableTypeAwareLinting && {
'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-enum-comparison': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
}),
},
Expand Down
13 changes: 7 additions & 6 deletions static/app/components/core/compactSelect/gridList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ import {
SizeLimitMessage,
useVirtualizedItems,
} from '@sentry/scraps/compactSelect';
import type {ListItemBase} from '@sentry/scraps/compactSelect/types';
import {Container} from '@sentry/scraps/layout';

import {t} from 'sentry/locale';

import {GridListOption, type GridListOptionProps} from './option';
import {GridListSection} from './section';

interface GridListProps
interface GridListProps<T extends ListItemBase>
extends
Omit<React.HTMLAttributes<HTMLUListElement>, 'children'>,
Omit<
Expand All @@ -43,13 +44,13 @@ interface GridListProps
* Object containing the selection state and focus position, needed for
* `useGridList()`.
*/
listState: ListState<any>;
children?: CollectionChildren<any>;
listState: ListState<T>;
children?: CollectionChildren<T>;
/**
* Text label to be rendered as heading on top of grid list.
*/
label?: React.ReactNode;
size?: GridListOptionProps['size'];
size?: GridListOptionProps<ListItemBase>['size'];
/**
* Message to be displayed when some options are hidden due to `sizeLimit`.
*/
Expand All @@ -70,15 +71,15 @@ interface GridListProps
* inside. Grid lists allow users to focus on those child elements (using the Arrow
* Left/Right keys) and interact with them, which isn't possible with list boxes.
*/
function GridList({
function GridList<T extends ListItemBase>({
listState,
size = 'md',
label,
sizeLimitMessage,
keyDownHandler,
virtualized,
...props
}: GridListProps) {
}: GridListProps<T>) {
const ref = useRef<HTMLUListElement>(null);
const labelId = useId();
const {gridProps} = useGridList(
Expand Down
17 changes: 12 additions & 5 deletions static/app/components/core/compactSelect/gridList/option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {Node} from '@react-types/shared';

import {Checkbox} from '@sentry/scraps/checkbox';
import {LeadWrap} from '@sentry/scraps/compactSelect';
import type {ListItemBase} from '@sentry/scraps/compactSelect/types';
import {
InnerWrap,
MenuListItem,
Expand All @@ -18,17 +19,23 @@ import {
import {IconCheckmark} from 'sentry/icons';
import type {FormSize} from 'sentry/utils/theme';

export interface GridListOptionProps extends AriaGridListItemOptions {
listState: ListState<any>;
node: Node<any>;
export interface GridListOptionProps<
T extends ListItemBase,
> extends AriaGridListItemOptions {
listState: ListState<T>;
node: Node<T>;
size: FormSize;
}

/**
* A <li /> element with accessibile behaviors & attributes.
* https://react-spectrum.adobe.com/react-aria/useGridList.html
*/
export function GridListOption({node, listState, size}: GridListOptionProps) {
export function GridListOption<T extends ListItemBase>({
node,
listState,
size,
}: GridListOptionProps<T>) {
const ref = useRef<HTMLLIElement>(null);
const {
label,
Expand Down Expand Up @@ -73,7 +80,7 @@ export function GridListOption({node, listState, size}: GridListOptionProps) {
[label]
);

const leadingItems: MenuListItemProps['leadingItems'] = node.props.leadingItems;
const leadingItems = (node.props as MenuListItemProps).leadingItems;
const leadingItemsMemo = useMemo(() => {
const checkboxSize = size === 'xs' ? 'xs' : 'sm';

Expand Down
17 changes: 11 additions & 6 deletions static/app/components/core/compactSelect/gridList/section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,31 @@ import {
SectionWrap,
SelectFilterContext,
} from '@sentry/scraps/compactSelect';
import type {ListItemBase} from '@sentry/scraps/compactSelect/types';

import {GridListOption, type GridListOptionProps} from './option';

interface GridListSectionProps {
listState: ListState<any>;
node: Node<any>;
size: GridListOptionProps['size'];
interface GridListSectionProps<T extends ListItemBase> {
listState: ListState<T>;
node: Node<T>;
size: GridListOptionProps<T>['size'];
}

/**
* A <li /> element that functions as a grid list section (renders a nested <ul />
* inside). https://react-spectrum.adobe.com/react-aria/useGridList.html
*/
export function GridListSection({node, listState, size}: GridListSectionProps) {
export function GridListSection<T extends ListItemBase>({
node,
listState,
size,
}: GridListSectionProps<T>) {
const titleId = useId();
const {separatorProps} = useSeparator({elementType: 'li'});

const showToggleAllButton =
listState.selectionManager.selectionMode === 'multiple' &&
node.value.showToggleAllButton;
node.value?.showToggleAllButton;

const hiddenOptions = useContext(SelectFilterContext);
const childNodes = useMemo(
Expand Down
5 changes: 3 additions & 2 deletions static/app/components/core/compactSelect/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {ControlContext} from './control';
import {GridList} from './gridList';
import {ListBox} from './listBox';
import type {
ListItemBase,
SelectKey,
SelectOption,
SelectOptionOrSectionWithKey,
Expand Down Expand Up @@ -184,7 +185,7 @@ export function List<Value extends SelectKey>({
/**
* Props to be passed into useListState()
*/
const listStateProps = useMemo<Partial<ListProps<any>>>(() => {
const listStateProps = useMemo<Partial<ListProps<ListItemBase>>>(() => {
const disabledKeys = [
...getDisabledOptions(items, isOptionDisabled),
...hiddenOptions,
Expand Down Expand Up @@ -374,7 +375,7 @@ export function List<Value extends SelectKey>({
)}
{multiple &&
sections.map(section =>
section.value.showToggleAllButton ? (
section.value?.showToggleAllButton ? (
<HiddenSectionToggle
key={section.key}
item={section}
Expand Down
12 changes: 5 additions & 7 deletions static/app/components/core/compactSelect/listBox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SizeLimitMessage,
} from '@sentry/scraps/compactSelect';
import type {SelectKey} from '@sentry/scraps/compactSelect';
import type {ListItemBase} from '@sentry/scraps/compactSelect/types';
import {Container} from '@sentry/scraps/layout';

import {t} from 'sentry/locale';
Expand All @@ -21,11 +22,7 @@ import type {FormSize} from 'sentry/utils/theme';
import {ListBoxOption} from './option';
import {ListBoxSection} from './section';

// explicitly using object here because Record<PropertyKey, unknown> requires an index signature
// eslint-disable-next-line @typescript-eslint/no-restricted-types
type ObjectLike = object;

interface ListBoxProps<T extends ObjectLike>
interface ListBoxProps<T extends ListItemBase>
extends
Omit<
React.HTMLAttributes<HTMLUListElement>,
Expand Down Expand Up @@ -122,7 +119,7 @@ const DEFAULT_KEY_DOWN_HANDLER = () => true;
* If interactive children are necessary, consider using grid lists instead (by setting
* the `grid` prop on CompactSelect to true).
*/
export function ListBox<T extends ObjectLike>({
export function ListBox<T extends ListItemBase>({
ref,
listState,
size = 'md',
Expand Down Expand Up @@ -296,7 +293,7 @@ const heightEstimations = {
*/
const listPaddingVertical = 4;

function useVirtualizedItems<T extends ObjectLike>({
function useVirtualizedItems<T extends ListItemBase>({
listItems,
virtualized = false,
size,
Expand All @@ -315,6 +312,7 @@ function useVirtualizedItems<T extends ObjectLike>({
getScrollElement: () => scrollElementRef?.current,
estimateSize: index => {
const item = listItems[index];
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (item?.props?.details) {
return heightEstimation.large;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function ListBoxOption({
[labelProps.id, label]
);

const leadingItems: MenuListItemProps['leadingItems'] = item.props.leadingItems;
const leadingItems = (item.props as MenuListItemProps).leadingItems;
const leadingItemsMemo = useMemo(() => {
const checkboxSize = size === 'xs' ? 'xs' : 'sm';

Expand Down
13 changes: 7 additions & 6 deletions static/app/components/core/compactSelect/listBox/section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ import {
SectionWrap,
} from '@sentry/scraps/compactSelect';
import type {SelectKey} from '@sentry/scraps/compactSelect';
import type {ListItemBase} from '@sentry/scraps/compactSelect/types';

import {ListBoxOption, type ListBoxOptionProps} from './option';

interface ListBoxSectionProps extends AriaListBoxSectionProps {
interface ListBoxSectionProps<T extends ListItemBase> extends AriaListBoxSectionProps {
hiddenOptions: Set<SelectKey>;
item: Node<any>;
listState: ListState<any>;
item: Node<T>;
listState: ListState<T>;
showSectionHeaders: boolean;
size: ListBoxOptionProps['size'];
'data-index'?: number;
Expand All @@ -32,7 +33,7 @@ interface ListBoxSectionProps extends AriaListBoxSectionProps {
* A <li /> element that functions as a list box section (renders a nested <ul />
* inside). https://react-spectrum.adobe.com/react-aria/useListBox.html
*/
export function ListBoxSection({
export function ListBoxSection<T extends ListItemBase>({
item,
listState,
size,
Expand All @@ -41,7 +42,7 @@ export function ListBoxSection({
showDetails = true,
ref,
'data-index': dataIndex,
}: ListBoxSectionProps) {
}: ListBoxSectionProps<T>) {
const {itemProps, headingProps, groupProps} = useListBoxSection({
heading: item.rendered,
'aria-label': item['aria-label'],
Expand All @@ -51,7 +52,7 @@ export function ListBoxSection({

const showToggleAllButton =
listState.selectionManager.selectionMode === 'multiple' &&
item.value.showToggleAllButton;
item.value?.showToggleAllButton;

const childItems = useMemo(
() => [...item.childNodes].filter(child => !hiddenOptions.has(child.key)),
Expand Down
3 changes: 3 additions & 0 deletions static/app/components/core/compactSelect/types.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type {SelectValue} from 'sentry/types/core';

// explicitly using object here because Record<PropertyKey, unknown> requires an index signature
// eslint-disable-next-line @typescript-eslint/no-restricted-types
export type ListItemBase = object & {showToggleAllButton?: boolean};
export type SelectKey = string | number;

export interface SelectOption<Value extends SelectKey> extends SelectValue<Value> {
Expand Down
6 changes: 4 additions & 2 deletions static/app/components/core/layout/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,13 @@ const omitContainerProps = new Set<keyof ContainerLayoutProps | 'as'>([

export const Container = styled(
<T extends ContainerElement = 'div'>(
props: ContainerProps<T> | ContainerPropsWithRenderFunction<T>
props: (ContainerProps<T> | ContainerPropsWithRenderFunction<T>) & {
className?: string;
}
) => {
if (typeof props.children === 'function') {
// When using render prop, only pass className to the child function
return props.children({className: (props as any).className});
return props.children({className: props.className ?? ''});
}

const {as, ...rest} = props;
Expand Down
5 changes: 3 additions & 2 deletions static/app/components/core/layout/surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,17 @@ function isRenderFunction(

export const Surface = styled(
<T extends ContainerElement = 'div'>(
props:
props: (
| SurfaceProps<T>
| FlatSurfacePropsWithRenderFunction
| OverlaySurfacePropsWithRenderFunction
) & {className?: string}
) => {
if (isRenderFunction(props)) {
// When using render prop, only pass className to the child function. T
// The className in this case is internally generated by emotion, and not part of the
// passed props.
return props.children({className: (props as any).className ?? ''});
return props.children({className: props.className ?? ''});
}

const {variant, elevation: _, ...rest} = props;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export function SegmentedControl<Value extends string>({
nextKey={option.nextKey}
prevKey={option.prevKey}
value={String(option.key)}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
isDisabled={option.props.disabled || disabled}
state={state}
size={size}
Expand Down
3 changes: 3 additions & 0 deletions static/app/components/core/select/option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ export function SelectOption(props: Props) {
innerWrapProps={{'data-test-id': value}}
labelProps={{as: typeof label === 'string' ? 'p' : 'div'}}
leadingItems={
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
itemProps.__isNew__ ? (
<Fragment>
<IconAdd size="sm" />
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */}
{data.leadingItems}
</Fragment>
) : (
Expand All @@ -72,6 +74,7 @@ export function SelectOption(props: Props) {
/>
)}
</CheckWrap>
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */}
{data.leadingItems}
</Fragment>
)
Expand Down
5 changes: 4 additions & 1 deletion static/app/components/core/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const getStylesConfig = ({
// Unfortunately we cannot use emotions `css` helper here, since react-select
// *requires* object styles, which the css helper cannot produce.
const indicatorStyles: StylesConfig['clearIndicator'] &
StylesConfig['loadingIndicator'] = (provided, state: any) => ({
StylesConfig['loadingIndicator'] = (provided, state: {isDisabled?: boolean}) => ({
...provided,
padding: '0 4px 0 4px',
alignItems: 'center',
Expand Down Expand Up @@ -639,13 +639,16 @@ export function Select<OptionType extends GeneralSelectValue = GeneralSelectValu
isClearable={clearable}
backspaceRemovesValue={clearable}
value={mappedValue}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
isMulti={props.multiple || anyProps.multi}
isDisabled={props.isDisabled || props.disabled}
isOptionDisabled={opt => !!opt.disabled}
showDividers={props.showDividers}
options={options || (choicesOrOptions as OptionsType<OptionType>)}
openMenuOnFocus={props.openMenuOnFocus}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
blurInputOnSelect={!props.multiple && !anyProps.multi}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
closeMenuOnSelect={!(props.multiple || anyProps.multi)}
hideSelectedOptions={false}
tabSelectsValue={false}
Expand Down
Loading
Loading