From 4325201393d8d9060cecd0c5e341009b114a6825 Mon Sep 17 00:00:00 2001 From: Louis-Maxime Piton Date: Tue, 9 Dec 2025 10:00:17 +0100 Subject: [PATCH 01/12] feat(lib): Add Select input component --- .bundlewatch.config.json | 2 +- build/vnu-jar.mjs | 4 +- packages/orange/scss/tokens/_composite.scss | 4 + packages/sosh/scss/tokens/_composite.scss | 4 + scss/_forms.scss | 2 +- scss/forms/_form-select.scss | 77 --- scss/forms/_select-input.scss | 292 +++++++++++ site/data/sidebar.yml | 3 +- site/src/content/docs/components/tags.mdx | 2 +- site/src/content/docs/forms/select-input.mdx | 477 ++++++++++++++++++ site/src/content/docs/forms/select.mdx | 9 - site/src/content/docs/forms/text-area.mdx | 4 +- site/src/content/docs/forms/text-input.mdx | 4 +- .../migrations/migration-from-boosted.mdx | 34 ++ .../src/content/docs/migrations/migration.mdx | 4 + 15 files changed, 826 insertions(+), 96 deletions(-) delete mode 100644 scss/forms/_form-select.scss create mode 100644 scss/forms/_select-input.scss create mode 100644 site/src/content/docs/forms/select-input.mdx delete mode 100644 site/src/content/docs/forms/select.mdx diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 4277c54383..f12f4fba2d 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -38,7 +38,7 @@ }, { "path": "./packages/orange/dist/css/ouds-web.min.css", - "maxSize": "58.5 kB" + "maxSize": "58.75 kB" }, { "path": "./dist/js/ouds-web.bundle.js", diff --git a/build/vnu-jar.mjs b/build/vnu-jar.mjs index ffcf21f03b..726a154cb2 100644 --- a/build/vnu-jar.mjs +++ b/build/vnu-jar.mjs @@ -42,7 +42,9 @@ execFile('java', ['-version'], (error, stdout, stderr) => { 'Trailing slash on void elements has no effect and interacts badly with unquoted attribute values.', // Allow `switch` attribute. 'Attribute “switch” not allowed on element “input” at this point.', - 'Element “style” not allowed as child of element “div” in this context.*' + 'Element “style” not allowed as child of element “div” in this context.*', + // Allow empty option in select + 'Element “option” without attribute “label” must not be empty.' // End mod ].join('|') diff --git a/packages/orange/scss/tokens/_composite.scss b/packages/orange/scss/tokens/_composite.scss index a94d826487..c257053f35 100644 --- a/packages/orange/scss/tokens/_composite.scss +++ b/packages/orange/scss/tokens/_composite.scss @@ -104,6 +104,10 @@ $alert-icon-warning-internal: url("data:image/svg+xml,") !default; +// Select input +$select-input-chevron: url("data:image/svg+xml,") !default; +$select-input-expanded-chevron: url("data:image/svg+xml,") !default; + //// SVG used several times $svg-as-custom-props: ( "chevron": $chevron-icon, diff --git a/packages/sosh/scss/tokens/_composite.scss b/packages/sosh/scss/tokens/_composite.scss index 70a04c755e..fc1185d4c7 100644 --- a/packages/sosh/scss/tokens/_composite.scss +++ b/packages/sosh/scss/tokens/_composite.scss @@ -99,6 +99,10 @@ $alert-icon-warning-internal: url("data:image/svg+xml,") !default; +// Select input +$select-input-chevron: url("data:image/svg+xml,") !default; +$select-input-expanded-chevron: url("data:image/svg+xml,") !default; + //// SVG used several times $svg-as-custom-props: ( "chevron": $chevron-icon, diff --git a/scss/_forms.scss b/scss/_forms.scss index fbfe0f36fa..b2edfd3ad6 100644 --- a/scss/_forms.scss +++ b/scss/_forms.scss @@ -1,7 +1,6 @@ @import "forms/labels"; @import "forms/form-text"; @import "forms/form-control"; -@import "forms/form-select"; @import "forms/control-item"; @import "forms/form-range"; // OUDS mod: no floating-labels @@ -9,4 +8,5 @@ @import "forms/star-rating"; // OUDS mod @import "forms/validation"; @import "forms/quantity-selector"; // OUDS mod +@import "forms/select-input"; // OUDS mod @import "forms/text-input"; // OUDS mod diff --git a/scss/forms/_form-select.scss b/scss/forms/_form-select.scss deleted file mode 100644 index 1769e12ea2..0000000000 --- a/scss/forms/_form-select.scss +++ /dev/null @@ -1,77 +0,0 @@ -// Select -// -// Replaces the browser default select with a custom one, mostly pulled from -// https://primer.github.io/. - -.form-select { - --#{$prefix}form-select-bg-img: var(--#{$prefix}form-select-indicator); // OUDS mod: instead of `#{escape-svg($form-select-indicator)}` - - display: block; - width: 100%; - padding: subtract($form-select-padding-y, 1px) $form-select-indicator-padding add($form-select-padding-y, 1px) $form-select-padding-x; // OUDS mod - font-family: $form-select-font-family; - @include font-size($form-select-font-size); - font-weight: $form-select-font-weight; - line-height: $form-select-line-height; - color: $form-select-color; - appearance: none; - background-color: $form-select-bg; - background-image: var(--#{$prefix}form-select-bg-img), var(--#{$prefix}form-select-bg-icon, none); - background-repeat: no-repeat; - background-position: $form-select-bg-position; - background-size: $form-select-bg-size; - border: $form-select-border-width solid $form-select-border-color; - @include border-radius($form-select-border-radius, 0); - @include box-shadow($form-select-box-shadow); - @include transition($form-select-transition); - - &:focus { - border-color: $form-select-focus-border-color !important; // stylelint-disable-line declaration-no-important - outline: 0; - @if $enable-shadows { - @include box-shadow($form-select-box-shadow, $form-select-focus-box-shadow); - } @else { - // Avoid using mixin so we can pass custom focus shadow properly - box-shadow: $form-select-focus-box-shadow; - } - } - - &[multiple], - &[size]:not([size="1"]) { - padding-right: $form-select-padding-x; - background-image: none; - } - - &:disabled { - color: $form-select-disabled-color; - background-color: $form-select-disabled-bg; - background-image: var(--#{$prefix}form-select-disabled-indicator); // OUDS mod - border-color: $form-select-disabled-border-color; - } - - // Remove outline from select box in FF - &:-moz-focusring { - color: transparent; - text-shadow: 0 0 0 $form-select-color; - } -} - -.form-select-sm { - padding-top: subtract($form-select-padding-y-sm, 1px); // OUDS mod - padding-bottom: add($form-select-padding-y-sm, 1px); // OUDS mod - padding-left: $form-select-padding-x-sm; - @include font-size($form-select-font-size-sm); - @include border-radius($form-select-border-radius-sm); - line-height: $line-height-sm; // OUDS mod -} - -.form-select-lg { - padding-top: subtract($form-select-padding-y-lg, 1px); // OUDS mod - padding-bottom: $form-select-padding-y-lg; - padding-left: $form-select-padding-x-lg; - @include font-size($form-select-font-size-lg); - @include border-radius($form-select-border-radius-lg); - line-height: 1.5; // OUDS mod -} - -// OUDS mod: no `@if enable-dark-mode` diff --git a/scss/forms/_select-input.scss b/scss/forms/_select-input.scss new file mode 100644 index 0000000000..276b1cda37 --- /dev/null +++ b/scss/forms/_select-input.scss @@ -0,0 +1,292 @@ +// Select + +.select-input { + @extend %text-item-common; + --#{$prefix}text-input-min-width: #{px-to-rem($ouds-select-input-size-min-width)}; + --#{$prefix}text-input-max-width: #{px-to-rem($ouds-text-input-size-max-width)}; + --#{$prefix}text-input-min-height: #{px-to-rem($ouds-text-input-size-min-height)}; +} + +.select-input-field { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding-top: calc(var(--#{$prefix}text-input-padding-y) - var(--#{$prefix}text-input-border-width-top) + var(--#{$prefix}font-size-label-small) * var(--#{$prefix}font-line-height-label-small)); // stylelint-disable-line function-disallowed-list + padding-right: calc(var(--#{$prefix}text-input-trailing-action-padding-right) - var(--#{$prefix}text-input-border-width-right) + var(--#{$prefix}text-input-column-gap) + var(--#{$prefix}text-input-trailing-action-width)); // stylelint-disable-line function-disallowed-list + padding-bottom: calc(var(--#{$prefix}text-input-padding-y) - var(--#{$prefix}text-input-border-width-bottom)); // stylelint-disable-line function-disallowed-list + padding-left: calc(var(--#{$prefix}text-input-padding-x) - var(--#{$prefix}text-input-border-width-left)); // stylelint-disable-line function-disallowed-list + color: $ouds-color-content-default; + appearance: none; + background-color: var(--#{$prefix}text-input-background-color); + border-color: var(--#{$prefix}text-input-border-color); + border-style: solid; + border-width: var(--#{$prefix}text-input-border-width-top) var(--#{$prefix}text-input-border-width-right) var(--#{$prefix}text-input-border-width-bottom) var(--#{$prefix}text-input-border-width-left); + @include border-radius(var(--#{$prefix}text-input-border-radius), 0); + + &:hover { + --#{$prefix}text-input-background-color: #{$ouds-color-action-support-hover}; + --#{$prefix}text-input-border-color: #{$ouds-text-input-color-border-hover}; + } + + &:focus-visible { + --#{$prefix}text-input-border-width-bottom: #{$ouds-text-input-border-width-focus}; + --#{$prefix}text-input-background-color: #{$ouds-color-action-support-focus}; + --#{$prefix}text-input-border-color: #{$ouds-text-input-color-border-focus}; + } + + &:open, + &:active { + --#{$prefix}text-input-border-width-bottom: #{$ouds-text-input-border-width-focus}; + --#{$prefix}text-input-background-color: #{$ouds-color-action-support-pressed}; + --#{$prefix}text-input-border-color: #{$ouds-text-input-color-border-focus}; + } + + &:disabled:where(:not(.loading-indeterminate, .loading-determinate)) { + --#{$prefix}text-input-background-color: #{$ouds-color-action-support-disabled}; + --#{$prefix}text-input-border-color: #{$ouds-color-action-disabled}; + color: $ouds-color-action-disabled; + } + + &:disabled { + pointer-events: none; + } +} + +.was-validated .select-input-field:invalid, +.select-input-field.is-invalid { + --#{$prefix}text-input-background-color: #{$ouds-color-surface-status-negative-muted}; + --#{$prefix}text-input-border-color: #{$ouds-color-action-negative-enabled}; + padding-right: calc(var(--#{$prefix}text-input-trailing-action-padding-right) - var(--#{$prefix}text-input-border-width-right) + var(--#{$prefix}text-input-column-gap) + var(--#{$prefix}text-input-trailing-action-width) + var(--#{$prefix}text-input-column-gap-trailing-error) + px-to-rem($ouds-button-size-icon-only)); // stylelint-disable-line function-disallowed-list + + &:hover { + --#{$prefix}text-input-border-color: #{$ouds-color-action-negative-hover}; + } + + &:focus-visible { + --#{$prefix}text-input-border-color: #{$ouds-color-action-negative-focus}; + } + + &:open, + &:active { + --#{$prefix}text-input-border-color: #{$ouds-color-action-negative-pressed}; + } +} + +.select-input-container { + position: relative; + min-width: var(--#{$prefix}text-input-min-width); + max-width: var(--#{$prefix}text-input-max-width); + min-height: var(--#{$prefix}text-input-min-height); + padding-top: calc(var(--#{$prefix}text-input-padding-y) - var(--#{$prefix}text-input-border-width-top)); // stylelint-disable-line function-disallowed-list + padding-right: calc(var(--#{$prefix}text-input-trailing-action-padding-right) - var(--#{$prefix}text-input-border-width-right)); // stylelint-disable-line function-disallowed-list + padding-bottom: calc(var(--#{$prefix}text-input-padding-y) - var(--#{$prefix}text-input-border-width-bottom)); // stylelint-disable-line function-disallowed-list + padding-left: calc(var(--#{$prefix}text-input-padding-x) - var(--#{$prefix}text-input-border-width-left)); // stylelint-disable-line function-disallowed-list + color: $ouds-color-content-muted; + @include get-font-size("label-large"); + + &::after { + position: absolute; + top: 50%; + right: calc(var(--#{$prefix}text-input-trailing-action-padding-right) + $ouds-button-space-inset-icon-only); // stylelint-disable-line function-disallowed-list + display: block; + width: px-to-rem($ouds-button-size-icon-only); + height: px-to-rem($ouds-button-size-icon-only); + pointer-events: none; + content: ""; + background-color: var(--#{$prefix}color-content-default); + mask: $select-input-chevron no-repeat 50% / px-to-rem($ouds-button-size-icon-only); + transform: translate(0, -50%); + } + + // Leading icon + > img, + > svg { + position: absolute; + top: calc(var(--#{$prefix}text-input-min-height) / 2 - var(--#{$prefix}text-input-icon-size) / 2 - var(--#{$prefix}text-input-border-width-top)); // stylelint-disable-line function-disallowed-list + left: calc(var(--#{$prefix}text-input-padding-x) - var(--#{$prefix}text-input-border-width-left)); // stylelint-disable-line function-disallowed-list + z-index: 2; + width: var(--#{$prefix}text-input-icon-size); + min-width: var(--#{$prefix}text-input-icon-size); + height: var(--#{$prefix}text-input-icon-size); + color: var(--#{$prefix}color-content-muted); + pointer-events: none; + } + + > label { + position: absolute; + top: 50%; + max-width: calc(100% - var(--#{$prefix}text-input-trailing-action-padding-right) - var(--#{$prefix}text-input-padding-x) - var(--#{$prefix}text-input-trailing-action-width) - var(--#{$prefix}text-input-column-gap)); // stylelint-disable-line function-disallowed-list + max-height: 100%; + overflow: hidden; + text-overflow: ellipsis; + transform: translate(0, -50%); + @include transition(font-size .15s ease-in-out, top .15s ease-out); + } + + &:has(> svg), + &:has(> img) { + > .select-input-field { + padding-left: calc(var(--#{$prefix}text-input-icon-size) + var(--#{$prefix}text-input-column-gap) + var(--#{$prefix}text-input-padding-x) - var(--#{$prefix}text-input-border-width-left)); // stylelint-disable-line function-disallowed-list + } + + > label { + padding-left: calc(var(--#{$prefix}text-input-icon-size) + var(--#{$prefix}text-input-column-gap)); // stylelint-disable-line function-disallowed-list + } + } + + &:has(:open)::after { + mask-image: $select-input-expanded-chevron; + } + + &:has(.select-input-field:disabled:where(:not(.loading-indeterminate, .loading-determinate))) { + color: $ouds-color-action-disabled; + + &::after { + background-color: $ouds-color-content-disabled; + } + } + + &:has(.select-input-field:disabled) { + > label { + pointer-events: none; + } + } + + &:not(:has(:disabled:checked)), + &:has(:open) { + > label { + top: calc(var(--#{$prefix}text-input-padding-y) + .25rem + .5 * (var(--#{$prefix}font-size-label-small) * var(--#{$prefix}font-line-height-label-small))); // stylelint-disable-line function-disallowed-list + white-space: nowrap; + @include get-font-size("label-small"); + } + } + + .loader { + position: absolute; + top: 50%; + right: calc(var(--#{$prefix}text-input-trailing-action-padding-right) + $ouds-button-space-inset-icon-only); // stylelint-disable-line function-disallowed-list + left: auto; + display: none; + width: px-to-rem($ouds-button-size-icon-only); + height: px-to-rem($ouds-button-size-icon-only); + pointer-events: none; + transform: translate(0, -50%) rotate(-90deg); + + > .loader-inner { + fill: none; + stroke: var(--#{$prefix}color-content-default); + stroke-dasharray: var(--#{$prefix}loading-dasharray); + stroke-width: 6; + transform-origin: center; + animation: var(--#{$prefix}loading-animation); + } + } + + .loading-message { + @include visually-hidden(); + display: none; + } + + &:has(.loading-indeterminate) { + --#{$prefix}loading-dasharray: 96; + --#{$prefix}loading-animation: 2.1875s infinite linear rotate1-indeterminate, 1.25s linear infinite rotate2-indeterminate; + } + + &:has(.loading-determinate) { + --#{$prefix}loading-dasharray: 107; + --#{$prefix}loading-animation: var(--#{$prefix}loading-time) infinite linear rotate-determinate; + } + + &:has(.loading-indeterminate, .loading-determinate) { + .loader { + display: block; + } + + &::after { + display: none; + } + + .loading-message { + display: block; + } + } + + ~ .link { + @extend %text-input-message; + } +} + +.was-validated .select-input-container:has(:invalid), +.select-input-container:has(.is-invalid) { + color: $ouds-color-action-negative-enabled; + + &::before { + position: absolute; + top: 50%; + right: calc(var(--#{$prefix}text-input-trailing-action-width) + var(--#{$prefix}text-input-trailing-action-padding-right) + var(--#{$prefix}text-input-column-gap-trailing-error)); // stylelint-disable-line function-disallowed-list + display: block; + width: px-to-rem($ouds-button-size-icon-only); + height: px-to-rem($ouds-button-size-icon-only); + pointer-events: none; + content: ""; + background-color: currentcolor; + mask: var(--#{$prefix}error-icon) no-repeat 50% / px-to-rem($ouds-button-size-icon-only); + transform: translate(0, -50%); + } + + &:has(:hover) { + color: $ouds-color-action-negative-hover; + } + + &:has(:focus-visible) { + color: $ouds-color-action-negative-focus; + } + + &:has(:open), + &:has(:active) { + color: $ouds-color-action-negative-pressed; + } + + > label { + max-width: calc(100% - var(--#{$prefix}text-input-trailing-action-padding-right) - var(--#{$prefix}text-input-padding-x) - var(--#{$prefix}text-input-trailing-action-width) - var(--#{$prefix}text-input-column-gap) - px-to-rem($ouds-button-size-icon-only) - var(--#{$prefix}text-input-column-gap-trailing-error)); // stylelint-disable-line function-disallowed-list + } + + ~ .helper-text { + display: none; + } + + ~ .error-text { + display: block; + } +} + +.select-input-container-outlined .select-input-field { + --#{$prefix}text-input-background-color: transparent; + --#{$prefix}text-input-border-width-top: #{$ouds-text-input-border-width-default}; + --#{$prefix}text-input-border-width-right: #{$ouds-text-input-border-width-default}; + --#{$prefix}text-input-border-width-left: #{$ouds-text-input-border-width-default}; + + &:focus-visible, + &:open, + &:active { + --#{$prefix}text-input-border-width-top: #{$ouds-text-input-border-width-focus}; + --#{$prefix}text-input-border-width-right: #{$ouds-text-input-border-width-focus}; + --#{$prefix}text-input-border-width-left: #{$ouds-text-input-border-width-focus}; + } +} + +.select-input-container-rounded { + --#{$prefix}text-input-border-radius: #{$ouds-text-input-border-radius-rounded}; +} + +option:disabled[value=""] { + display: none; +} + +option, +optgroup { + color: var(--#{$prefix}color-content-default); + background-color: var(--#{$prefix}color-bg-primary); +} diff --git a/site/data/sidebar.yml b/site/data/sidebar.yml index fff962efe3..6620166c8d 100644 --- a/site/data/sidebar.yml +++ b/site/data/sidebar.yml @@ -79,8 +79,7 @@ - title: Overview - title: Text input - title: Text area - - title: Select - draft: true + - title: Select input - title: Checkbox - title: Radio button - title: Switch diff --git a/site/src/content/docs/components/tags.mdx b/site/src/content/docs/components/tags.mdx index 65c178cea1..eadc84ef0f 100644 --- a/site/src/content/docs/components/tags.mdx +++ b/site/src/content/docs/components/tags.mdx @@ -64,7 +64,7 @@ For accessibility reasons, any informative element, must either be a semantic el Tag and input tag lists must be wrapped in a semantic container (usually a `