diff --git a/docs/components/rating.md b/docs/components/rating.md index 5989f9da..d2e05ad5 100644 --- a/docs/components/rating.md +++ b/docs/components/rating.md @@ -2,10 +2,79 @@ [component-header:sl-rating] -Ratings show feedback, typically in the form of stars, and let users provide their own feedback. +Ratings give users a way to quickly view and provide feedback. ```html preview ``` +## Examples + +### Maximum Value + +Ratings are 0-5 by default. To change the maximum possible value, use the `max` attribute. + +```html preview + +``` + +### Precision + +Use the `precision` attribute to let users select fractional ratings. + +```html preview + +``` + +## Symbol Sizes + +Set the `--symbol-size` custom property to adjust the size. + +```html preview + +``` + +### Readonly + +Use the `readonly` attribute to display a rating that users can't change. + +```html preview + +``` + +### Disabled + +Use the `disable` attribute to disable the rating. + +```html preview + +``` + +### Custom Icons + +```html preview + + + +``` + +### Value-based Icons + +```html preview + + + +``` + [component-metadata:sl-rating] diff --git a/src/components.d.ts b/src/components.d.ts index 80cc2712..0682cb36 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -572,6 +572,10 @@ export namespace Components { * Disables the rating. */ "disabled": boolean; + /** + * A function that returns the symbols to display. Accepts an option `value` parameter you can use to map a specific symbol to a value. + */ + "getSymbol": (value?: number) => string; /** * The highest rating to show. */ @@ -584,6 +588,14 @@ export namespace Components { * Makes the rating readonly. */ "readonly": boolean; + /** + * Removes focus from the rating. + */ + "removeFocus": () => Promise; + /** + * Sets focus on the rating. + */ + "setFocus": () => Promise; /** * The current rating. */ @@ -1735,6 +1747,10 @@ declare namespace LocalJSX { * Disables the rating. */ "disabled"?: boolean; + /** + * A function that returns the symbols to display. Accepts an option `value` parameter you can use to map a specific symbol to a value. + */ + "getSymbol"?: (value?: number) => string; /** * The highest rating to show. */ diff --git a/src/components/rating/rating.scss b/src/components/rating/rating.scss index 7cc5f6c5..0c839d93 100644 --- a/src/components/rating/rating.scss +++ b/src/components/rating/rating.scss @@ -1,16 +1,25 @@ @import 'component'; +/** + * @prop --symbol-color: The inactive color for symbols. + * @prop --symbol-color-active: The active color for symbols. + * @prop --symbol-size: The size of symbols. + * @prop --symbol-spacing: The spacing to use around symbols. + */ :host { - display: inline-flex; + display: inline-block; - --inactive-color: var(--sl-color-gray-90); - --active-color: #f8e71c; + --symbol-color: var(--sl-color-gray-85); + --symbol-color-active: #ffbe00; + --symbol-size: 1.2rem; + --symbol-spacing: var(--sl-spacing-xxx-small); } .rating { position: relative; display: inline-flex; border-radius: var(--sl-border-radius-medium); + vertical-align: middle; &:focus { outline: none; @@ -22,28 +31,50 @@ } .rating__symbols { - display: inline-block; + display: inline-flex; position: relative; - font-size: 1.4rem; + font-size: var(--symbol-size); line-height: 0; - color: var(--inactive-color); - cursor: pointer; + color: var(--symbol-color); white-space: nowrap; + cursor: pointer; - > :not(:last-of-type) { - margin-right: var(--sl-spacing-xx-small); + > * { + padding: var(--symbol-spacing); } } -.rating__indicator { +.rating__symbols--indicator { position: absolute; top: 0; left: 0; - color: var(--active-color); - overflow: hidden; + color: var(--symbol-color-active); + pointer-events: none; } .rating__symbol { - display: inline-block; - transition: var(--sl-transition-medium) transform; + transition: var(--sl-transition-fast) transform; +} + +.rating__symbol--hover { + transform: scale(1.2); +} + +.rating--disabled, +.rating--readonly { + .rating__symbols { + cursor: default; + } + + .rating__symbol--hover { + transform: none; + } +} + +.rating--disabled { + opacity: 0.5; + + .rating__symbols { + cursor: not-allowed; + } } diff --git a/src/components/rating/rating.tsx b/src/components/rating/rating.tsx index f3362e10..98bf925a 100644 --- a/src/components/rating/rating.tsx +++ b/src/components/rating/rating.tsx @@ -1,4 +1,4 @@ -import { Component, Event, EventEmitter, Prop, State, Watch, h } from '@stencil/core'; +import { Component, Element, Event, EventEmitter, Method, Prop, State, Watch, h } from '@stencil/core'; import { focusVisible } from '../../utilities/focus-visible'; import { clamp } from '../../utilities/math'; @@ -9,17 +9,6 @@ import { clamp } from '../../utilities/math'; * @part base - The component's base wrapper. */ -// -// TODO: -// -// - sizing -// - labels -// - disabled -// - readonly -// - custom icons -// - icon should grow on hover -// - @Component({ tag: 'sl-rating', styleUrl: 'rating.scss', @@ -29,24 +18,26 @@ export class Rating { constructor() { this.handleClick = this.handleClick.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); - this.handleMouseOver = this.handleMouseOver.bind(this); - this.handleMouseOut = this.handleMouseOut.bind(this); + this.handleMouseEnter = this.handleMouseEnter.bind(this); + this.handleMouseLeave = this.handleMouseLeave.bind(this); this.handleMouseMove = this.handleMouseMove.bind(this); } rating: HTMLElement; + @Element() host: HTMLSlRatingElement; + @State() hoverValue = 0; @State() isHovering = false; /** The current rating. */ - @Prop({ mutable: true, reflect: true }) value = 2.5; + @Prop({ mutable: true, reflect: true }) value = 0; /** The highest rating to show. */ @Prop() max = 5; /** The minimum increment value allowed by the control. */ - @Prop() precision = 0.5; + @Prop() precision = 1; /** Makes the rating readonly. */ @Prop() readonly = false; @@ -54,6 +45,11 @@ export class Rating { /** Disables the rating. */ @Prop() disabled = false; + /** A function that returns the symbols to display. Accepts an option `value` parameter you can use to map a specific + * symbol to a value. */ + // @ts-ignore + @Prop() getSymbol = (value?: number) => ''; + @Watch('value') handleValueChange() { this.slChange.emit(); @@ -62,6 +58,18 @@ export class Rating { /** Emitted when the rating's value changes. */ @Event() slChange: EventEmitter; + /** Sets focus on the rating. */ + @Method() + async setFocus() { + this.rating.focus(); + } + + /** Removes focus from the rating. */ + @Method() + async removeFocus() { + this.rating.blur(); + } + componentDidLoad() { focusVisible.observe(this.rating); } @@ -73,14 +81,33 @@ export class Rating { getValueFromMousePosition(event: MouseEvent) { const containerLeft = this.rating.getBoundingClientRect().left; const containerWidth = this.rating.getBoundingClientRect().width; - return clamp(this.roundToPrecision(((event.clientX - containerLeft) / containerWidth) * this.max), 0, this.max); + return clamp( + this.roundToPrecision(((event.clientX - containerLeft) / containerWidth) * this.max, this.precision), + 0, + this.max + ); } handleClick(event: MouseEvent) { - this.value = this.getValueFromMousePosition(event); + if (this.disabled || this.readonly) { + return; + } + + const newValue = this.getValueFromMousePosition(event); + + if (newValue === this.value) { + this.value = 0; + this.isHovering = false; + } else { + this.value = newValue; + } } handleKeyDown(event: KeyboardEvent) { + if (this.disabled || this.readonly) { + return; + } + if (event.key === 'ArrowLeft') { const decrement = event.shiftKey ? 1 : this.precision; this.value = Math.max(0, this.value - decrement); @@ -104,11 +131,11 @@ export class Rating { } } - handleMouseOver() { + handleMouseEnter() { this.isHovering = true; } - handleMouseOut() { + handleMouseLeave() { this.isHovering = false; } @@ -122,42 +149,66 @@ export class Rating { } render() { - const counter = Array.from(Array(this.max)); - const displayValue = this.isHovering ? this.hoverValue : this.value; + const counter = Array.from(Array(this.max).keys()); + let displayValue = 0; + + if (this.disabled || this.readonly) { + displayValue = this.value; + } else { + displayValue = this.isHovering ? this.hoverValue : this.value; + } return (
(this.rating = el)} part="base" - class="rating" + class={{ + rating: true, + 'rating--readonly': this.readonly, + 'rating--disabled': this.disabled + }} + aria-disabled={this.disabled} + aria-readonly={this.readonly} aria-value={this.value} aria-valuemin={0} aria-valuemax={this.max} - tabIndex={0} + tabIndex={this.disabled ? -1 : 0} onClick={this.handleClick} onKeyDown={this.handleKeyDown} - onMouseEnter={this.handleMouseOver} - onMouseLeave={this.handleMouseOut} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} onMouseMove={this.handleMouseMove} > - - {counter.map(() => ( - - - + + {counter.map(index => ( + ))} - - {counter.map(() => ( - - - + + {counter.map(index => ( + index + 1 ? null : `inset(0 ${100 - ((displayValue - index) / 1) * 100}% 0 0)` + }} + role="presentation" + innerHTML={this.getSymbol(index + 1)} + /> ))}