From 06c37c748faaf0971ea82ca5b049a67adae061eb Mon Sep 17 00:00:00 2001 From: Maxim Dietz Date: Fri, 15 Nov 2024 16:48:21 -0500 Subject: [PATCH] test: Add Stories for new control components --- .../Controls/MultiselectMenu.story.tsx | 148 ++++++++++++++++++ .../components/Controls/MultiselectMenu.tsx | 3 +- .../components/Controls/SortMenu.story.tsx | 88 +++++++++++ .../Controls/ViewModeSwitch.story.tsx | 66 ++++++++ .../components/Controls/ViewModeSwitch.tsx | 28 ++-- 5 files changed, 320 insertions(+), 13 deletions(-) create mode 100644 web/packages/shared/components/Controls/MultiselectMenu.story.tsx create mode 100644 web/packages/shared/components/Controls/SortMenu.story.tsx create mode 100644 web/packages/shared/components/Controls/ViewModeSwitch.story.tsx diff --git a/web/packages/shared/components/Controls/MultiselectMenu.story.tsx b/web/packages/shared/components/Controls/MultiselectMenu.story.tsx new file mode 100644 index 0000000000000..17a960d121d00 --- /dev/null +++ b/web/packages/shared/components/Controls/MultiselectMenu.story.tsx @@ -0,0 +1,148 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React, { useState } from 'react'; +import { Flex } from 'design'; + +import { MultiselectMenu } from './MultiselectMenu'; + +import type { Meta, StoryFn, StoryObj } from '@storybook/react'; + +export default { + title: 'Shared/Controls/MultiselectMenu', + component: MultiselectMenu, + argTypes: { + buffered: { + control: { type: 'boolean' }, + description: 'Buffer selections until "Apply" is clicked', + table: { defaultValue: { summary: 'false' } }, + }, + showIndicator: { + control: { type: 'boolean' }, + description: 'Show indicator when there are selected options', + table: { defaultValue: { summary: 'true' } }, + }, + showSelectControls: { + control: { type: 'boolean' }, + description: 'Show select controls (Select All/Select None)', + table: { defaultValue: { summary: 'true' } }, + }, + label: { + control: { type: 'text' }, + description: 'Label for the multiselect', + }, + tooltip: { + control: { type: 'text' }, + description: 'Tooltip for the label', + }, + selected: { + control: false, + description: 'Currently selected options', + table: { type: { summary: 'T[]' } }, + }, + onChange: { + control: false, + description: 'Callback when selection changes', + table: { type: { summary: 'selected: T[]' } }, + }, + options: { + control: false, + description: 'Options to select from', + table: { + type: { + summary: + 'Array<{ value: T; label: string | ReactNode; disabled?: boolean; disabledTooltip?: string; }>', + }, + }, + }, + }, + args: { + label: 'Select Options', + tooltip: 'Choose multiple options', + buffered: false, + showIndicator: true, + showSelectControls: true, + }, + parameters: { controls: { expanded: true, exclude: ['userContext'] } }, +} satisfies Meta; + +type Story = StoryObj; + +type OptionValue = 'option1' | 'option2' | 'option3' | 'option4'; + +const options: { + value: OptionValue; + label: string | React.ReactNode; + disabled?: boolean; + disabledTooltip?: string; +}[] = [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + { value: 'option3', label: 'Option 3' }, + { value: 'option4', label: 'Option 4' }, +]; + +const Template: StoryFn = args => { + const [selected, setSelected] = useState([]); + return ( + + + + ); +}; + +export const Default: Story = { + args: { options }, + render: Template, +}; + +const customOptions: typeof options = [ + { + value: 'option1', + label: Bold Option 1, + }, + { + value: 'option3', + label: Italic Option 3, + }, +]; + +export const WithCustomLabels: Story = { + args: { options: customOptions }, + render: Template, +}; + +export const WithDisabledOption: Story = { + args: { + options: [ + ...options, + { + value: 'option5', + label: 'Option 5', + disabled: true, + disabledTooltip: 'Option is disabled', + }, + ], + }, + render: Template, +}; diff --git a/web/packages/shared/components/Controls/MultiselectMenu.tsx b/web/packages/shared/components/Controls/MultiselectMenu.tsx index ac57a1de1d3b7..f252cf7aa21be 100644 --- a/web/packages/shared/components/Controls/MultiselectMenu.tsx +++ b/web/packages/shared/components/Controls/MultiselectMenu.tsx @@ -164,8 +164,7 @@ export const MultiselectMenu = ({ <> { handleSelect(opt.value); diff --git a/web/packages/shared/components/Controls/SortMenu.story.tsx b/web/packages/shared/components/Controls/SortMenu.story.tsx new file mode 100644 index 0000000000000..9a2edd7b6ad50 --- /dev/null +++ b/web/packages/shared/components/Controls/SortMenu.story.tsx @@ -0,0 +1,88 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React, { useState } from 'react'; +import { Flex } from 'design'; + +import { SortMenu } from './SortMenu'; + +import type { Meta, StoryFn, StoryObj } from '@storybook/react'; + +export default { + title: 'Shared/Controls/SortMenu', + component: SortMenu, + argTypes: { + current: { + control: false, + description: 'Current sort', + table: { + type: { + summary: + "Array<{ fieldName: Exclude; dir: 'ASC' | 'DESC'>", + }, + }, + }, + fields: { + control: false, + description: 'Fields to sort by', + table: { + type: { + summary: + '{ value: Exclude; label: string }[]', + }, + }, + }, + onChange: { + control: false, + description: 'Callback when fieldName or dir is changed', + table: { + type: { + summary: + "(value: { fieldName: Exclude; dir: 'ASC' | 'DESC' }) => void", + }, + }, + }, + }, + args: { + current: { fieldName: 'name', dir: 'ASC' }, + fields: [ + { value: 'name', label: 'Name' }, + { value: 'created', label: 'Created' }, + { value: 'updated', label: 'Updated' }, + ], + }, + parameters: { controls: { expanded: true, exclude: ['userContext'] } }, +} satisfies Meta>; + +type Story = StoryObj; + +export const Default: Story = { + render: (({ current, fields }) => { + const [sort, setSort] = useState(current); + return ( + + + + ); + }) satisfies StoryFn, +}; diff --git a/web/packages/shared/components/Controls/ViewModeSwitch.story.tsx b/web/packages/shared/components/Controls/ViewModeSwitch.story.tsx new file mode 100644 index 0000000000000..9ed4743222614 --- /dev/null +++ b/web/packages/shared/components/Controls/ViewModeSwitch.story.tsx @@ -0,0 +1,66 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React, { useState } from 'react'; +import { Flex } from 'design'; + +import { ViewMode } from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb'; + +import { ViewModeSwitch } from './ViewModeSwitch'; + +import type { Meta, StoryFn, StoryObj } from '@storybook/react'; + +export default { + title: 'Shared/Controls/ViewModeSwitch', + component: ViewModeSwitch, + argTypes: { + currentViewMode: { + control: { type: 'radio', options: [ViewMode.CARD, ViewMode.LIST] }, + description: 'Current view mode', + table: { defaultValue: { summary: ViewMode.CARD.toString() } }, + }, + setCurrentViewMode: { + control: false, + description: 'Callback to set current view mode', + table: { type: { summary: '(newViewMode: ViewMode) => void' } }, + }, + }, + args: { currentViewMode: ViewMode.CARD }, + parameters: { controls: { expanded: true, exclude: ['userContext'] } }, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (({ currentViewMode }) => { + const [viewMode, setViewMode] = useState(currentViewMode); + return ( + + + + ); + }) satisfies StoryFn, +}; diff --git a/web/packages/shared/components/Controls/ViewModeSwitch.tsx b/web/packages/shared/components/Controls/ViewModeSwitch.tsx index 09aa577342ec3..6a471a51d294e 100644 --- a/web/packages/shared/components/Controls/ViewModeSwitch.tsx +++ b/web/packages/shared/components/Controls/ViewModeSwitch.tsx @@ -41,15 +41,10 @@ export const ViewModeSwitch = ({ setCurrentViewMode(ViewMode.CARD)} - css={` - border-right: 1px solid - ${props => props.theme.colors.spotBackground[2]}; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; - `} role="radio" aria-label="Card View" aria-checked={currentViewMode === ViewMode.CARD} + first > @@ -58,13 +53,10 @@ export const ViewModeSwitch = ({ setCurrentViewMode(ViewMode.LIST)} - css={` - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - `} role="radio" aria-label="List View" aria-checked={currentViewMode === ViewMode.LIST} + last > @@ -90,7 +82,7 @@ const ViewModeSwitchContainer = styled.div` } `; -const ViewModeSwitchButton = styled.button` +const ViewModeSwitchButton = styled.button<{ first?: boolean; last?: boolean }>` height: 100%; width: 100%; overflow: hidden; @@ -103,6 +95,20 @@ const ViewModeSwitchButton = styled.button` outline: none; transition: outline-width 150ms ease; + ${p => + p.first && + ` + border-top-left-radius: ${p.theme.radii[2]}px; + border-bottom-left-radius: ${p.theme.radii[2]}px; + border-right: ${p.theme.borders[1]} ${p.theme.colors.spotBackground[2]}; + `} + ${p => + p.last && + ` + border-top-right-radius: ${p.theme.radii[2]}px; + border-bottom-right-radius: ${p.theme.radii[2]}px; + `} + &:focus-visible { outline: ${p => p.theme.borders[1]} ${p => p.theme.colors.text.slightlyMuted};