1- import { attr , FASTElement , nullableNumberConverter , observable } from '@microsoft/fast-element' ;
1+ import { attr , FASTElement , nullableNumberConverter } from '@microsoft/fast-element' ;
2+
3+ const SUPPORTS_ATTR_TYPE = CSS . supports ( 'width: attr(value type(<number>))' ) ;
4+ const CUSTOM_PROPERTY_NAME = {
5+ max : '--_attr-max' ,
6+ value : '--_attr-value' ,
7+ maskImageFilled : '--_mask-image-filled' ,
8+ maskImageOutlined : '--_mask-image-outlined' ,
9+ } ;
10+ type PropertyNameForCalculation = 'max' | 'value' ;
11+
12+ export function svgToDataURI ( svg : string ) {
13+ if ( ! svg ) {
14+ return '' ;
15+ }
16+
17+ return [ 'data:image/svg+xml' , encodeURIComponent ( svg . replace ( / \n / g, '' ) . replace ( / \s + / g, ' ' ) ) ] . join ( ',' ) ;
18+ }
219
320/**
421 * The base class used for constructing a fluent-rating-display custom element
@@ -8,13 +25,26 @@ import { attr, FASTElement, nullableNumberConverter, observable } from '@microso
825 * @public
926 */
1027export class BaseRatingDisplay extends FASTElement {
28+ private numberFormatter ! : Intl . NumberFormat ;
29+
1130 /**
1231 * The internal {@link https://developer.mozilla.org/docs/Web/API/ElementInternals | `ElementInternals`} instance for the component.
1332 *
1433 * @internal
1534 */
1635 public elementInternals : ElementInternals = this . attachInternals ( ) ;
1736
37+ /** @internal */
38+ public iconSlot ! : HTMLSlotElement ;
39+
40+ protected defaultCustomIconViewBox = '0 0 20 20' ;
41+
42+ /**
43+ * The element that displays the rating icons.
44+ * @internal
45+ */
46+ public display ! : HTMLElement ;
47+
1848 /**
1949 * The number of ratings.
2050 *
@@ -29,9 +59,9 @@ export class BaseRatingDisplay extends FASTElement {
2959 * The `viewBox` attribute of the icon <svg> element.
3060 *
3161 * @public
32- * @default `0 0 20 20`
3362 * @remarks
3463 * HTML Attribute: `icon-view-box`
64+ * @deprecated Add `viewBox` attribute on the custom SVG directly.
3565 */
3666 @attr ( { attribute : 'icon-view-box' } )
3767 iconViewBox ?: string ;
@@ -47,6 +77,9 @@ export class BaseRatingDisplay extends FASTElement {
4777 */
4878 @attr ( { converter : nullableNumberConverter } )
4979 public max ?: number ;
80+ protected maxChanged ( ) {
81+ this . setCustomPropertyValue ( 'max' ) ;
82+ }
5083
5184 /**
5285 * The value of the rating.
@@ -57,34 +90,21 @@ export class BaseRatingDisplay extends FASTElement {
5790 */
5891 @attr ( { converter : nullableNumberConverter } )
5992 public value ?: number ;
60-
61- /**
62- * @internal
63- */
64- @observable
65- public slottedIcon ! : HTMLElement [ ] ;
66-
67- /**
68- * @internal
69- */
70- public slottedIconChanged ( ) : void {
71- if ( this . $fastController . isConnected ) {
72- this . customIcon = this . slottedIcon [ 0 ] ?. outerHTML ;
73- }
93+ protected valueChanged ( ) {
94+ this . setCustomPropertyValue ( 'value' ) ;
7495 }
7596
76- /**
77- * @internal
78- */
79- @observable
80- private customIcon ?: string ;
81-
82- private intlNumberFormatter = new Intl . NumberFormat ( ) ;
83-
8497 constructor ( ) {
8598 super ( ) ;
8699
87100 this . elementInternals . role = 'img' ;
101+ this . numberFormatter = new Intl . NumberFormat ( ) ;
102+ }
103+
104+ connectedCallback ( ) {
105+ super . connectedCallback ( ) ;
106+ this . setCustomPropertyValue ( 'value' ) ;
107+ this . setCustomPropertyValue ( 'max' ) ;
88108 }
89109
90110 /**
@@ -93,53 +113,54 @@ export class BaseRatingDisplay extends FASTElement {
93113 * @internal
94114 */
95115 public get formattedCount ( ) : string {
96- return this . count ? this . intlNumberFormatter . format ( this . count ) : '' ;
116+ return this . count ? this . numberFormatter . format ( this . count ) : '' ;
97117 }
98118
99- /**
100- * Gets the selected value
101- *
102- * @protected
103- */
104- protected getSelectedValue ( ) : number {
105- return Math . round ( ( this . value ?? 0 ) * 2 ) / 2 ;
106- }
119+ /** @internal */
120+ public handleSlotChange ( ) {
121+ const icon = this . iconSlot . assignedElements ( ) ?. find ( el => el . nodeName . toLowerCase ( ) === 'svg' ) as SVGSVGElement ;
107122
108- /**
109- * Gets the maximum icons to render
110- *
111- * @protected
112- */
113- protected getMaxIcons ( ) : number {
114- return ( this . max ?? 5 ) * 2 ;
123+ this . renderSlottedIcon ( icon ?? null ) ;
115124 }
116125
117- /**
118- * Generates the icon SVG elements based on the "max" attribute.
119- *
120- * @internal
121- */
122- public generateIcons ( ) : string {
123- let htmlString : string = '' ;
124- let customIcon : string | undefined ;
125-
126- if ( this . customIcon ) {
127- // Extract the SVG element content
128- customIcon = / < s v g [ ^ > ] * > ( [ \s \S ] * ?) < \/ s v g > / . exec ( this . customIcon ) ?. [ 1 ] ?? '' ;
126+ protected renderSlottedIcon ( svg : SVGSVGElement | null ) {
127+ if ( ! svg ) {
128+ this . display . style . removeProperty ( CUSTOM_PROPERTY_NAME . maskImageFilled ) ;
129+ this . display . style . removeProperty ( CUSTOM_PROPERTY_NAME . maskImageOutlined ) ;
130+ return ;
129131 }
130132
131- // The value of the selected icon. Based on the "value" attribute, rounded to the nearest half.
132- const selectedValue : number = this . getSelectedValue ( ) ;
133-
134- // Render the icons based on the "max" attribute. If "max" is not set, render 5 icons.
135- for ( let i : number = 0 ; i < this . getMaxIcons ( ) ; i ++ ) {
136- const iconValue : number = ( i + 1 ) / 2 ;
133+ const innerSvg = svg . innerHTML ;
134+ const viewBox = svg . getAttribute ( 'viewBox' ) ?? this . iconViewBox ?? this . defaultCustomIconViewBox ;
135+
136+ const customSvgFilled = `
137+ <svg
138+ viewBox="${ viewBox } "
139+ xmlns="http://www.w3.org/2000/svg"
140+ >${ innerSvg } </svg>` ;
141+ const customSvgOutlined = `
142+ <svg
143+ viewBox="${ viewBox } "
144+ xmlns="http://www.w3.org/2000/svg"
145+ fill="none"
146+ stroke="black"
147+ stroke-width="2"
148+ >${ innerSvg } </svg>` ;
149+ this . display . style . setProperty ( CUSTOM_PROPERTY_NAME . maskImageFilled , `url(${ svgToDataURI ( customSvgFilled ) } )` ) ;
150+ this . display . style . setProperty ( CUSTOM_PROPERTY_NAME . maskImageOutlined , `url(${ svgToDataURI ( customSvgOutlined ) } )` ) ;
151+ }
137152
138- htmlString += `<svg aria-hidden="true" viewBox=" ${ this . iconViewBox ?? '0 0 20 20' } " $ {
139- iconValue === selectedValue ? 'selected' : ''
140- } > ${ customIcon ?? '<use href="#star"></use>' } </svg>` ;
153+ protected setCustomPropertyValue ( propertyName : PropertyNameForCalculation ) {
154+ if ( ! this . display || SUPPORTS_ATTR_TYPE ) {
155+ return ;
141156 }
142157
143- return htmlString ;
158+ const propertyValue = this [ propertyName ] ;
159+
160+ if ( typeof propertyValue !== 'number' || Number . isNaN ( propertyValue ) ) {
161+ this . display . style . removeProperty ( CUSTOM_PROPERTY_NAME [ propertyName ] ) ;
162+ } else {
163+ this . display . style . setProperty ( CUSTOM_PROPERTY_NAME [ propertyName ] , `${ propertyValue } ` ) ;
164+ }
144165 }
145166}
0 commit comments