diff --git a/.changeset/spotty-penguins-sort.md b/.changeset/spotty-penguins-sort.md new file mode 100644 index 00000000000..716b4e14b14 --- /dev/null +++ b/.changeset/spotty-penguins-sort.md @@ -0,0 +1,8 @@ +--- +"@spectrum-css/rating": major +--- + +Provides more granular control over the hover behavior of child stars within the rating component to prevent hovering in space adjacent to the component from highlighting all stars. + +- `.is-focused` has been renamed to `.is-keyboardFocused` to better reflect its intended use. Clicking the rating component no longer renders the focus ring. +- `.is-hoverSelection` has been added to the rating component and may be leveraged to update the hover and select state of the star icons the component contains. diff --git a/components/rating/index.css b/components/rating/index.css index ff4f5bd8195..d1cb4757341 100644 --- a/components/rating/index.css +++ b/components/rating/index.css @@ -44,7 +44,8 @@ } .spectrum-Rating { - &.is-focused { + &:has(:focus-visible), + &.is-keyboardFocused { box-shadow: 0 0 0 var(--mod-rating-focus-indicator-thickness, var(--spectrum-rating-focus-indicator-thickness)) var(--highcontrast-rating-focus-indicator-color, var(--mod-rating-focus-indicator-color, var(--spectrum-rating-focus-indicator-color))); .spectrum-Rating-icon { @@ -98,17 +99,6 @@ cursor: default; pointer-events: none; } - - /* When the entire component is hovered, show all solid icons */ - &:hover { - .spectrum-Rating-starActive { - display: block; - } - - .spectrum-Rating-starInactive { - display: none; - } - } } .spectrum-Rating-input { @@ -158,14 +148,14 @@ position: absolute; } - /* All stars following the hovered star */ - &:hover ~ .spectrum-Rating-icon { + &:hover, + &.is-hoverSelection { .spectrum-Rating-starActive { - display: none; + display: block; } .spectrum-Rating-starInactive { - display: block; + display: none; } } } @@ -191,7 +181,7 @@ } .spectrum-Rating--emphasized { - &.is-focused { + &.is-keyboardFocused { .spectrum-Rating-icon.is-selected { color: var(--highcontrast-rating-emphasized-icon-color-key-focus, var(--mod-rating-emphasized-icon-color-key-focus, var(--spectrum-rating-emphasized-icon-color-key-focus))); } diff --git a/components/rating/metadata/metadata.json b/components/rating/metadata/metadata.json index 7902bda44e7..233fed1c14c 100644 --- a/components/rating/metadata/metadata.json +++ b/components/rating/metadata/metadata.json @@ -5,7 +5,7 @@ ".spectrum-Rating--emphasized .spectrum-Rating-icon", ".spectrum-Rating--emphasized .spectrum-Rating-icon.is-selected", ".spectrum-Rating--emphasized .spectrum-Rating-icon:hover ~ .spectrum-Rating-icon", - ".spectrum-Rating--emphasized.is-focused .spectrum-Rating-icon.is-selected", + ".spectrum-Rating--emphasized.is-keyboardFocused .spectrum-Rating-icon.is-selected", ".spectrum-Rating--emphasized:hover .spectrum-Rating-icon", ".spectrum-Rating--emphasized:hover .spectrum-Rating-icon.is-currentValue:after", ".spectrum-Rating--emphasized:hover .spectrum-Rating-icon:active", @@ -15,28 +15,31 @@ ".spectrum-Rating-icon .spectrum-Rating-starActive", ".spectrum-Rating-icon .spectrum-Rating-starInactive", ".spectrum-Rating-icon.is-currentValue:after", + ".spectrum-Rating-icon.is-hoverSelection .spectrum-Rating-starActive", + ".spectrum-Rating-icon.is-hoverSelection .spectrum-Rating-starInactive", ".spectrum-Rating-icon.is-selected", ".spectrum-Rating-icon.is-selected .spectrum-Rating-starActive", ".spectrum-Rating-icon.is-selected .spectrum-Rating-starInactive", + ".spectrum-Rating-icon:hover .spectrum-Rating-starActive", + ".spectrum-Rating-icon:hover .spectrum-Rating-starInactive", ".spectrum-Rating-icon:hover ~ .spectrum-Rating-icon", - ".spectrum-Rating-icon:hover ~ .spectrum-Rating-icon .spectrum-Rating-starActive", - ".spectrum-Rating-icon:hover ~ .spectrum-Rating-icon .spectrum-Rating-starInactive", ".spectrum-Rating-input", ".spectrum-Rating-starActive", ".spectrum-Rating-starInactive", ".spectrum-Rating.is-disabled", ".spectrum-Rating.is-disabled .spectrum-Rating-icon", ".spectrum-Rating.is-disabled .spectrum-Rating-icon.is-selected", - ".spectrum-Rating.is-focused", - ".spectrum-Rating.is-focused .spectrum-Rating-icon", - ".spectrum-Rating.is-focused .spectrum-Rating-icon.is-selected", + ".spectrum-Rating.is-keyboardFocused", + ".spectrum-Rating.is-keyboardFocused .spectrum-Rating-icon", + ".spectrum-Rating.is-keyboardFocused .spectrum-Rating-icon.is-selected", ".spectrum-Rating.is-readOnly", + ".spectrum-Rating:has(:focus-visible)", + ".spectrum-Rating:has(:focus-visible) .spectrum-Rating-icon", + ".spectrum-Rating:has(:focus-visible) .spectrum-Rating-icon.is-selected", ".spectrum-Rating:hover .spectrum-Rating-icon", ".spectrum-Rating:hover .spectrum-Rating-icon.is-currentValue:after", ".spectrum-Rating:hover .spectrum-Rating-icon:active", - ".spectrum-Rating:hover .spectrum-Rating-icon:hover", - ".spectrum-Rating:hover .spectrum-Rating-starActive", - ".spectrum-Rating:hover .spectrum-Rating-starInactive" + ".spectrum-Rating:hover .spectrum-Rating-icon:hover" ], "modifiers": [ "--mod-rating-border-radius", diff --git a/components/rating/stories/rating.stories.js b/components/rating/stories/rating.stories.js index 79efe8c3b83..f70f5e1d399 100644 --- a/components/rating/stories/rating.stories.js +++ b/components/rating/stories/rating.stories.js @@ -1,5 +1,5 @@ import { disableDefaultModes } from "@spectrum-css/preview/modes"; -import { isDisabled, isEmphasized, isFocused, isReadOnly } from "@spectrum-css/preview/types"; +import { isDisabled, isEmphasized, isKeyboardFocused, isReadOnly } from "@spectrum-css/preview/types"; import metadata from "../metadata/metadata.json"; import packageJson from "../package.json"; import { RatingGroup } from "./rating.test.js"; @@ -8,17 +8,18 @@ import { Template } from "./template.js"; /** * The rating component is used to display or collect a user's rating of an item as represented by a number of stars. * - * ### Usage notes + * ## Usage notes * - All active stars have the class `is-selected` applied. * - The current value (the last active star) has the class `is-currentValue` applied. - * - When the rating receives focus, the class `is-focused` should be added to the component's root element (`.spectrum-Rating`). +* - When the rating receives keyboard focus, the class `.is-keyboardFocused` should be added to the component's root element (`.spectrum-Rating`). +* - When the rating is hovered, the class `.is-hoverSelection` should be added to the `.spectrum-Rating-icon` being hovered over. */ export default { title: "Rating", component: "Rating", argTypes: { isEmphasized, - isFocused, + isKeyboardFocused, isDisabled, isReadOnly, max: { @@ -49,6 +50,7 @@ export default { isDisabled: false, isEmphasized: false, isReadOnly: false, + isKeyboardFocused: false, max: 5, value: 3, }, @@ -59,7 +61,7 @@ export default { }; /** - * A initial value of three is used for the following examples, to demonstrate both active and inactive stars. + * An initial value of three is used for the following examples, to demonstrate both active and inactive stars. * When hovering over a rating component that has a previously entered value, an underline appears under the * current selection to provide context. */ diff --git a/components/rating/stories/rating.test.js b/components/rating/stories/rating.test.js index 3de9373689b..8cada8d4210 100644 --- a/components/rating/stories/rating.test.js +++ b/components/rating/stories/rating.test.js @@ -18,8 +18,8 @@ export const RatingGroup = Variants({ isDisabled: true, }, { - testHeading: "Focused", - isFocused: true, + testHeading: "Keyboard focused", + isKeyboardFocused: true, }, { testHeading: "Read-only", diff --git a/components/rating/stories/template.js b/components/rating/stories/template.js index 245694af8fa..26bf868e37d 100644 --- a/components/rating/stories/template.js +++ b/components/rating/stories/template.js @@ -12,13 +12,72 @@ export const Template = ({ max = 5, value = 0, isReadOnly = false, - isFocused = false, + isKeyboardFocused = false, isDisabled = true, isEmphasized = false, customClasses = [], id = getRandomId("rating"), } = {}, context = {}) => { const { updateArgs } = context; + document.addEventListener("DOMContentLoaded", function() { + const rating = document.getElementById(id); + if (!rating) return; + if (rating.classList.contains("is-disabled") || rating.classList.contains("is-readOnly")) return; + const icons = Array.from(rating.getElementsByClassName("spectrum-Rating-icon")); + let hoverIndex = -1; + let selectedIndex = -1; + + const updateHoverState = () => { + icons.forEach((icon, index) => { + const activeStar = icon.querySelector(".spectrum-Rating-starActive"); + const inactiveStar = icon.querySelector(".spectrum-Rating-starInactive"); + + if (index <= hoverIndex || + (index <= selectedIndex && hoverIndex === -1) + ) { + icon.classList.add("is-hoverSelection"); + activeStar.style.display = "block"; + inactiveStar.style.display = "none"; + } + else { + icon.classList.remove("is-hoverSelection"); + activeStar.style.display = "none"; + inactiveStar.style.display = "block"; + } + }); + }; + + rating.addEventListener("mouseleave", function() { + icons.forEach(icon => { + icon.classList.remove("is-hoverSelection"); + icon.querySelector(".spectrum-Rating-starActive").style.display = ""; + icon.querySelector(".spectrum-Rating-starInactive").style.display = ""; + }); + }); + + icons.forEach((icon, index) => { + if (icon.classList.contains("is-selected")) selectedIndex = index; + + icon.addEventListener("mouseover", function() { + hoverIndex = index; + updateHoverState(); + }); + + icon.addEventListener("mouseleave", function(event) { + if (!rating.contains(event.relatedTarget)) { + hoverIndex = -1; + updateHoverState(); + } + }); + + icon.addEventListener("click", function() { + selectedIndex = index; + updateHoverState(); + }); + }); + + updateHoverState(); + }); return html`