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 (
+
+
+
+ );
}
}