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`
({ ...a, [c]: true }), {}), })} id=${ifDefined(id)} @focusin=${function() { - updateArgs({ isFocused: true }); + updateArgs({ isKeyboardFocused: true }); }} @focusout=${function() { - updateArgs({ isFocused: false }); + updateArgs({ isKeyboardFocused: false }); }} > ${Icon({ diff --git a/yarn.lock b/yarn.lock index 52215ee9ffd..1404badc487 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7413,7 +7413,18 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10c0/5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.5": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -8789,13 +8800,13 @@ __metadata: linkType: hard "eslint-compat-utils@npm:^0.6.0": - version: 0.6.4 - resolution: "eslint-compat-utils@npm:0.6.4" + version: 0.6.3 + resolution: "eslint-compat-utils@npm:0.6.3" dependencies: semver: "npm:^7.5.4" peerDependencies: eslint: ">=6.0.0" - checksum: 10c0/5b665c4051e978b9f9c48621f63d07e6b2a8ba1b334fc430f1ce0d8b596968677bdb54c23c00ca961ad5b4673d5e83e014a52b4baf9a2f7d4ccd79e3c213acfb + checksum: 10c0/5015cd92590ab4630dfddc4416b058c2e40c58c548203761745e6874bf3e06f15dd84fc447187853025924f86d1870efc959032fcffaf5003e32a7d4f822c43f languageName: node linkType: hard @@ -12289,11 +12300,11 @@ __metadata: linkType: hard "nanoid@npm:^3.3.7": - version: 3.3.8 - resolution: "nanoid@npm:3.3.8" + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" bin: nanoid: bin/nanoid.cjs - checksum: 10c0/4b1bb29f6cfebf3be3bc4ad1f1296fb0a10a3043a79f34fbffe75d1621b4318319211cd420549459018ea3592f0d2f159247a6f874911d6d26eaaadda2478120 + checksum: 10c0/e3fb661aa083454f40500473bb69eedb85dc160e763150b9a2c567c7e9ff560ce028a9f833123b618a6ea742e311138b591910e795614a629029e86e180660f3 languageName: node linkType: hard