Skip to content

Commit

Permalink
Merge pull request #180 from US-CBP/feature/numeric-input
Browse files Browse the repository at this point in the history
Feature/numeric input
  • Loading branch information
dgibson666 authored Aug 14, 2024
2 parents 84e1c9d + 134f903 commit 0add37f
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 14 deletions.
16 changes: 12 additions & 4 deletions packages/web-components/src/components/cbp-button/cbp-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export class CbpButton {
@Prop({ reflect: true }) color: 'primary' | 'secondary' | 'danger' = 'primary';
/** Specifies a variant of the buttons, such as square for buttons with only an icon and call-to-action button. */
@Prop({ reflect: true }) variant: 'square' | 'cta';
/** The `value` attribute of the button, which is passed as part of formData for the the pressed submit button. */

/** The `name` attribute of the button, which is passed as part of formData (as a key) for the the pressed submit button. */
@Prop() name: string;
/** The `value` attribute of the button, which is passed as part of formData (as a value) for the the pressed submit button. */
@Prop() value: string;

/** The `href` attribute of a link button. */
Expand Down Expand Up @@ -100,13 +103,16 @@ export class CbpButton {
}
}

//console.log('this.button',this.button);

this.buttonClick.emit({
host: this.host,
nativeElement: this.button,
controls: this.controls ? this.controls : null,
pressed: this.pressed,
expanded: this.expanded,
value: this.button.tagName == 'button' ? this.button.value : null,
name: this.button.tagName == 'BUTTON' ? this.button.name : null,
value: this.button.tagName == 'BUTTON' ? this.button.value : null,
});
}

Expand Down Expand Up @@ -157,17 +163,19 @@ export class CbpButton {
this.componentLoad.emit({
host: this.host,
nativeElement: this.button,
value: this.button.tagName == 'button' ? this.button.value : null,
name: this.button.tagName == 'BUTTON' ? this.button.name : null,
value: this.button.tagName == 'BUTTON' ? this.button.value : null,
});
}

render() {
const { type, value, pressed, expanded, disabled, rel, target, href, download } = this;
const { type, name, value, pressed, expanded, disabled, rel, target, href, download } = this;

const attrs =
this.tag === 'button'
? {
type,
name,
value,
disabled,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@

cbp-form-field-wrapper {
position: relative;
display: flex;
gap: var(--cbp-space-4x);

.cbp-form-field-wrapper-shrinkwrap {
display: block;
position: relative;
display: block;
flex-basis: 100%; // for child flex context

// 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 {
Expand Down Expand Up @@ -51,4 +54,9 @@ cbp-form-field-wrapper {
inset-inline-end: 0;
}
}

[slot="cbp-form-field-unattached-buttons"] {
display: flex;
gap: var(--cbp-space-4x);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ The Form Field Wrapper component offers means for applying overlays and button c

* TBD

### Specific Pattern Requirements (implemented by application logic)

* Password/Obfuscated field toggle:
* The field should include an `input type="password"` with an attached button showing the `eye` icon.
* Upon activating the button, the icon is toggled to `eye-slash` and the input changed to an appropriate type (usually `type="text"`).
* Numeric Counter field:
* The field should include an `input type="number"` with an unattached decrement and increment buttons per the story.
* Clicking either button should increment/decrement by the defined `step` attribute on the input.
* If no step is defined, default it to 1.
* If no value is defined, default it to 0.
* Remember that even for `input type="number"` the input value is a string and must be converted to a number to perform arithmetic on. The `Number()` function works great, as does `parseInt()` for integers.

### Additional Notes and Considerations

* TBD
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export default {
},
};



const InputWithOverlaysTemplate = ({ label, description, inputType, overlayStart, overlayEnd, fieldId, error, readonly, disabled, value, context, sx }) => {
return `
<cbp-form-field
Expand Down Expand Up @@ -74,8 +76,115 @@ InputWithOverlays.args = {
value: '',
};

// TechDebt: needs an event listener to swap the button's icon and input type (password | text)


const NumericCounterTemplate = ({ label, description, inputType, overlayStart, overlayEnd, fieldId, error, readonly, disabled, value, context, sx }) => {

// Ideally, this should be placed on the button component itself, not the document; but the event bubbles, so it works here.
document.addEventListener('buttonClick', function(e) {
const buttonComponent = e.target as HTMLCbpButtonElement;
const button = buttonComponent.querySelector('button') as HTMLButtonElement;
const input: HTMLInputElement = document.querySelector(`#${fieldId}`) || document.querySelector('input');
const step:number = Number(input.getAttribute('step')) || 1;

let value:number = Number(input.value) || 0;
if(button.getAttribute('name')==="increment") {
input.value = `${value + step}`;
}
if(button.getAttribute('name')==="decrement") {
input.value = `${value - step}`;
}
});


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-unattached-buttons">
<cbp-button
name="decrement"
type="button"
fill="outline"
color="secondary"
variant="square"
accessibility-text="Toggle visibility"
aria-controls="${fieldId}"
>
<cbp-icon name="minus" size="1rem"></cbp-icon>
</cbp-button>
<cbp-button
name="increment"
type="button"
fill="outline"
color="secondary"
variant="square"
accessibility-text="Toggle visibility"
aria-controls="${fieldId}"
>
<cbp-icon name="plus" size="1rem"></cbp-icon>
</cbp-button>
</span>
</cbp-form-field-wrapper>
</cbp-form-field>
`;
};

export const NumericCounter = NumericCounterTemplate.bind({});
NumericCounter.args = {
label: 'Numeric Counter Field',
description: '',
fieldId: 'numeric-input',
inputType: 'number',
value: '',
};



const PasswordTemplate = ({ label, description, inputType, overlayStart, overlayEnd, fieldId, error, readonly, disabled, value, context, sx }) => {

// Ideally, this should be placed on the button component itself, not the document; but the event bubbles, so it works here.
document.addEventListener('buttonClick', function(e) {
const buttonComponent = e.target as HTMLCbpButtonElement;
const button = buttonComponent.querySelector('button') as HTMLButtonElement;
const buttonIcon = buttonComponent.querySelector('cbp-icon');
const input = document.querySelector(`#${fieldId}`) || document.querySelector('input');
const type = input.getAttribute('type');

if(button.getAttribute('name')==="togglepw") {
// Toggle the input type
(input.getAttribute('type') !== 'password')
? input.setAttribute('type','password')
: input.setAttribute('type', type !== 'password' ? type : 'text');
// Toggle the button icon
input.getAttribute('type') !== 'password'
? buttonIcon.name = 'eye-slash'
: buttonIcon.name = 'eye';
}
});

return `
<cbp-form-field
${label ? `label="${label}"` : ''}
Expand All @@ -101,14 +210,15 @@ const PasswordTemplate = ({ label, description, inputType, overlayStart, overla
<span slot="cbp-form-field-attached-button">
<cbp-button
type="submit"
name="togglepw"
type="button"
fill="solid"
color="secondary"
variant="square"
accessibility-text="Toggle visibility"
aria-controls="${fieldId}"
>
<cbp-icon name="eye"></cbp-icon>
<cbp-icon name="eye" size="1rem"></cbp-icon>
</cbp-button>
</span>
Expand All @@ -127,6 +237,7 @@ Password.args = {
};



const SearchTemplate = ({ label, description, inputType, overlayStart, overlayEnd, fieldId, error, readonly, disabled, value, context, sx }) => {
return `
<cbp-form-field
Expand Down Expand Up @@ -161,7 +272,7 @@ const SearchTemplate = ({ label, description, inputType, overlayStart, overlayE
variant="square"
accessibility-text="Search"
>
<cbp-icon name="magnifying-glass"></cbp-icon>
<cbp-icon name="magnifying-glass" size="1rem"></cbp-icon>
</cbp-button>
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,24 @@ cbp-form-field {

// For readonly inputs, attached buttons become disabled but with different styling.
// These overrides are fairly minimal because the disabled state already sets the interactive states to the base color.
cbp-button:has(button:disabled) {
cbp-button[fill=solid]:has(button:disabled) {
--cbp-button-color: var(--cbp-color-text-lightest);
--cbp-button-color-bg: var(--cbp-color-interactive-secondary-light);
--cbp-button-color-border: var(--cbp-color-interactive-secondary-light);
--cbp-button-color-bg: var(--cbp-color-white);
--cbp-button-color-border: var(--cbp-form-field-color-border);

--cbp-button-color-dark: var(--cbp-color-text-base);
--cbp-button-color-bg-dark: var(--cbp-color-interactive-secondary-dark);
--cbp-button-color-border-dark: var(--cbp-color-interactive-secondary-dark);
--cbp-button-color-bg-dark: var(--cbp-form-field-color-border-dark);
--cbp-button-color-border-dark: var(--cbp-form-field-color-border-dark);
}

cbp-button[fill=outline]:has(button:disabled) {
--cbp-button-color: var(--cbp-color-interactive-secondary-base);
--cbp-button-color-bg: var(--cbp-color-interactive-secondary-light);
--cbp-button-color-border: var(--cbp-color-interactive-secondary-base);

--cbp-button-color-dark: var(--cbp-color-interactive-secondary-base);
--cbp-button-color-bg-dark: var(--cbp-form-field-color-bg-dark);
--cbp-button-color-border-dark: var(--cbp-color-interactive-secondary-base);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ The Form Field component represents a generic, reusable pattern for form fields
* Use `inputmode` instead of HTML5 input types to provide a browser hint for the proper virtual keyboard on mobile devices without side effects noted above.
* The `select` with `multiple` (and `size`) attribute should not be used, as they have terrible usability. Use our `cbp-multiselect` (coming soon) instead.
* Input types such as `date`, `datetime` (deprecated), `datetime-local`, `month`, `week`, `time` may also vary by browser and not be styleable in accordance with the design system.
* Use of the `size` attribute is discouraged as it does not represent a linear/consistent scale across input sizes and browsers.
* Furthermore, `size` is not valid on some input types such as `type="number"`.
* When necessary, it is recommended to use CSS to style the width of form fields (using a relative unit such as `ch` or `rem`) separate from their containers.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class CbpFormField {

private formField: any;
private buttons: any;
private attachedButtons: any;
private hasDescription: boolean;

@Element() host: HTMLElement;
Expand Down Expand Up @@ -111,6 +112,7 @@ export class CbpFormField {
// 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,select,textarea');
this.buttons = this.host.querySelectorAll('cbp-button');
this.attachedButtons = this.host.querySelectorAll('[slot=cbp-form-field-attached-button] cbp-button');
this.hasDescription = !!this.description || !!this.host.querySelector('[slot=cbp-form-field-description]');

if (this.formField) {
Expand All @@ -131,6 +133,11 @@ export class CbpFormField {
if (!!this.buttons) {
this.buttons.forEach( (el) => {
if (this.disabled || this.readonly) el.disabled=true;
});
}
// only attached buttons inherit the danger color when errors are present
if (!!this.attachedButtons) {
this.attachedButtons.forEach( (el) => {
if (this.error) el.color="danger";
});
}
Expand Down

0 comments on commit 0add37f

Please sign in to comment.