Skip to content
52 changes: 33 additions & 19 deletions src/components/select-menu/SelectMenu.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,39 @@ const Container = (Story, options) => {

<PageHeader>SelectMenu</PageHeader>

This component is a controlled component, so you have to handle selecting options.
- [Stories](#stories)
- [Accessibility](#accessibility)

## Overview

<Canvas>
<Story
name="Default"
parameters={{
chromatic: {disableSnapshot: true},
}}
>
{args => {
const [selectedOptions, setSelectedOptions] = React.useState([]);
return (
<div style={{width: '200px'}}>
<SelectMenu
{...args}
selectedOptions={selectedOptions}
onOptionChange={option => {
setSelectedOptions([option]);
}}
/>
</div>
);
}}
</Story>
</Canvas>

<ArgsTable story="Default" />

Note: This component is a controlled component, so you have to handle selecting options.
Example:

```jsx
const [selectedOptions, setSelectedOptions] = React.useState([]);
Expand All @@ -89,24 +121,6 @@ return (
);
```

- [Stories](#stories)
- [Accessibility](#accessibility)

## Overview

<Canvas>
<Story
name="Default"
parameters={{
chromatic: {disableSnapshot: true},
}}
>
{args => <SelectMenu {...args} />}
</Story>
</Canvas>

<ArgsTable story="Default" />

## Stories

### Controlled
Expand Down
16 changes: 8 additions & 8 deletions src/components/select-menu/SelectMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,11 @@ const SelectMenu = React.forwardRef<HTMLDivElement, SelectMenuPropsType>(

// this is to not block clicking and hovering outside
// when the exit animation plays
// and when on touch screen
const overlayPointerEvents =
status === 'open' || status === 'initial' ? 'all' : 'none';
(status === 'open' || status === 'initial') && !isTouchScreen()
? 'auto'
: 'none';

return (
<div id={wrapperId} className={selectClass} onClick={onClick}>
Expand All @@ -415,7 +418,7 @@ const SelectMenu = React.forwardRef<HTMLDivElement, SelectMenuPropsType>(
id={id}
className={selectElementClassName}
role="combobox"
tabIndex={disabled ? -1 : 0}
tabIndex={!disabled ? 0 : -1}
aria-disabled={disabled}
aria-invalid={invalid ? true : undefined}
aria-controls={`${id}-listbox`}
Expand All @@ -424,10 +427,6 @@ const SelectMenu = React.forwardRef<HTMLDivElement, SelectMenuPropsType>(
aria-multiselectable={multiSelect}
data-status={status}
{...interactions.getReferenceProps({
// Handle pointer
onClick() {
if (!disabled) onOpenChange(!isExpanded);
},
// Handle keyboard
onKeyDown(event) {
if ((event.key === 'Enter' || event.key === ' ') && !disabled) {
Expand Down Expand Up @@ -459,6 +458,8 @@ const SelectMenu = React.forwardRef<HTMLDivElement, SelectMenuPropsType>(
context={context}
modal={false}
visuallyHiddenDismiss
order={['reference', 'content']}
initialFocus={-1}
>
<div
ref={refs.setFloating}
Expand All @@ -471,15 +472,14 @@ const SelectMenu = React.forwardRef<HTMLDivElement, SelectMenuPropsType>(
width: 'max-content',
maxWidth: 320,
zIndex: 1,
pointerEvents: 'auto',
}}
{...interactions.getFloatingProps()}
tabIndex={-1}
data-placement={floatingProps.placement}
>
<div
className={popupClassName}
data-placement={floatingProps.placement}
tabIndex={activeIndex === null ? 0 : -1}
role="presentation"
>
<div
Expand Down
56 changes: 44 additions & 12 deletions src/components/select-menu/_select-menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -173,17 +173,16 @@

&__options-floating-container {
overflow: auto;
transition: transform $durationGentle1 Cubic-Bezier(0.35, 0, 0.2, 1);
transform: translateY(-8px);

&.open {
overflow: visible;
}

&.sg-animate-on-transforms {
transition: top $durationGentle1 Cubic-Bezier(0.35, 0, 0.2, 1);
@media (prefers-reduced-motion) {
transition-duration: 0s;
}

&[data-placement^='top'] {
transform-origin: bottom left;
transform: translateY(8px);
}

&[data-placement^='top-end'] {
Expand All @@ -193,6 +192,22 @@
&[data-placement^='bottom-end'] {
transform-origin: top right;
}

&.open {
overflow: visible;
}

&.sg-animate-on-transforms {
transform: translateY(0);
}

&.exit-state {
transform: translateY(-8px);

&[data-placement^='top'] {
transform: translateY(8px);
}
}
}

&__popup {
Expand All @@ -208,6 +223,7 @@
display: flex;
flex-direction: column;
flex: 1 0 auto;
opacity: 0;

@media (forced-colors: active) {
border: 2px solid FieldText;
Expand All @@ -216,20 +232,31 @@
}
}

&__popup.sg-animate-on-transforms {
opacity: 1;
&__popup {
transition: height $durationGentle1 $easingRegular,
width $durationGentle1 $easingRegular,
opacity $durationQuick1 $easingLinear;
position: absolute;

transform-origin: top left;
@media (prefers-reduced-motion) {
transition-duration: 0s;
}

&.exit-animation {
transition-delay: 0, 0, 0.07s;
&.exit-state {
transition-delay: 0s, 0s, 7ms;
transition-duration: $durationGentle1, $durationGentle1,
$durationModerate1;
opacity: 0;

@media (prefers-reduced-motion) {
transition-duration: 0s;
}
}
}

&__popup.sg-animate-on-transforms {
position: absolute;
transform-origin: top left;
opacity: 1;

&[data-placement^='top'] {
bottom: 0;
Expand Down Expand Up @@ -282,6 +309,11 @@

&:hover {
background-color: var(--gray-10);

.sg-checkbox {
--checkboxColor: var(--checkboxHoverColor);
--checkboxCheckedColor: var(--checkboxHoverColor);
}
}

&--selected {
Expand Down
2 changes: 1 addition & 1 deletion src/components/select-menu/useFloatingSelectMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const useFloatingSelectMenu = (props: UseFloatingSelectMenuPropsType) => {
},
});

const click = useClick(context, {event: 'mousedown'});
const click = useClick(context);
const dismiss = useDismiss(context);
const role = useRole(context, {role: 'listbox'});

Expand Down
66 changes: 18 additions & 48 deletions src/components/select-menu/useSelectMenuAnimations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,11 @@ type UseSelectMenuAnimationsPropsType = {

const MIN_POPUP_WIDTH = 120;
const ANIMATE_CLASSNAME = 'sg-animate-on-transforms';
const EXIT_STATE_CLASSNAME = 'exit-state';
const SCROLL_HIDE_CLASSNAME = 'hide-scroll';
const OPEN_CLASSNAME = 'open';
const MINIMAL_POPUP_TO_INPUT_RATIO = 0.7;

/**
* Move floating container by 8px from the initial top position.
*/
const resetFloatingContainerTopPosition = (
floatingContainerElement,
originalElementRef
) => {
let transformTopAmount = -8;
const placement = floatingContainerElement.getAttribute('data-placement');

if (placement.includes('top')) {
transformTopAmount = 8;
}

if (!originalElementRef.current) return;

floatingContainerElement.style.top = `${
originalElementRef.current.top + transformTopAmount
}px`;
};

const useSelectMenuAnimations = (props: UseSelectMenuAnimationsPropsType) => {
const {
selectId,
Expand All @@ -44,7 +24,7 @@ const useSelectMenuAnimations = (props: UseSelectMenuAnimationsPropsType) => {
floatingContainerClassName,
selectElementClassName,
} = props;
const lastRef = React.useRef<DOMRect>();
const initialElementRef = React.useRef<DOMRect>();
const selectRef = React.useRef<DOMRect>();
const hasReduceMotion = useReducedMotion();

Expand All @@ -60,18 +40,15 @@ const useSelectMenuAnimations = (props: UseSelectMenuAnimationsPropsType) => {
popupContentClassName
)[0] as HTMLDivElement;

popupContainer.classList.add('exit-animation');
popupContainer.style.opacity = `0`;
popupContainer.style.height = `0px`;
if (selectRef.current)
popupContainer.style.width = `${selectRef.current.width}px`;
popupContainer.style.opacity = `0`;
popupContent.classList.add(SCROLL_HIDE_CLASSNAME);

popupContainer.classList.add(EXIT_STATE_CLASSNAME);
floatingContainer.classList.add(EXIT_STATE_CLASSNAME);
requestAnimationFrame(() => {
popupContainer.classList.add(ANIMATE_CLASSNAME);
floatingContainer.classList.add(ANIMATE_CLASSNAME);
resetFloatingContainerTopPosition(floatingContainer, lastRef);

if (callback) callback();
});
};
Expand All @@ -98,39 +75,34 @@ const useSelectMenuAnimations = (props: UseSelectMenuAnimationsPropsType) => {
)[0] as HTMLDivElement;

// Register desired position
lastRef.current = floatingContainer.getBoundingClientRect();
initialElementRef.current = floatingContainer.getBoundingClientRect();
selectRef.current = selectElement.getBoundingClientRect();
const initialContainerSize = lastRef.current;
const initialContainerSize = initialElementRef.current;
const selectElementSize = selectRef.current;

popupContent.style.width = `${initialContainerSize.width}px`;
popupContent.style.height = `${initialContainerSize.height}px`;
popupContent.classList.add(SCROLL_HIDE_CLASSNAME);

if (!hasReduceMotion) {
// Reset the popup height to the pre-appear position
popupContainer.style.height = `0`;
popupContainer.style.opacity = `0`;
// Popup width at the start of animation
// should be the same as element select width
popupContainer.style.width = `${selectElementSize.width}px`;

// Popup width at the start of animation
// should be the same as element select width
popupContainer.style.width = `${selectElementSize.width}px`;
popupContainer.style.height = `0px`;

resetFloatingContainerTopPosition(floatingContainer, lastRef);
}
// Reset the popup and container to the pre-appear position
floatingContainer.classList.add(EXIT_STATE_CLASSNAME);
popupContainer.classList.add(EXIT_STATE_CLASSNAME);

// Wait for the next frame so we
// know all the style changes have
// taken hold.
requestAnimationFrame(() => {
if (!hasReduceMotion) {
popupContainer.classList.add(ANIMATE_CLASSNAME);
floatingContainer.classList.add(ANIMATE_CLASSNAME);
}

floatingContainer.classList.remove(EXIT_STATE_CLASSNAME);
popupContainer.classList.remove(EXIT_STATE_CLASSNAME);
floatingContainer.classList.add(OPEN_CLASSNAME);

popupContainer.style.opacity = `1`;
popupContainer.classList.add(ANIMATE_CLASSNAME);
floatingContainer.classList.add(ANIMATE_CLASSNAME);

popupContainer.style.height = `${initialContainerSize.height}px`;
const popupWidth: number = Math.max(
Expand All @@ -142,8 +114,6 @@ const useSelectMenuAnimations = (props: UseSelectMenuAnimationsPropsType) => {
popupContainer.style.width = `${popupWidth}px`;
popupContent.style.width = `${popupWidth}px`;

// Animate the floating container position back to it's initial state
floatingContainer.style.top = `${initialContainerSize.top}px`;
// Ensure manipulating popup height doesn't affect the floating container
floatingContainer.style.height = `${initialContainerSize.height}px`;
floatingContainer.style.width = `${Math.max(
Expand Down