Skip to content

Commit

Permalink
Merge pull request #175 from US-CBP/feature/form-field-wrapper
Browse files Browse the repository at this point in the history
Feature/form field wrapper
  • Loading branch information
dgibson666 authored Aug 7, 2024
2 parents e8e3791 + e882f1f commit 4335a64
Show file tree
Hide file tree
Showing 10 changed files with 575 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const CbpFlex = /*@__PURE__*/createReactComponent<JSX.CbpFlex, HTMLCbpFle
export const CbpFlexItem = /*@__PURE__*/createReactComponent<JSX.CbpFlexItem, HTMLCbpFlexItemElement>('cbp-flex-item');
export const CbpFooter = /*@__PURE__*/createReactComponent<JSX.CbpFooter, HTMLCbpFooterElement>('cbp-footer');
export const CbpFormField = /*@__PURE__*/createReactComponent<JSX.CbpFormField, HTMLCbpFormFieldElement>('cbp-form-field');
export const CbpFormFieldWrapper = /*@__PURE__*/createReactComponent<JSX.CbpFormFieldWrapper, HTMLCbpFormFieldWrapperElement>('cbp-form-field-wrapper');
export const CbpGrid = /*@__PURE__*/createReactComponent<JSX.CbpGrid, HTMLCbpGridElement>('cbp-grid');
export const CbpGridItem = /*@__PURE__*/createReactComponent<JSX.CbpGridItem, HTMLCbpGridItemElement>('cbp-grid-item');
export const CbpHide = /*@__PURE__*/createReactComponent<JSX.CbpHide, HTMLCbpHideElement>('cbp-hide');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* @prop --cbp-form-field-wrapper-padding-start: inherit;
* @prop --cbp-form-field-wrapper-padding-end: inherit;
*/
:root {
--cbp-form-field-wrapper-padding-start: var(--cbp-form-field-padding-inline);
--cbp-form-field-wrapper-padding-end: var(--cbp-form-field-padding-inline);

// These are used by the component and are not intended to be used by consumers
--cbp-form-field-overlay-start-width: 0;
--cbp-form-field-overlay-end-width: 0;
--cbp-form-field-attached-button-width: 0;
}

cbp-form-field-wrapper {
position: relative;

.cbp-form-field-wrapper-shrinkwrap {
display: block;
position: relative;

// Override the input padding based on overlay size to prevent input text from being obscured (text may still be obscured if there's not enough space for it)
input {
padding-inline-start: calc(var(--cbp-form-field-overlay-start-width) + var(--cbp-form-field-wrapper-padding-start));
padding-inline-end: calc(var(--cbp-form-field-overlay-end-width) + var(--cbp-form-field-wrapper-padding-end));
}

// All named slots within the shrinkwrap element are overlays
[slot] {
position: absolute;
display: inline-flex;
align-items: center;
height: 100%;
color: var(--cbp-form-field-color);
}

[slot="cbp-form-field-overlay-start"] {
inset-inline-start: var(--cbp-space-2x);
font-weight: var(--cbp-font-weight-bold);
font-style: italic;
}

[slot="cbp-form-field-overlay-end"] {
inset-inline-end: calc(var(--cbp-form-field-attached-button-width) + var(--cbp-space-2x));
font-style: italic;
}

// Attached buttons act like an overlay in order to wrap the input focus highlight around them.
[slot="cbp-form-field-attached-button"] {
--cbp-button-border-radius: 0 var(--cbp-border-radius-soft) var(--cbp-border-radius-soft) 0;
inset-inline-end: 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Meta } from '@storybook/addon-docs';

<Meta title="Components/Form Field Wrapper/Specifications" />

# cbp-form-field-wrapper

## Purpose

The Form Field Wrapper component offers means for applying overlays and button controls to inputs in accordance with design requirements.

## Functional Requirements

* The Form Field Wrapper component is meant to be used in the default slot of the `cbp-form-field` component, wrapping the native input.
* The Form Field Wrapper component provides a structured way to add additional overlays and related controls to an input field via named slots:
* A left-aligned overlay (e.g., "$")
* A right-aligned overlay (e.g., "kg")
* Buttons that appear attached to the input (e.g., search or password toggle)
* Buttons that appear next to the input, but not attached (e.g., increment/decrement on numeric inputs)

## Technical Specifications

### User Interactions

* The Form Field Wrapper component does not provide any native interactions.
* Any user interactions based on the slotted button controls are application-specified.

### Responsiveness

* Form controls such as text input, textarea, and selects are styled at 100% of their container by default, with a max-width of 100% so that they cannot break out of their container.

### Accessibility

* TBD

### Additional Notes and Considerations

* TBD
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
export default {
title: 'Components/Form Field Wrapper',
tags: ['autodocs'],
argTypes: {
label: {
control: 'text',
},
description: {
control: 'text',
},
fieldId: {
control: 'text',
},
inputType:{
control: 'select',
options: [ "text", "number", "password", "search"] // additional types: email, tel, url, color, range, date, datetime-local, month, week, time
},
error: {
control: 'boolean',
},
readonly: {
control: 'boolean',
},
disabled: {
control: 'boolean',
},
overlayStart: {
control: 'text',
},
overlayEnd: {
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',
},
},
args: {
label: 'Field Label',
description: 'Field description.',
inputType: 'text'
},
};

const InputWithOverlaysTemplate = ({ label, description, inputType, overlayStart, overlayEnd, fieldId, error, readonly, disabled, value, context, sx }) => {
return `
<cbp-form-field
${label ? `label="${label}"` : ''}
${description ? `description="${description}"` : ''}
${fieldId ? `field-id="${fieldId}"` : ''}
${error ? `error` : ''}
${context && context != 'light-inverts' ? `context=${context}` : ''}
${sx ? `sx=${JSON.stringify(sx)}` : ''}
>
<cbp-form-field-wrapper
${sx ? `sx=${JSON.stringify(sx)}` : ''}
>
<input type="${inputType}" name="textinput" ${value ? `value="${value}"` : ''} ${readonly ? `readonly` : ''} ${disabled ? `disabled` : ''} />
${overlayStart != undefined ? `<span slot="cbp-form-field-overlay-start">${overlayStart}</span>` : ''}
${overlayEnd != undefined ? `<span slot="cbp-form-field-overlay-end">${overlayEnd}</span>` : ''}
</cbp-form-field-wrapper>
</cbp-form-field>
`;
};

export const InputWithOverlays = InputWithOverlaysTemplate.bind({});
InputWithOverlays.args = {
value: '',
};

// TechDebt: needs an event listener to swap the button's icon and input type (password | text)
const PasswordTemplate = ({ label, description, inputType, overlayStart, overlayEnd, fieldId, error, readonly, disabled, value, context, sx }) => {
return `
<cbp-form-field
${label ? `label="${label}"` : ''}
${description ? `description="${description}"` : ''}
${fieldId ? `field-id="${fieldId}"` : ''}
${error ? `error` : ''}
${readonly ? `readonly` : ''}
${disabled ? `disabled` : ''}
${context && context != 'light-inverts' ? `context=${context}` : ''}
${sx ? `sx=${JSON.stringify(sx)}` : ''}
>
<cbp-form-field-wrapper
${sx ? `sx=${JSON.stringify(sx)}` : ''}
>
<input
type="${inputType}"
name="search"
${value ? `value="${value}"` : ''}
/>
${overlayStart != undefined ? `<span slot="cbp-form-field-overlay-start">${overlayStart}</span>` : ''}
${overlayEnd != undefined ? `<span slot="cbp-form-field-overlay-end">${overlayEnd}</span>` : ''}
<span slot="cbp-form-field-attached-button">
<cbp-button
type="submit"
fill="solid"
color="secondary"
variant="square"
accessibility-text="Toggle visibility"
aria-controls="${fieldId}"
>
<cbp-icon name="eye"></cbp-icon>
</cbp-button>
</span>
</cbp-form-field-wrapper>
</cbp-form-field>
`;
};

export const Password = PasswordTemplate.bind({});
Password.args = {
label: 'Password',
description: '',
fieldId: 'pw',
inputType: 'password',
value: '',
};


const SearchTemplate = ({ label, description, inputType, overlayStart, overlayEnd, fieldId, error, readonly, disabled, value, context, sx }) => {
return `
<cbp-form-field
${label ? `label="${label}"` : ''}
${description ? `description="${description}"` : ''}
${fieldId ? `field-id="${fieldId}"` : ''}
${error ? `error` : ''}
${readonly ? `readonly` : ''}
${disabled ? `disabled` : ''}
${context && context != 'light-inverts' ? `context=${context}` : ''}
${sx ? `sx=${JSON.stringify(sx)}` : ''}
>
<cbp-form-field-wrapper
${sx ? `sx=${JSON.stringify(sx)}` : ''}
>
<input
type="${inputType}"
name="search"
${value ? `value="${value}"` : ''}
${readonly ? `readonly` : ''}
${disabled ? `disabled` : ''}
/>
${overlayStart != undefined ? `<span slot="cbp-form-field-overlay-start">${overlayStart}</span>` : ''}
${overlayEnd != undefined ? `<span slot="cbp-form-field-overlay-end">${overlayEnd}</span>` : ''}
<span slot="cbp-form-field-attached-button">
<cbp-button
type="submit"
fill="solid"
color="secondary"
variant="square"
accessibility-text="Search"
>
<cbp-icon name="magnifying-glass"></cbp-icon>
</cbp-button>
</span>
</cbp-form-field-wrapper>
</cbp-form-field>
`;
};

export const Search = SearchTemplate.bind({});
Search.args = {
label: 'Search',
description: '',
fieldId: 'search',
inputType: 'search',
value: '',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Component, Element, Host, h } from '@stencil/core';
import { setCSSProps } from '../../utils/utils';

/**
* @slot - The default slot holds the form control.
* @slot cbp-form-field-overlay-start - Holds an overlay positioned on the left side of the form field.
* @slot cbp-form-field-overlay-end - Holds an overlay positioned on the right side of the form field.
* @slot cbp-form-field-attached-button
* @slot cbp-form-field-unattached-buttons
*/
@Component({
tag: 'cbp-form-field-wrapper',
styleUrl: 'cbp-form-field-wrapper.scss'
})

export class CbpFormFieldWrapper {

private overlayStartWidth;
private overlayEndWidth;
private attachedButtonWidth;

@Element() host: HTMLElement;

componentDidLoad() {
// Calculate the size of the overlays to set the input padding accordingly
// TechDebt: as a first cut, this is not reactive. How reactive does it need to be?
const overlayStart: HTMLElement = this.host.querySelector('[slot="cbp-form-field-overlay-start"]');
this.overlayStartWidth = overlayStart ? overlayStart.offsetWidth + 8 : 0;

const overlayEnd: HTMLElement = this.host.querySelector('[slot="cbp-form-field-overlay-end"]');
this.overlayEndWidth = overlayEnd ? overlayEnd.offsetWidth + 8 : 0;

const attachedButton: HTMLElement = this.host.querySelector('[slot="cbp-form-field-attached-button"]');
this.attachedButtonWidth = attachedButton ? attachedButton.offsetWidth : 0;

// Update this with the buttons size
this.overlayEndWidth = this.overlayEndWidth + this.attachedButtonWidth

setCSSProps(this.host, {
"--cbp-form-field-overlay-start-width": `${this.overlayStartWidth}px`,
"--cbp-form-field-overlay-end-width": `${this.overlayEndWidth}px`,
"--cbp-form-field-attached-button-width": `${this.attachedButtonWidth}px`,
});
}

render() {
return (
<Host>
<div class="cbp-form-field-wrapper-shrinkwrap">
<slot name="cbp-form-field-overlay-start" />
<slot />
<slot name="cbp-form-field-overlay-end" />
<slot name="cbp-form-field-attached-button" />
</div>
<slot name="cbp-form-field-unattached-buttons" />
</Host>
);
}

}
Loading

0 comments on commit 4335a64

Please sign in to comment.