diff --git a/frontend/app/element/multiselect.scss b/frontend/app/element/multiselect.scss new file mode 100644 index 000000000..850de4229 --- /dev/null +++ b/frontend/app/element/multiselect.scss @@ -0,0 +1,44 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +.multi-select { + border-radius: 7px; + background: rgb(from var(--block-bg-color) r g b / 70%); + color: var(--main-text-color); + padding: 4px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 3px; + align-self: stretch; + border: 1px solid rgb(from var(--main-text-color) r g b / 15%); + width: 100%; + + .option { + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + padding: 9px 12px; + border-radius: 4px; + align-self: stretch; + border: 1px solid transparent; + + &:hover { + background-color: rgb(from var(--main-bg-color) r g b / 60%); + } + + &.selected { + border: 1px solid var(--success-color); + background: rgb(from var(--success-color) r g b / 15%); + + i { + color: var(--success-color); + } + } + } + + i { + font-size: 1rem; + } +} diff --git a/frontend/app/element/multiselect.stories.tsx b/frontend/app/element/multiselect.stories.tsx new file mode 100644 index 000000000..8c6c4de1f --- /dev/null +++ b/frontend/app/element/multiselect.stories.tsx @@ -0,0 +1,55 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import type { Meta, StoryObj } from "@storybook/react"; +import { MultiSelect } from "./multiselect"; + +const meta: Meta = { + title: "Components/MultiSelect", + component: MultiSelect, + args: { + options: [ + { label: "macOS", value: "macos" }, + { label: "Windows", value: "windows" }, + { label: "Linux", value: "linux" }, + ], + }, + argTypes: { + options: { + description: "List of selectable options.", + }, + selectedValues: { + description: "Array of selected option values.", + }, + onChange: { + description: "Callback triggered when selected options change.", + action: "changed", + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const WithPreselectedValues: Story = { + render: (args) => ( +
+ +
+ ), + args: { + selectedValues: ["macos", "windows"], + }, +}; + +export const WithNoSelection: Story = { + render: (args) => ( +
+ +
+ ), + args: { + selectedValues: [], + }, +}; diff --git a/frontend/app/element/multiselect.tsx b/frontend/app/element/multiselect.tsx new file mode 100644 index 000000000..15a25207c --- /dev/null +++ b/frontend/app/element/multiselect.tsx @@ -0,0 +1,63 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import React, { useState } from "react"; +import "./multiselect.scss"; + +type Option = { + label: string; + value: string; +}; + +type MultiSelectProps = { + options: Option[]; + selectedValues?: string[]; + onChange: (values: string[]) => void; +}; + +const MultiSelect = ({ options, selectedValues = [], onChange }: MultiSelectProps) => { + const [selected, setSelected] = useState(selectedValues); + + const handleToggle = (value: string) => { + setSelected((prevSelected) => { + const newSelected = prevSelected.includes(value) + ? prevSelected.filter((v) => v !== value) // Remove if already selected + : [...prevSelected, value]; // Add if not selected + + onChange(newSelected); + return newSelected; + }); + }; + + const handleKeyDown = (event: React.KeyboardEvent, value: string) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleToggle(value); + } + }; + + return ( +
+ {options.map((option) => { + const isSelected = selected.includes(option.value); + + return ( +
handleToggle(option.value)} + onKeyDown={(e) => handleKeyDown(e, option.value)} + > + {option.label} + {isSelected &&