From 576175b274fd7320288553f99adc1e0867b0061a 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 | 137 ++++++++++++++++++ .../components/Controls/MultiselectMenu.tsx | 3 +- .../components/Controls/SortMenu.story.tsx | 83 +++++++++++ .../Controls/ViewModeSwitch.story.tsx | 61 ++++++++ .../components/Controls/ViewModeSwitch.tsx | 29 ++-- 5 files changed, 299 insertions(+), 14 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..3016d892c64a5 --- /dev/null +++ b/web/packages/shared/components/Controls/MultiselectMenu.story.tsx @@ -0,0 +1,137 @@ +/** + * 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'; + +type OptionValue = `option-${number}`; + +const options: { + value: OptionValue; + label: string | React.ReactNode; + disabled?: boolean; + disabledTooltip?: string; +}[] = [ + { value: 'option-1', label: 'Option 1' }, + { value: 'option-2', label: 'Option 2' }, + { value: 'option-3', label: 'Option 3' }, + { value: 'option-4', label: 'Option 4' }, +]; + +const optionsWithCustomLabels: typeof options = [ + { + value: 'option-1', + label: Bold Option 1, + }, + { + value: 'option-3', + label: Italic Option 3, + }, +]; + +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'] } }, + render: (args => { + const [selected, setSelected] = useState([]); + return ( + + + + ); + }) satisfies StoryFn>, +} satisfies Meta>; + +type Story = StoryObj>; + +const Default: Story = { args: { options } }; + +const WithCustomLabels: Story = { args: { options: optionsWithCustomLabels } }; + +const WithDisabledOption: Story = { + args: { + options: [ + ...options, + { + value: 'option-5', + label: 'Option 5', + disabled: true, + disabledTooltip: 'Lorum ipsum dolor sit amet', + }, + ], + }, +}; + +export { Default, WithCustomLabels, WithDisabledOption }; 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..56158b8a8d228 --- /dev/null +++ b/web/packages/shared/components/Controls/SortMenu.story.tsx @@ -0,0 +1,83 @@ +/** + * 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>; + +const Default: StoryObj = { + render: (({ current, fields }) => { + const [sort, setSort] = useState(current); + return ( + + + + ); + }) satisfies StoryFn, +}; + +export { Default as SortMenu }; 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..adc77e1975715 --- /dev/null +++ b/web/packages/shared/components/Controls/ViewModeSwitch.story.tsx @@ -0,0 +1,61 @@ +/** + * 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; + +const Default: StoryObj = { + render: (({ currentViewMode }) => { + const [viewMode, setViewMode] = useState(currentViewMode); + return ( + + + + ); + }) satisfies StoryFn, +}; + +export { Default as ViewModeSwitch }; diff --git a/web/packages/shared/components/Controls/ViewModeSwitch.tsx b/web/packages/shared/components/Controls/ViewModeSwitch.tsx index 09aa577342ec3..7997f2de29f66 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 > @@ -75,7 +67,6 @@ export const ViewModeSwitch = ({ const ViewModeSwitchContainer = styled.div` height: 22px; - width: 48px; border: ${p => p.theme.borders[1]} ${p => p.theme.colors.spotBackground[2]}; border-radius: ${p => p.theme.radii[2]}px; display: flex; @@ -90,7 +81,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 +94,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};