-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
365 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
import { html, LitElement } from 'lit'; | ||
import { property, queryAll, state } from 'lit/decorators.js'; | ||
import { ifDefined } from 'lit/directives/if-defined.js'; | ||
import { Constructor } from '../common/mixins/constructor'; | ||
import { EventEmitterMixin } from '../common/mixins/event-emitter'; | ||
import { SizableMixin } from '../common/mixins/sizable'; | ||
import IgcIconComponent from '../icon/icon'; | ||
|
||
export interface IgcRatingEventMap { | ||
igcChange: CustomEvent<number>; | ||
igcHover: CustomEvent<number>; | ||
} | ||
|
||
/** | ||
* @element igc-rating | ||
* | ||
* @fires igcChange - Emitted when the value of the control changes. | ||
*/ | ||
export default class igcRatingComponent extends SizableMixin( | ||
EventEmitterMixin<IgcRatingEventMap, Constructor<LitElement>>(LitElement) | ||
) { | ||
/** @private */ | ||
public static tagName = 'igc-rating'; | ||
|
||
@queryAll('igc-icon') | ||
protected icons!: NodeListOf<IgcIconComponent>; | ||
|
||
@state() | ||
protected hoverValue = -1; | ||
|
||
@state() | ||
protected hoverState = false; | ||
|
||
protected navigationKeys = new Set([ | ||
'ArrowUp', | ||
'ArrowDown', | ||
'ArrowLeft', | ||
'ArrowRight', | ||
'Home', | ||
'End', | ||
]); | ||
|
||
/** | ||
* The number of icons to render | ||
* @attr [length=5] | ||
* */ | ||
@property({ type: Number }) | ||
public length = 5; | ||
|
||
/** | ||
* 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: string | ((index: number) => string) = 'dollar-circled'; | ||
|
||
/** | ||
* 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: string | ((index: number) => string) = 'apple'; | ||
|
||
/** The name attribute of the control */ | ||
@property() | ||
public name!: string; | ||
|
||
/** The label of the control. */ | ||
@property() | ||
public label!: string; | ||
|
||
/** | ||
* The current value of the component | ||
* @attr [value=0] | ||
*/ | ||
@property({ type: Number }) | ||
public value = -1; | ||
|
||
/** Sets the disabled state of the component */ | ||
@property({ type: Boolean, reflect: true }) | ||
public disabled = false; | ||
|
||
/** Sets hover preview behavior for the component */ | ||
@property({ type: Boolean, reflect: true }) | ||
public hover = false; | ||
|
||
/** Sets the readonly state of the component */ | ||
@property({ type: Boolean, reflect: true }) | ||
public readonly = false; | ||
|
||
public render() { | ||
return this.hover | ||
? html` | ||
<div | ||
part="base" | ||
tabindex=${ifDefined(this.readonly ? undefined : 0)} | ||
aria-labelledby=${ifDefined(this.label)} | ||
aria-valuemin="0" | ||
aria-valuenow=${this.value} | ||
aria-valuemax=${this.length} | ||
@mouseenter=${() => (this.hoverState = true)} | ||
@mouseleave=${() => (this.hoverState = false)} | ||
@mouseover=${this.handleMouseOver} | ||
@keydown=${this.handleKeyDown} | ||
@click=${this.handleClick} | ||
> | ||
${this.renderIcons()} | ||
</div> | ||
` | ||
: html` | ||
<div | ||
part="base" | ||
tabindex=${ifDefined(this.readonly ? undefined : 0)} | ||
aria-labelledby=${ifDefined(this.label)} | ||
aria-valuemin="0" | ||
aria-valuenow=${this.value} | ||
aria-valuemax=${this.length} | ||
@keydown=${this.handleKeyDown} | ||
@click=${this.handleClick} | ||
> | ||
${this.renderIcons()} | ||
</div> | ||
`; | ||
} | ||
|
||
protected *renderIcons() { | ||
for (let i = 0; i < this.length; i++) { | ||
yield html`<igc-icon | ||
.size=${this.size} | ||
.name=${this.bindValue(i)} | ||
></igc-icon>`; | ||
} | ||
} | ||
|
||
protected handleClick(event: MouseEvent) { | ||
if (this.isIconElement(event.target) && !(this.readonly || this.disabled)) { | ||
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 }); | ||
} | ||
} | ||
|
||
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 }); | ||
} | ||
} | ||
|
||
protected handleKeyDown(event: KeyboardEvent) { | ||
if (!this.navigationKeys.has(event.key)) { | ||
return; | ||
} | ||
switch (event.key) { | ||
case 'ArrowUp': | ||
case 'ArrowRight': | ||
this.value += 1; | ||
break; | ||
case 'ArrowDown': | ||
case 'ArrowLeft': | ||
this.value -= 1; | ||
break; | ||
case 'Home': | ||
this.value = 1; | ||
break; | ||
case 'End': | ||
this.value = this.length; | ||
break; | ||
default: | ||
return; | ||
} | ||
this.emitEvent('igcChange', { detail: this.value }); | ||
} | ||
|
||
private bindValue(index: number) { | ||
const value = this.hoverState ? this.hoverValue : this.value; | ||
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 { | ||
return el.tagName.toLowerCase() === 'igc-icon'; | ||
} | ||
} | ||
|
||
declare global { | ||
interface HTMLElementTagNameMap { | ||
'igc-rating': igcRatingComponent; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import { html } from 'lit-html'; | ||
import { Context, Story } from './story.js'; | ||
import { ifDefined } from 'lit/directives/if-defined.js'; | ||
|
||
// region default | ||
const metadata = { | ||
title: 'Rating', | ||
component: 'igc-rating', | ||
argTypes: { | ||
length: { | ||
type: 'number', | ||
description: 'The number of icons to render', | ||
control: 'number', | ||
defaultValue: '5', | ||
}, | ||
icon: { | ||
type: 'string', | ||
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.\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.', | ||
control: 'text', | ||
}, | ||
value: { | ||
type: 'number', | ||
description: 'The current value of the component', | ||
control: 'number', | ||
defaultValue: '-1', | ||
}, | ||
disabled: { | ||
type: 'boolean', | ||
description: 'Sets the disabled state of the component', | ||
control: 'boolean', | ||
defaultValue: false, | ||
}, | ||
hover: { | ||
type: 'boolean', | ||
description: 'Sets hover preview behavior for the component', | ||
control: 'boolean', | ||
defaultValue: false, | ||
}, | ||
readonly: { | ||
type: 'boolean', | ||
description: 'Sets the readonly state of the component', | ||
control: 'boolean', | ||
defaultValue: false, | ||
}, | ||
size: { | ||
type: '"small" | "medium" | "large"', | ||
description: 'Determines the size of the component.', | ||
options: ['small', 'medium', 'large'], | ||
control: { | ||
type: 'inline-radio', | ||
}, | ||
defaultValue: 'large', | ||
}, | ||
}, | ||
}; | ||
export default metadata; | ||
interface ArgTypes { | ||
length: number; | ||
icon: string; | ||
filledIcon: string; | ||
name: string; | ||
label: string; | ||
value: number; | ||
disabled: boolean; | ||
hover: boolean; | ||
readonly: boolean; | ||
size: 'small' | 'medium' | 'large'; | ||
} | ||
// endregion | ||
|
||
(metadata as any).parameters = { | ||
actions: { | ||
handles: ['igcChange', 'igcHover'], | ||
}, | ||
}; | ||
|
||
const Template: Story<ArgTypes, Context> = ( | ||
{ | ||
size, | ||
hover, | ||
icon, | ||
filledIcon, | ||
length, | ||
disabled, | ||
readonly, | ||
label, | ||
value, | ||
}: ArgTypes, | ||
{ globals: { direction } }: Context | ||
) => { | ||
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` | ||
<igc-rating | ||
label=${ifDefined(label)} | ||
dir=${ifDefined(direction)} | ||
?disabled=${disabled} | ||
?hover=${hover} | ||
?readonly=${readonly} | ||
.icon=${icon || unfilled} | ||
.filledIcon=${filledIcon} | ||
.length=${length} | ||
.value=${value} | ||
.size=${size} | ||
> | ||
</igc-rating> | ||
<h5> | ||
If you set an empty string for the <em>icon</em> attribute the callback | ||
for unrated symbols will take over. | ||
</h5> | ||
`; | ||
}; | ||
|
||
export const Basic = Template.bind({}); |