diff --git a/plugins/beta/map-styles/src/mapStyles.scss b/plugins/beta/map-styles/src/mapStyles.scss index 0f7e564b0..9d140ac19 100755 --- a/plugins/beta/map-styles/src/mapStyles.scss +++ b/plugins/beta/map-styles/src/mapStyles.scss @@ -33,8 +33,13 @@ .im-c-map-styles__image { position: relative; - padding: 2px; - border: 3px solid transparent; + padding: 1px; + border: 4px solid transparent; + margin-bottom: 2px; + + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + border-width: 2px; + } } .im-c-map-styles__image svg { @@ -48,7 +53,7 @@ .im-c-map-styles img { display: block; - // border: 1px solid var(--image-placeholder-color); + } // Text size @@ -66,16 +71,25 @@ outline: 0; } -.im-c-map-styles__button[data-focus-visible="true"] { - .im-c-map-styles__image::after { +.im-c-map-styles__button[aria-pressed="true"] .im-c-map-styles__image::after { + @media (-ms-high-contrast:active), screen and (forced-colors: active) { content: ''; position: absolute; - top: -3px; - right: -3px; - bottom: -3px; - left: -3px; - outline: 3px solid var(--focus-outline-color); - border: 3px solid var(--focus-border-color); + inset: -4px; + border: 4px solid var(--focus-border-color); + } +} + +.im-c-map-styles__button[data-focus-visible="true"] .im-c-map-styles__image::after { + content: ''; + position: absolute; + inset: -4px; + outline: 3px solid var(--focus-outline-color); + border: 4px solid var(--focus-border-color); + + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + outline-offset: 1px; + outline-color: Highlight; } } diff --git a/plugins/search/src/components/CloseButton/CloseButton.module.scss b/plugins/search/src/components/CloseButton/CloseButton.module.scss index e23c03d32..a16e59625 100755 --- a/plugins/search/src/components/CloseButton/CloseButton.module.scss +++ b/plugins/search/src/components/CloseButton/CloseButton.module.scss @@ -4,7 +4,12 @@ &:before { box-shadow: none; } + &[data-focus-visible="true"] { z-index: 1; } + + &:hover { + z-index: 1; + } } \ No newline at end of file diff --git a/plugins/search/src/components/Form/Form.module.scss b/plugins/search/src/components/Form/Form.module.scss index 17c87ff7a..a6fb456e3 100755 --- a/plugins/search/src/components/Form/Form.module.scss +++ b/plugins/search/src/components/Form/Form.module.scss @@ -37,7 +37,7 @@ .im-c-search-suggestions { position: absolute; - top: calc(100% + 1px); + top: calc(100% + 2px); left: 0; right: 0; z-index: 1; diff --git a/plugins/search/src/components/Suggestions/Suggestions.module.scss b/plugins/search/src/components/Suggestions/Suggestions.module.scss index 14f7fc029..66340744f 100755 --- a/plugins/search/src/components/Suggestions/Suggestions.module.scss +++ b/plugins/search/src/components/Suggestions/Suggestions.module.scss @@ -19,11 +19,19 @@ border-top: 1px solid var(--button-hover-color); cursor: pointer; + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + color: Highlight; + } + mark { font-weight: bold; color: var(--foreground-color); background-color: transparent; pointer-events: none; + + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + color: Highlight; + } } &:hover:not([aria-selected="true"]) { @@ -38,6 +46,10 @@ &[aria-selected="true"] mark { color: var(--fixed-foreground-color); + + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + color: Highlight; + } } &:hover:not([aria-selected="true"]) .im-c-search-suggestions__label { diff --git a/plugins/search/src/search.scss b/plugins/search/src/search.scss index 671c74b03..8c000c423 100755 --- a/plugins/search/src/search.scss +++ b/plugins/search/src/search.scss @@ -32,19 +32,31 @@ content: ''; z-index: 1; position: absolute; - top: -1px; - right: -1px; - bottom: -1px; - left: -1px; + top: -2px; + right: -2px; + bottom: -2px; + left: -2px; border-radius: var(--button-border-radius); border: 2px solid var(--foreground-color); pointer-events: none; + + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + border-color: ButtonText; + } + } + + // Excludes the close button so its own hover indicator (shared via + // .im-c-map-button) doesn't also light up this container's ring. + &:hover:not(:has(.im-c-search-close-button:hover)):before { + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + border-color: Highlight; + } } &:after { @include tools.focus-base($type: 'button'); opacity: 0; - + @media (prefers-reduced-motion: no-preference) { transition: opacity var(--duration) ease; } @@ -64,6 +76,11 @@ &:focus { outline: 3px solid transparent; box-shadow: none; + + // The container's own :before is the only focus indicator we want. + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + outline: none; + } } &::placeholder { @@ -107,6 +124,27 @@ margin: var(--primary-gap); } +.im-c-search__input-container--keyboard-focus-within:before { + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + border-color: Highlight; + } +} + .im-c-search__input-container--keyboard-focus-within:after { opacity: 1; + + // Forced-colors uses a Highlight outline on the container itself instead + // (see below); hiding this avoids a mega-thick double ring that + // doesn't pick up forced colors correctly. + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + opacity: 0; + } +} + +// A real outline (not the :before border) so it sits flush against the real +// edge and grows outward exactly like the close button's own focus ring. +.im-c-search__input-container--keyboard-focus-within { + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + outline: var(--focus-highlight-width) solid Highlight; + } } diff --git a/sonar-project.properties b/sonar-project.properties index fe09fd311..2d6422805 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -17,7 +17,7 @@ sonar.test.inclusions=**/*.test.*,**/__mocks__/**,**/__stubs__/** sonar.cpd.exclusions=**/*.test.*,**/__mocks__/**,**/__stubs__/** # Ignored rules -sonar.issue.ignore.multicriteria=reactPropsJs,reactPropsJsx,preferGlobalThisJs,preferGlobalThisJsx,preferAtJs,preferAtJsx,replaceAllJs,replaceAllJsx,stringReplaceAllJs,stringReplaceAllJsx,pascalCaseFunctionsJs,pascalCaseFunctionsJsx,pascalCaseFunctionsTs,pascalCaseFunctionsTsx +sonar.issue.ignore.multicriteria=reactPropsJs,reactPropsJsx,preferGlobalThisJs,preferGlobalThisJsx,preferAtJs,preferAtJsx,replaceAllJs,replaceAllJsx,stringReplaceAllJs,stringReplaceAllJsx,pascalCaseFunctionsJs,pascalCaseFunctionsJsx,pascalCaseFunctionsTs,pascalCaseFunctionsTsx,cssScopingRootMixins # S6774: React props validation - using TypeScript/JSDoc for prop types instead sonar.issue.ignore.multicriteria.reactPropsJs.ruleKey=javascript:S6774 @@ -58,3 +58,8 @@ sonar.issue.ignore.multicriteria.pascalCaseFunctionsTs.ruleKey=javascript:S100 sonar.issue.ignore.multicriteria.pascalCaseFunctionsTs.resourceKey=**/*.ts sonar.issue.ignore.multicriteria.pascalCaseFunctionsTsx.ruleKey=javascript:S100 sonar.issue.ignore.multicriteria.pascalCaseFunctionsTsx.resourceKey=**/*.tsx + +# S8776: Missing scoping root for & - false positive on Sass mixins, which are always +# @included inside a real selector so & resolves correctly once compiled +sonar.issue.ignore.multicriteria.cssScopingRootMixins.ruleKey=css:S8776 +sonar.issue.ignore.multicriteria.cssScopingRootMixins.resourceKey=src/scss/tools/**/*.scss diff --git a/src/App/components/MapButton/MapButton.module.scss b/src/App/components/MapButton/MapButton.module.scss index 44f09c48c..25d7432cb 100755 --- a/src/App/components/MapButton/MapButton.module.scss +++ b/src/App/components/MapButton/MapButton.module.scss @@ -70,6 +70,14 @@ background-color: var(--button-hover-color); } + + // Excludes focus-visible so the thicker focus outline (border-focus-base) + // always wins when a button is both hovered and keyboard-focused. + .im-c-map-button:hover:not([data-focus-visible="true"]) { + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + outline: var(--hover-outline-width) solid Highlight; + } + } } // 2. Elements @@ -172,6 +180,7 @@ } // 4. State styles + .im-c-map-button[aria-haspopup="menu"][aria-expanded="true"] svg { transform: rotate(180deg); } diff --git a/src/App/components/Panel/Panel.module.scss b/src/App/components/Panel/Panel.module.scss index 0bce8dd76..adb2c2818 100755 --- a/src/App/components/Panel/Panel.module.scss +++ b/src/App/components/Panel/Panel.module.scss @@ -63,6 +63,10 @@ outline: var(--scrollable-outline); outline-offset: var(--scrollable-outline-offset); box-shadow: var(--scrollable-box-shadow); + + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + outline-width: 4px; + } } } @@ -76,6 +80,11 @@ } background-color: var(--button-hover-color); + + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + border: 2px solid Highlight; + padding: calc(var(--button-padding) - 2px); + } } // 3. Modifiers @@ -108,4 +117,4 @@ // 4. State styles -// 5. Responsive tweaks \ No newline at end of file +// 5. Responsive tweaks diff --git a/src/App/components/Tabs/Tabs.module.scss b/src/App/components/Tabs/Tabs.module.scss index 9e61a92b9..6e4fbde27 100644 --- a/src/App/components/Tabs/Tabs.module.scss +++ b/src/App/components/Tabs/Tabs.module.scss @@ -28,6 +28,20 @@ font-weight: normal; outline: 3px solid transparent; + // The tab's own outline is otherwise invisible when focused, so forced-colors + // would substitute a default ring around the whole button. The label carries + // the intended ring (via link-focus below), so suppress this one entirely + // and leave the 1px border untouched. + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + outline: none; + } + + .im-c-tabs__label { + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + color: Highlight; + } + } + &[aria-selected='true'] { background: var(--background-color); border-color: var(--app-border-color); @@ -39,6 +53,16 @@ &:focus .im-c-tabs__label { @include tools.link-focus; } + + // Chromium/Firefox force a native button's border to Highlight on + // :hover/:focus regardless of author border-color. Pin it to the system + // equivalent of "currentColor" so hovering/focusing a tab doesn't change + // its border — only the label (above) should react to focus. + &:hover, &:focus { + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + border-color: ButtonBorder; + } + } } .im-c-tabs__tab:not([aria-selected='true']) { diff --git a/src/App/components/Tooltip/Tooltip.module.scss b/src/App/components/Tooltip/Tooltip.module.scss index 9869a3f47..086227a02 100755 --- a/src/App/components/Tooltip/Tooltip.module.scss +++ b/src/App/components/Tooltip/Tooltip.module.scss @@ -51,6 +51,10 @@ height: var(--tooltip-arrow-size); color: var(--tooltip-background-color); border: calc(var(--tooltip-arrow-size) / 2) solid currentcolor; + + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + border-color: Canvas; + } } // 2. Elements diff --git a/src/App/layout/layout.module.scss b/src/App/layout/layout.module.scss index 4f5a94dcb..f52a01730 100755 --- a/src/App/layout/layout.module.scss +++ b/src/App/layout/layout.module.scss @@ -503,10 +503,10 @@ .im-o-app--mobile .im-o-app__drawer .im-c-panel::after, .im-o-app--mobile .im-o-app__actions .im-c-panel::after, .im-o-app__modal .im-c-panel--drawer::after { - top: var(--focus-border-width); - right: var(--focus-border-width); - bottom: var(--focus-border-width); - left: var(--focus-border-width); + top: var(--focus-box-shadow-width); + right: var(--focus-box-shadow-width); + bottom: var(--focus-box-shadow-width); + left: var(--focus-box-shadow-width); } // 4. State styles diff --git a/src/scss/settings/_colors.scss b/src/scss/settings/_colors.scss index 9aa7d9e56..0d9d49122 100755 --- a/src/scss/settings/_colors.scss +++ b/src/scss/settings/_colors.scss @@ -21,7 +21,7 @@ // Scrollable content --scrollable-outline: 3px solid #ffdd00; --scrollable-outline-offset: -3px; - --scrollable-box-shadow: inset 0 0 0 6px #002d18; + --scrollable-box-shadow: inset 0 0 0 7px #002d18; // Tooltips --tooltip-background-color: #383F43; diff --git a/src/scss/settings/_dimensions.scss b/src/scss/settings/_dimensions.scss index 944459257..8d78b9148 100755 --- a/src/scss/settings/_dimensions.scss +++ b/src/scss/settings/_dimensions.scss @@ -14,8 +14,11 @@ --app-border-width: 1px; --button-border-radius: 0px; // 4px; --panel-border-radius: 0px; // 8px; - --focus-border-width: 3px; - --focus-outline-width: 3px; + --focus-border-width: 4px; // The black inner focus stroke + --focus-box-shadow-width: 3px; // The yellow outer focus stroke + --focus-highlight-width: 4px; + --focus-outline-width: 3px; // Transparent, used for forced-colors focus + --hover-outline-width: 2px; // Used with forced-colors hover state // Modal offset placeholders (These are set with JS) --modal-offset-top: 0px; diff --git a/src/scss/tools/_border-focus.scss b/src/scss/tools/_border-focus.scss index e61ed9b96..7eeec2537 100755 --- a/src/scss/tools/_border-focus.scss +++ b/src/scss/tools/_border-focus.scss @@ -43,23 +43,29 @@ } @else { box-shadow: null; } + + // Forced-colors mode mis-resolves this pseudo-element's height: 100% + // (the panel is an auto-height flex column, not a definite height), + // producing a solid painted block instead of the intended soft shadow. + // The shadow is purely decorative, so drop the box entirely here. + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + content: none; + } } // Offset $offset: -1px; + $outline-offset: 0px; @if $is-inset == true { - $offset: var(--focus-border-width); + $offset: var(--focus-box-shadow-width); + $outline-offset: calc(-1 * var(--focus-outline-width)); } &:focus { outline: 0; } - &[data-focus-visible="true"] { - outline: 3px solid transparent; - } - &::after { content: ''; pointer-events: none; @@ -70,17 +76,37 @@ bottom: $offset; left: $offset; border: var(--focus-border-width) solid var(--focus-border-color); - outline: var(--focus-outline-width) solid var(--focus-outline-color); + outline: var(--focus-outline-width) solid transparent; + outline-offset: 1px; + box-shadow: 0 0 0 var(--focus-box-shadow-width) var(--focus-outline-color); border-radius: calc($border-radius - 2px); // default radius on focus ring opacity: 0; @media (prefers-reduced-motion: no-preference) { transition: opacity var(--duration) ease; } + + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + outline-color: Highlight; + } } &[data-focus-visible="true"]::after { opacity: 1; + outline-style: solid; + + // Forced-colors uses a plain outline directly on the element instead + // (see below) so the ::after ring is hidden to avoid a double ring. + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + opacity: 0; + } + } + + &[data-focus-visible="true"] { + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + outline: var(--focus-highlight-width) solid Highlight; + outline-offset: $outline-offset; + } } } @@ -195,7 +221,7 @@ bottom: $offset; left: $offset; border: var(--focus-border-width) solid var(--focus-border-color); - outline: var(--focus-outline-width) solid var(--focus-outline-color); + outline: var(--focus-box-shadow-width) solid var(--focus-outline-color); border-radius: calc(#{$border-radius} - 2px); } diff --git a/src/scss/tools/_link-focus.scss b/src/scss/tools/_link-focus.scss index d9ebfaf57..9c839de8c 100755 --- a/src/scss/tools/_link-focus.scss +++ b/src/scss/tools/_link-focus.scss @@ -4,4 +4,12 @@ background-color: var(--focus-outline-color); box-shadow: 0 -2px var(--focus-outline-color), 0 4px var(--focus-border-color); text-decoration: none; + + // Forced-colors mode ignores the transparent outline-color and substitutes + // a UA default (rendering as the page foreground colour) unless a system + // colour keyword is set explicitly. + @media (-ms-high-contrast:active), screen and (forced-colors: active) { + outline-color: Highlight; + color: Highlight; + } } \ No newline at end of file