diff --git a/src/components/forms/controls/Checkbox/Checkbox.module.scss b/src/components/forms/controls/Checkbox/Checkbox.module.scss index dba73d1..5106747 100644 --- a/src/components/forms/controls/Checkbox/Checkbox.module.scss +++ b/src/components/forms/controls/Checkbox/Checkbox.module.scss @@ -4,40 +4,61 @@ @use '../../../../styling/defs.scss' as bk; +@mixin svg-checkbox { + background-image: url('data:image/svg+xml;utf8,'); +} + +@mixin svg-dash { + background-image: url('data:image/svg+xml;utf8,'); +} + @layer baklava.components { .bk-checkbox { @include bk.component-base(bk-checkbox); - - --bk-checkbox-background-color: transparent; - --bk-checkbox-border-color: #{bk.$theme-checkbox-border-default}; - + cursor: pointer; - + appearance: none; width: 18px; aspect-ratio: 1; border-radius: 3px; - - background: var(--bk-checkbox-background-color); + + background: transparent; background-position: top; /* Transition background-image from top */ - border: 1px solid var(--bk-checkbox-border-color); - + border: 1px solid bk.$theme-checkbox-border-default; + + &:checked, &:indeterminate { + background-color: bk.$theme-checkbox-background-default; + background-position: center; + background-repeat: no-repeat; + border: none; + } &:checked { - --bk-checkbox-background-color: #{bk.$theme-checkbox-background-default}; - --bk-checkbox-border-color: var(--bk-checkbox-background-color); - background: - center url('data:image/svg+xml;utf8,') no-repeat, - var(--bk-checkbox-background-color); + @include svg-checkbox; + } + &:indeterminate { + @include svg-dash; } &:disabled { - --bk-checkbox-border-color: #{bk.$theme-checkbox-border-disabled}; - --bk-checkbox-background-color: transparent; - + border-color: bk.$theme-checkbox-border-disabled; + background-color: transparent; + cursor: not-allowed; + + &:checked, &:indeterminate { + background-color: bk.$theme-checkbox-background-non-active; + } &:checked { - --bk-checkbox-background-color: bk.$theme-checkbox-background-non-active; + @include svg-checkbox; } + &:indeterminate { + @include svg-dash; + } + } + &:focus-visible, &.pseudo-focused { + outline: 2px solid bk.$theme-checkbox-border-focus !important; + outline-offset: 0 !important; } - + @media (prefers-reduced-motion: no-preference) { transition: none 100ms ease-out; transition-property: background-color, background-position, border-color; diff --git a/src/components/forms/controls/Checkbox/Checkbox.stories.tsx b/src/components/forms/controls/Checkbox/Checkbox.stories.tsx index 2c2722a..73cb17f 100644 --- a/src/components/forms/controls/Checkbox/Checkbox.stories.tsx +++ b/src/components/forms/controls/Checkbox/Checkbox.stories.tsx @@ -8,6 +8,8 @@ import * as React from 'react'; import { Checkbox } from './Checkbox.tsx'; +import cl from './Checkbox.module.scss'; + type CheckboxArgs = React.ComponentProps; type Story = StoryObj; @@ -20,9 +22,7 @@ export default { tags: ['autodocs'], argTypes: { }, - args: { - defaultChecked: true, - }, + args: {}, decorators: [ Story =>
{ event.preventDefault(); }}>, ], @@ -30,18 +30,90 @@ export default { } satisfies Meta; -export const Checked: Story = {}; - export const Unchecked: Story = { - args: { defaultChecked: false }, + args: {}, }; -export const DisabledChecked: Story = { - name: 'Disabled (checked)', - args: { disabled: true, defaultChecked: true }, +export const Checked: Story = { + args: { defaultChecked: true }, +}; + +export const Indeterminate: Story = { + args: { + defaultChecked: false, + indeterminate: true, + }, }; export const DisabledUnchecked: Story = { name: 'Disabled (unchecked)', - args: { disabled: true, defaultChecked: false }, + args: { disabled: true }, +}; + +export const DisabledChecked: Story = { + name: 'Disabled (checked)', + args: { + defaultChecked: true, + disabled: true, + }, +}; + +export const DisabledIndeterminate: Story = { + name: 'Disabled (indeterminate)', + args: { + defaultChecked: false, + disabled: true, + indeterminate: true, + }, +}; + +export const FocusedUnchecked: Story = { + name: 'Focused (unchecked)', + args: { + className: cl['pseudo-focused'], + }, +}; + +export const FocusedChecked: Story = { + name: 'Focused (checked)', + args: { + className: cl['pseudo-focused'], + defaultChecked: true, + }, +}; + +export const FocusedIndeterminate: Story = { + name: 'Focused (indeterminate)', + args: { + className: cl['pseudo-focused'], + defaultChecked: false, + indeterminate: true, + }, +}; + +export const FocusedDisabledUnchecked: Story = { + name: 'Focused & Disabled (unchecked)', + args: { + className: cl['pseudo-focused'], + disabled: true, + }, +}; + +export const FocusedDisabledChecked: Story = { + name: 'Focused & Disabled (checked)', + args: { + className: cl['pseudo-focused'], + defaultChecked: true, + disabled: true, + }, +}; + +export const FocusedDisabledIndeterminate: Story = { + name: 'Focused & Disabled (indeterminate)', + args: { + className: cl['pseudo-focused'], + defaultChecked: false, + disabled: true, + indeterminate: true, + }, }; diff --git a/src/components/forms/controls/Checkbox/Checkbox.tsx b/src/components/forms/controls/Checkbox/Checkbox.tsx index 49977d7..fb77e29 100644 --- a/src/components/forms/controls/Checkbox/Checkbox.tsx +++ b/src/components/forms/controls/Checkbox/Checkbox.tsx @@ -13,19 +13,35 @@ export { cl as CheckboxClassNames }; export type CheckboxProps = ComponentProps<'input'> & { /** Whether this component should be unstyled. */ unstyled?: undefined | boolean, + + /** Whether the checkbox is in indeterminate state (minus sign) */ + indeterminate?: undefined | boolean, }; /** - * Checkbox control. + * A simple Checkbox control, just the <input type="checkbox"> and nothing else.. */ export const Checkbox = (props: CheckboxProps) => { const { unstyled = false, + indeterminate = false, ...propsRest } = props; + + const checkboxRef = React.useRef>(null); + + React.useEffect(() => { + if (checkboxRef?.current) { + if (indeterminate) { + checkboxRef.current.checked = false; + } + checkboxRef.current.indeterminate = indeterminate; + } + }, [indeterminate]); return ( ; -type Story = StoryObj; - -export default { - component: Checkbox, - parameters: { - layout: 'centered', - }, - tags: ['autodocs'], - argTypes: { - }, -} satisfies Meta; - - -const CheckboxField = () => { - return ( - - ); -}; - -export const Standard: Story = { - render: () => -
- - - -
, - play: async ({ canvasElement }) => { - // const canvas = within(canvasElement); - }, -}; diff --git a/src/components/forms/controls/CheckboxGroup/Checkbox.tsx b/src/components/forms/controls/CheckboxGroup/Checkbox.tsx deleted file mode 100644 index 41a0355..0000000 --- a/src/components/forms/controls/CheckboxGroup/Checkbox.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* Copyright (c) Fortanix, Inc. -|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of -|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; - -import cl from './Checkbox.module.scss'; - - -type CheckboxProps = { -}; - -/** - * Checkbox component. - */ -export const Checkbox = ({}: CheckboxProps) => { - return ( - - ); -}; diff --git a/src/components/forms/fields/CheckboxField/CheckboxField.module.scss b/src/components/forms/fields/CheckboxField/CheckboxField.module.scss new file mode 100644 index 0000000..981b732 --- /dev/null +++ b/src/components/forms/fields/CheckboxField/CheckboxField.module.scss @@ -0,0 +1,46 @@ +/* Copyright (c) Fortanix, Inc. +|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of +|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@use '../../../../styling/defs.scss' as bk; + +@layer baklava.components { + .bk-checkbox-field { + @include bk.component-base(bk-checkbox-field); + + .bk-checkbox-field__title { + color: bk.$theme-text-label-default; + font-weight: bk.$font-weight-semibold; + margin-bottom: bk.$spacing-1; + + .bk-checkbox-field__title__icon { + font-size: 18px; + margin-left: bk.$spacing-1; + } + + .bk-checkbox-field__title__optional { + font-size: bk.$font-size-xs; + font-weight: bk.$font-weight-regular; + margin-left: bk.$spacing-1; + } + } + + .bk-checkbox-field__label { + display: flex; + align-items: flex-start; + } + + .bk-checkbox-field__label__content { + color: bk.$theme-text-label-default; + cursor: pointer; + position: relative; + margin-left: bk.$spacing-2; + top: -2px; + } + + .bk-checkbox-field__sublabel { + font-size: bk.$font-size-xs; + padding-left: 26px; + } + } +} diff --git a/src/components/forms/fields/CheckboxField/CheckboxField.stories.tsx b/src/components/forms/fields/CheckboxField/CheckboxField.stories.tsx new file mode 100644 index 0000000..8011969 --- /dev/null +++ b/src/components/forms/fields/CheckboxField/CheckboxField.stories.tsx @@ -0,0 +1,74 @@ +/* Copyright (c) Fortanix, Inc. +|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of +|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import * as React from 'react'; + +import { CheckboxField } from './CheckboxField.tsx'; + + +type CheckboxFieldArgs = React.ComponentProps; +type Story = StoryObj; + +export default { + component: CheckboxField, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + }, + args: {}, + decorators: [ + Story =>
{ event.preventDefault(); }}>, + ], + render: (args) => , +} satisfies Meta; + + +export const CheckboxFieldWithLabel: Story = { + args: { + label: 'Label', + }, +}; + +export const CheckboxFieldWithLabelAndTitle: Story = { + args: { + title: 'Title', + label: 'Label', + }, +}; + +export const CheckboxFieldWithLabelWithTitleWithTooltip: Story = { + args: { + title: 'Title', + label: 'Label', + titleTooltip: 'This is a tooltip', + } +}; + +export const CheckboxFieldWithLabelWithTitleWithOptional: Story = { + args: { + title: 'Title', + label: 'Label', + titleOptional: true, + }, +}; + +export const CheckboxFieldWithLabelWithTitleWithTooltipWithOptional: Story = { + args: { + title: 'Title', + label: 'Label', + titleTooltip: 'This is a tooltip', + titleOptional: true, + }, +}; + +export const CheckboxFieldWithLabelAndSublabel: Story = { + args: { + label: 'Label', + sublabel: 'Supporting copy', + }, +}; diff --git a/src/components/forms/fields/CheckboxField/CheckboxField.tsx b/src/components/forms/fields/CheckboxField/CheckboxField.tsx new file mode 100644 index 0000000..1046974 --- /dev/null +++ b/src/components/forms/fields/CheckboxField/CheckboxField.tsx @@ -0,0 +1,118 @@ +/* Copyright (c) Fortanix, Inc. +|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of +|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { classNames as cx, type ComponentProps, type ClassNameArgument } from '../../../../util/componentUtil.ts'; +import * as React from 'react'; + +import { Checkbox } from '../../controls/Checkbox/Checkbox.tsx'; +import { Icon } from '../../../graphics/Icon/Icon.tsx'; +import { TooltipProvider } from '../../../overlays/Tooltip/TooltipProvider.tsx'; + +import cl from './CheckboxField.module.scss'; + + +export { cl as CheckboxFieldClassNames }; + +export type CheckboxFieldTitleProps = React.PropsWithChildren<{ + className?: ClassNameArgument; + + /** Whether to display the optional observation on title. */ + titleOptional?: undefined | boolean, + + /** An optional tooltip to be displayed on an info icon next to the title. */ + titleTooltip?: undefined | string, +}>; + +export const CheckboxFieldTitle = ({ className, children, titleOptional, titleTooltip }: CheckboxFieldTitleProps) => ( +

+ {children} + {titleTooltip && ( + + + + )} + {titleOptional && ( + (Optional) + )} +

+); + +export type CheckboxFieldProps = ComponentProps<'div'> & { + /** Whether this component should be unstyled. */ + unstyled?: undefined | boolean, + + /** A label to be displayed after the checkbox. */ + label: string, + + /** An optional supporting copy to be displayed under the label. */ + sublabel?: undefined | string, + + /** An optional title. */ + title?: undefined | string, + + /** An optional tooltip to be displayed on an info icon next to the title. */ + titleTooltip?: undefined | string, + + /** Whether to display the optional observation on title. */ + titleOptional?: undefined | boolean, + + /** Whether the checkbox is checked by default. Passed down to Checkbox component. */ + defaultChecked?: undefined | boolean, + + /** Whether the checkbox is checked. Passed down to Checkbox component. */ + checked?: undefined | boolean, + + /** Whether the checkbox is disabled. Passed down to Checkbox component. */ + disabled?: undefined | boolean, +}; + +/** + * A full-fledged Checkbox field, with optional label, title, icon etc. + */ +export const CheckboxField = (props: CheckboxFieldProps) => { + const { + unstyled = false, + label = '', + sublabel, + title, + titleOptional, + titleTooltip, + className, + } = props; + + return ( +
+ {title && ( + + {title} + + )} + {/* biome ignore lint/a11y/noLabelWithoutControl: the `` will resolve to an `` */} + + {sublabel && ( +
{sublabel}
+ )} +
+ ); +}; diff --git a/src/components/forms/fields/CheckboxGroup/CheckboxGroup.module.scss b/src/components/forms/fields/CheckboxGroup/CheckboxGroup.module.scss new file mode 100644 index 0000000..34b03ca --- /dev/null +++ b/src/components/forms/fields/CheckboxGroup/CheckboxGroup.module.scss @@ -0,0 +1,22 @@ +/* Copyright (c) Fortanix, Inc. +|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of +|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@use '../../../../styling/defs.scss' as bk; + +@layer baklava.components { + .bk-checkbox-group { + @include bk.component-base(bk-checkbox-group); + display: flex; + + &.bk-checkbox-group--vertical { + flex-direction: column; + row-gap: 20px; + } + + &.bk-checkbox-group--horizontal { + flex-direction: row; + column-gap: bk.$spacing-7; + } + } +} diff --git a/src/components/forms/fields/CheckboxGroup/CheckboxGroup.stories.tsx b/src/components/forms/fields/CheckboxGroup/CheckboxGroup.stories.tsx new file mode 100644 index 0000000..b332db5 --- /dev/null +++ b/src/components/forms/fields/CheckboxGroup/CheckboxGroup.stories.tsx @@ -0,0 +1,36 @@ +/* Copyright (c) Fortanix, Inc. +|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of +|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import * as React from 'react'; + +import type { Meta, StoryObj } from '@storybook/react'; + +import { CheckboxGroup } from './CheckboxGroup.tsx'; + + +type CheckboxGroupArgs = React.ComponentProps; +type Story = StoryObj; + +export default { + component: CheckboxGroup, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + }, + render: (args) => + + + + , +} satisfies Meta; + +export const CheckboxGroupVertical: Story = {}; + +export const CheckboxGroupHorizontal: Story = { + args: { + direction: 'horizontal', + }, +}; diff --git a/src/components/forms/fields/CheckboxGroup/CheckboxGroup.tsx b/src/components/forms/fields/CheckboxGroup/CheckboxGroup.tsx new file mode 100644 index 0000000..2396639 --- /dev/null +++ b/src/components/forms/fields/CheckboxGroup/CheckboxGroup.tsx @@ -0,0 +1,37 @@ +/* Copyright (c) Fortanix, Inc. +|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of +|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { classNames as cx } from '../../../../util/componentUtil.ts'; +import * as React from 'react'; + +import cl from './CheckboxGroup.module.scss'; + +import { CheckboxField } from '../CheckboxField/CheckboxField.tsx'; + + +export { cl as CheckboxGroupClassNames }; + +export type CheckboxGroupProps = React.PropsWithChildren<{ + direction?: undefined | "vertical" | "horizontal"; +}>; + +/** + * Checkbox group component, wrapping multiple CheckboxField components vertically or horizontally. + */ +export const CheckboxGroup = Object.assign( + (props: CheckboxGroupProps) => { + const { children, direction = 'vertical' } = props; + return ( +
+ {children} +
+ ); + }, + { CheckboxField }, +); diff --git a/src/styling/variables.scss b/src/styling/variables.scss index 1db7619..7d1a9e3 100644 --- a/src/styling/variables.scss +++ b/src/styling/variables.scss @@ -16,29 +16,18 @@ // Primitive colors // -$color-neutral-0: #ffffff !default; -$color-neutral-10: #fbfbfb !default; -$color-neutral-20: #f7f7f7 !default; -$color-neutral-30: #eeeeef !default; -$color-neutral-40: #e3e4e5 !default; -$color-neutral-50: #cacacd !default; -$color-neutral-70: #b3b3b7 !default; -$color-neutral-80: #a6a6ab !default; -$color-blueberry-800: #0d4e8a !default; -$color-blueberry-900: #0a3b69 !default; -$color-blueberry-700: #1064b2 !default; -$color-neutral-60: #bdbec1 !default; -$color-blueberry-600: #1580e4 !default; -$color-blueberry-500: #178dfb !default; $color-apple-50: #f1f9e8 !default; $color-apple-100: #d2ebb9 !default; -$color-blueberry-100: #b7dcfe !default; +$color-apple-200: #bde197 !default; +$color-apple-300: #9fd368 !default; +$color-apple-400: #8ccb4a !default; +$color-apple-500: #6fbe1d !default; +$color-apple-600: #65ad1a !default; +$color-apple-700: #4f8715 !default; +$color-apple-800: #3d6910 !default; +$color-apple-900: #2f500c !default; +$color-blackberry-0: #f6f6fb !default; $color-blackberry-10: #e8e7f6 !default; -$color-blueberry-50: #e8f4ff !default; -$color-blueberry-200: #94cbfd !default; -$color-blueberry-300: #64b3fc !default; -$color-blueberry-400: #45a4fc !default; -$color-grape-50: #f0eff9 !default; $color-blackberry-20: #dfdef2 !default; $color-blackberry-30: #c4c2d9 !default; $color-blackberry-40: #8e98a8 !default; @@ -53,34 +42,16 @@ $color-blackberry-700: #202440 !default; $color-blackberry-800: #1a1c37 !default; $color-blackberry-900: #14152e !default; $color-blackberry-1000: #0f0e25 !default; -$color-blackberry-0: #f6f6fb !default; -$color-grape-100: #cfcdec !default; -$color-grape-200: #b8b5e3 !default; -$color-grape-300: #9793d7 !default; -$color-grape-400: #837ecf !default; -$color-grape-500: #645ec3 !default; -$color-grape-600: #5b56b1 !default; -$color-grape-700: #47438a !default; -$color-grape-800: #37346b !default; -$color-grape-900: #2a2752 !default; -$color-neutral-90: #999a9f !default; -$color-neutral-100: #8d8d92 !default; -$color-neutral-200: #808086 !default; -$color-neutral-300: #73747a !default; -$color-neutral-400: #686970 !default; -$color-neutral-500: #5c5d64 !default; -$color-neutral-600: #51525a !default; -$color-neutral-700: #42434c !default; -$color-neutral-800: #363740 !default; -$color-neutral-900: #2b2c36 !default; -$color-apple-200: #bde197 !default; -$color-apple-300: #9fd368 !default; -$color-apple-400: #8ccb4a !default; -$color-apple-500: #6fbe1d !default; -$color-apple-600: #65ad1a !default; -$color-apple-700: #4f8715 !default; -$color-apple-800: #3d6910 !default; -$color-apple-900: #2f500c !default; +$color-blueberry-50: #e8f4ff !default; +$color-blueberry-100: #b7dcfe !default; +$color-blueberry-200: #94cbfd !default; +$color-blueberry-300: #64b3fc !default; +$color-blueberry-400: #45a4fc !default; +$color-blueberry-500: #178dfb !default; +$color-blueberry-600: #1580e4 !default; +$color-blueberry-700: #1064b2 !default; +$color-blueberry-800: #0d4e8a !default; +$color-blueberry-900: #0a3b69 !default; $color-cherry-50: #ffebe7 !default; $color-cherry-100: #ffc0b5 !default; $color-cherry-200: #ffa191 !default; @@ -91,6 +62,16 @@ $color-cherry-600: #e82e0f !default; $color-cherry-700: #b5240b !default; $color-cherry-800: #8c1c09 !default; $color-cherry-900: #6b1507 !default; +$color-grape-50: #f0eff9 !default; +$color-grape-100: #cfcdec !default; +$color-grape-200: #b8b5e3 !default; +$color-grape-300: #9793d7 !default; +$color-grape-400: #837ecf !default; +$color-grape-500: #645ec3 !default; +$color-grape-600: #5b56b1 !default; +$color-grape-700: #47438a !default; +$color-grape-800: #37346b !default; +$color-grape-900: #2a2752 !default; $color-lemon-50: #fbf9e6 !default; $color-lemon-100: #f2ebb1 !default; $color-lemon-200: #ece28b !default; @@ -101,6 +82,25 @@ $color-lemon-600: #c2ae02 !default; $color-lemon-700: #978801 !default; $color-lemon-800: #756901 !default; $color-lemon-900: #595001 !default; +$color-neutral-0: #ffffff !default; +$color-neutral-10: #fbfbfb !default; +$color-neutral-20: #f7f7f7 !default; +$color-neutral-30: #eeeeef !default; +$color-neutral-40: #e3e4e5 !default; +$color-neutral-50: #cacacd !default; +$color-neutral-60: #bdbec1 !default; +$color-neutral-70: #b3b3b7 !default; +$color-neutral-80: #a6a6ab !default; +$color-neutral-90: #999a9f !default; +$color-neutral-100: #8d8d92 !default; +$color-neutral-200: #808086 !default; +$color-neutral-300: #73747a !default; +$color-neutral-400: #686970 !default; +$color-neutral-500: #5c5d64 !default; +$color-neutral-600: #51525a !default; +$color-neutral-700: #42434c !default; +$color-neutral-800: #363740 !default; +$color-neutral-900: #2b2c36 !default; $color-orange-50: #fbf2e6 !default; $color-orange-100: #f2d8b0 !default; $color-orange-200: #ecc58a !default;