Skip to content

Commit

Permalink
feat: add multiselect component (#1036)
Browse files Browse the repository at this point in the history
  • Loading branch information
petermakowski committed Feb 2, 2024
1 parent 91e1814 commit d8a13a3
Show file tree
Hide file tree
Showing 10 changed files with 858 additions and 0 deletions.
22 changes: 22 additions & 0 deletions src/components/MultiSelect/FadeInDown/FadeInDown.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@import "vanilla-framework";

.fade-in--down {
@include vf-transition(#{transform, opacity, visibility}, fast);

position: relative;
z-index: 1;

&[aria-hidden="true"] {
height: 0;
opacity: 0;
transform: translate3d(0, -0.5rem, 0);
visibility: hidden;
}

&[aria-hidden="false"] {
height: auto;
opacity: 1;
transform: translate3d(0, 0, 0);
visibility: visible;
}
}
34 changes: 34 additions & 0 deletions src/components/MultiSelect/FadeInDown/FadeInDown.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from "react";
import { render, screen } from "@testing-library/react";

import { FadeInDown } from "./FadeInDown";

it("renders with correct attributes", () => {
render(
<FadeInDown className="test-class" isVisible>
<div>Content</div>
</FadeInDown>
);

// eslint-disable-next-line testing-library/no-node-access
const element = screen.getByText("Content").parentElement;
expect(element).toHaveAttribute("aria-hidden", "false");
expect(element).toHaveClass("fade-in--down test-class");
});

it("hides and reveals children", () => {
const { rerender } = render(
<FadeInDown isVisible>
<div>Content</div>
</FadeInDown>
);
expect(screen.getByText("Content")).toBeInTheDocument();

rerender(
<FadeInDown className="test-class" isVisible={false}>
<div>Test child</div>
</FadeInDown>
);

expect(screen.queryByText("Content")).not.toBeInTheDocument();
});
28 changes: 28 additions & 0 deletions src/components/MultiSelect/FadeInDown/FadeInDown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { FC, PropsWithChildren } from "react";

import classNames from "classnames";
import "./FadeInDown.scss";

export interface FadeInDownProps extends PropsWithChildren {
isVisible: boolean;
className?: string;
}

/**
* EXPERIMENTAL: This component is experimental and should be used internally only.
*/
export const FadeInDown: FC<FadeInDownProps> = ({
children,
className,
isVisible,
}) => {
return (
<div
className={classNames("fade-in--down", className)}
aria-hidden={isVisible ? "false" : "true"}
style={{ visibility: isVisible ? "visible" : "hidden" }}
>
{children}
</div>
);
};
1 change: 1 addition & 0 deletions src/components/MultiSelect/FadeInDown/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./FadeInDown";
158 changes: 158 additions & 0 deletions src/components/MultiSelect/MultiSelect.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
@use "sass:map";
@import "vanilla-framework";
@include vf-base;
@include vf-p-lists;

$dropdown-max-height: 20rem;

.multi-select {
position: relative;
}

.multi-select .p-form-validation__message {
margin-top: 0;
}

.multi-select__condensed-text {
margin-right: $sph--large + $sph--x-small;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.multi-select__input {
cursor: pointer;
position: relative;

&.items-selected {
border-top: 0;
box-shadow: none;
top: -#{$border-radius};
}

&[disabled],
&[disabled="disabled"] {
opacity: 1;
}
}

.multi-select__dropdown {
@extend %vf-bg--x-light;
@extend %vf-has-box-shadow;
left: 0;
max-height: $dropdown-max-height;
overflow: auto;

padding-top: $spv--small;
position: absolute;
right: 0;
top: calc(100% - #{$input-margin-bottom});
}

.multi-select__dropdown--side-by-side {
display: flex;
flex-wrap: wrap;
}

.multi-select__group {
flex: 1 0 auto;
}

.multi-select__dropdown-list {
@extend %vf-list;

margin-bottom: $sph--x-small;
}

.multi-select__footer {
background: white;
border-top: 1px solid $color-mid-light;
bottom: 0;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: $sph--small $sph--large 0 $sph--large;
position: sticky;
}

.multi-select__dropdown-header {
font-size: #{map.get($font-sizes, small)}rem;
letter-spacing: #{map.get($font-sizes, small)}px;
margin-bottom: 0;
padding: $spv--x-small $sph--large;
position: relative;
text-transform: uppercase;
}

.multi-select__dropdown-item {
padding: 0 $sph--large;

.p-checkbox {
padding-top: $sph--x-small;
}

&,
.p-checkbox {
width: 100%;
}
}

.multi-select__dropdown-item-description {
@extend %small-text;

color: $color-mid-dark;
}

.multi-select__dropdown-button {
border: 0;
margin-bottom: 0;
padding-left: $sph--small;
padding-right: $sph--small;
text-align: left;
width: 100%;
}

.multi-select__selected-list {
background-color: $colors--light-theme--background-inputs;
border-bottom: 0;
margin: 0;
overflow: hidden;
padding: $spv--x-small $sph--small;
text-overflow: ellipsis;
white-space: nowrap;
}

.multi-select__select-button {
@extend %vf-input-elements;
align-items: center;
display: inline-flex;
height: 2.5rem;
justify-content: space-between;
overflow: auto;

position: relative;
z-index: 0;

&::after {
content: "";
margin-left: $sph--large;
position: absolute;
right: $sph--small;
top: 50%;
transform: translateY(-50%) rotate(-180deg);

@extend %icon;
@include vf-icon-chevron($color-mid-dark);
@include vf-transition($property: transform, $duration: fast);
}

&[aria-expanded="true"] {
background-color: $colors--light-theme--background-hover;
}

&[aria-expanded="false"] {
&::after {
transform: translateY(-50%) rotate(0);
}
}
}
72 changes: 72 additions & 0 deletions src/components/MultiSelect/MultiSelect.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from "react";
import { useState } from "react";

import { Meta } from "@storybook/react";

import { MultiSelect, MultiSelectItem, MultiSelectProps } from "./MultiSelect";

const Template = (props: MultiSelectProps) => {
const [selectedItems, setSelectedItems] = useState<MultiSelectItem[]>(
props.selectedItems || []
);
return (
<MultiSelect
{...props}
selectedItems={selectedItems}
onItemsUpdate={setSelectedItems}
/>
);
};

const meta: Meta<typeof MultiSelect> = {
title: "MultiSelect",
component: MultiSelect,
render: Template,
tags: ["autodocs"],
parameters: {},
};

export default meta;

export const CondensedExample = {
args: {
items: [
...Array.from({ length: 26 }, (_, i) => ({
label: `${String.fromCharCode(i + 65)}`,
value: `${String.fromCharCode(i + 65)}`,
})),
...Array.from({ length: 26 }, (_, i) => ({
label: `Item ${i + 1}`,
value: i + 1,
})),
],
selectedItems: [
{ label: "A", value: "A" },
{ label: "Item 2", value: 2 },
],
variant: "condensed",
},
};

export const SearchExample = {
args: {
...CondensedExample.args,
variant: "search",
items: [
...CondensedExample.args.items.map((item, i) => ({
...item,
group: i % 2 === 0 ? "Group 1" : "Group 2",
})),
],
},
};

export const WithDisabledItems = {
args: {
...CondensedExample.args,
disabledItems: [
{ label: "Item 1", value: 1 },
{ label: "Item 2", value: 2 },
],
},
};
Loading

0 comments on commit d8a13a3

Please sign in to comment.