From 52ba7f854b422ff049dbd4733a0fcc23f926f18c Mon Sep 17 00:00:00 2001 From: Bartosz Leper Date: Tue, 28 May 2024 16:26:10 +0200 Subject: [PATCH 1/3] Update StyledCheckbox design --- .../design/src/Checkbox/Checkbox.story.tsx | 131 ++++++++++-- web/packages/design/src/Checkbox/Checkbox.tsx | 196 ++++++++++++++++-- web/packages/design/src/Icon/Icons.story.tsx | 1 + .../design/src/Icon/Icons/CheckThick.tsx | 68 ++++++ .../design/src/Icon/assets/CheckThick.svg | 3 + web/packages/design/src/Icon/index.ts | 1 + .../UnifiedResources/FilterPanel.tsx | 2 +- 7 files changed, 362 insertions(+), 40 deletions(-) create mode 100644 web/packages/design/src/Icon/Icons/CheckThick.tsx create mode 100644 web/packages/design/src/Icon/assets/CheckThick.svg diff --git a/web/packages/design/src/Checkbox/Checkbox.story.tsx b/web/packages/design/src/Checkbox/Checkbox.story.tsx index 159636627e17..047ab380f39c 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 c3354aa1b5ff..d47c1d153745 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,194 @@ 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 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 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); + } + } + + &: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; - &::before { - content: ''; - display: block; + &: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 2297a858be6b..c4672580afa4 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 000000000000..17dedac23d24 --- /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 000000000000..986ca4ed115a --- /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 dbf549a9ee3f..24769fbac3ae 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 633b2930f108..178b6ca8341f 100644 --- a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx +++ b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx @@ -126,7 +126,7 @@ export function FilterPanel({ Date: Fri, 7 Jun 2024 18:55:19 +0200 Subject: [PATCH 2/3] Correct the line-height attribute --- web/packages/design/src/Checkbox/Checkbox.tsx | 28 +++++++++++-------- .../UnifiedResources/FilterPanel.tsx | 2 +- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/web/packages/design/src/Checkbox/Checkbox.tsx b/web/packages/design/src/Checkbox/Checkbox.tsx index d47c1d153745..3cd1506d060a 100644 --- a/web/packages/design/src/Checkbox/Checkbox.tsx +++ b/web/packages/design/src/Checkbox/Checkbox.tsx @@ -83,15 +83,11 @@ interface StyledCheckboxProps { 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, @@ -100,11 +96,21 @@ export function StyledCheckbox(props: StyledCheckboxProps) { an actually rendered input with a custom appearance. */} - - + + ); } +const OuterWrapper = styled.span` + line-height: 0; +`; + +const InnerWrapper = styled.span` + display: inline-block; + position: relative; + margin: 3px; +`; + const Checkmark = styled(Icon.CheckThick)` position: absolute; left: 1px; diff --git a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx index 178b6ca8341f..51aa629f8227 100644 --- a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx +++ b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx @@ -126,7 +126,7 @@ export function FilterPanel({ Date: Mon, 10 Jun 2024 19:03:25 +0200 Subject: [PATCH 3/3] Push the margin property to the outer container --- web/packages/design/src/Checkbox/Checkbox.tsx | 2 +- web/packages/shared/components/UnifiedResources/FilterPanel.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/packages/design/src/Checkbox/Checkbox.tsx b/web/packages/design/src/Checkbox/Checkbox.tsx index 3cd1506d060a..fb6b5f458a3f 100644 --- a/web/packages/design/src/Checkbox/Checkbox.tsx +++ b/web/packages/design/src/Checkbox/Checkbox.tsx @@ -103,12 +103,12 @@ export function StyledCheckbox(props: StyledCheckboxProps) { const OuterWrapper = styled.span` line-height: 0; + margin: 3px; `; const InnerWrapper = styled.span` display: inline-block; position: relative; - margin: 3px; `; const Checkmark = styled(Icon.CheckThick)` diff --git a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx index 51aa629f8227..21e98ab0af72 100644 --- a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx +++ b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx @@ -126,7 +126,7 @@ export function FilterPanel({