diff --git a/.changeset/four-lemons-warn.md b/.changeset/four-lemons-warn.md new file mode 100644 index 00000000000..8b21f988fa5 --- /dev/null +++ b/.changeset/four-lemons-warn.md @@ -0,0 +1,29 @@ +--- +"@spectrum-css/combobox": major +--- + +### Combobox S2 Migration + +#### New Changes + +- Removed quiet styling variant +- Updated corner radius to match S2 specifications +- Changed outline thickness for better visibility +- Replaced picker button with in-field button component +- Added help text along with invalid state +- Modified the WHCM invalid/error state in help text + +### New tokens + +`--spectrum-combobox-font-weight` +`--spectrum-combobox-line-height-cjk` +`--spectrum-combobox-spacing-alert-icon-to-text` +`--spectrum-combobox-spacing-to-help-text` + +### New mods + +`--mod-combobox-line-height-cjk` +`--mod-combobox-popover-animation-distance` +`--mod-combobox-spacing-alert-icon-to-text` +`--mod-combobox-spacing-to-help-text` +`--mod-combobox-textfield-background-color` diff --git a/components/combobox/dist/metadata.json b/components/combobox/dist/metadata.json index c0626f6390e..8dd3138302a 100644 --- a/components/combobox/dist/metadata.json +++ b/components/combobox/dist/metadata.json @@ -14,7 +14,6 @@ ".spectrum-Combobox--quiet .spectrum-Combobox-textfield.is-readOnly.is-disabled .spectrum-Combobox-input:read-only", ".spectrum-Combobox--quiet .spectrum-Combobox-textfield.is-readOnly.is-invalid > .spectrum-Combobox-input:read-only", ".spectrum-Combobox--quiet.spectrum-Combobox--sizeL", - ".spectrum-Combobox--quiet.spectrum-Combobox--sizeM", ".spectrum-Combobox--quiet.spectrum-Combobox--sizeS", ".spectrum-Combobox--quiet.spectrum-Combobox--sizeXL", ".spectrum-Combobox--sizeL", diff --git a/components/combobox/index.css b/components/combobox/index.css index a1e1797971b..b2e36c58526 100644 --- a/components/combobox/index.css +++ b/components/combobox/index.css @@ -16,16 +16,17 @@ --spectrum-combobox-icon-size: var(--spectrum-workflow-icon-size-100); --spectrum-combobox-font-size: var(--spectrum-font-size-100); - --spectrum-combobox-spacing-inline-icon-to-button: var(--spectrum-combo-box-visual-to-field-button-medium); + --spectrum-combobox-spacing-inline-icon-to-button: var(--spectrum-combo-box-visual-to-field-button); --spectrum-combobox-block-spacing-edge-to-progress-circle: var(--spectrum-field-top-to-progress-circle-medium); - --spectrum-combobox-block-spacing-edge-to-alert: var(--spectrum-field-top-to-alert-icon-medium); - --spectrum-combobox-spacing-edge-to-menu: var(--spectrum-component-to-menu-medium); + --spectrum-combobox-block-spacing-edge-to-icon: var(--spectrum-component-top-to-workflow-icon-100); --spectrum-combobox-spacing-block-start-edge-to-text: var(--spectrum-component-top-to-text-100); --spectrum-combobox-spacing-block-end-edge-to-text: var(--spectrum-component-bottom-to-text-100); --spectrum-combobox-spacing-inline-start-edge-to-text: var(--spectrum-component-edge-to-text-100); - --spectrum-combobox-spacing-inline-end-edge-to-text: var(--spectrum-component-edge-to-text-100); + --spectrum-combobox-spacing-inline-icon-to-text: var(--spectrum-text-to-visual-100); - --spectrum-combobox-inline-size: var(--spectrum-field-width); + --spectrum-combobox-spacing-to-help-text: var(--spectrum-help-text-to-component); + + --spectrum-combobox-inline-size: var(--spectrum-field-width-medium); --spectrum-combobox-min-inline-size: calc(var(--spectrum-combo-box-minimum-width-multiplier) * var(--spectrum-combobox-block-size)); --spectrum-combobox-button-width: var(--spectrum-combobox-block-size); @@ -33,18 +34,22 @@ --spectrum-combobox-focus-indicator-gap: var(--spectrum-focus-indicator-gap); --spectrum-combobox-focus-indicator-color: var(--spectrum-focus-indicator-color); - --spectrum-combobox-border-radius: var(--spectrum-corner-radius-100); - --spectrum-combobox-border-width: var(--spectrum-border-width-100); + --spectrum-combobox-border-radius: var(--spectrum-corner-radius-medium-size-medium); + --spectrum-combobox-border-width: var(--spectrum-border-width-200); --spectrum-combobox-spacing-label-to-combobox: var(--spectrum-field-label-to-component); + --spectrum-combobox-spacing-side-label-to-field: var(--spectrum-spacing-200); --spectrum-combobox-font-style: var(--spectrum-default-font-style); + --spectrum-combobox-font-weight: var(--spectrum-regular-font-weight); --spectrum-combobox-line-height: var(--spectrum-line-height-100); + --spectrum-combobox-line-height-cjk: var(--spectrum-cjk-line-height-100); --spectrum-combobox-background-color-disabled: var(--spectrum-gray-25); - --spectrum-combobox-border-color-default: var(--spectrum-gray-500); - --spectrum-combobox-border-color-hover: var(--spectrum-gray-600); + --spectrum-combobox-background-color-default: var(--spectrum-gray-25); + --spectrum-combobox-border-color-default: var(--spectrum-gray-300); + --spectrum-combobox-border-color-hover: var(--spectrum-gray-400); --spectrum-combobox-border-color-focus: var(--spectrum-gray-800); --spectrum-combobox-border-color-focus-hover: var(--spectrum-gray-900); --spectrum-combobox-border-color-key-focus: var(--spectrum-gray-800); @@ -56,6 +61,8 @@ --spectrum-combobox-border-color-invalid-focus-hover: var(--spectrum-negative-border-color-focus-hover); --spectrum-combobox-border-color-invalid-key-focus: var(--spectrum-negative-border-color-key-focus); + --mod-combobox-popover-animation-distance: var(--spectrum-component-to-menu-medium); + /* @passthroughs start -- settings for nested Textfield component */ --mod-textfield-focus-indicator-gap: var(--mod-combobox-focus-indicator-gap, var(--spectrum-combobox-focus-indicator-gap)); --mod-textfield-focus-indicator-width: var(--mod-combobox-focus-indicator-thickness, var(--spectrum-combobox-focus-indicator-thickness)); @@ -65,8 +72,11 @@ --mod-textfield-background-color-disabled: var(--mod-combobox-background-color-disabled, var(--spectrum-combobox-background-color-disabled)); --mod-textfield-font-family: var(--mod-combobox-font-family); - --mod-textfield-font-weight: var(--mod-combobox-font-weight); - + --mod-textfield-font-weight: var(--spectrum-combobox-font-weight); + --mod-textfield-font-size: var(--spectrum-combobox-font-size); + --mod-textfield-font-style: var(--spectrum-combobox-font-style); + --mod-textfield-spacing-block-start: calc(var(--mod-combobox-spacing-block-start-edge-to-text, var(--spectrum-combobox-spacing-block-start-edge-to-text)) - var(--mod-combobox-border-width, var(--spectrum-combobox-border-width))); + --mod-textfield-spacing-block-end: calc(var(--mod-combobox-spacing-block-end-edge-to-text, var(--spectrum-combobox-spacing-block-end-edge-to-text)) - var(--mod-combobox-border-width, var(--spectrum-combobox-border-width))); --mod-textfield-text-color-default: var(--mod-combobox-font-color-default); --mod-textfield-text-color-hover: var(--mod-combobox-font-color-hover); --mod-textfield-text-color-focus: var(--mod-combobox-font-color-focus); @@ -101,11 +111,12 @@ /* @passthroughs end -- settings for nested Picker Button component */ /*** Read-only Colors ***/ - --spectrum-combobox-readonly-input-background-color: var(--spectrum-gray-50); + --spectrum-combobox-readonly-input-background-color: var(--spectrum-combobox-background-color-default); --spectrum-combobox-readonly-border-color-invalid-default: var(--spectrum-negative-border-color-default); --spectrum-combobox-readonly-background-color-disabled: var(--spectrum-disabled-background-color); --spectrum-combobox-readonly-text-color-disabled: var(--spectrum-disabled-content-color); --spectrum-combobox-readonly-border-color-disabled: var(--spectrum-disabled-border-color); + --spectrum-combobox-readonly-input-border-color: var(--spectrum-combobox-border-color-default); } .spectrum-Combobox--sizeS { @@ -113,14 +124,15 @@ --spectrum-combobox-icon-size: var(--spectrum-workflow-icon-size-75); --spectrum-combobox-font-size: var(--spectrum-font-size-75); - --spectrum-combobox-spacing-inline-icon-to-button: var(--spectrum-combo-box-visual-to-field-button-small); --spectrum-combobox-block-spacing-edge-to-progress-circle: var(--spectrum-field-top-to-progress-circle-small); - --spectrum-combobox-block-spacing-edge-to-alert: var(--spectrum-field-top-to-alert-icon-small); - --spectrum-combobox-spacing-edge-to-menu: var(--spectrum-component-to-menu-small); + --spectrum-combobox-block-spacing-edge-to-icon: var(--spectrum-component-top-to-workflow-icon-75); --spectrum-combobox-spacing-block-start-edge-to-text: var(--spectrum-component-top-to-text-75); --spectrum-combobox-spacing-block-end-edge-to-text: var(--spectrum-component-bottom-to-text-75); --spectrum-combobox-spacing-inline-start-edge-to-text: var(--spectrum-component-edge-to-text-75); - --spectrum-combobox-spacing-inline-end-edge-to-text: var(--spectrum-component-edge-to-text-75); + --spectrum-combobox-spacing-inline-icon-to-text: var(--spectrum-text-to-visual-75); + --spectrum-combobox-inline-size: var(--spectrum-field-width-small); + --spectrum-combobox-border-radius: var(--spectrum-corner-radius-medium-size-small); + --mod-combobox-popover-animation-distance: var(--spectrum-component-to-menu-small); } .spectrum-Combobox--sizeL { @@ -128,14 +140,15 @@ --spectrum-combobox-icon-size: var(--spectrum-workflow-icon-size-200); --spectrum-combobox-font-size: var(--spectrum-font-size-200); - --spectrum-combobox-spacing-inline-icon-to-button: var(--spectrum-combo-box-visual-to-field-button-large); --spectrum-combobox-block-spacing-edge-to-progress-circle: var(--spectrum-field-top-to-progress-circle-large); - --spectrum-combobox-block-spacing-edge-to-alert: var(--spectrum-field-top-to-alert-icon-large); - --spectrum-combobox-spacing-edge-to-menu: var(--spectrum-component-to-menu-large); + --spectrum-combobox-block-spacing-edge-to-icon: var(--spectrum-component-top-to-workflow-icon-200); --spectrum-combobox-spacing-block-start-edge-to-text: var(--spectrum-component-top-to-text-200); --spectrum-combobox-spacing-block-end-edge-to-text: var(--spectrum-component-bottom-to-text-200); --spectrum-combobox-spacing-inline-start-edge-to-text: var(--spectrum-component-edge-to-text-200); - --spectrum-combobox-spacing-inline-end-edge-to-text: var(--spectrum-component-edge-to-text-200); + --spectrum-combobox-spacing-inline-icon-to-text: var(--spectrum-text-to-visual-200); + --spectrum-combobox-inline-size: var(--spectrum-field-width-large); + --spectrum-combobox-border-radius: var(--spectrum-corner-radius-medium-size-large); + --mod-combobox-popover-animation-distance: var(--spectrum-component-to-menu-large); } .spectrum-Combobox--sizeXL { @@ -143,81 +156,32 @@ --spectrum-combobox-icon-size: var(--spectrum-workflow-icon-size-300); --spectrum-combobox-font-size: var(--spectrum-font-size-300); - --spectrum-combobox-spacing-inline-icon-to-button: var(--spectrum-combo-box-visual-to-field-button-extra-large); --spectrum-combobox-block-spacing-edge-to-progress-circle: var(--spectrum-field-top-to-progress-circle-extra-large); - --spectrum-combobox-block-spacing-edge-to-alert: var(--spectrum-field-top-to-alert-icon-extra-large); - --spectrum-combobox-spacing-edge-to-menu: var(--spectrum-component-to-menu-extra-large); + --spectrum-combobox-block-spacing-edge-to-icon: var(--spectrum-component-top-to-workflow-icon-300); --spectrum-combobox-spacing-block-start-edge-to-text: var(--spectrum-component-top-to-text-300); --spectrum-combobox-spacing-block-end-edge-to-text: var(--spectrum-component-bottom-to-text-300); --spectrum-combobox-spacing-inline-start-edge-to-text: var(--spectrum-component-edge-to-text-300); - --spectrum-combobox-spacing-inline-end-edge-to-text: var(--spectrum-component-edge-to-text-300); -} - -.spectrum-Combobox--quiet { - --spectrum-combobox-min-inline-size: calc(var(--spectrum-combo-box-quiet-minimum-width-multiplier) * var(--spectrum-combobox-block-size)); - --spectrum-combobox-spacing-inline-icon-to-button: var(--spectrum-combo-box-visual-to-field-button-quiet); - --spectrum-combobox-spacing-inline-start-edge-to-text: var(--spectrum-field-edge-to-text-quiet); - --spectrum-combobox-spacing-label-to-combobox: var(--spectrum-field-label-to-component-quiet-medium); - --spectrum-combobox-button-inline-offset: calc((var(--mod-combobox-block-size, var(--spectrum-combobox-block-size)) / 2) - (var(--mod-combobox-icon-size, var(--spectrum-combobox-icon-size)) / 2)); - - --mod-textfield-border-color-disabled: var(--mod-combobox-border-color-disabled, initial); - - /* Settings for nested Picker Button component. */ - --mod-picker-button-background-color-quiet: transparent; - --mod-picker-button-border-color-quiet: transparent; - - &.spectrum-Combobox--sizeS { - --spectrum-combobox-spacing-label-to-combobox: var(--spectrum-field-label-to-component-quiet-small); - } - - &.spectrum-Combobox--sizeM { - --spectrum-combobox-spacing-label-to-combobox: var(--spectrum-field-label-to-component-quiet-medium); - } - - &.spectrum-Combobox--sizeL { - --spectrum-combobox-spacing-label-to-combobox: var(--spectrum-field-label-to-component-quiet-large); - } - - &.spectrum-Combobox--sizeXL { - --spectrum-combobox-spacing-label-to-combobox: var(--spectrum-field-label-to-component-quiet-extra-large); - } -} - -@media (forced-colors: active) { - .spectrum-Combobox { - --highcontrast-combobox-border-color-highlight: Highlight; - --highcontrast-combobox-border-color-invalid: Highlight; - - .spectrum-Combobox-button.spectrum-PickerButton--quiet { - .spectrum-PickerButton-fill { - forced-color-adjust: none; - } - - .spectrum-PickerButton-icon { - /* Should match foreground color of the Textfield. */ - color: CanvasText; - } - } - } + --spectrum-combobox-spacing-inline-icon-to-text: var(--spectrum-text-to-visual-300); + --spectrum-combobox-inline-size: var(--spectrum-field-width-extra-large); + --spectrum-combobox-border-radius: var(--spectrum-corner-radius-medium-size-extra-large); + --mod-combobox-popover-animation-distance: var(--spectrum-component-to-menu-extra-large); } .spectrum-Combobox { position: relative; - display: inline-flex; - flex-flow: row nowrap; + display: inline-grid; inline-size: var(--mod-combobox-inline-size, var(--spectrum-combobox-inline-size)); min-inline-size: var(--mod-combobox-min-inline-size, var(--spectrum-combobox-min-inline-size)); - block-size: var(--mod-combobox-block-size, var(--spectrum-combobox-block-size)); margin-block-start: var(--mod-combobox-spacing-label-to-combobox, var(--spectrum-combobox-spacing-label-to-combobox)); border-radius: var(--mod-combobox-border-radius, var(--spectrum-combobox-border-radius)); - .spectrum-Popover.is-open { - transform: translateY(var(--mod-combobox-spacing-edge-to-menu, var(--spectrum-combobox-spacing-edge-to-menu))); + .spectrum-Combobox-popover { + --mod-popover-animation-distance: var(--mod-combobox-popover-animation-distance); } - &.is-readOnly:not(.spectrum-Combobox--quiet) { + &.is-readOnly { .spectrum-Combobox-textfield { &.is-keyboardFocused .spectrum-Combobox-input { outline-offset: var(--mod-textfield-focus-indicator-gap); @@ -227,8 +191,8 @@ } .spectrum-Combobox-input:read-only { - background-color: var(--spectrum-combobox-readonly-input-background-color); - border-color: var(--spectrum-combobox-readonly-input-border-color); + background-color: var(--highcontrast-combobox-background-color-default, var(--spectrum-combobox-readonly-input-background-color)); + border-color: var(--highcontrast-combobox-readonly-border-color, var(--spectrum-combobox-readonly-input-border-color)); &:hover { background-color: revert; @@ -256,7 +220,8 @@ position: absolute; inset-inline-end: calc(var(--mod-combobox-spacing-inline-icon-to-button, var(--spectrum-combobox-spacing-inline-icon-to-button)) + var(--mod-combobox-button-width, var(--spectrum-combobox-button-width))); inset-block-start: var(--mod-combobox-block-spacing-edge-to-progress-circle, var(--spectrum-combobox-block-spacing-edge-to-progress-circle)); - inset-block-end: var(--mod-combobox-block-spacing-edge-to-alert, var(--spectrum-combobox-block-spacing-edge-to-alert)); + inset-block-end: var(--mod-combobox-block-spacing-edge-to-icon, var(--spectrum-combobox-block-spacing-edge-to-icon)); + padding-inline-start: var(--mod-combobox-spacing-inline-icon-to-text, var(--spectrum-combobox-spacing-inline-icon-to-text)); &:dir(rtl) { inset-inline-end: inherit; @@ -264,10 +229,16 @@ } } +.spectrum-Combobox-label { + display: flex; + align-items: center; + justify-content: space-between; +} + /* PICKER BUTTON */ .spectrum-Combobox-button { position: absolute; - inset-inline-end: calc(-1 * var(--mod-combobox-button-inline-offset, var(--spectrum-combobox-button-inline-offset, 0px))); + inset-inline-end: 0; /* Default */ &:not(:disabled, .is-invalid, .spectrum-PickerButton--quiet) { @@ -334,21 +305,19 @@ } } -/* TEXTFIELD (wrapper) */ -.spectrum-Combobox-textfield { - inline-size: 100%; +.spectrum-Combobox-content { + display: inline-grid; + block-size: var(--mod-combobox-block-size, var(--spectrum-combobox-block-size)); + position: relative; } /* TEXT INPUT */ .spectrum-Combobox-input { - padding-block-start: calc(var(--mod-combobox-spacing-block-start-edge-to-text, var(--spectrum-combobox-spacing-block-start-edge-to-text)) - var(--mod-combobox-border-width, var(--spectrum-combobox-border-width))); - padding-block-end: calc(var(--mod-combobox-spacing-block-end-edge-to-text, var(--spectrum-combobox-spacing-block-end-edge-to-text)) - var(--mod-combobox-border-width, var(--spectrum-combobox-border-width))); + background-color: var(--highcontrast-combobox-background-color-default, var(--mod-combobox-textfield-background-color, var(--spectrum-combobox-background-color-default))); padding-inline-start: calc(var(--mod-combobox-spacing-inline-start-edge-to-text, var(--spectrum-combobox-spacing-inline-start-edge-to-text)) - var(--mod-combobox-border-width, var(--spectrum-combobox-border-width))); - padding-inline-end: calc(var(--mod-combobox-button-width, var(--spectrum-combobox-button-width)) + var(--mod-combobox-spacing-inline-end-edge-to-text, var(--spectrum-combobox-spacing-inline-end-edge-to-text)) - (var(--mod-combobox-border-width, var(--spectrum-combobox-border-width)) * 2)); + padding-inline-end: calc(var(--mod-combobox-button-width, var(--spectrum-combobox-button-width)) + var(--mod-combobox-spacing-inline-icon-to-text, var(--spectrum-combobox-spacing-inline-icon-to-text)) - var(--mod-combobox-border-width, var(--spectrum-combobox-border-width))); backface-visibility: hidden; line-height: var(--mod-combobox-line-height, var(--spectrum-combobox-line-height)); - font-size: var(--mod-combobox-font-size, var(--spectrum-combobox-font-size)); - font-style: var(--mod-combobox-font-style, var(--spectrum-combobox-font-style)); &::placeholder { --mod-textfield-text-color-default: var(--mod-combobox-font-color-placeholder); @@ -383,10 +352,50 @@ /* ****** Invalid & Loading ****** */ .spectrum-Combobox-textfield.is-invalid &, .spectrum-Combobox-textfield.is-loading & { - padding-inline-end: calc( - var(--mod-combobox-button-width, var(--spectrum-combobox-button-width)) + var(--mod-combobox-spacing-inline-icon-to-button, var(--spectrum-combobox-spacing-inline-icon-to-button)) + var(--mod-combobox-icon-size, var(--spectrum-combobox-icon-size)) + var(--mod-combobox-spacing-inline-end-edge-to-text, var(--spectrum-combobox-spacing-inline-end-edge-to-text)) - var(--mod-combobox-button-inline-offset, var(--spectrum-combobox-button-inline-offset, 0px)) - - (var(--mod-combobox-border-width, var(--spectrum-combobox-border-width)) * 2) - ); + padding-inline-end: calc(var(--mod-combobox-button-width, var(--spectrum-combobox-button-width)) + var(--mod-combobox-spacing-inline-icon-to-button, var(--spectrum-combobox-spacing-inline-icon-to-button)) + var(--mod-combobox-icon-size, var(--spectrum-combobox-icon-size)) + var(--mod-combobox-spacing-alert-icon-to-text, var(--spectrum-combobox-spacing-inline-icon-to-text)) - var(--mod-combobox-border-width, var(--spectrum-combobox-border-width))); + } + + &:lang(ja), + &:lang(zh), + &:lang(ko) { + --mod-combobox-line-height: var(--mod-combobox-line-height-cjk, var(--spectrum-combobox-line-height-cjk)); + } +} + +.spectrum-Combobox-helptext { + inset-block-start: var(--mod-combobox-spacing-to-help-text, var(--spectrum-combobox-spacing-to-help-text)); + + .spectrum-HelpText-text { + inline-size: 100%; + word-wrap: break-word; + text-wrap: wrap; + } +} + +.spectrum-Combobox--sideLabel { + .spectrum-Combobox-textfield { + inline-size: auto; + } + + .spectrum-Combobox-label { + grid-row: 1; + grid-column: 1 / span 1; + margin-inline-end: var(--mod-combobox-spacing-side-label-to-field, var(--spectrum-combobox-spacing-side-label-to-field)); + } + + .spectrum-Combobox-content { + grid-row: 1 / span 1; + grid-column: 2 / span 1; + } + + .spectrum-Combobox-helptext { + grid-row: 2 / span 2; + grid-column: 2 / span 1; + } + + .spectrum-Combobox-label, + .spectrum-Combobox-content { + min-inline-size: 50%; } } @@ -396,9 +405,10 @@ inline-size: var(--mod-combobox-icon-size, var(--spectrum-combobox-icon-size)); block-size: var(--mod-combobox-icon-size, var(--spectrum-combobox-icon-size)); - inset-block-start: var(--mod-combobox-block-spacing-edge-to-alert, var(--spectrum-combobox-block-spacing-edge-to-alert)); - inset-block-end: var(--mod-combobox-block-spacing-edge-to-alert, var(--spectrum-combobox-block-spacing-edge-to-alert)); + inset-block-start: var(--mod-combobox-block-spacing-edge-to-icon, var(--spectrum-combobox-block-spacing-edge-to-icon)); + inset-block-end: var(--mod-combobox-block-spacing-edge-to-icon, var(--spectrum-combobox-block-spacing-edge-to-icon)); inset-inline-end: calc(var(--mod-combobox-spacing-inline-icon-to-button, var(--spectrum-combobox-spacing-inline-icon-to-button)) + var(--mod-combobox-button-width, var(--spectrum-combobox-button-width))); + padding-inline-start: var(--mod-combobox-spacing-inline-icon-to-text, var(--spectrum-combobox-spacing-inline-icon-to-text)); } .spectrum-Textfield.is-disabled &, @@ -408,43 +418,22 @@ } } -/* QUIET VARIATION (no visible background) */ -.spectrum-Combobox--quiet { - border-radius: 0; - - .spectrum-Combobox-textfield { - &.is-invalid .spectrum-Textfield-validationIcon { - inset-inline-end: var(--mod-combobox-button-width, var(--spectrum-combobox-button-width)); - } - - &.is-readOnly { - .spectrum-Combobox-input:read-only { - border-block-end: var(--mod-combobox-border-width, var(--spectrum-combobox-border-width)) solid var(--mod-combobox-readonly-input-border-color, var(--spectrum-combobox-readonly-input-border-color)); - } +@media (forced-colors: active) { + .spectrum-Combobox { + --highcontrast-combobox-border-color-highlight: Highlight; + --highcontrast-combobox-border-color-invalid: Highlight; + --highcontrast-combobox-background-color-default: Canvas; + --highcontrast-combobox-readonly-border-color: CanvasText; - &.is-invalid > .spectrum-Combobox-input:read-only { - border-color: var(--highcontrast-textfield-border-color-invalid-default, var(--mod-textfield-border-color-invalid-default, var(--spectrum-combobox-readonly-border-color-invalid-default))); + .spectrum-Combobox-button.spectrum-PickerButton--quiet { + .spectrum-PickerButton-fill { + forced-color-adjust: none; } - &.is-disabled .spectrum-Combobox-input:read-only { - color: var(--highcontrast-textfield-text-color-disabled, var(--mod-textfield-text-color-disabled, var(--spectrum-combobox-readonly-text-color-disabled))); - border-color: var(--mod-textfield-border-color-disabled, var(--spectrum-combobox-readonly-border-color-disabled)); + .spectrum-PickerButton-icon { + /* Should match foreground color of the Textfield. */ + color: CanvasText; } } } - - .spectrum-Combobox-input { - border-block-end-width: var(--mod-combobox-border-width, var(--spectrum-combobox-border-width)); - - padding-block-start: var(--mod-combobox-spacing-block-start-edge-to-text, var(--spectrum-combobox-spacing-block-start-edge-to-text)); - padding-block-end: calc(var(--mod-combobox-spacing-block-end-edge-to-text, var(--spectrum-combobox-spacing-block-end-edge-to-text)) - var(--mod-combobox-border-width, var(--spectrum-combobox-border-width))); - - padding-inline-start: var(--mod-combobox-spacing-inline-start-edge-to-text, var(--spectrum-combobox-spacing-inline-start-edge-to-text)); - padding-inline-end: calc(var(--mod-combobox-button-width, var(--spectrum-combobox-button-width)) + var(--mod-combobox-spacing-inline-end-edge-to-text, var(--spectrum-combobox-spacing-inline-end-edge-to-text)) - var(--mod-combobox-button-inline-offset, var(--spectrum-combobox-button-inline-offset, 0px))); - } - - .spectrum-Combobox-textfield.is-invalid .spectrum-Combobox-input, - .spectrum-Combobox-textfield.is-loading .spectrum-Combobox-input { - padding-inline-end: calc(var(--mod-combobox-button-width, var(--spectrum-combobox-button-width)) + var(--mod-combobox-spacing-inline-icon-to-button, var(--spectrum-combobox-spacing-inline-icon-to-button)) + var(--mod-combobox-icon-size, var(--spectrum-combobox-icon-size)) + var(--mod-combobox-spacing-inline-end-edge-to-text, var(--spectrum-combobox-spacing-inline-end-edge-to-text)) - var(--mod-combobox-button-inline-offset, var(--spectrum-combobox-button-inline-offset, 0px))); - } } diff --git a/components/combobox/stories/combobox.stories.js b/components/combobox/stories/combobox.stories.js index df57778d72d..2ae7fd3623c 100644 --- a/components/combobox/stories/combobox.stories.js +++ b/components/combobox/stories/combobox.stories.js @@ -1,11 +1,12 @@ import { Template as Menu } from "@spectrum-css/menu/stories/template.js"; +import { Sizes, withDownStateDimensionCapture } from "@spectrum-css/preview/decorators"; import { disableDefaultModes } from "@spectrum-css/preview/modes"; -import { isDisabled, isFocused, isInvalid, isKeyboardFocused, isLoading, isOpen, isQuiet, isReadOnly, size } from "@spectrum-css/preview/types"; +import { isDisabled, isFocused, isHovered, isInvalid, isKeyboardFocused, isLoading, isOpen, isReadOnly, size } from "@spectrum-css/preview/types"; import { within } from "@storybook/test"; import metadata from "../dist/metadata.json"; import packageJson from "../package.json"; import { ComboBoxGroup } from "./combobox.test.js"; -import { Template, VariantGroup } from "./template.js"; +import { HelpTextTemplate, Template, VariantGroup } from "./template.js"; /** * Comboboxes combine a text entry with a picker menu, allowing users to filter longer lists to only the selections matching a query. @@ -14,11 +15,11 @@ import { Template, VariantGroup } from "./template.js"; * * ### General notes * - * - Combobox uses `.spectrum-PickerButton` instead of a `.spectrum-Picker` + * - Combobox uses `.spectrum-InfieldButton` as a menu trigger. * - The following classes must be added: * - `.spectrum-Combobox-textfield` is required on the Textfield outer element (`.spectrum-Textfield`) * - `.spectrum-Combobox-input` is required on the `` element inside of Textfields (`.spectrum-Textfield-input`) - * - `.spectrum-Combobox-button` is required on the FieldButton (`.spectrum-ActionButton spectrum-ActionButton--sizeM`) + * - `.spectrum-Combobox-button` is required on the InfieldButton (`.spectrum-InfieldButton`) * * ### Indicating validity and focus * @@ -26,8 +27,7 @@ import { Template, VariantGroup } from "./template.js"; * * - `.is-focused` - when the input or button is focused with the mouse * - `.is-keyboardFocused` - when the input or button is focused with the keyboard - * - `.is-valid` - when the input has an explicit valid state - * - `.is-invalid` - when the input has an explicit invalid state + * - `.is-invalid` - when the input has an explicit invalid state; should also show help text for error messaging * - `.is-disabled` - when the control is disabled; should also add to the `.spectrum-Combobox-textfield` and include a `[disabled]` attribute to the `.spectrum-Combobox-button` * - `.is-loading` - when the progress circle is being shown * @@ -45,13 +45,23 @@ export default { ...isOpen, if: { arg: "isReadOnly", truthy: false }, }, - isQuiet, isInvalid, + isHovered, isFocused, isKeyboardFocused, isLoading, isDisabled, isReadOnly, + isLabelRequired: { + name: "Label required", + type: { name: "boolean" }, + table: { + type: { summary: "boolean" }, + category: "Component", + }, + control: "boolean", + if: { arg: "showFieldLabel", truthy: true }, + }, showFieldLabel: { name: "Show field label", type: { name: "boolean" }, @@ -78,10 +88,24 @@ export default { type: { summary: "string" }, category: "Component", }, - options: ["top", "left"], + options: ["top", "side"], control: "select", if: { arg: "showFieldLabel", truthy: true }, }, + autocomplete: { + table: { + disable: true, + }, + }, + helpText: { + name: "Help text", + type: { name: "text" }, + table: { + type: { summary: "text" }, + category: "Component", + }, + control: "text", + }, value: { name: "Value", description: "The value shows the option that a user has selected.", @@ -98,15 +122,20 @@ export default { rootClass: "spectrum-Combobox", size: "m", isOpen: false, - isQuiet: false, isInvalid: false, + isHovered: false, isFocused: false, isKeyboardFocused: false, isLoading: false, isDisabled: false, isReadOnly: false, + isLabelRequired: false, showFieldLabel: false, + autocomplete: false, testId: "combobox", + fieldLabelText: "Select location", + helpText: "", + value: "Ballard", content: [ (passthroughs, context) => Menu({ role: "listbox", @@ -125,9 +154,6 @@ export default { { label: "Greenwood", }, - { - type: "divider", - }, { label: "United States of America", isDisabled: true, @@ -142,9 +168,19 @@ export default { type: "figma", url: "https://www.figma.com/design/Mngz9H7WZLbrCvGQf3GnsY/S2-%2F-Desktop?node-id=727-2550", }, + downState: { + selectors: [".spectrum-InfieldButton:not(:disabled)"], + }, + status: { + type: "migrated", + }, + tags: ["migrated"], packageJson, metadata, }, + decorators: [ + withDownStateDimensionCapture, + ], play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -155,11 +191,6 @@ export default { export const Default = ComboBoxGroup.bind({}); Default.tags = ["!autodocs"]; -Default.args = { - isOpen: true, - fieldLabelText: "Select location", - value: "Ballard", -}; Default.parameters = { chromatic: { delay: 1000 } }; @@ -167,7 +198,9 @@ Default.parameters = { // ********* DOCS ONLY ********* // export const DefaultGroup = VariantGroup.bind({}); DefaultGroup.storyName = "Default"; -DefaultGroup.args = Default.args; +DefaultGroup.args = { + isOpen: true, +}; DefaultGroup.tags = ["!dev"]; DefaultGroup.parameters = { chromatic: { disableSnapshot: true }, @@ -178,24 +211,23 @@ DefaultGroup.parameters = { }, }; -export const QuietGroup = VariantGroup.bind({}); -QuietGroup.storyName = "Quiet"; -QuietGroup.args = { - ...Default.args, - isQuiet: true, -}; -QuietGroup.tags = ["!dev"]; -QuietGroup.parameters = { - chromatic: { disableSnapshot: true }, - docs: { - story: { - height: "360px", - }, - }, +/** + * Comboboxes can show help text to provide feedback to users. The description in the help text is flexible and encompasses a range of guidance. Sometimes this guidance is about what to input, and sometime it’s about how to input. This includes information such as: + * + * - An overall description of the input field + * - Hints for what kind of information needs to be input + * - Specific formatting examples or requirements + */ +export const HelpText = HelpTextTemplate.bind({}); +HelpText.tags = ["!dev"]; +HelpText.args = { + showHelpText: true, + helpText: "This is a help text. Select an option", }; + /** - * Comboboxes have a read-only option for when content in the disabled state still needs to be shown. This allows for content to be copied, but not interacted with or changed. A combobox does not have a read-only option if no selection has been made. To enable this feature, add the `.isReadOnly` class to the combobox. To enable this feature, add the .isReadOnly class to the combobox. Then within the nested textfield component, add the .isReadOnly class and readonly attribute to the `` element. + * Comboboxes have a read-only option for when content in the disabled state still needs to be shown. This allows for content to be copied, but not interacted with or changed. A combobox does not have a read-only option if no selection has been made. To enable this feature, add the `.is-readOnly` class to the combobox. To enable this feature, add the `.is-readOnly` class to the combobox. Then within the nested textfield component, add the `.is-readOnly class and readonly attribute to the `input` element. */ export const ReadOnly = Template.bind({}); ReadOnly.tags = ["!dev"]; @@ -209,6 +241,17 @@ ReadOnly.parameters = { ReadOnly.storyName = "Read-only"; +export const Sizing = (args, context) => Sizes({ + Template, + withBorder: false, + withHeading: false, + ...args, +}, context); +Sizing.tags = ["!dev"]; +Sizing.parameters = { + chromatic: { disableSnapshot: true }, +}; + // ********* VRT ONLY ********* // export const WithForcedColors = ComboBoxGroup.bind({}); WithForcedColors.tags = ["!autodocs", "!dev"]; diff --git a/components/combobox/stories/combobox.test.js b/components/combobox/stories/combobox.test.js index b74a71a388b..97b12395259 100644 --- a/components/combobox/stories/combobox.test.js +++ b/components/combobox/stories/combobox.test.js @@ -10,9 +10,8 @@ export const ComboBoxGroup = Variants({ isOpen: false, }, { - testHeading: "Quiet", - isQuiet: true, - isOpen: false, + testHeading: "Invalid", + isInvalid: true, }, { testHeading: "Open", @@ -22,49 +21,39 @@ export const ComboBoxGroup = Variants({ }, }, { - testHeading: "Quiet + open", - isQuiet: true, - isOpen: true, - wrapperStyles: { - "min-block-size": "250px", - }, + testHeading: "Help text with label", + showFieldLabel: true, + helpText: "Choose a location. Add a new location by typing it in the field, then selecting 'Enter.'", }, { - testHeading: "With field label top", + testHeading: "With field label on the side", showFieldLabel: true, - isOpen: false, - fieldLabelPosition: "top", + fieldLabelPosition: "side", }, { - testHeading: "With field label left", + testHeading: "Help text with label on the side", showFieldLabel: true, - isOpen: false, - fieldLabelPosition: "left", + helpText: "Choose a location. Add a new location by typing it in the field, then selecting 'Enter.'", + fieldLabelPosition: "side", }, { testHeading: "Truncation", - isOpen: false, - value: "United States of America and to the republic", - }, - { - testHeading: "Quiet + truncation", - isOpen: false, - isQuiet: true, value: "United States of America and to the republic", }, ], stateData: [ { - testHeading: "Disabled", - isDisabled: true, + testHeading: "Hovered", + isHovered: true, }, { - testHeading: "Invalid", - isInvalid: true, + testHeading: "Focused", + isFocused: true, }, { - testHeading: "Focused", + testHeading: "Focused + Hovered", isFocused: true, + isHovered: true, }, { testHeading: "Keyboard focused", @@ -78,5 +67,9 @@ export const ComboBoxGroup = Variants({ testHeading: "Read-only", isReadOnly: true }, + { + testHeading: "Disabled", + isDisabled: true, + }, ], }); diff --git a/components/combobox/stories/template.js b/components/combobox/stories/template.js index f8cc0dbc1b1..b00f43445f8 100644 --- a/components/combobox/stories/template.js +++ b/components/combobox/stories/template.js @@ -1,5 +1,6 @@ import { Template as FieldLabel } from "@spectrum-css/fieldlabel/stories/template.js"; -import { Template as PickerButton } from "@spectrum-css/pickerbutton/stories/template.js"; +import { Template as HelpText } from "@spectrum-css/helptext/stories/template.js"; +import { Template as InfieldButton } from "@spectrum-css/infieldbutton/stories/template.js"; import { Template as Popover } from "@spectrum-css/popover/stories/template.js"; import { Container, getRandomId } from "@spectrum-css/preview/decorators"; import { Template as TextField } from "@spectrum-css/textfield/stories/template.js"; @@ -11,7 +12,7 @@ import { when } from "lit/directives/when.js"; import "../index.css"; -const Combobox = ({ +export const Template = ({ rootClass = "spectrum-Combobox", id = getRandomId("combobox"), testId, @@ -20,15 +21,43 @@ const Combobox = ({ size = "m", isOpen = true, isInvalid = false, - isQuiet = false, isDisabled = false, + isHovered = false, isFocused = false, isKeyboardFocused = false, isLoading = false, isReadOnly = false, + helpText, + fieldLabelText = "Select location", + fieldLabelPosition = "top", + isLabelRequired = false, + showFieldLabel = false, value = "", + autocomplete = true, + content = [], } = {}, context = {}) => { const { updateArgs } = context; + const comboboxId = id || getRandomId("combobox"); + + // Handle click outside of the combobox to close it + if (typeof window !== "undefined" && isOpen) { + // Use setTimeout to allow DOM to render first + setTimeout(() => { + const comboboxEl = document.getElementById(comboboxId); + + const handleClickOutside = (event) => { + if (comboboxEl && !comboboxEl.contains(event.target)) { + updateArgs({ isOpen: false }); + document.removeEventListener("mousedown", handleClickOutside); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + }, 0); + } + + const popoverHeight = size === "s" ? 106 : size === "l" ? 170 : size === "xl" ? 229 : 142; // default value is "m" + const adjustedPopoverHeight = showFieldLabel ? popoverHeight : popoverHeight + 32; // Subtract label height when no label return html`
({ ...a, [c]: true }), {}), })} id=${ifDefined(id)} data-testid=${ifDefined(testId ?? id)} - style=${styleMap(customStyles)} + style=${styleMap({ + ...customStyles, + ["margin-block-end"]: !isReadOnly && isOpen && !isDisabled ? `${adjustedPopoverHeight}px` : undefined, + })} + role="combobox" + aria-expanded=${isOpen} + aria-haspopup="listbox" + aria-controls=${`${comboboxId}-popover`} + aria-owns=${`${comboboxId}-popover`} > - ${TextField({ - size, - isQuiet, - isDisabled, - isInvalid, - isFocused, - isKeyboardFocused, - customClasses: [ - `${rootClass}-textfield`, - ...(isLoading ? ["is-loading"] : []), - ], - customInputClasses: [`${rootClass}-input`], - isLoading, - customInfieldProgressCircleClasses: ["spectrum-Combobox-progress-circle"], - name: "field", - isReadOnly, - value, - onclick: function () { - if (!isOpen) updateArgs({ isOpen: true }); - }, - }, context)} - ${PickerButton({ - customClasses: [ - `${rootClass}-button`, - ... isInvalid ? ["is-invalid"] : [], - ], - size, - iconSet: "ui", - iconName: "ChevronDown", - isQuiet, - id: getRandomId("picker"), - isOpen, - isFocused, - isKeyboardFocused, - isDisabled, - position: "right", - onclick: function () { - updateArgs({ isOpen: !isOpen }); - }, - tabindex: "-1", - }, context)} -
- `; -}; - -export const Template = ({ - size = "m", - isOpen = true, - isQuiet = false, - isDisabled = false, - showFieldLabel = false, - isReadOnly = false, - fieldLabelText = "Select location", - fieldLabelPosition = "top", - content = [], - value = "", - ...args -} = {}, context = {}) => { - const popoverHeight = size === "s" ? 106 : size === "l" ? 170 : size === "xl" ? 229 : 142; // default value is "m" - return html` -
${when(showFieldLabel, () => FieldLabel({ size, label: fieldLabelText, isDisabled, - customStyles: { "max-inline-size": "100px"}, - alignment: fieldLabelPosition === "left" && "left", + customClasses: [`${rootClass}-label`], + alignment: fieldLabelPosition === "side" && "side", + isRequired: isLabelRequired, + }, context) + )} +
+ ${TextField({ + size, + isDisabled, + isInvalid, + isFocused, + isHovered, + isKeyboardFocused, + customClasses: [ + `${rootClass}-textfield`, + ...(isLoading ? ["is-loading"] : []), + ], + customInputClasses: [`${rootClass}-input`, `${rootClass}-autocomplete`], + isLoading, + customInfieldProgressCircleClasses: ["spectrum-Combobox-progress-circle"], + name: "field", + isReadOnly, + value, + autocomplete: autocomplete ? undefined : "off", + onclick: function () { + if (!isOpen) updateArgs({ isOpen: true }); + }, + }, context)} + ${InfieldButton({ + customClasses: [ + `${rootClass}-button`, + ...(!isDisabled && isOpen ? ["is-open"] : []), + ], + size, + id: getRandomId("infieldbutton"), + isDisabled: isDisabled || isReadOnly, + tabindex: "-1", + onclick: function () { + updateArgs({ + isOpen: !isOpen, + }); + }, + }, context)} +
+ ${Popover({ + isOpen: isOpen && !isDisabled && !isReadOnly, + withTip: false, + position: "bottom-start", + customClasses: [`${rootClass}-popover`], + customStyles: { + "inline-size": size === "s" ? "192px" : size === "l" ? "224px" : size === "xl" ? "240px" : "208px", + }, + content, + popoverHeight, + }, context)} + ${when(helpText, () => + HelpText({ + customClasses: [`${rootClass}-helptext`], + size, + isDisabled, + hideIcon: true, + text: helpText, + variant: isInvalid ? "negative" : "neutral", }, context) )} - ${[ - Popover({ - isOpen: isOpen && !isDisabled && !isReadOnly, - withTip: false, - position: "bottom-start", - isQuiet, - trigger: (passthrough) => Combobox({ - size, - isOpen, - isQuiet, - isDisabled, - isReadOnly, - value, - ...args, - ...passthrough, - }, context), - content, - popoverWidth: size === "s" ? 140 : size === "l" ? 191 : size === "xl" ? 192 : 166, // default value is "m" - popoverHeight, - }, context), - ]} +
`; }; +export const HelpTextTemplate = (args, context) => { + const variants = [ + { + heading: "Help text", + args: {...args, helpText: "Choose a topic. Add a new topic by typing it in the field, then selecting 'Enter.'"}, + }, + { + heading: "Help text with error", + args: {...args, helpText: "Choose or add at least one topic.", isInvalid: true}, + } + ]; + + return Container({ + direction: "row", + withHeading: false, + withBorder: false, + content: html`${variants.map(variant => Container({ + withBorder: false, + heading: variant.heading, + containerStyles: {"display": "inline"}, + content: Template({ + ...variant.args, + customStyles: {"margin-top": "8px"} + }, context)}, + context))}`, + }, context); +}; + export const VariantGroup = (args, context) => { const variants = [ { diff --git a/components/helptext/index.css b/components/helptext/index.css index ab3051efa3f..3ef91377442 100644 --- a/components/helptext/index.css +++ b/components/helptext/index.css @@ -127,11 +127,11 @@ &.is-disabled { .spectrum-HelpText-text { - color: var(--highcontrast-helptext-content-color-default, var(--mod-helptext-content-color-default, var(--spectrum-helptext-content-color-default))); + color: var(--highcontrast-helptext-content-color-disabled, var(--mod-helptext-content-color-default, var(--spectrum-helptext-content-color-default))); } .spectrum-HelpText-validationIcon { - color: var(--highcontrast-helptext-icon-color-default, var(--mod-helptext-icon-color-default, var(--spectrum-helptext-icon-color-default))); + color: var(--highcontrast-helptext-icon-color-disabled, var(--mod-helptext-icon-color-default, var(--spectrum-helptext-icon-color-default))); } } } @@ -140,5 +140,7 @@ .spectrum-HelpText { --highcontrast-helptext-content-color-default: CanvasText; --highcontrast-helptext-icon-color-default: CanvasText; + --highcontrast-helptext-icon-color-disabled: GrayText; + --highcontrast-helptext-content-color-disabled: GrayText; } } diff --git a/components/infieldbutton/stories/template.js b/components/infieldbutton/stories/template.js index fe54a063aa6..02185b17d4e 100644 --- a/components/infieldbutton/stories/template.js +++ b/components/infieldbutton/stories/template.js @@ -111,6 +111,7 @@ export const Template = ( type="button" tabindex=${tabIndex} @click=${onclick} + role="presentation" >
${when(iconName, () =>