From 89511708f78172187b6877e991fcd178e9342955 Mon Sep 17 00:00:00 2001 From: Radoslav Karaivanov Date: Thu, 18 Nov 2021 17:56:30 +0200 Subject: [PATCH] feat: Rating improvements * Selecting the same value will reset the rating * Rated/unrated symbols accept a callback function that can resolve the symbol for the appropriate position * Added event on hover which returns the current "active" index --- scripts/build-stories.js | 12 ++++++ src/components/rating/rating.ts | 34 ++++++++++++++--- stories/rating.stories.ts | 67 +++++++++++++++++++++++++-------- 3 files changed, 92 insertions(+), 21 deletions(-) diff --git a/scripts/build-stories.js b/scripts/build-stories.js index 6edcfa089..1e3c9c76b 100644 --- a/scripts/build-stories.js +++ b/scripts/build-stories.js @@ -70,6 +70,18 @@ function extractTags(meta) { return { component: meta.name, args: Array.from(meta.properties || []) + // Strip function types from the storybook generation + // TODO: Revise this whole pipeline and refactor it as it becomes unwieldy + .map((prop) => { + if (prop.type.includes('(')) { + prop.type = prop.type.split('|') + .map(part => part.trim()) + .filter(part => !part.startsWith('(')) + .join(' | '); + return prop; + } + return prop; + }) .filter( (prop) => SUPPORTED_TYPES.includes(prop.type) || diff --git a/src/components/rating/rating.ts b/src/components/rating/rating.ts index c2bbf0ec1..38c9b8c44 100644 --- a/src/components/rating/rating.ts +++ b/src/components/rating/rating.ts @@ -8,6 +8,7 @@ import IgcIconComponent from '../icon/icon'; export interface IgcRatingEventMap { igcChange: CustomEvent; + igcHover: CustomEvent; } /** @@ -46,13 +47,21 @@ export default class igcRatingComponent extends SizableMixin( @property({ type: Number }) public length = 5; - /** The unfilled symbol/icon to use */ + /** + * The unfilled symbol/icon to use. + * Additionally it accepts a callback function which accepts the current position + * index so the symbol can be resolved per position. + */ @property() - public icon = 'dollar-circled'; + public icon: string | ((index: number) => string) = 'dollar-circled'; - /** The filled symbol/icon to use */ + /** + * The filled symbol/icon to use. + * Additionally it accepts a callback function which accepts the current position + * index so the symbol can be resolved per position. + */ @property() - public filledIcon = 'apple'; + public filledIcon: string | ((index: number) => string) = 'apple'; /** The name attribute of the control */ @property() @@ -127,7 +136,12 @@ export default class igcRatingComponent extends SizableMixin( protected handleClick(event: MouseEvent) { if (this.isIconElement(event.target) && !(this.readonly || this.disabled)) { - this.value = [...this.icons].indexOf(event.target) + 1; + const index = [...this.icons].indexOf(event.target) + 1; + if (index === this.value) { + this.value = 0; + } else { + this.value = index; + } this.emitEvent('igcChange', { detail: this.value }); } } @@ -135,6 +149,7 @@ export default class igcRatingComponent extends SizableMixin( protected handleMouseOver(event: MouseEvent) { if (this.isIconElement(event.target) && !(this.readonly || this.disabled)) { this.hoverValue = [...this.icons].indexOf(event.target) + 1; + this.emitEvent('igcHover', { detail: this.hoverValue }); } } @@ -165,7 +180,14 @@ export default class igcRatingComponent extends SizableMixin( private bindValue(index: number) { const value = this.hoverState ? this.hoverValue : this.value; - return index < value ? this.filledIcon : this.icon; + return index < value + ? this.renderIcon(index, 'rated') + : this.renderIcon(index, 'not-rated'); + } + + private renderIcon(index: number, state: 'rated' | 'not-rated') { + const symbol = state === 'rated' ? this.filledIcon : this.icon; + return typeof symbol === 'function' ? symbol(index) : symbol; } private isIconElement(el: any): el is IgcIconComponent { diff --git a/stories/rating.stories.ts b/stories/rating.stories.ts index c49d1d08a..f7108df08 100644 --- a/stories/rating.stories.ts +++ b/stories/rating.stories.ts @@ -15,16 +15,23 @@ const metadata = { }, icon: { type: 'string', - description: 'The unfilled symbol/icon to use', + description: + 'The unfilled symbol/icon to use.\nAdditionally it accepts a callback function which accepts the current position\nindex so the symbol can be resolved per position.', control: 'text', defaultValue: 'dollar-circled', }, filledIcon: { type: 'string', - description: 'The filled symbol/icon to use', + description: + 'The filled symbol/icon to use.\nAdditionally it accepts a callback function which accepts the current position\nindex so the symbol can be resolved per position.', control: 'text', defaultValue: 'apple', }, + name: { + type: 'string', + description: 'The name attribute of the control', + control: 'text', + }, label: { type: 'string', description: 'The label of the control.', @@ -70,6 +77,7 @@ interface ArgTypes { length: number; icon: string; filledIcon: string; + name: string; label: string; value: number; disabled: boolean; @@ -79,6 +87,12 @@ interface ArgTypes { } // endregion +(metadata as any).parameters = { + actions: { + handles: ['igcChange', 'igcHover'], + }, +}; + const Template: Story = ( { size, @@ -93,19 +107,42 @@ const Template: Story = ( }: ArgTypes, { globals: { direction } }: Context ) => { - return html` - `; + const unfilled = (index: number) => { + switch (index) { + case 0: + return 'coronavirus'; + case 1: + return 'atm'; + case 2: + return 'biking'; + case 3: + return 'award'; + case 4: + return 'bacteria'; + default: + return 'dollar-circled'; + } + }; + + return html` + + +
+ If you set an empty string for the icon attribute the callback + for unrated symbols will take over. +
+ `; }; export const Basic = Template.bind({});