Skip to content

Commit

Permalink
Merge 8951170 into 9b3d276
Browse files Browse the repository at this point in the history
  • Loading branch information
rkaraivanov committed Nov 18, 2021
2 parents 9b3d276 + 8951170 commit 835e29b
Show file tree
Hide file tree
Showing 5 changed files with 365 additions and 0 deletions.
12 changes: 12 additions & 0 deletions scripts/build-stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) ||
Expand Down
2 changes: 2 additions & 0 deletions src/components/common/definitions/defineAllComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import IgcNavDrawerItemComponent from '../../nav-drawer/nav-drawer-item';
import IgcNavbarComponent from '../../navbar/navbar';
import IgcRadioGroupComponent from '../../radio-group/radio-group';
import IgcRadioComponent from '../../radio/radio';
import igcRatingComponent from '../../rating/rating';
import IgcRippleComponent from '../../ripple/ripple';
import { defineComponents } from './defineComponents';

Expand Down Expand Up @@ -50,6 +51,7 @@ const allComponents: CustomElementConstructor[] = [
IgcNavbarComponent,
IgcRadioComponent,
IgcRadioGroupComponent,
igcRatingComponent,
IgcRippleComponent,
];

Expand Down
202 changes: 202 additions & 0 deletions src/components/rating/rating.ts
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;
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export { default as IgcNavDrawerHeaderItemComponent } from './components/nav-dra
export { default as IgcNavDrawerItemComponent } from './components/nav-drawer/nav-drawer-item';
export { default as IgcRadioComponent } from './components/radio/radio';
export { default as IgcRadioGroupComponent } from './components/radio-group/radio-group';
export { default as igcRatingComponent } from './components/rating/rating';
export { default as IgcRippleComponent } from './components/ripple/ripple';
export { default as IgcSwitchComponent } from './components/checkbox/switch';

Expand Down
148 changes: 148 additions & 0 deletions stories/rating.stories.ts
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({});

0 comments on commit 835e29b

Please sign in to comment.