-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #175 from US-CBP/feature/form-field-wrapper
Feature/form field wrapper
- Loading branch information
Showing
10 changed files
with
575 additions
and
131 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
packages/web-components/src/components/cbp-form-field-wrapper/cbp-form-field-wrapper.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
...mponents/src/components/cbp-form-field-wrapper/cbp-form-field-wrapper.specs.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
180 changes: 180 additions & 0 deletions
180
...s/web-components/src/components/cbp-form-field-wrapper/cbp-form-field-wrapper.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: '', | ||
}; |
60 changes: 60 additions & 0 deletions
60
packages/web-components/src/components/cbp-form-field-wrapper/cbp-form-field-wrapper.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} | ||
|
||
} |
Oops, something went wrong.