Repository: marigold-ui/marigold
Pull request #2683 (2022-12-13 09:43:21)

feat: add <Tray> for responsiveness in <Menu> and <Select>


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/cool-bats-type.md b/.changeset/cool-bats-type.md
new file mode 100644
index 000000000..e4b5d5cde
--- /dev/null
+++ b/.changeset/cool-bats-type.md
@@ -0,0 +1,5 @@
+---
+"@marigold/components": patch
+---
+
+refa: removed icon dependency from @marigold/components
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/HelpText/HelpText.tsx b/packages/components/src/HelpText/HelpText.tsx
index 67674cc91..1c4e8cdbe 100644
--- a/packages/components/src/HelpText/HelpText.tsx
+++ b/packages/components/src/HelpText/HelpText.tsx
@@ -1,7 +1,7 @@
 import React, { HTMLAttributes, ReactNode } from 'react';
-import { Exclamation } from '@marigold/icons';
 import {
   Box,
+  SVG,
   ThemeExtensionsWithParts,
   useComponentStyles,
 } from '@marigold/system';
@@ -54,10 +54,14 @@ export const HelpText = ({
     >
       {hasErrorMessage ? (
         <>
-          <Exclamation
+          <SVG
+            viewBox="0 0 24 24"
             role="presentation"
             size={(styles?.icon?.size as any) || 16}
-          />
+          >
+            <path d="M2.25 20.3097H21.75L12 3.46875L2.25 20.3097ZM12.8864 17.2606H11.1136V15.4879H12.8864V17.2606ZM12.8864 13.7151H11.1136V10.1697H12.8864V13.7151Z" />
+          </SVG>
+
           {errorMessage}
         </>
       ) : (
diff --git a/packages/components/src/Label/Label.tsx b/packages/components/src/Label/Label.tsx
index d9d2357aa..164de00f9 100644
--- a/packages/components/src/Label/Label.tsx
+++ b/packages/components/src/Label/Label.tsx
@@ -1,7 +1,6 @@
 // TODO: But this back into the root, when we realease all the new fields!
 import React from 'react';
-import { Required } from '@marigold/icons';
-import { Box, ThemeExtension, useComponentStyles } from '@marigold/system';
+import { Box, SVG, ThemeExtension, useComponentStyles } from '@marigold/system';
 import { ComponentProps } from '@marigold/types';
 
 // Theme Extension
@@ -39,7 +38,11 @@ export const Label = ({
       css={styles}
     >
       {children}
-      {required && <Required role="presentation" size={16} fill="error" />}
+      {required && (
+        <SVG viewBox="0 0 24 24" role="presentation" size={16} fill="error">
+          <path d="M10.8 3.84003H13.2V9.85259L18.1543 7.01815L19.3461 9.10132L14.3584 11.9549L19.3371 14.7999L18.1463 16.8836L13.2 14.0572V20.16H10.8V13.9907L5.76116 16.8735L4.56935 14.7903L9.5232 11.9561L4.56 9.12003L5.75073 7.03624L10.8 9.92154V3.84003Z" />
+        </SVG>
+      )}
     </Box>
   );
 };
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/Message/Message.tsx b/packages/components/src/Message/Message.tsx
index 0ae3e2008..d6a7d217b 100755
--- a/packages/components/src/Message/Message.tsx
+++ b/packages/components/src/Message/Message.tsx
@@ -1,5 +1,4 @@
 import React, { ReactNode } from 'react';
-import { Exclamation, Info, Notification } from '@marigold/icons';
 import { ThemeExtensionsWithParts, useComponentStyles } from '@marigold/system';
 import { ComponentProps } from '@marigold/types';
 
@@ -38,12 +37,24 @@ export const Message = ({
     },
     { parts: ['container', 'icon', 'title', 'content'] }
   );
-  var icon = <Info />;
+  var icon = (
+    <svg viewBox="0 0 24 24">
+      <path d="M12 2.85938C6.95437 2.85938 2.85938 6.95437 2.85938 12C2.85938 17.0456 6.95437 21.1406 12 21.1406C17.0456 21.1406 21.1406 17.0456 21.1406 12C21.1406 6.95437 17.0456 2.85938 12 2.85938ZM12.7875 15.9374H11.2125V11.2124H12.7875V15.9374ZM12.7875 9.6375H11.2125V8.0625H12.7875V9.6375Z" />
+    </svg>
+  );
   if (variant === 'warning') {
-    icon = <Notification />;
+    icon = (
+      <svg viewBox="0 0 24 24">
+        <path d="M19.2 3H4.8C3.81 3 3.009 3.81 3.009 4.8L3 21L6.6 17.4H19.2C20.19 17.4 21 16.59 21 15.6V4.8C21 3.81 20.19 3 19.2 3ZM12.9 13.8H11.1V12H12.9V13.8ZM12.9 10.2001H11.1V6.60008H12.9V10.2001Z" />
+      </svg>
+    );
   }
   if (variant === 'error') {
-    icon = <Exclamation />;
+    icon = (
+      <svg viewBox="0 0 24 24">
+        <path d="M2.25 20.3097H21.75L12 3.46875L2.25 20.3097ZM12.8864 17.2606H11.1136V15.4879H12.8864V17.2606ZM12.8864 13.7151H11.1136V10.1697H12.8864V13.7151Z" />
+      </svg>
+    );
   }
 
   return (
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