diff --git a/web/packages/design/src/Checkbox/Checkbox.story.tsx b/web/packages/design/src/Checkbox/Checkbox.story.tsx index 159636627e17f..047ab380f39c3 100644 --- a/web/packages/design/src/Checkbox/Checkbox.story.tsx +++ b/web/packages/design/src/Checkbox/Checkbox.story.tsx @@ -18,27 +18,126 @@ import React from 'react'; -import { Box } from 'design'; +import styled from 'styled-components'; -import { CheckboxWrapper, CheckboxInput } from './Checkbox'; +import { Flex } from '..'; + +import { StyledCheckbox } from './Checkbox'; export default { title: 'Design/Checkbox', }; export const Checkbox = () => ( - - - - Input 1 - - - - Input 2 - - - - Input 3 - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ LargeSmall
EnabledDefault + + + + + +
Hover + + + + + +
Active + + + + + +
Focus + + + + + +
DisabledDefault + + + + + +
Hover + + + + + +
Active + + + + + +
+ + +
); + +const Table = styled.table` + border-collapse: collapse; + th, + td { + border: ${p => p.theme.borders[1]}; + padding: 10px; + } +`; diff --git a/web/packages/design/src/Checkbox/Checkbox.tsx b/web/packages/design/src/Checkbox/Checkbox.tsx index c3354aa1b5ff8..fb6b5f458a3f7 100644 --- a/web/packages/design/src/Checkbox/Checkbox.tsx +++ b/web/packages/design/src/Checkbox/Checkbox.tsx @@ -18,8 +18,11 @@ import styled from 'styled-components'; +import React from 'react'; + import { Flex } from 'design'; import { space } from 'design/system'; +import * as Icon from 'design/Icon'; export const CheckboxWrapper = styled(Flex)` padding: 8px; @@ -39,47 +42,200 @@ export const CheckboxInput = styled.input` margin-right: 10px; accent-color: ${props => props.theme.colors.brand}; - &:hover { + // The "force" class is required for Storybook, where we want to show all the + // states, even though we can't enforce them. + &:hover, + .teleport-checkbox__force-hover & { cursor: pointer; } ${space} `; -// TODO (avatus): Make this the default checkbox -export const StyledCheckbox = styled.input.attrs(props => ({ +type CheckboxSize = 'large' | 'small'; + +interface StyledCheckboxProps { + size?: CheckboxSize; + + // Input properties + autoFocus?: boolean; + checked?: boolean; + defaultChecked?: boolean; + disabled?: boolean; + id?: string; + name?: string; + placeholder?: string; + readonly?: boolean; + role?: string; + type?: 'checkbox' | 'radio'; + value?: string; + + // TODO(bl-nero): Support the "indeterminate" property. + + // Container properties + className?: string; + style?: React.CSSProperties; + + onChange?: (e: React.ChangeEvent) => void; +} + +// TODO (bl-nero): Make this the default checkbox +export function StyledCheckbox(props: StyledCheckboxProps) { + const { style, className, size, ...inputProps } = props; + return ( + // The outer wrapper and inner wrapper are separate to allow using + // positioning CSS attributes on the checkbox while still maintaining its + // internal integrity that requires the internal wrapper to be positioned. + + + {/* The checkbox is rendered as two items placed on top of each other: + the actual checkbox, which is a native input control, and an SVG + checkmark. Note that we avoid the usual "label with content" trick, + because we want to be able to use this component both with and + without surrounding labels. Instead, we use absolute positioning and + an actually rendered input with a custom appearance. */} + + + + + ); +} + +const OuterWrapper = styled.span` + line-height: 0; + margin: 3px; +`; + +const InnerWrapper = styled.span` + display: inline-block; + position: relative; +`; + +const Checkmark = styled(Icon.CheckThick)` + position: absolute; + left: 1px; + top: 1px; + right: 1px; + bottom: 1px; + pointer-events: none; + color: ${props => props.theme.colors.text.primaryInverse}; + opacity: 0; + + transition: all 150ms; + + input:checked + & { + opacity: 1; + } + + input:disabled + & { + color: ${props => props.theme.colors.text.main}; + } +`; + +export const StyledCheckboxInternal = styled.input.attrs(props => ({ + // TODO(bl-nero): Make radio buttons a separate control. type: props.type || 'checkbox', }))` // reset the appearance so we can style the background -webkit-appearance: none; -moz-appearance: none; appearance: none; - width: 16px; - height: 16px; - border: 1px solid ${props => props.theme.colors.text.muted}; - border-radius: ${props => props.theme.radii[1]}px; + border: 1.5px solid ${props => props.theme.colors.text.muted}; + border-radius: ${props => props.theme.radii[2]}px; background: transparent; + cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')}; + position: relative; + margin: 0; - &:checked { - border: 1px solid ${props => props.theme.colors.brand}; - background-color: ${props => props.theme.colors.brand}; - } + // Give it some animation, but don't animate focus-related properties. + transition: + border-color 150ms, + background-color 150ms, + box-shadow 150ms; - &:hover { - cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')}; - } + // State-specific styles. Note: the "force" classes are required for + // Storybook, where we want to show all the states, even though we can't + // enforce them. + &:enabled { + &:checked { + background-color: ${props => props.theme.colors.buttons.primary.default}; + border-color: transparent; + } + + &:hover, + .teleport-checkbox__force-hover & { + background-color: ${props => + props.theme.colors.interactive.tonal.neutral[0]}; + border-color: ${props => props.theme.colors.text.slightlyMuted}; + + &:checked { + background-color: ${props => props.theme.colors.buttons.primary.hover}; + border-color: transparent; + box-shadow: + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), + 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + } + } - &::before { - content: ''; - display: block; + &:focus-visible, + .teleport-checkbox__force-focus-visible & { + background-color: ${props => + props.theme.colors.interactive.tonal.neutral[0]}; + border-color: ${props => props.theme.colors.buttons.primary.default}; + outline: none; + border-width: 2px; + + &:checked { + background-color: ${props => + props.theme.colors.buttons.primary.default}; + border-color: transparent; + outline: 2px solid + ${props => props.theme.colors.buttons.primary.default}; + outline-offset: 1px; + } + } + + &:active, + .teleport-checkbox__force-active & { + background-color: ${props => + props.theme.colors.interactive.tonal.neutral[1]}; + border-color: ${props => props.theme.colors.text.slightlyMuted}; + + &:checked { + background-color: ${props => props.theme.colors.buttons.primary.active}; + border-color: transparent; + } + } } - &:checked::before { - content: '✓'; - color: ${props => props.theme.colors.levels.deep}; - position: absolute; - right: 1px; - top: -1px; + &:disabled { + background-color: ${props => + props.theme.colors.interactive.tonal.neutral[0]}; + border-color: transparent; } + + ${size} `; + +/** + * Returns dimensions of a checkbox with a given `size` property. Since its name + * conflicts with the native `size` attribute with a different type and + * semantics, we use `cbSize` here. + */ +function size(props: { cbSize?: CheckboxSize }) { + const { cbSize = 'large' } = props; + let s = ''; + switch (cbSize) { + case 'large': + s = '18px'; + break; + case 'small': + s = '14px'; + break; + default: + cbSize satisfies never; + } + return { width: s, height: s }; +} diff --git a/web/packages/design/src/Icon/Icons.story.tsx b/web/packages/design/src/Icon/Icons.story.tsx index 2297a858be6b5..c4672580afa47 100644 --- a/web/packages/design/src/Icon/Icons.story.tsx +++ b/web/packages/design/src/Icon/Icons.story.tsx @@ -73,6 +73,7 @@ export const Icons = () => ( + diff --git a/web/packages/design/src/Icon/Icons/CheckThick.tsx b/web/packages/design/src/Icon/Icons/CheckThick.tsx new file mode 100644 index 0000000000000..17dedac23d247 --- /dev/null +++ b/web/packages/design/src/Icon/Icons/CheckThick.tsx @@ -0,0 +1,68 @@ +/** + * Teleport + * Copyright (C) 2023 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 . + */ + +/* MIT License + +Copyright (c) 2020 Phosphor Icons + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +import React from 'react'; + +import { Icon, IconProps } from '../Icon'; + +/* + +THIS FILE IS GENERATED. DO NOT EDIT. + +*/ + +export function CheckThick({ size = 24, color, ...otherProps }: IconProps) { + return ( + + + + ); +} diff --git a/web/packages/design/src/Icon/assets/CheckThick.svg b/web/packages/design/src/Icon/assets/CheckThick.svg new file mode 100644 index 0000000000000..986ca4ed115a0 --- /dev/null +++ b/web/packages/design/src/Icon/assets/CheckThick.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/packages/design/src/Icon/index.ts b/web/packages/design/src/Icon/index.ts index dbf549a9ee3fa..24769fbac3aec 100644 --- a/web/packages/design/src/Icon/index.ts +++ b/web/packages/design/src/Icon/index.ts @@ -61,6 +61,7 @@ export { Chart } from './Icons/Chart'; export { ChatBubble } from './Icons/ChatBubble'; export { ChatCircleSparkle } from './Icons/ChatCircleSparkle'; export { Check } from './Icons/Check'; +export { CheckThick } from './Icons/CheckThick'; export { ChevronCircleDown } from './Icons/ChevronCircleDown'; export { ChevronCircleLeft } from './Icons/ChevronCircleLeft'; export { ChevronCircleRight } from './Icons/ChevronCircleRight'; diff --git a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx index 633b2930f1085..21e98ab0af72c 100644 --- a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx +++ b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx @@ -126,7 +126,7 @@ export function FilterPanel({