Browse Source

feat: Improve stories' <Matrix> interface (#57253)

Improves types for the <Matrix> component. So all the prop names inside
of `propMatrix` are validated, and renames `component` to `render` which
seems more appropriate given the examples we have.

Types are better with thank in
https://github.com/getsentry/sentry/pull/56873
Ryan Albrecht 1 year ago
parent
commit
8c68bd360d

+ 13 - 21
static/app/components/button.stories.tsx

@@ -1,19 +1,11 @@
 import {Fragment, useState} from 'react';
 
-import {Button} from 'sentry/components/button';
-import Matrix from 'sentry/components/stories/matrix';
+import {Button, ButtonProps} from 'sentry/components/button';
+import Matrix, {PropMatrix} from 'sentry/components/stories/matrix';
 import {IconDelete} from 'sentry/icons';
 import storyBook from 'sentry/stories/storyBook';
 
 export default storyBook('Button', story => {
-  const sizes = ['md' as const, 'sm' as const, 'xs' as const, 'zero' as const];
-  const priorities = [
-    'default' as const,
-    'primary' as const,
-    'danger' as const,
-    'link' as const,
-  ];
-
   story('Default', () => <Button>Default Button</Button>);
 
   story('onClick', () => {
@@ -26,13 +18,13 @@ export default storyBook('Button', story => {
     );
   });
 
-  const propMatrix = {
+  const propMatrix: PropMatrix<ButtonProps> = {
     borderless: [false, true],
     busy: [false, true],
     children: ['Save', undefined],
     icon: [undefined, <IconDelete key="" />],
-    priority: priorities,
-    size: sizes,
+    priority: ['default', 'primary', 'danger', 'link', undefined],
+    size: ['md', 'sm', 'xs', 'zero'],
     disabled: [false, true],
     external: [false, true],
     title: [undefined, 'Save Now'],
@@ -40,23 +32,23 @@ export default storyBook('Button', story => {
   };
   story('Props', () => (
     <div>
-      <Matrix
-        component={Button}
+      <Matrix<ButtonProps>
+        render={Button}
         propMatrix={propMatrix}
         selectedProps={['priority', 'size']}
       />
-      <Matrix
-        component={Button}
+      <Matrix<ButtonProps>
+        render={Button}
         propMatrix={propMatrix}
         selectedProps={['children', 'icon']}
       />
-      <Matrix
-        component={Button}
+      <Matrix<ButtonProps>
+        render={Button}
         propMatrix={propMatrix}
         selectedProps={['borderless', 'translucentBorder']}
       />
-      <Matrix
-        component={Button}
+      <Matrix<ButtonProps>
+        render={Button}
         propMatrix={propMatrix}
         selectedProps={['disabled', 'busy']}
       />

+ 3 - 3
static/app/components/featureBadge.stories.tsx

@@ -1,4 +1,4 @@
-import {Fragment} from 'react';
+import {ComponentProps, Fragment} from 'react';
 
 import FeatureBadge from 'sentry/components/featureBadge';
 import Matrix from 'sentry/components/stories/matrix';
@@ -33,8 +33,8 @@ export default storyBook(FeatureBadge, story => {
         When using an indicator you might want to position it manually using{' '}
         <kbd>styled(FeatureBadge)</kbd>.
       </p>
-      <Matrix
-        component={props => (
+      <Matrix<ComponentProps<typeof FeatureBadge>>
+        render={props => (
           <span>
             Feature X
             <FeatureBadge {...props} />

+ 36 - 17
static/app/components/stories/matrix.tsx

@@ -1,25 +1,40 @@
 import {type ElementType} from 'react';
 import styled from '@emotion/styled';
+import first from 'lodash/first';
 
 import JSXProperty from 'sentry/components/stories/jsxProperty';
-import SizingWindow from 'sentry/components/stories/sizingWindow';
+import SizingWindow, {
+  Props as SizingWindowProps,
+} from 'sentry/components/stories/sizingWindow';
 import {space} from 'sentry/styles/space';
 
-interface Props {
-  component: ElementType;
-  propMatrix: Record<string, unknown[]>;
-  selectedProps: [string, string];
+type RenderProps = {};
+
+export type PropMatrix<P extends RenderProps> = Partial<{
+  [Prop in keyof P]: P[Prop][];
+}>;
+
+interface Props<P extends RenderProps> {
+  propMatrix: PropMatrix<P>;
+  render: ElementType<P>;
+  selectedProps: [keyof P, keyof P];
+  sizingWindowProps?: SizingWindowProps;
 }
 
-export default function Matrix({component, propMatrix, selectedProps}: Props) {
+export default function Matrix<P extends RenderProps>({
+  propMatrix,
+  render,
+  selectedProps,
+  sizingWindowProps,
+}: Props<P>) {
   const defaultValues = Object.fromEntries(
     Object.entries(propMatrix).map(([key, values]) => {
-      return [key, values[0]];
+      return [key, first(values as any)];
     })
   );
 
-  const values1 = propMatrix[selectedProps[0]];
-  const values2 = propMatrix[selectedProps[1]];
+  const values1 = propMatrix[selectedProps[0]] ?? [];
+  const values2 = propMatrix[selectedProps[1]] ?? [];
 
   const items = values1.flatMap(value1 => {
     const label = (
@@ -28,11 +43,15 @@ export default function Matrix({component, propMatrix, selectedProps}: Props) {
       </div>
     );
     const content = values2.map(value2 => {
-      return item(component, {
-        ...defaultValues,
-        [selectedProps[0]]: value1,
-        [selectedProps[1]]: value2,
-      });
+      return item(
+        render,
+        {
+          ...defaultValues,
+          [selectedProps[0]]: value1,
+          [selectedProps[1]]: value2,
+        },
+        sizingWindowProps
+      );
     });
     return [label, ...content];
   });
@@ -59,15 +78,15 @@ export default function Matrix({component, propMatrix, selectedProps}: Props) {
   );
 }
 
-function item(Component, props) {
+function item(Component, props, sizingWindowProps) {
   const hasChildren = 'children' in props;
 
   return hasChildren ? (
-    <SizingWindow key={JSON.stringify(props)}>
+    <SizingWindow key={JSON.stringify(props)} {...sizingWindowProps}>
       <Component {...props}>{props.children}</Component>
     </SizingWindow>
   ) : (
-    <SizingWindow key={JSON.stringify(props)}>
+    <SizingWindow key={JSON.stringify(props)} {...sizingWindowProps}>
       <Component {...props} />
     </SizingWindow>
   );

+ 5 - 1
static/app/components/stories/sizingWindow.tsx

@@ -3,7 +3,11 @@ import styled from '@emotion/styled';
 import NegativeSpaceContainer from 'sentry/components/container/negativeSpaceContainer';
 import {space} from 'sentry/styles/space';
 
-const SizingWindow = styled(NegativeSpaceContainer)<{display?: 'block' | 'flex'}>`
+export interface Props {
+  display?: 'block' | 'flex';
+}
+
+const SizingWindow = styled(NegativeSpaceContainer)<Props>`
   border: 1px solid ${p => p.theme.yellow400};
   border-radius: ${p => p.theme.borderRadius};
 

+ 3 - 3
static/app/components/tabs/index.stories.tsx

@@ -5,7 +5,7 @@ import JSXNode from 'sentry/components/stories/jsxNode';
 import Matrix from 'sentry/components/stories/matrix';
 import SideBySide from 'sentry/components/stories/sideBySide';
 import SizingWindow from 'sentry/components/stories/sizingWindow';
-import {TabList, TabPanels, Tabs} from 'sentry/components/tabs';
+import {TabList, TabListProps, TabPanels, Tabs, TabsProps} from 'sentry/components/tabs';
 import storyBook from 'sentry/stories/storyBook';
 
 export default storyBook(Tabs, story => {
@@ -120,8 +120,8 @@ export default storyBook(Tabs, story => {
   });
 
   story('Rendering', () => (
-    <Matrix
-      component={props => (
+    <Matrix<TabsProps<string> & TabListProps>
+      render={props => (
         <Tabs orientation={props.orientation}>
           <TabList hideBorder={props.hideBorder}>
             {TABS.map(tab => (

+ 2 - 2
static/app/components/tabs/index.tsx

@@ -6,11 +6,11 @@ import {AriaTabListOptions} from '@react-aria/tabs';
 import {TabListState, TabListStateOptions} from '@react-stately/tabs';
 import {Orientation} from '@react-types/shared';
 
-import {TabList} from './tabList';
+import {TabList, TabListProps} from './tabList';
 import {TabPanels} from './tabPanels';
 import {tabsShouldForwardProp} from './utils';
 
-export {TabList, TabPanels};
+export {TabList, TabListProps, TabPanels};
 
 export interface TabsProps<T>
   extends Omit<

+ 1 - 1
static/app/components/tabs/tabList.tsx

@@ -81,7 +81,7 @@ function useOverflowTabs({
   return overflowTabs.filter(tabKey => !tabItemKeyToHiddenMap[tabKey]);
 }
 
-interface TabListProps
+export interface TabListProps
   extends AriaTabListOptions<TabListItemProps>,
     TabListStateOptions<TabListItemProps> {
   className?: string;