Repository: marigold-ui/marigold
Pull request #2686 (2022-12-13 13:11:20)

fix: required label

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: d9eac8ba46abd691cd4493f929fb250d46d1702d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/marigold-ui/marigold/new/fix-label-required?filename=.changeset/modern-maps-do.md&value=---%0A%22%40marigold%2Fcomponents%22%3A%20patch%0A---%0A%0Afix%3A%20required%20label%0A)



Patch:
diff --git a/.changeset/blue-items-repeat.md b/.changeset/blue-items-repeat.md
new file mode 100644
index 000000000..d572cb107
--- /dev/null
+++ b/.changeset/blue-items-repeat.md
@@ -0,0 +1,7 @@
+---
+"@marigold/docs": patch
+"@marigold/components": patch
+"@marigold/theme-b2b": patch
+---
+
+feat: add <Tray> for responsiveness in <Menu> and <Select>
diff --git a/.changeset/modern-maps-do.md b/.changeset/modern-maps-do.md
new file mode 100644
index 000000000..1d8a26819
--- /dev/null
+++ b/.changeset/modern-maps-do.md
@@ -0,0 +1,5 @@
+---
+"@marigold/components": patch
+---
+
+fix: required label
diff --git a/docs/src/theme/components/Underlay.style.ts b/docs/src/theme/components/Underlay.style.ts
new file mode 100644
index 000000000..2fa7010cd
--- /dev/null
+++ b/docs/src/theme/components/Underlay.style.ts
@@ -0,0 +1,11 @@
+import { Theme } from '@marigold/components';
+
+export const Underlay: Theme['components']['Underlay'] = {
+  base: {},
+  variant: {
+    modal: {
+      bg: 'rgba(206, 212, 218, 0.5)',
+      backdropFilter: 'blur(1px)',
+    },
+  },
+};
diff --git a/docs/src/theme/components/index.ts b/docs/src/theme/components/index.ts
index 3834d9bce..e84d12d90 100644
--- a/docs/src/theme/components/index.ts
+++ b/docs/src/theme/components/index.ts
@@ -13,3 +13,4 @@ export * from './Text.style';
 export * from './Table.style';
 export * from './ScrollToTop.style';
 export * from './Toc.style';
+export * from './Underlay.style';
diff --git a/packages/components/src/Label/Label.tsx b/packages/components/src/Label/Label.tsx
index 164de00f9..65e12e33f 100644
--- a/packages/components/src/Label/Label.tsx
+++ b/packages/components/src/Label/Label.tsx
@@ -35,6 +35,7 @@ export const Label = ({
       // aria-required is set on the field and will already be announced,
       // so we don't need to add it here. BUT we need it for styling the required label, so this is needed.
       aria-required={required}
+      __baseCSS={{ display: 'flex' }}
       css={styles}
     >
       {children}
diff --git a/packages/components/src/Menu/Menu.tsx b/packages/components/src/Menu/Menu.tsx
index 5e1ee7a78..bf910f00d 100644
--- a/packages/components/src/Menu/Menu.tsx
+++ b/packages/components/src/Menu/Menu.tsx
@@ -1,6 +1,5 @@
 import React, { Key, useRef } from 'react';
 import { useMenu } from '@react-aria/menu';
-import { DismissButton } from '@react-aria/overlays';
 import { Item } from '@react-stately/collections';
 import { useTreeState } from '@react-stately/tree';
 import { CollectionElement } from '@react-types/shared';
@@ -50,36 +49,28 @@ export const Menu = ({ variant, size, ...props }: MenuProps) => {
     { parts: ['container', 'item'] }
   );
 
-  /**
-   * - FocusScope: restore focus back to the trigger when menu is closed
-   * - DismissButton: allow screen reader to easily dimiss menu
-   */
   return (
-    <div>
-      <DismissButton onDismiss={ownProps.onClose} />
-      <Box
-        as="ul"
-        ref={ref}
-        __baseCSS={{
-          listStyle: 'none',
-          p: 0,
-          overflowWrap: 'break-word',
-        }}
-        css={styles.container}
-        {...menuProps}
-      >
-        {[...state.collection].map(item => (
-          <MenuItem
-            key={item.key}
-            item={item}
-            state={state}
-            onAction={props.onSelect}
-            css={styles.item}
-          />
-        ))}
-      </Box>
-      <DismissButton onDismiss={ownProps.onClose} />
-    </div>
+    <Box
+      as="ul"
+      ref={ref}
+      __baseCSS={{
+        listStyle: 'none',
+        p: 0,
+        overflowWrap: 'break-word',
+      }}
+      css={styles.container}
+      {...menuProps}
+    >
+      {[...state.collection].map(item => (
+        <MenuItem
+          key={item.key}
+          item={item}
+          state={state}
+          onAction={props.onSelect}
+          css={styles.item}
+        />
+      ))}
+    </Box>
   );
 };
 
diff --git a/packages/components/src/Menu/MenuTrigger.tsx b/packages/components/src/Menu/MenuTrigger.tsx
index eac332b79..4c350cdf8 100644
--- a/packages/components/src/Menu/MenuTrigger.tsx
+++ b/packages/components/src/Menu/MenuTrigger.tsx
@@ -1,10 +1,11 @@
 import React, { ReactNode, useRef } from 'react';
 import { useMenuTriggerState } from '@react-stately/menu';
-import { useMenuTrigger } from '@react-aria/menu';
-import { MenuContext, MenuContextProps } from './Context';
 import { PressResponder } from '@react-aria/interactions';
-import { Popover } from '../Overlay/Popover';
+import { useMenuTrigger } from '@react-aria/menu';
 import { useObjectRef } from '@react-aria/utils';
+import { useResponsiveValue } from '@marigold/system';
+import { MenuContext, MenuContextProps } from './Context';
+import { Popover, Tray } from '../Overlay';
 
 export interface MenuTriggerProps {
   children: [trigger: ReactNode, menu: ReactNode];
@@ -33,6 +34,8 @@ export const MenuTrigger = ({ disabled, children }: MenuTriggerProps) => {
     autoFocus: state.focusStrategy,
   };
 
+  const isSmallScreen = useResponsiveValue([true, false, false], 2);
+
   return (
     <MenuContext.Provider value={menuContext}>
       <PressResponder
@@ -42,9 +45,13 @@ export const MenuTrigger = ({ disabled, children }: MenuTriggerProps) => {
       >
         {menuTrigger}
       </PressResponder>
-      <Popover triggerRef={menuTriggerRef} scrollRef={menuRef} state={state}>
-        {menu}
-      </Popover>
+      {isSmallScreen ? (
+        <Tray state={state}>{menu}</Tray>
+      ) : (
+        <Popover triggerRef={menuTriggerRef} scrollRef={menuRef} state={state}>
+          {menu}
+        </Popover>
+      )}
     </MenuContext.Provider>
   );
 };
diff --git a/packages/components/src/Overlay/Tray.tsx b/packages/components/src/Overlay/Tray.tsx
new file mode 100644
index 000000000..659d266c8
--- /dev/null
+++ b/packages/components/src/Overlay/Tray.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { Box } from '@marigold/system';
+import { FocusScope } from '@react-aria/focus';
+import {
+  AriaModalOverlayProps,
+  DismissButton,
+  OverlayProps,
+  useModalOverlay,
+} from '@react-aria/overlays';
+import { useObjectRef } from '@react-aria/utils';
+import { OverlayTriggerState } from '@react-stately/overlays';
+import { StyleProps } from '@react-types/shared';
+import { forwardRef, ReactNode, RefObject } from 'react';
+import { Overlay } from './Overlay';
+import { Underlay } from './Underlay';
+
+// Props
+// ---------------
+interface TrayProps extends AriaModalOverlayProps, StyleProps, OverlayProps {
+  children: ReactNode;
+  state: OverlayTriggerState;
+}
+
+interface TrayWrapperProps extends TrayProps {}
+
+// Component
+// ---------------
+export const Tray = forwardRef<HTMLDivElement, TrayProps>(
+  ({ state, children, ...props }, ref) => {
+    const trayRef = useObjectRef(ref);
+    return (
+      <Overlay open={state.isOpen}>
+        <TrayWrapper state={state} {...props} ref={trayRef}>
+          {children}
+        </TrayWrapper>
+      </Overlay>
+    );
+  }
+);
+
+export const TrayWrapper = forwardRef<HTMLDivElement, TrayWrapperProps>(
+  ({ children, state, ...props }, ref: RefObject<HTMLDivElement>) => {
+    let { modalProps, underlayProps } = useModalOverlay(
+      {
+        ...props,
+        isDismissable: true,
+      },
+      state,
+      ref
+    );
+    return (
+      <FocusScope contain restoreFocus autoFocus>
+        <Underlay {...underlayProps} variant="modal">
+          <Box
+            {...modalProps}
+            ref={ref}
+            __baseCSS={{ position: 'absolute', width: '100%', bottom: 0 }}
+            data-testid="tray"
+          >
+            <DismissButton onDismiss={state.close} />
+            {children}
+            <DismissButton onDismiss={state.close} />
+          </Box>
+        </Underlay>
+      </FocusScope>
+    );
+  }
+);
diff --git a/packages/components/src/Overlay/index.ts b/packages/components/src/Overlay/index.ts
index 1c321bbc2..4bf2e1e2a 100644
--- a/packages/components/src/Overlay/index.ts
+++ b/packages/components/src/Overlay/index.ts
@@ -1,4 +1,5 @@
 export * from './Modal';
 export * from './Overlay';
 export * from './Popover';
+export * from './Tray';
 export * from './Underlay';
diff --git a/packages/components/src/Select/Select.tsx b/packages/components/src/Select/Select.tsx
index b4abcd6b7..43b89e811 100644
--- a/packages/components/src/Select/Select.tsx
+++ b/packages/components/src/Select/Select.tsx
@@ -18,6 +18,7 @@ import {
   CSSObject,
   ThemeExtensionsWithParts,
   useComponentStyles,
+  useResponsiveValue,
   useStateProps,
 } from '@marigold/system';
 import { ComponentProps } from '@marigold/types';
@@ -25,7 +26,7 @@ import { ComponentProps } from '@marigold/types';
 import { FieldBase } from '../FieldBase';
 import { ListBox } from '../ListBox';
 import { messages } from './intl';
-import { Popover } from '../Overlay';
+import { Popover, Tray } from '../Overlay';
 
 // Select Icon
 // ---------------
@@ -97,6 +98,8 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
     const buttonRef = useObjectRef(ref);
     const listboxRef = useRef(null);
 
+    const isSmallScreen = useResponsiveValue([true, false, false], 2);
+
     const {
       labelProps,
       triggerProps,
@@ -174,15 +177,27 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
           </Box>
           <Chevron css={styles.icon} />
         </Box>
-        <Popover state={state} triggerRef={buttonRef} scrollRef={listboxRef}>
-          <ListBox
-            ref={listboxRef}
-            state={state}
-            variant={variant}
-            size={size}
-            {...menuProps}
-          />
-        </Popover>
+        {isSmallScreen ? (
+          <Tray state={state}>
+            <ListBox
+              ref={listboxRef}
+              state={state}
+              variant={variant}
+              size={size}
+              {...menuProps}
+            />
+          </Tray>
+        ) : (
+          <Popover state={state} triggerRef={buttonRef} scrollRef={listboxRef}>
+            <ListBox
+              ref={listboxRef}
+              state={state}
+              variant={variant}
+              size={size}
+              {...menuProps}
+            />
+          </Popover>
+        )}
       </FieldBase>
     );
   }
diff --git a/themes/theme-core/src/components/Underlay.style.ts b/themes/theme-core/src/components/Underlay.style.ts
index 1f0940798..2fa7010cd 100644
--- a/themes/theme-core/src/components/Underlay.style.ts
+++ b/themes/theme-core/src/components/Underlay.style.ts
@@ -4,8 +4,8 @@ export const Underlay: Theme['components']['Underlay'] = {
   base: {},
   variant: {
     modal: {
-      bg: 'gray70',
-      opacity: 0.33,
+      bg: 'rgba(206, 212, 218, 0.5)',
+      backdropFilter: 'blur(1px)',
     },
   },
 };


Test patch:
diff --git a/packages/components/src/Menu/Menu.test.tsx b/packages/components/src/Menu/Menu.test.tsx
index beeb533de..0c4eaa006 100644
--- a/packages/components/src/Menu/Menu.test.tsx
+++ b/packages/components/src/Menu/Menu.test.tsx
@@ -1,7 +1,14 @@
 import React from 'react';
-import { fireEvent, render, screen } from '@testing-library/react';
+import {
+  act,
+  cleanup,
+  fireEvent,
+  render,
+  renderHook,
+  screen,
+} from '@testing-library/react';
 import { OverlayProvider } from '@react-aria/overlays';
-import { ThemeProvider } from '@marigold/system';
+import { ThemeProvider, useResponsiveValue } from '@marigold/system';
 
 import { Button } from '../Button';
 import { Menu } from './Menu';
@@ -58,7 +65,22 @@ const theme = {
   },
 };
 
+/**
+ * We need to mock `matchMedia` because JSOM does not
+ * implements it.
+ */
+const mockMatchMedia = (matches: string[]) =>
+  jest.fn().mockImplementation(query => ({
+    matches: matches.includes(query),
+  }));
+afterEach(cleanup);
+
 test('renders the button but no menu by default', () => {
+  window.matchMedia = mockMatchMedia([
+    'screen and (min-width: 40em)',
+    'screen and (min-width: 52em)',
+    'screen and (min-width: 64em)',
+  ]);
   render(
     <OverlayProvider>
       <ThemeProvider theme={theme}>
@@ -329,3 +351,37 @@ test('apply focus style on focus', () => {
   fireEvent.focus(item);
   expect(item).toHaveStyle(`background: ${theme.colors.pink}`);
 });
+
+test('renders as tray', () => {
+  let resize: Function;
+  window.addEventListener = jest.fn().mockImplementation((event, cb) => {
+    if (event === 'resize') resize = cb;
+  });
+
+  const { result } = renderHook(() =>
+    useResponsiveValue(['one', 'two', 'three', 'four'])
+  );
+  window.matchMedia = mockMatchMedia([]);
+  act(() => resize());
+
+  expect(result.current).toEqual('one');
+
+  render(
+    <OverlayProvider>
+      <ThemeProvider theme={theme}>
+        <Menu.Trigger>
+          <Button>Choose</Button>
+          <Menu data-testid="menu">
+            <Menu.Item key="burger">Burger</Menu.Item>
+            <Menu.Item key="pizza">Pizza</Menu.Item>
+          </Menu>
+        </Menu.Trigger>
+      </ThemeProvider>
+    </OverlayProvider>
+  );
+
+  const button = screen.getByText('Choose');
+  fireEvent.click(button);
+  const tray = screen.getByTestId('tray');
+  expect(tray).toBeInTheDocument();
+});
diff --git a/packages/components/src/Overlay/Popover.test.tsx b/packages/components/src/Overlay/Popover.test.tsx
new file mode 100644
index 000000000..1ee25e655
--- /dev/null
+++ b/packages/components/src/Overlay/Popover.test.tsx
@@ -0,0 +1,48 @@
+/* eslint-disable testing-library/no-node-access */
+import React from 'react';
+import { OverlayProvider } from '@react-aria/overlays';
+import { useObjectRef } from '@react-aria/utils';
+import { useOverlayTriggerState } from '@react-stately/overlays';
+import { render, screen } from '@testing-library/react';
+import { forwardRef } from 'react';
+import { Popover } from './Popover';
+
+interface TestPopoverProps {
+  open: boolean;
+}
+const TestPopover = forwardRef<HTMLDivElement, TestPopoverProps>(
+  ({ open }, ref) => {
+    const popoverRef = useObjectRef(ref);
+    const state = useOverlayTriggerState({ isOpen: open });
+
+    return (
+      <OverlayProvider>
+        <Popover triggerRef={popoverRef} state={state}>
+          <div>something</div>
+        </Popover>
+      </OverlayProvider>
+    );
+  }
+);
+
+test('renders open popover', () => {
+  render(<TestPopover open={true} />);
+
+  const popover = screen.getByRole('presentation');
+  expect(popover).toBeInTheDocument();
+});
+
+test('popover is per default closed', () => {
+  render(<TestPopover open={false} />);
+
+  const popover = screen.queryByRole('presentation');
+  expect(popover).not.toBeInTheDocument();
+});
+
+test('popover has children', () => {
+  render(<TestPopover open={true} />);
+
+  const popover = screen.getByRole('presentation');
+  expect(popover).toBeInTheDocument();
+  expect(popover.firstChild).toBeInTheDocument();
+});
diff --git a/packages/components/src/Overlay/Tray.test.tsx b/packages/components/src/Overlay/Tray.test.tsx
new file mode 100644
index 000000000..ea950f006
--- /dev/null
+++ b/packages/components/src/Overlay/Tray.test.tsx
@@ -0,0 +1,33 @@
+import React, { forwardRef } from 'react';
+import { OverlayProvider } from '@react-aria/overlays';
+import { render, screen } from '@testing-library/react';
+import { Tray } from './Tray';
+import { useOverlayTriggerState } from '@react-stately/overlays';
+import { useObjectRef } from '@react-aria/utils';
+
+interface TestTrayProps {
+  open: boolean;
+  variant?: string;
+  size?: string;
+}
+const TestTray = forwardRef<HTMLDivElement, TestTrayProps>(
+  ({ open, variant }, ref) => {
+    const trayRef = useObjectRef(ref);
+    const state = useOverlayTriggerState({ isOpen: open });
+
+    return (
+      <OverlayProvider>
+        <Tray state={state} ref={trayRef}>
+          <div>something</div>
+        </Tray>
+      </OverlayProvider>
+    );
+  }
+);
+
+test('renders tray', () => {
+  render(<TestTray open={true} />);
+
+  const tray = screen.getByTestId('tray');
+  expect(tray).toBeInTheDocument();
+});
diff --git a/packages/components/src/Overlay/Underlay.test.tsx b/packages/components/src/Overlay/Underlay.test.tsx
new file mode 100644
index 000000000..8156ea2f7
--- /dev/null
+++ b/packages/components/src/Overlay/Underlay.test.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import { ThemeProvider } from '@marigold/system';
+import { OverlayProvider } from '@react-aria/overlays';
+import { render, screen } from '@testing-library/react';
+import { Underlay } from './Underlay';
+
+const theme = {
+  colors: {
+    black: '#212529',
+    white: '#f8f9fa',
+    pink: '#fcc2d7',
+  },
+  space: {
+    none: 0,
+    small: 4,
+    large: 10,
+  },
+  components: {
+    Underlay: {
+      variant: {
+        one: {
+          bg: 'pink',
+        },
+      },
+      size: {
+        small: {
+          p: 'small',
+        },
+      },
+    },
+  },
+};
+
+test('renders underlay', () => {
+  render(
+    <OverlayProvider>
+      <Underlay data-testid="underlay">
+        <div>something</div>
+      </Underlay>
+    </OverlayProvider>
+  );
+
+  const underlay = screen.getByTestId('underlay');
+  expect(underlay).toBeInTheDocument();
+});
+
+test('underlay supports variant', () => {
+  render(
+    <ThemeProvider theme={theme}>
+      <OverlayProvider>
+        <Underlay data-testid="underlay" variant="one">
+          <div>something</div>
+        </Underlay>
+      </OverlayProvider>
+    </ThemeProvider>
+  );
+
+  const underlay = screen.getByTestId('underlay');
+  expect(underlay).toHaveStyle(`background-color: ${theme.colors.pink}`);
+});
+
+test('underlay supports size', () => {
+  render(
+    <ThemeProvider theme={theme}>
+      <OverlayProvider>
+        <Underlay data-testid="underlay" size="small">
+          <div>something</div>
+        </Underlay>
+      </OverlayProvider>
+    </ThemeProvider>
+  );
+
+  const underlay = screen.getByTestId('underlay');
+  expect(underlay).toHaveStyle(`padding: ${theme.space.small}px`);
+});
diff --git a/packages/components/src/Overlay/overlay.test.tsx b/packages/components/src/Overlay/overlay.test.tsx
index 952485f35..3cf2e821f 100644
--- a/packages/components/src/Overlay/overlay.test.tsx
+++ b/packages/components/src/Overlay/overlay.test.tsx
@@ -1,43 +1,9 @@
 /* eslint-disable testing-library/no-node-access */
-import React, { forwardRef } from 'react';
+import React from 'react';
 import { render, screen } from '@testing-library/react';
 import { OverlayProvider } from '@react-aria/overlays';
-import { useOverlayTriggerState } from '@react-stately/overlays';
-import { useObjectRef } from '@react-aria/utils';
-import { ThemeProvider } from '@marigold/system';
 import { Overlay } from './Overlay';
-import { Popover } from './Popover';
-import { Underlay } from './Underlay';
 
-const theme = {
-  colors: {
-    black: '#212529',
-    white: '#f8f9fa',
-    pink: '#fcc2d7',
-  },
-  space: {
-    none: 0,
-    small: 4,
-    large: 10,
-  },
-  components: {
-    Underlay: {
-      variant: {
-        one: {
-          bg: 'pink',
-        },
-      },
-      size: {
-        small: {
-          p: 'small',
-        },
-      },
-    },
-  },
-};
-
-// Overlay tests
-// ---------------
 test('renders open overlay', () => {
   render(
     <OverlayProvider>
@@ -91,90 +57,3 @@ test('overlay has container', () => {
   expect(overlay).toBeInTheDocument();
   expect(overlay).toBeInstanceOf(HTMLDivElement);
 });
-
-// Popover tests
-// ---------------
-interface TestPopoverProps {
-  open: boolean;
-}
-const TestPopover = forwardRef<HTMLDivElement, TestPopoverProps>(
-  ({ open }, ref) => {
-    const popoverRef = useObjectRef(ref);
-    const state = useOverlayTriggerState({ isOpen: open });
-
-    return (
-      <OverlayProvider>
-        <Popover triggerRef={popoverRef} state={state}>
-          <div>something</div>
-        </Popover>
-      </OverlayProvider>
-    );
-  }
-);
-
-test('renders open popover', () => {
-  render(<TestPopover open={true} />);
-
-  const popover = screen.getByRole('presentation');
-  expect(popover).toBeInTheDocument();
-});
-
-test('popover is per default closed', () => {
-  render(<TestPopover open={false} />);
-
-  const popover = screen.queryByRole('presentation');
-  expect(popover).not.toBeInTheDocument();
-});
-
-test('popover has children', () => {
-  render(<TestPopover open={true} />);
-
-  const popover = screen.getByRole('presentation');
-  expect(popover).toBeInTheDocument();
-  expect(popover.firstChild).toBeInTheDocument();
-});
-
-// Underlay tests
-// ---------------
-test('renders underlay', () => {
-  render(
-    <OverlayProvider>
-      <Underlay data-testid="underlay">
-        <div>something</div>
-      </Underlay>
-    </OverlayProvider>
-  );
-
-  const underlay = screen.getByTestId('underlay');
-  expect(underlay).toBeInTheDocument();
-});
-
-test('underlay supports variant', () => {
-  render(
-    <ThemeProvider theme={theme}>
-      <OverlayProvider>
-        <Underlay data-testid="underlay" variant="one">
-          <div>something</div>
-        </Underlay>
-      </OverlayProvider>
-    </ThemeProvider>
-  );
-
-  const underlay = screen.getByTestId('underlay');
-  expect(underlay).toHaveStyle(`background-color: ${theme.colors.pink}`);
-});
-
-test('underlay supports size', () => {
-  render(
-    <ThemeProvider theme={theme}>
-      <OverlayProvider>
-        <Underlay data-testid="underlay" size="small">
-          <div>something</div>
-        </Underlay>
-      </OverlayProvider>
-    </ThemeProvider>
-  );
-
-  const underlay = screen.getByTestId('underlay');
-  expect(underlay).toHaveStyle(`padding: ${theme.space.small}px`);
-});
diff --git a/packages/components/src/Select/Select.test.tsx b/packages/components/src/Select/Select.test.tsx
index 9844f779a..f6c78f042 100644
--- a/packages/components/src/Select/Select.test.tsx
+++ b/packages/components/src/Select/Select.test.tsx
@@ -1,9 +1,17 @@
 import React from 'react';
 import { OverlayProvider } from '@react-aria/overlays';
-import { fireEvent, render, screen, within } from '@testing-library/react';
+import {
+  act,
+  fireEvent,
+  render,
+  renderHook,
+  screen,
+  within,
+} from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
+import { cleanup } from '@testing-library/react';
 
-import { ThemeProvider } from '@marigold/system';
+import { ThemeProvider, useResponsiveValue } from '@marigold/system';
 
 import { Select } from './Select';
 
@@ -132,7 +140,23 @@ const theme = {
   },
 };
 
+/**
+ * We need to mock `matchMedia` because JSOM does not
+ * implements it.
+ */
+const mockMatchMedia = (matches: string[]) =>
+  jest.fn().mockImplementation(query => ({
+    matches: matches.includes(query),
+  }));
+afterEach(cleanup);
+
 test('renders a field (label, helptext, select)', () => {
+  window.matchMedia = mockMatchMedia([
+    'screen and (min-width: 40em)',
+    'screen and (min-width: 52em)',
+    'screen and (min-width: 64em)',
+  ]);
+
   render(
     <OverlayProvider>
       <ThemeProvider theme={theme}>
@@ -266,6 +290,11 @@ test('option list closes when button is clicked', () => {
 });
 
 test('supports to select an option and closes listbox afterwards', () => {
+  window.matchMedia = mockMatchMedia([
+    'screen and (min-width: 40em)',
+    'screen and (min-width: 52em)',
+    'screen and (min-width: 64em)',
+  ]);
   render(
     <OverlayProvider>
       <ThemeProvider theme={theme}>
@@ -672,4 +701,39 @@ test('forwards ref', () => {
   expect(ref.current).toBeInstanceOf(HTMLButtonElement);
 });
 
+test('renders as tray', () => {
+  const ref = React.createRef<HTMLButtonElement>();
+
+  let resize: Function;
+  window.addEventListener = jest.fn().mockImplementation((event, cb) => {
+    if (event === 'resize') resize = cb;
+  });
+
+  const { result } = renderHook(() =>
+    useResponsiveValue(['one', 'two', 'three', 'four'])
+  );
+  window.matchMedia = mockMatchMedia([]);
+  act(() => resize());
+
+  expect(result.current).toEqual('one');
+
+  render(
+    <OverlayProvider>
+      <ThemeProvider theme={theme}>
+        <Select label="Label" data-testid="select" ref={ref}>
+          <Select.Section title="Section 1">
+            <Select.Option key="one">one</Select.Option>
+            <Select.Option key="two">two</Select.Option>
+          </Select.Section>
+        </Select>
+      </ThemeProvider>
+    </OverlayProvider>
+  );
+
+  const button = screen.getByTestId('select');
+  fireEvent.click(button);
+  const tray = screen.getByTestId('tray');
+  expect(tray).toBeInTheDocument();
+});
+
 // FIXME: We currently have no easy way to test the focus + hover styling