diff --git a/packages/web-components/examples/stackblitz/components/dotcom-shell/src/index.scss b/packages/web-components/examples/stackblitz/components/dotcom-shell/src/index.scss index cc462c650e0..1997dd1267a 100644 --- a/packages/web-components/examples/stackblitz/components/dotcom-shell/src/index.scss +++ b/packages/web-components/examples/stackblitz/components/dotcom-shell/src/index.scss @@ -16,9 +16,9 @@ $feature-flags: ( @import 'carbon-components/scss/globals/grid/grid'; @import 'carbon-components/scss/components/ui-shell/content'; -@media (min-width: 66rem) { +@media (width >= 66rem) { .cds--offset-lg-3 { - margin-left: 0; + margin-inline-start: 0; } } @@ -27,7 +27,7 @@ $feature-flags: ( margin: 30px 0; &:first-of-type { - margin-top: 0; + margin-block-start: 0; } } } diff --git a/packages/web-components/examples/stackblitz/components/masthead-l1/src/index.scss b/packages/web-components/examples/stackblitz/components/masthead-l1/src/index.scss index 3fcda169c02..ff2595fa38b 100644 --- a/packages/web-components/examples/stackblitz/components/masthead-l1/src/index.scss +++ b/packages/web-components/examples/stackblitz/components/masthead-l1/src/index.scss @@ -17,12 +17,13 @@ $feature-flags: ( @import 'carbon-components/scss/components/ui-shell/content'; body { - padding: calc(#{$spacing-09} + #{mini-units(6)} + 1px) $spacing-09 $spacing-09 $spacing-09; + padding: calc(#{$spacing-09} + #{mini-units(6)} + 1px) $spacing-09 $spacing-09 + $spacing-09; } -@media (min-width: 66rem) { +@media (width >= 66rem) { .cds--offset-lg-3 { - margin-left: 0; + margin-inline-start: 0; } } @@ -31,7 +32,7 @@ body { margin: 30px 0; &:first-of-type { - margin-top: 0; + margin-block-start: 0; } } } diff --git a/packages/web-components/examples/stackblitz/components/masthead/src/index.scss b/packages/web-components/examples/stackblitz/components/masthead/src/index.scss index ae33d782be7..9bf16dca006 100644 --- a/packages/web-components/examples/stackblitz/components/masthead/src/index.scss +++ b/packages/web-components/examples/stackblitz/components/masthead/src/index.scss @@ -17,20 +17,22 @@ $feature-flags: ( @import 'carbon-components/scss/components/ui-shell/content'; body { - padding: calc(#{$spacing-09} + #{mini-units(6)} + 1px) $spacing-09 $spacing-09 $spacing-09; + padding: calc(#{$spacing-09} + #{mini-units(6)} + 1px) $spacing-09 $spacing-09 + $spacing-09; } .cds--content.dds-ce-demo--ui-shell-content { - @media (min-width: 66rem) { + @media (width >= 66rem) { .cds--offset-lg-3 { - margin-left: 0; + margin-inline-start: 0; } } + h2 { margin: 30px 0; &:first-of-type { - margin-top: 0; + margin-block-start: 0; } } } diff --git a/packages/web-components/examples/stackblitz/components/table-of-contents/src/index.scss b/packages/web-components/examples/stackblitz/components/table-of-contents/src/index.scss index e623371ed6e..22001e1c7c3 100644 --- a/packages/web-components/examples/stackblitz/components/table-of-contents/src/index.scss +++ b/packages/web-components/examples/stackblitz/components/table-of-contents/src/index.scss @@ -21,6 +21,5 @@ body { } h3 { - padding-top: $spacing-05; - padding-bottom: $spacing-07; + padding-block: $spacing-05 $spacing-07; } diff --git a/packages/web-components/examples/stackblitz/components/tabs-extended/src/styles.scss b/packages/web-components/examples/stackblitz/components/tabs-extended/src/styles.scss index b1d7dab187b..29117089235 100644 --- a/packages/web-components/examples/stackblitz/components/tabs-extended/src/styles.scss +++ b/packages/web-components/examples/stackblitz/components/tabs-extended/src/styles.scss @@ -11,6 +11,7 @@ :root { @include theme.theme(themes.$white); + background-color: var(--cds-background); color: var(--cds-text-primary); } diff --git a/packages/web-components/examples/stackblitz/usage/webpack-handlebars/src/index.scss b/packages/web-components/examples/stackblitz/usage/webpack-handlebars/src/index.scss index 46f95ba302a..aad0192bfa8 100644 --- a/packages/web-components/examples/stackblitz/usage/webpack-handlebars/src/index.scss +++ b/packages/web-components/examples/stackblitz/usage/webpack-handlebars/src/index.scss @@ -16,8 +16,8 @@ @include carbon--font-face-sans(); body { - visibility: inherit; // Initially hidden to avoid FOUC padding: calc(#{mini-units(6)} + 1px) 0 0 0; + visibility: inherit; // Initially hidden to avoid FOUC } .bx--content.cds-ce-demo--ui-shell-content { @@ -25,7 +25,7 @@ body { margin: 30px 0; &:first-of-type { - margin-top: 0; + margin-block-start: 0; } } } diff --git a/packages/web-components/examples/stackblitz/usage/webpack-sass/src/index.scss b/packages/web-components/examples/stackblitz/usage/webpack-sass/src/index.scss index 411093dad0c..883cdc610fe 100644 --- a/packages/web-components/examples/stackblitz/usage/webpack-sass/src/index.scss +++ b/packages/web-components/examples/stackblitz/usage/webpack-sass/src/index.scss @@ -30,12 +30,12 @@ h2 { margin: $spacing-07 0; &:first-of-type { - margin-top: 0; + margin-block-start: 0; } } main { - margin: calc(#{mini-units(6)} + 1px) auto 0 auto; + max-inline-size: 99rem; padding: $spacing-09; - max-width: 99rem; + margin: calc(#{mini-units(6)} + 1px) auto 0 auto; } diff --git a/packages/web-components/src/components/star-rating/__stories__/README.stories.mdx b/packages/web-components/src/components/star-rating/__stories__/README.stories.mdx new file mode 100644 index 00000000000..58cbd24bdc1 --- /dev/null +++ b/packages/web-components/src/components/star-rating/__stories__/README.stories.mdx @@ -0,0 +1,17 @@ +import { Meta, Props, Story, Canvas, Description } from '@storybook/addon-docs'; +import { cdnJs } from '../../../globals/internal/storybook-cdn'; +import '../index'; + + + +# Star Rating + + + + + + + +## `` attributes, properties, and events + + diff --git a/packages/web-components/src/components/star-rating/__stories__/star-rating.stories.ts b/packages/web-components/src/components/star-rating/__stories__/star-rating.stories.ts new file mode 100644 index 00000000000..bd819d53821 --- /dev/null +++ b/packages/web-components/src/components/star-rating/__stories__/star-rating.stories.ts @@ -0,0 +1,72 @@ +/** + * @license + * + * Copyright IBM Corp. 2020, 2024 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import '../index'; +import readme from './README.stories.mdx'; +import { grid8ColCentered } from '../../../globals/internal/storybook-decorators'; +import { boolean, number, text } from '@storybook/addon-knobs'; + +export const Default = (args) => { + const { disableTooltip, label, labelHref, rating, starCount, tooltipText } = + args?.StarRating ?? {}; + return html` + + `; +}; + +export const NoLabel = (args) => { + const { disableTooltip, rating, starCount, tooltipText } = (args = + args?.StarRating ?? {}); + + return html` + + `; +}; + +export default { + title: 'Components/Star Rating', + parameters: { + ...readme.parameters, + hasStoryPadding: true, + knobs: { + StarRating: () => { + const rating = number('Star Rating', 4.5); + const label = text('Label text (optional)', '42 G2 reviews'); + const labelHref = text('Label link (optional)', ''); + const starCount = number('Max number of stars (optional)', 5); + const tooltipText = text('Tooltip text (optional)', ''); + const disableTooltip = boolean('Disable tooltip (optional)', false); + return { + rating, + label, + labelHref, + starCount, + tooltipText, + disableTooltip, + }; + }, + }, + }, + decorators: [ + (story) => grid8ColCentered(story), + (story) => html`
${story()}
`, + ], +}; diff --git a/packages/web-components/src/components/star-rating/index.ts b/packages/web-components/src/components/star-rating/index.ts new file mode 100644 index 00000000000..100f74422e0 --- /dev/null +++ b/packages/web-components/src/components/star-rating/index.ts @@ -0,0 +1,10 @@ +/** + * @license + * + * Copyright IBM Corp. 2020, 2024 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import './star-rating'; diff --git a/packages/web-components/src/components/star-rating/star-rating.scss b/packages/web-components/src/components/star-rating/star-rating.scss new file mode 100644 index 00000000000..66148bca198 --- /dev/null +++ b/packages/web-components/src/components/star-rating/star-rating.scss @@ -0,0 +1,76 @@ +// +// Copyright IBM Corp. 2024 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@use '@carbon/styles/scss/config' as *; +@use '@carbon/styles/scss/spacing' as *; +@use '@carbon/styles/scss/type' as *; +@use '@carbon/styles/scss/themes' as *; +@use '@carbon/styles/scss/theme' as *; +@use '@carbon/styles/scss/utilities/tooltip' as *; +@use '@carbon/styles/scss/components/button/tokens' as *; +@use '@carbon/ibmdotcom-styles/scss/globals/vars' as *; + +:host(#{$c4d-prefix}-star-rating) { + display: inline-block; + + .#{$prefix}-star-rating { + display: flex; + flex-wrap: wrap; + gap: $spacing-03 $spacing-05; + } + + .#{$prefix}-star-rating__stars { + display: inline-flex; + align-items: center; + margin: 0; + + &:not([disableTooltip]) { + @include tooltip--trigger('definition', top); + @include tooltip--placement('definition', 'top', 'center'); + } + } + + .#{$prefix}-star-count__star { + svg { + display: block; + fill: $button-primary; + } + } + + .#{$prefix}-star-count__star--half { + position: relative; + + svg { + position: absolute; + inset: 0; + } + + svg:nth-of-type(1) { + position: initial; + } + + svg:nth-of-type(2):dir(rtl) { + transform: scaleX(-1); + } + } + + .#{$prefix}-star-rating__label, + .#{$prefix}-star-rating__label a { + @include type-style('body-compact-02'); + + color: $border-inverse; + } + + .#{$prefix}-star-rating__label a { + text-decoration: none; + + &:hover, + &:focus { + text-decoration: underline; + } + } +} diff --git a/packages/web-components/src/components/star-rating/star-rating.ts b/packages/web-components/src/components/star-rating/star-rating.ts new file mode 100644 index 00000000000..7dbb9adf51a --- /dev/null +++ b/packages/web-components/src/components/star-rating/star-rating.ts @@ -0,0 +1,207 @@ +/** + * @license + * + * Copyright IBM Corp. 2020, 2024 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { html, LitElement } from 'lit'; +import { property } from 'lit/decorators.js'; +import '@carbon/web-components/es/components/tooltip/tooltip.js'; +import { carbonElement as customElement } from '@carbon/web-components/es/globals/decorators/carbon-element.js'; +import Star16 from '@carbon/web-components/es/icons/star/16.js'; +import StarHalf16 from '@carbon/web-components/es/icons/star--half/16.js'; +import StarFilled16 from '@carbon/web-components/es/icons/star--filled/16.js'; +import styles from './star-rating.scss'; +import StableSelectorMixin from '../../globals/mixins/stable-selector'; +import settings from '@carbon/ibmdotcom-utilities/es/utilities/settings/settings.js'; + +const { stablePrefix: c4dPrefix, prefix } = settings; + +/** + * The Star Rating component. + * @element c4d-star-rating + */ +@customElement(`${c4dPrefix}-star-rating`) +class C4DStarRating extends StableSelectorMixin(LitElement) { + /** + * Maximum number of stars that may be in a rating. + */ + protected maxStarCount = 10; + + /** + * Internal store for rating value. + */ + private _rating = 0; + + /** + * Internal store for star count value. + */ + private _starCount = 5; + + /** + * Internal store for custom tooltip text. + */ + private _tooltip: string | null = null; + + /** + * The rating that will inform the number of stars to display. + */ + @property({ attribute: 'rating', reflect: true, type: Number }) + set rating(value: number) { + const oldValue = this._rating; + // Place boundaries to avoid out-of-memory issues. + this._rating = Math.min(Math.max(value, 0), this.starCount); + this.requestUpdate('rating', oldValue); + } + get rating() { + return this._rating; + } + + /** + * The text to display beside the rating. + */ + @property({ attribute: 'label', reflect: true }) + label?: string; + + /** + * An optional href for the label. + */ + @property({ attribute: 'label-href', reflect: true }) + labelHref?: string; + + /** + * The number of stars to display. + */ + @property({ attribute: 'star-count', reflect: true, type: Number }) + set starCount(value: number) { + const oldValue = this._starCount; + // Place boundaries to avoid out-of-memory issues. + this._starCount = Math.min(Math.max(value, 0), this.maxStarCount); + this.requestUpdate('starCount', oldValue); + } + get starCount() { + return this._starCount; + } + + /** + * The tooltip text that appears when hovering over the stars. Also used as an + * accessible label for the stars. + */ + @property({ attribute: 'tooltip', reflect: true }) + set tooltip(value: string | null) { + const oldValue = this._tooltip; + this._tooltip = value; + this.requestUpdate('tooltip', oldValue); + } + get tooltip() { + return !this._tooltip + ? `${this.rating} out of ${this.starCount} stars` + : this._tooltip; + } + + /** + * Disables the visible tooltip without removing accessibility text. + */ + @property({ reflect: true, type: Boolean }) + disableTooltip = false; + + /** + * Renders the label. + * + * @returns {TemplateResult} A template fragment representing the label. + */ + protected _renderLabel() { + const { label, labelHref } = this; + if (!label) { + return ''; + } + return html` +
+ ${labelHref ? html` ${label} ` : label} +
+ `; + } + + /** + * Renders the rating as a series of stars. + * + * @returns {TemplateResult} A template fragment representing a series of stars. + */ + protected _renderStars() { + const { disableTooltip, tooltip, rating, starCount } = this; + const { renderStar } = this.constructor as typeof C4DStarRating; + const integer = Math.floor(rating); + const decimal = rating - integer; + + const fillValues = Array(starCount) + .fill(1, 0, integer) + .fill(0, integer, starCount); + + if (decimal) { + fillValues[integer] = decimal; + } + + return html` +
+ ${fillValues.map((fillValue) => renderStar(fillValue))} +
+ `; + } + + shouldUpdate() { + if (isNaN(this.rating) || isNaN(this.starCount)) { + return false; + } + return true; + } + + render() { + return html` +
+ ${this._renderStars()} ${this._renderLabel()} +
+ `; + } + + /** + * Renders an individual star at a given fill value. + * + * @param {number} fill The star's fill value. + * @returns {TemplateResult} A template fragment representing a single star. + */ + static renderStar(fill) { + let markup, classModifier; + if (fill >= 0.75) { + markup = StarFilled16(); + classModifier = 'filled'; + } else if (fill >= 0.25) { + markup = html` ${Star16()}${StarHalf16()} `; + classModifier = 'half'; + } else { + markup = Star16(); + classModifier = 'empty'; + } + return html` +
+ ${markup} +
+ `; + } + + static get stableSelector() { + return `${c4dPrefix}--star-rating`; + } + + static styles = styles; +} + +export default C4DStarRating;