diff --git a/packages/web-components/src/components/cbp-form-field/cbp-form-field.scss b/packages/web-components/src/components/cbp-form-field/cbp-form-field.scss index c87abaed..f3ec4ecb 100644 --- a/packages/web-components/src/components/cbp-form-field/cbp-form-field.scss +++ b/packages/web-components/src/components/cbp-form-field/cbp-form-field.scss @@ -58,6 +58,10 @@ cbp-form-field { all: unset; } + fieldset{ + display: block; + } + .cbp-form-field-label { display: block; color: var(--cbp-form-field-color-label); diff --git a/packages/web-components/src/components/cbp-toggle/cbp-toggle.scss b/packages/web-components/src/components/cbp-toggle/cbp-toggle.scss index 48a4f98b..7533d6af 100644 --- a/packages/web-components/src/components/cbp-toggle/cbp-toggle.scss +++ b/packages/web-components/src/components/cbp-toggle/cbp-toggle.scss @@ -1,17 +1,238 @@ /** - * @Prop --cbp-toggle-XXX + * @Prop --cbp-toggle-color-bg: var(--cbp-color-interactive-secondary-light); + * @Prop --cbp-toggle-color-bg-dark: var(--cbp-color-interactive-secondary-dark); + * @Prop --cbp-toggle-color-circle: var(--cbp-color-white); + * @Prop --cbp-toggle-color-circle-dark: var(--cbp-color-white); + * @Prop --cbp-toggle-color-circle-border: var(--cbp-toggle-color-circle); + * @Prop --cbp-toggle-color-circle-border-dark: var(--cbp-toggle-color-circle-dark); + * @Prop --cbp-toggle-outline-width: 2px; + * @Prop --cbp-toggle-outline-style: solid; + * @Prop --cbp-toggle-outline-color: transparent; + * @Prop --cbp-toggle-outline: var(--cbp-toggle-outline-width) var(--cbp-toggle-outline-style) var(--cbp-toggle-outline-color); + * @Prop --cbp-toggle-outline-color-dark: 2px solid transparent; + * @Prop --cbp-toggle-outline-dark: var(--cbp-toggle-outline-width) var(--cbp-toggle-outline-style) var(--cbp-toggle-outline-color-dark); + * @Prop --cbp-toggle-color-bg-hover: var(--cbp-color-interactive-secondary-dark); + * @Prop --cbp-toggle-color-bg-hover-dark: var(--cbp-color-interactive-secondary-base); + * @Prop --cbp-toggle-circle-selected-color-hover: var(--cbp-color-interactive-selected-dark); + * @Prop --cbp-toggle-circle-selected-color-hover-dark: var(--cbp-color-interactive-selected-light); + * @Prop --cbp-toggle-circle-selected-border-color-hover: var(--cbp-color-white); + * @Prop --cbp-toggle-circle-selected-border-color-hover-dark: var(--cbp-color-interactive-secondary-darker); + * @Prop --cbp-toggle-color-circle-hover: var(--cbp-color-interactive-secondary-darker); + * @Prop --cbp-toggle-color-circle-hover-dark: var(--cbp-color-interactive-secondary-base); + * @Prop --cbp-toggle-color-circle-border-hover: var(--cbp-color-white); + * @Prop --cbp-toggle-color-circle-border-hover-dark: var(--cbp-color-white); + * @Prop --cbp-toggle-color-bg-focus: var(--cbp-color-interactive-focus-dark); + * @Prop --cbp-toggle-color-bg-focus-dark: var(--cbp-color-interactive-focus-light); + * @Prop --cbp-toggle-outline-focus: 2px solid var(--cbp-color-white); + * @Prop --cbp-toggle-outline-focus-dark: 2px solid var(--cbp-color-black); + * @Prop --cbp-toggle-color-bg-disabled: var(--cbp-color-interactive-disabled-dark); + * @Prop --cbp-toggle-color-bg-disabled-dark: var(--cbp-color-interactive-disabled-light); + * @Prop --cbp-toggle-color-circle-disabled: var(--cbp-color-interactive-disabled-light); + * @Prop --cbp-toggle-color-circle-disabled-dark: var(--cbp-color-interactive-disabled-dark); + * @Prop --cbp-toggle-color-circle-border-disabled: var(--cbp-color-interactive-disabled-light); + * @Prop --cbp-toggle-color-circle-border-disabled-dark: var(--cbp-color-interactive-disabled-dark); + * @Prop --cbp-toggle-control-width: 3.25rem; + * @Prop --cbp-toggle-control-height: 1.75rem; + * @Prop --cbp-toggle-circle-diameter: 1rem; + * @Prop --cbp-toggle-circle-inset: 0.375rem; + * @Prop --cbp-toggle-circle-border-width: 0.25rem; + * @Prop --cbp-toggle-gap: 1rem; + * @Prop --cbp-toggle-margin: 0 0 var(--cbp-space-4x) 0; */ -// :root{ -// --cbp-toggle-XXX: red; -// } +:root{ + --cbp-toggle-color-bg: var(--cbp-color-interactive-secondary-light); + --cbp-toggle-color-bg-dark: var(--cbp-color-interactive-secondary-dark); + --cbp-toggle-color-circle: var(--cbp-color-white); + --cbp-toggle-color-circle-dark: var(--cbp-color-white); + --cbp-toggle-color-circle-border: var(--cbp-toggle-color-circle); + --cbp-toggle-color-circle-border-dark: var(--cbp-toggle-color-circle-dark); + + --cbp-toggle-outline-width: 2px; + --cbp-toggle-outline-style: solid; + --cbp-toggle-outline-color: transparent; + --cbp-toggle-outline-color-dark: transparent; -// [data-cbp-theme=light] cbp-toggle[context*=dark]:not([context=light-always]), -// [data-cbp-theme=dark] cbp-toggle:not([context=dark-inverts]):not([context=light-always]) { + /** Hover */ + --cbp-toggle-color-bg-hover: var(--cbp-color-interactive-secondary-dark); + --cbp-toggle-color-bg-hover-dark: var(--cbp-color-interactive-secondary-base); + + --cbp-toggle-circle-selected-color-hover: var(--cbp-color-interactive-selected-dark); + --cbp-toggle-circle-selected-color-hover-dark: var(--cbp-color-interactive-selected-light); + --cbp-toggle-circle-selected-border-color-hover: var(--cbp-color-white); + --cbp-toggle-circle-selected-border-color-hover-dark: var(--cbp-color-interactive-secondary-darker); -// // --cbp-tooltip-color-bg: var(--cbp-tooltip-color-bg-dark); -// } + --cbp-toggle-color-circle-hover: var(--cbp-color-interactive-secondary-darker); + --cbp-toggle-color-circle-hover-dark: var(--cbp-color-interactive-secondary-base); + --cbp-toggle-color-circle-border-hover: var(--cbp-color-white); + --cbp-toggle-color-circle-border-hover-dark: var(--cbp-color-white); + + /** Focus */ + --cbp-toggle-color-bg-focus: var(--cbp-color-interactive-focus-dark); + --cbp-toggle-color-bg-focus-dark: var(--cbp-color-interactive-focus-light); + --cbp-toggle-outline-color-focus: var(--cbp-color-white); + --cbp-toggle-outline-color-focus-dark: var(--cbp-color-black); + + /** Disabled */ + --cbp-toggle-color-bg-disabled: var(--cbp-color-interactive-disabled-dark); + --cbp-toggle-color-bg-disabled-dark: var(--cbp-color-interactive-disabled-light); + --cbp-toggle-color-circle-disabled: var(--cbp-color-interactive-disabled-light); + --cbp-toggle-color-circle-disabled-dark: var(--cbp-color-interactive-disabled-dark); + --cbp-toggle-color-circle-border-disabled: var(--cbp-color-interactive-disabled-light); + --cbp-toggle-color-circle-border-disabled-dark: var(--cbp-color-interactive-disabled-dark); + + --cbp-toggle-text-color: var(--cbp-color-text-darkest); + --cbp-toggle-text-color-dark: var(--cbp-color-text-lightest); + + --cbp-toggle-control-width: 3.25rem; + --cbp-toggle-control-height: 1.75rem; + --cbp-toggle-circle-diameter: 1rem; + --cbp-toggle-circle-inset: 0.375rem; + --cbp-toggle-circle-border-width: 0.25rem; + --cbp-status-text-width: var(--cbp-space-16x); + --cbp-toggle-description-text-width: min-content; + + --cbp-toggle-gap: var(--cbp-space-4x); + --cbp-toggle-margin: 0 0 var(--cbp-space-4x) 0; +} + +[data-cbp-theme=light] cbp-toggle[context*=dark]:not([context=light-always]), +[data-cbp-theme=dark] cbp-toggle:not([context=dark-inverts]):not([context=light-always]) { + + --cbp-toggle-color-bg: var(--cbp-toggle-color-bg-dark); + --cbp-toggle-color-circle: var(--cbp-toggle-color-circle-dark); + --cbp-toggle-color-circle-border: var(--cbp-toggle-color-circle-border-dark); + --cbp-toggle-outline-color: var(--cbp-toggle-outline-color-dark); + + --cbp-toggle-color-bg-hover: var(--cbp-toggle-color-bg-hover-dark); + --cbp-toggle-circle-selected-color-hover: var(--cbp-toggle-circle-selected-color-hover-dark); + --cbp-toggle-circle-selected-border-color-hover: var(--cbp-toggle-circle-selected-border-color-hover-dark); + --cbp-toggle-color-circle-hover: var(--cbp-toggle-color-circle-hover-dark); + --cbp-toggle-color-circle-border-hover: var(--cbp-toggle-color-circle-border-hover-dark); + + --cbp-toggle-color-bg-focus: var(--cbp-toggle-color-bg-focus-dark); + --cbp-toggle-outline-color-focus: var(--cbp-toggle-outline-color-focus-dark); + + --cbp-toggle-color-bg-disabled: var(--cbp-toggle-color-bg-disabled-dark); + --cbp-toggle-color-circle-disabled: var(--cbp-toggle-color-circle-disabled-dark); + --cbp-toggle-color-circle-border-disabled: var(--cbp-toggle-color-circle-border-disabled-dark); + + --cbp-toggle-text-color: var(--cbp-toggle-text-color-dark); +} cbp-toggle { - border: 1px solid red; //TODO: local testing, remove before push + display: flex; + align-items: center; + margin: var(--cbp-toggle-margin); + color: var(--cbp-toggle-text-color); + + label{ + display: grid; + grid-template-columns: 3fr var(--cbp-toggle-control-width) 1fr; + gap: var(--cbp-toggle-gap); + align-items: center; + width: 100%; + } + + input[type='checkbox']{ + appearance: none; + height: var(--cbp-toggle-control-height); + min-width: var(--cbp-toggle-control-width); + flex-basis: var(--cbp-toggle-control-width); + border-radius: var(--cbp-border-radius-pill); + background-color: var(--cbp-toggle-color-bg); + outline: var(--cbp-toggle-outline-width) var(--cbp-toggle-outline-style) var(--cbp-toggle-outline-color); + outline-offset: calc( -1 * var(--cbp-space-1x)); + + &:before{ + content: ""; + display: block; + margin-top: var(--cbp-toggle-circle-inset); + border-radius: var(--cbp-border-radius-circle); + height: var(--cbp-toggle-circle-diameter); + width: var(--cbp-toggle-circle-diameter); + background-color: var(--cbp-toggle-color-circle); + border: var(--cbp-toggle-circle-border-width) solid var(--cbp-toggle-color-circle-border); + } + } + + span:last-child{ + flex-basis: var(--cbp-status-text-width); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + /** 'On' States */ + &[checked] input[type='checkbox']{ + --cbp-toggle-color-bg: var(--cbp-toggle-color-bg-hover); + --cbp-toggle-color-bg-dark: var(--cbp-toggle-color-bg-hover-dark); + --cbp-toggle-color-circle: var(--cbp-color-white); + --cbp-toggle-color-circle-dark: var(--cbp-color-interactive-secondary-darker); + --cbp-toggle-color-circle-border-dark: var(--cbp-color-interactive-secondary-darker); + + &:before{ + margin-left: calc(var(--cbp-toggle-control-width) - var(--cbp-toggle-circle-diameter) - var(--cbp-toggle-circle-inset)); + } + + &:hover:not([disabled]){ + --cbp-toggle-color-circle: var(--cbp-toggle-circle-selected-color-hover); + --cbp-toggle-color-circle-border: var(--cbp-toggle-color-circle-border-hover); + + --cbp-toggle-color-circle-dark: var(--cbp-toggle-circle-selected-color-hover-dark); + --cbp-toggle-color-circle-border-dark: var(--cbp-toggle-circle-selected-border-color-hover-dark); + } + + &:focus:not([disabled]){ + + --cbp-toggle-color-bg: var(--cbp-toggle-color-bg-focus); + --cbp-toggle-outline-color: var(--cbp-toggle-outline-color-focus); + + --cbp-toggle-color-bg-dark: var(--cbp-toggle-color-bg-focus-dark); + --cbp-toggle-outline-color-dark: var(--cbp-toggle-outline-color-focus-dark); + } + } + + /** 'Off' States */ + &:not([checked]) input[type='checkbox']{ + + &:before{ + margin-left: var(--cbp-toggle-circle-inset); + } + + &:hover:not([disabled]) { + --cbp-toggle-color-bg: var(--cbp-toggle-color-bg-hover); + --cbp-toggle-color-circle: var(--cbp-toggle-color-circle-hover); + --cbp-toggle-color-circle-border: var(--cbp-toggle-color-circle-border-hover); + + --cbp-toggle-color-bg-dark: var(--cbp-toggle-color-bg-hover-dark); + --cbp-toggle-color-circle-dark: var(--cbp-toggle-color-circle-hover-dark); + --cbp-toggle-color-circle-border-dark: var(--cbp-toggle-color-circle-border-hover-dark); + } + + &:focus:not([disabled]){ + --cbp-toggle-color-bg: var(--cbp-toggle-color-bg-focus); + --cbp-toggle-outline-color: var(--cbp-toggle-outline-color-focus); + + --cbp-toggle-color-bg-dark: var(--cbp-toggle-color-bg-focus-dark); + --cbp-toggle-outline-color-dark: var(--cbp-toggle-outline-color-focus-dark); + --cbp-toggle-color-circle-dark: var(--cbp-color-interactive-secondary-darker); + --cbp-toggle-color-circle-border-dark: var(--cbp-color-interactive-secondary-darker);; + } + } + + /** Disabled*/ + &[disabled]{ + --cbp-toggle-color-bg: var(--cbp-toggle-color-bg-disabled); + --cbp-toggle-color-bg-dark: var(--cbp-toggle-color-bg-disabled-dark); + --cbp-toggle-color-circle: var(--cbp-toggle-color-circle-disabled); + --cbp-toggle-color-circle-dark: var(--cbp-toggle-color-circle-disabled-dark); + --cbp-toggle-color-circle-border: var(--cbp-toggle-color-circle-border-disabled); + --cbp-toggle-color-circle-border-dark: var(--cbp-toggle-color-circle-border-disabled-dark); + } + + // override a browser default + &:focus-visible{ + outline: none; + } } diff --git a/packages/web-components/src/components/cbp-toggle/cbp-toggle.specs.mdx b/packages/web-components/src/components/cbp-toggle/cbp-toggle.specs.mdx index 8bebbb81..b5bd7315 100644 --- a/packages/web-components/src/components/cbp-toggle/cbp-toggle.specs.mdx +++ b/packages/web-components/src/components/cbp-toggle/cbp-toggle.specs.mdx @@ -10,7 +10,7 @@ import { Meta } from '@storybook/addon-docs'; ## Functional Requirements -* The Toggle component provides visual styling for a checkbox form control to appear as a toggle switch, including its various states, including hover, focus, disabled, and checked states. +* The Toggle component provides visual styling for a checkbox form control to appear as a toggle switch, including its various states, including hover, focus, disabled, for both checked & unchecked states. * The Toggle component is a native HTML `input type="checkbox"` under the hood, which is slotted. * The Toggle component places the label to the left of the control, unlike the Checkbox component. * The Toggle component requires a label for an accessible name for form control. diff --git a/packages/web-components/src/components/cbp-toggle/cbp-toggle.stories.tsx b/packages/web-components/src/components/cbp-toggle/cbp-toggle.stories.tsx new file mode 100644 index 00000000..f14294e1 --- /dev/null +++ b/packages/web-components/src/components/cbp-toggle/cbp-toggle.stories.tsx @@ -0,0 +1,140 @@ +export default { + title: 'Components/Toggle', + //tags: ['autodocs'], + argTypes: { + hideStatus: { + description: 'Determines if the status text for the `on` and `off` is visible for the toggle control', + control: 'boolean' + }, + disabled: { + description: 'Sets the disable state for the toggle control', + control: 'boolean' + }, + statusTextOn: { + description: 'Sets the label for the `on` state of the toggle control', + control: 'text' + }, + statusTextOff: { + description: 'Sets the label for the `off` state of the toggle control', + control: 'text' + }, + context : { + control: 'select', + options: [ "light-inverts", "light-always", "dark-inverts", "dark-always"] + }, + sx: { + description: 'Supports adding inline styles as an object of key-value pairs comprised of CSS properties and values. Values should reference design tokens when possible.', + control: 'object', + }, + } + }; + +const Template = ({label, checked, name, value, hideStatus, statusTextOn, statusTextOff, disabled, context, sx }) => { + return ` + + ${label} + + + + `; +}; + +export const Toggle = Template.bind({}); + +Toggle.args = { + label: 'Toggle Label' +} +Toggle.argTypes = { + label: { + description: 'Sets the label for the toggle control', + control:'text' + }, + checked: { + description: 'Sets the state of the toggle', + control: 'boolean' + }, + name: { + description: 'Specifies the `name` attribute of the slotted checkbox.', + control: 'text', + }, + value: { + description: 'Specifies the `value` attribute of the slotted checkbox.', + control: 'text', + }, +} + + +function generateToggles(items, labelWidth, hideStatus, statusTextOn, statusTextOff, disabled, context, sx){ + const html = items.map(({label}, i) => { + return ` + ${label} + + + `; + }); + return html.join(''); + } + +const MultipleTemplate = ({ToggleItems, labelWidth, hideStatus, statusTextOn, statusTextOff, disabled, context, sx }) => { + return ` + + ${generateToggles(ToggleItems, labelWidth, hideStatus, statusTextOn, statusTextOff, disabled, context, sx )} + + `; +} + +export const MultipleToggle = MultipleTemplate.bind({}); + +MultipleToggle.args={ + ToggleItems: [ + { + label: 'Toggle #1', + disabled: false, + }, + { + label: 'Toggle #2', + disabled: false, + }, + { + label: 'Toggle #3', + disabled: false, + }, + { + label: 'Toggle #4', + disabled: false, + }, + { + label: 'Toggle #5', + disabled: false, + }, + ], + labelWidth: '10rem', +} \ No newline at end of file diff --git a/packages/web-components/src/components/cbp-toggle/cbp-toggle.tsx b/packages/web-components/src/components/cbp-toggle/cbp-toggle.tsx index 34747a2e..9846fe02 100644 --- a/packages/web-components/src/components/cbp-toggle/cbp-toggle.tsx +++ b/packages/web-components/src/components/cbp-toggle/cbp-toggle.tsx @@ -1,4 +1,4 @@ -import { Component, Element, Prop, Host, h } from '@stencil/core'; +import { Component, Element, Event, EventEmitter, Prop, Host, h, Watch} from '@stencil/core'; import { setCSSProps } from '../../utils/utils'; @Component({ @@ -7,22 +7,29 @@ import { setCSSProps } from '../../utils/utils'; }) export class CbpToggle { + private formField: HTMLInputElement; @Element() host: HTMLElement; /** Marks the toggle as checked by default when specified. */ - @Prop() checked: boolean; + @Prop({ reflect: true }) checked: boolean; /** Marks the toggle in a disabled state when specified. */ - @Prop() disabled: boolean; + @Prop({ reflect: true }) disabled: boolean; /** Determines if the status text is visible for the render*/ - @Prop() hideStatus: boolean = true; + @Prop() hideStatus: boolean; /** Determines the status text for the true toggle*/ - @Prop() statusTextOn: string = 'on'; + @Prop() statusTextOn: string = 'On'; /** Determines the status text for the false toggle*/ - @Prop() statusTextOff: string = 'off'; + @Prop() statusTextOff: string = 'Off'; + + /** The `name` attribute of the checkbox, which is passed as part of formData (as a key) only when the checkbox is checked. */ + @Prop() name: string; + + /** Optionally set the `value` attribute of the checkbox at the component level. Not needed if the slotted checkbox has a value. */ + @Prop() value: string; /** Specifies the context of the component as it applies to the visual design and whether it inverts when light/dark mode is toggled. Default behavior is "light-inverts" and does not have to be specified. */ @Prop({ reflect: true }) context: "light-inverts" | "light-always" | "dark-inverts" | "dark-always"; @@ -37,20 +44,56 @@ export class CbpToggle { setCSSProps(this.host, { ...this.sx, }); + + // query the DOM for the slotted form field and wire it up for accessibility and attach an event listener to it + this.formField = this.host.querySelector('input[type=checkbox]'); + if (this.formField) { + this.formField.addEventListener('change', () => this.toggleEvent()); + } + } + + componentDidLoad() { + // Set the disabled/indeterminate states on load only if true. (The Watch decorators only listen for changes, not initial state) + if (!!this.formField) { + if (this.checked) this.formField.checked=this.checked; + if (this.disabled) this.formField.setAttribute('disabled', ''); + if (this.name) this.formField.name=this.name; + if (this.value) this.formField.value=this.value; + } } + @Watch('disabled') + watchDisabledHandler(newValue: boolean) { + if (this.formField) { + (newValue) + ? this.formField.setAttribute('disabled', '') + : this.formField.removeAttribute('disabled'); + } + } + + @Event() toggleClick: EventEmitter; + /** Event: toggles the control true/false & updates DOM accordingly*/ toggleEvent(){ - //TODO: logic here + this.checked=this.formField.checked; + this.toggleClick.emit({ + host: this.host, + nativeElement: this.formField, + value: this.formField.value, + checked: this.formField.checked + }); } render() { - /**boilerplate HTML */ - return ( - - - - ); + + return ( + + + + ); } }