Skip to content

Commit

Permalink
React rating initial implementation (#29490)
Browse files Browse the repository at this point in the history
* Rating initial implementation

* Rating initial implementation

* version fix

* yarn lock update

* Add carat

* update exports

* change checked to defaultChecked in RatingItem

* add empty onChange to silence warnings

* api update

* add comments from code review

* api updates

* update Rating styles and stories

* Update indicator api to add filled and unfilled icons

* add shapes

* Update shape api to use iconFilled and iconOutline

* Default the color of the rating to neutralForeground1

* Add outlineIcon and support filled unselected icons

* Rename icon slots in RatingItem

* Fix label text centering

* update api

* Update tabster dependency version

* Update ratingitem styling and stories

* Clean up shape story

* Add outlineStyle prop and update api

* Remove conformance tests from RatingItem

* add suggestion from code review

* api update

* package update

* Dependency mismatch

* Temporarily remove conformance tests

* update snapshots

* update snapshots

* apply suggestions from code review

---------

Co-authored-by: Ben Howell <[email protected]>
  • Loading branch information
tomi-msft and behowell authored Dec 9, 2023
1 parent 8a6a455 commit 9149d53
Show file tree
Hide file tree
Showing 31 changed files with 980 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
```ts

/// <reference types="react" />

import type { ComponentProps } from '@fluentui/react-utilities';
import type { ComponentState } from '@fluentui/react-utilities';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
Expand All @@ -17,23 +19,97 @@ export const Rating: ForwardRefComponent<RatingProps>;
// @public (undocumented)
export const ratingClassNames: SlotClassNames<RatingSlots>;

// @public (undocumented)
export type RatingContextValue = Pick<RatingState, 'appearance' | 'compact' | 'iconFilled' | 'iconOutline' | 'name' | 'precision' | 'readOnly' | 'size' | 'value' | 'hoveredValue'>;

// @public (undocumented)
export type RatingContextValues = {
rating: RatingContextValue;
};

// @public
export const RatingItem: ForwardRefComponent<RatingItemProps>;

// @public (undocumented)
export const ratingItemClassNames: SlotClassNames<RatingItemSlots>;

// @public
export type RatingItemProps = ComponentProps<Partial<RatingItemSlots>> & {
value?: number;
};

// @public (undocumented)
export type RatingItemSlots = {
root: NonNullable<Slot<'span'>>;
selectedIcon?: NonNullable<Slot<'div'>>;
unselectedFilledIcon?: NonNullable<Slot<'div'>>;
unselectedOutlineIcon?: NonNullable<Slot<'div'>>;
halfValueInput?: NonNullable<Slot<'input'>>;
fullValueInput?: NonNullable<Slot<'input'>>;
};

// @public
export type RatingItemState = ComponentState<RatingItemSlots> & Required<Pick<RatingItemProps, 'value'>> & Pick<RatingState, 'compact' | 'precision' | 'size'> & {
iconFillWidth: number;
};

// @public
export type RatingProps = ComponentProps<RatingSlots> & {};
export type RatingOnChangeData = {
value?: number;
};

// @public
export type RatingProps = ComponentProps<RatingSlots> & {
appearance?: 'filled' | 'outline';
compact?: boolean;
defaultValue?: number;
iconFilled?: React_2.ReactElement;
iconOutline?: React_2.ReactElement;
max?: number;
name?: string;
onChange?: (ev: React_2.SyntheticEvent | Event, data: RatingOnChangeData) => void;
precision?: boolean;
readOnly?: boolean;
size?: 'small' | 'medium' | 'large';
value?: number;
};

// @public (undocumented)
export const RatingProvider: React_2.Provider<RatingContextValue | undefined>;

// @public (undocumented)
export type RatingSlots = {
root: Slot<'div'>;
root: NonNullable<Slot<'div'>>;
ratingLabel?: NonNullable<Slot<'label'>>;
ratingCountLabel?: NonNullable<Slot<'label'>>;
};

// @public
export type RatingState = ComponentState<RatingSlots> & Required<Pick<RatingProps, 'appearance' | 'compact' | 'iconFilled' | 'iconOutline' | 'name' | 'precision' | 'readOnly' | 'size' | 'value'>> & {
hoveredValue?: number | undefined;
};

// @public
export type RatingState = ComponentState<RatingSlots>;
export const renderRating_unstable: (state: RatingState, contextValues: RatingContextValues) => JSX.Element;

// @public
export const renderRating_unstable: (state: RatingState) => JSX.Element;
export const renderRatingItem_unstable: (state: RatingItemState) => JSX.Element;

// @public
export const useRating_unstable: (props: RatingProps, ref: React_2.Ref<HTMLDivElement>) => RatingState;

// @public
export const useRatingContextValue_unstable: () => RatingContextValue | undefined;

// @public (undocumented)
export const useRatingContextValues: (state: RatingState) => RatingContextValues;

// @public
export const useRatingItem_unstable: (props: RatingItemProps, ref: React_2.Ref<HTMLSpanElement>) => RatingItemState;

// @public
export const useRatingItemStyles_unstable: (state: RatingItemState) => RatingItemState;

// @public
export const useRatingStyles_unstable: (state: RatingState) => RatingState;

Expand Down
2 changes: 2 additions & 0 deletions packages/react-components/react-rating-preview/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
},
"dependencies": {
"@fluentui/react-jsx-runtime": "^9.0.20",
"@fluentui/react-icons": "^2.0.217",
"@fluentui/react-theme": "^9.1.16",
"@fluentui/react-tabster": "^9.15.0",
"@fluentui/react-utilities": "^9.15.2",
"@griffel/react": "^1.5.14",
"@swc/helpers": "^0.5.1"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './components/RatingItem/index';
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { isConformant } from '../../testing/isConformant';
// import { isConformant } from '../../testing/isConformant';
import { Rating } from './Rating';

describe('Rating', () => {
isConformant({
Component: Rating,
displayName: 'Rating',
});

// TODO add more tests here, and create visual regression tests in /apps/vr-tests

// isConformant({
// Component: Rating,
// displayName: 'Rating',
// });
it('renders a default state', () => {
const result = render(<Rating>Default Rating</Rating>);
const result = render(<Rating>Default RatingItem</Rating>);
expect(result.container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import { useRating_unstable } from './useRating';
import { renderRating_unstable } from './renderRating';
import { useRatingStyles_unstable } from './useRatingStyles.styles';
import type { RatingProps } from './Rating.types';
import { useRatingContextValues } from '../../contexts/useRatingContextValues';

/**
* Rating component - TODO: add more docs
*/
export const Rating: ForwardRefComponent<RatingProps> = React.forwardRef((props, ref) => {
const state = useRating_unstable(props, ref);
const contextValues = useRatingContextValues(state);

useRatingStyles_unstable(state);
return renderRating_unstable(state);
return renderRating_unstable(state, contextValues);
});

Rating.displayName = 'Rating';
Original file line number Diff line number Diff line change
@@ -1,17 +1,110 @@
import * as React from 'react';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';

export type RatingSlots = {
root: Slot<'div'>;
root: NonNullable<Slot<'div'>>;
ratingLabel?: NonNullable<Slot<'label'>>;
ratingCountLabel?: NonNullable<Slot<'label'>>;
};

/**
* Rating Props
*/
export type RatingProps = ComponentProps<RatingSlots> & {};
export type RatingProps = ComponentProps<RatingSlots> & {
/**
* Controls the appearance of unselected rating items.
* @default outline (filled if readOnly is set)
*/
appearance?: 'filled' | 'outline';
/**
* Sets whether to render a full or compact Rating
* @default false
*/
compact?: boolean;
/**
* Default value of the Rating
*/
defaultValue?: number;
/**
* The icon to display when the rating value is greater than or equal to the item's value.
*/
iconFilled?: React.ReactElement;
/**
* The icon to display when the rating value is less than the item's value.
*/
iconOutline?: React.ReactElement;
/**
* The max value of the rating. This controls the number of rating items displayed.
* Must be a whole number greater than 1.
* @default 5
*/
max?: number;
/**
* Name for the Radio inputs. If not provided, one will be automatically generated
*/
name?: string;
/**
* Callback when the rating value is changed by the user.
*/
onChange?: (ev: React.SyntheticEvent | Event, data: RatingOnChangeData) => void;
/**
* Sets the precision to allow half-filled shapes in Rating
* @default false
*/
precision?: boolean;
/**
* Sets Rating to be read only
* @default false
*/
readOnly?: boolean;
/**
* Sets the size of the Rating items.
* @default medium
*/
size?: 'small' | 'medium' | 'large';
/**
* The value of the rating
*/
value?: number;
};

/**
* Data for the onChange event for Rating.
*/
export type RatingOnChangeData = {
/**
* The new value of the rating.
*/
value?: number;
};

/**
* State used in rendering Rating
*/
export type RatingState = ComponentState<RatingSlots>;
// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from RatingProps.
// & Required<Pick<RatingProps, 'propName'>>
export type RatingState = ComponentState<RatingSlots> &
Required<
Pick<
RatingProps,
'appearance' | 'compact' | 'iconFilled' | 'iconOutline' | 'name' | 'precision' | 'readOnly' | 'size' | 'value'
>
> & {
hoveredValue?: number | undefined;
};

export type RatingContextValue = Pick<
RatingState,
| 'appearance'
| 'compact'
| 'iconFilled'
| 'iconOutline'
| 'name'
| 'precision'
| 'readOnly'
| 'size'
| 'value'
| 'hoveredValue'
>;

export type RatingContextValues = {
rating: RatingContextValue;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exports[`Rating renders a default state 1`] = `
<div
class="fui-Rating"
>
Default Rating
Default RatingItem
</div>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@
/** @jsxImportSource @fluentui/react-jsx-runtime */

import { assertSlots } from '@fluentui/react-utilities';
import type { RatingState, RatingSlots } from './Rating.types';
import type { RatingState, RatingSlots, RatingContextValues } from './Rating.types';
import { RatingProvider } from '../../contexts/RatingContext';

/**
* Render the final JSX of Rating
*/
export const renderRating_unstable = (state: RatingState) => {
export const renderRating_unstable = (state: RatingState, contextValues: RatingContextValues) => {
assertSlots<RatingSlots>(state);

// TODO Add additional slots in the appropriate place
return <state.root />;
return (
<RatingProvider value={contextValues.rating}>
<state.root>
{state.root.children}
{state.ratingLabel && <state.ratingLabel />}
{state.ratingCountLabel && <state.ratingCountLabel />}
</state.root>
</RatingProvider>
);
};

This file was deleted.

Loading

0 comments on commit 9149d53

Please sign in to comment.