Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: Updating doc stories and API.",
"packageName": "@fluentui/react-rating-preview",
"email": "[email protected]",
"dependentChangeType": "patch"
}
111 changes: 76 additions & 35 deletions packages/react-components/react-rating-preview/docs/Spec.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,104 @@
# @fluentui/react-rating-preview Spec
# @fluentui/react-rating Spec

## Background

_Description and use cases of this component_
A `Rating` component allows users to provide a rating for a particular item.

This component displays a set of stars or other symbols that represent the rating value. Users can interact with the component to select a rating by clicking on the stars or using touch gestures.

## Prior Art

_Include background research done for this component_
### Open UI

### Comparison of v8 and v0

- _Link to Open UI research_
- _Link to comparison of v7 and v0_
- _Link to GitHub epic issue for the converged component_
The existing components are:

- v8
- `Rating`
- v0

## Sample Code

_Provide some representative example code that uses the proposed API for the component_
Basic example:

## Variants
```jsx
import { Rating } from '@fluentui/react-rating';

_Describe visual or functional variants of this control, if applicable. For example, a slider could have a 2D variant._
function App() {
return <Rating />;
}
```

## API

_List the **Props** and **Slots** proposed for the component. Ideally this would just be a link to the component's `.types.ts` file_
### Slots

#### Rating slots

- `root` - The root slot of the `Rating` is the container that will contain the slots that make up a `Rating`. The default html element is a `div`.
- `ratingLabel` - This slot will render the value of the `Rating`. The default html element is a `label`.
- `ratingCountLabel`- This slot will render the total number of ratings. The default html element is a `label`.

#### RatingItem slots

- `root` - The root slot of the `RatingItem`. The default element is `span`.
- `selectedIcon` - Icon displayed when `Rating` value is greater than or equal to the `RatingItem` value.
- `selectedFilledIcon` - Icon displayed when `Rating` value is less than the `RatingItem` value. Outline style gray
- `selectedUnfilledIcon` - Icon displayed when `Rating` value is less than the `RatingItem` value. Outline style white.
- `halfValueInput` - Input slot for when `precision` prop is active and need to render half values of `RatingItem`.
- `fullValueInput` - Default input slot to render selected `RatingItem`

### Props

See API at [Rating.types.tsx](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-rating-preview/src/components/Rating/Rating.types.ts).

## Structure

- _**Public**_
- _**Internal**_
- _**DOM** - how the component will be rendered as HTML elements_
```html
<!-- Container for Rating -->
<div class="fui-Rating">
<input />
<!-- Container for RatingItem -->
<span class="fui-RatingItem">
<div class="fui-RatingItem">
<input />
<svg />
</div>
</span>
</div>
```

## Migration

_Describe what will need to be done to upgrade from the existing implementations:_

- _Migration from v8_
- _Migration from v0_
See [MIGRATION.md](./MIGRATION.md).

## Behaviors

_Explain how the component will behave in use, including:_
### States

- **Display** - The `Rating` will use the following priority:

- The `appearance` prop will dictate whether an unfilled `RatingItem` has a neutral white background or a gray background. Typically used for readOnly
- The `mode` prop will be used to set the type of `Rating`.
- The `max` prop sets how many `RatingItems` there are in the `Rating`
- Setting the `size` prop will allow the user to specify the size of the element.
- You can pass in filled and regular versions of icons to `iconFilled` and `iconOutline` slots to render custom `RatingItems`.

### Interaction

The Rating can be interactive or non-iteractive depending on the use case

- **Keyboard** - Can use arrow keys.
- **Mouse**

- Click: Selects a `RatingItem`
- Hover: Fills up to the hovered `RatingItem`

- _Component States_
- _Interaction_
- _Keyboard_
- _Cursor_
- _Touch_
- _Screen readers_
- **Touch**
- Press: Selects a `RatingItem`
- Drag: No behavior

## Accessibility

Base accessibility information is included in the design document. After the spec is filled and review, outcomes from it need to be communicated to design and incorporated in the design document.

- Decide whether to use **native element** or folow **ARIA** and provide reasons
- Identify the **[ARIA](https://www.w3.org/TR/wai-aria-practices-1.2/) pattern** and, if the component is listed there, follow its specification as possible.
- Identify accessibility **variants**, the `role` ([ARIA roles](https://www.w3.org/TR/wai-aria-1.1/#role_definitions)) of the component, its `slots` and `aria-*` props.
- Describe the **keyboard navigation**: Tab Oder and Arrow Key Navigation. Describe any other keyboard **shortcuts** used
- Specify texts for **state change announcements** - [ARIA live regions
](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) (number of available items in dropdown, error messages, confirmations, ...)
- Identify UI parts that appear on **hover or focus** and specify keyboard and screen reader interaction with them
- List cases when **focus** needs to be **trapped** in sections of the UI (for dialogs and popups or for hierarchical navigation)
- List cases when **focus** needs to be **moved programatically** (if parts of the UI are appearing/disappearing or other cases)
- Tbd. Needs some sort of labelling for the `RatingItem` when interactive and for the whole `Rating` component when readOnly
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const Rating: ForwardRefComponent<RatingProps>;
export const ratingClassNames: SlotClassNames<RatingSlots>;

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

// @public (undocumented)
export type RatingContextValues = {
Expand Down Expand Up @@ -49,7 +49,7 @@ export type RatingItemSlots = {
};

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

Expand All @@ -60,17 +60,16 @@ export type RatingOnChangeData = {

// @public
export type RatingProps = ComponentProps<RatingSlots> & {
appearance?: 'filled' | 'outline';
compact?: boolean;
color?: 'brand' | 'marigold' | 'neutral';
defaultValue?: number;
iconFilled?: React_2.ReactElement;
iconOutline?: React_2.ReactElement;
max?: number;
mode?: 'interactive' | 'read-only' | 'read-only-compact';
name?: string;
onChange?: (ev: React_2.SyntheticEvent | Event, data: RatingOnChangeData) => void;
precision?: boolean;
readOnly?: boolean;
size?: 'small' | 'medium' | 'large';
step?: 0.5 | 1;
size?: 'small' | 'medium' | 'large' | 'extra-large';
value?: number;
};

Expand All @@ -85,7 +84,7 @@ export type RatingSlots = {
};

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,10 @@ export type RatingSlots = {
*/
export type RatingProps = ComponentProps<RatingSlots> & {
/**
* Controls the appearance of unselected rating items.
* @default outline (filled if readOnly is set)
* Controls the appearance of the Rating.
* @default neutral
*/
appearance?: 'filled' | 'outline';
/**
* Sets whether to render a full or compact Rating
* @default false
*/
compact?: boolean;
color?: 'brand' | 'marigold' | 'neutral';
/**
* Default value of the Rating
*/
Expand All @@ -39,6 +34,11 @@ export type RatingProps = ComponentProps<RatingSlots> & {
* @default 5
*/
max?: number;
/**
* The mode of the rating.
* @default 'interactive'
*/
mode?: 'interactive' | 'read-only' | 'read-only-compact';
/**
* Name for the Radio inputs. If not provided, one will be automatically generated
*/
Expand All @@ -49,19 +49,14 @@ export type RatingProps = ComponentProps<RatingSlots> & {
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
* @default 1
*/
readOnly?: boolean;
step?: 0.5 | 1;
/**
* Sets the size of the Rating items.
* @default medium
*/
size?: 'small' | 'medium' | 'large';
size?: 'small' | 'medium' | 'large' | 'extra-large';
/**
* The value of the rating
*/
Expand All @@ -82,27 +77,13 @@ export type RatingOnChangeData = {
* State used in rendering Rating
*/
export type RatingState = ComponentState<RatingSlots> &
Required<
Pick<
RatingProps,
'appearance' | 'compact' | 'iconFilled' | 'iconOutline' | 'name' | 'precision' | 'readOnly' | 'size' | 'value'
>
> & {
Required<Pick<RatingProps, 'color' | 'iconFilled' | 'iconOutline' | 'mode' | 'name' | 'step' | 'size' | 'value'>> & {
hoveredValue?: number | undefined;
};

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

export type RatingContextValues = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,14 @@ import { StarFilled, StarRegular } from '@fluentui/react-icons';
export const useRating_unstable = (props: RatingProps, ref: React.Ref<HTMLDivElement>): RatingState => {
const generatedName = useId('rating-');
const {
appearance = props.readOnly ? 'filled' : 'outline',
compact = false,
color = 'neutral',
iconFilled = <StarFilled />,
iconOutline = <StarRegular />,
max = 5,
mode = 'interactive',
name = generatedName,
onChange,
precision = false,
readOnly = false,
step = 1,
size = 'medium',
} = props;

Expand All @@ -51,21 +50,20 @@ export const useRating_unstable = (props: RatingProps, ref: React.Ref<HTMLDivEle

//Prevents unnecessary rerendering of children
const rootChildren = React.useMemo(() => {
return !compact ? (
Array.from(Array(max), (_, i) => <RatingItem value={i + 1} key={i + 1} />)
) : (
return mode === 'read-only-compact' ? (
<RatingItem value={1} key={1} />
) : (
Array.from(Array(max), (_, i) => <RatingItem value={i + 1} key={i + 1} />)
);
}, [compact, max]);
}, [mode, max]);

const state: RatingState = {
appearance,
compact,
color,
iconFilled,
iconOutline,
mode,
name,
precision,
readOnly,
step,
size,
value,
hoveredValue,
Expand All @@ -92,7 +90,7 @@ export const useRating_unstable = (props: RatingProps, ref: React.Ref<HTMLDivEle
}),
};

if (!readOnly) {
if (mode === 'interactive') {
state.root.onChange = ev => {
if (isRatingRadioItem(ev.target)) {
const newValue = parseFloat(ev.target.value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ export type RatingItemProps = ComponentProps<Partial<RatingItemSlots>> & {
*/
export type RatingItemState = ComponentState<RatingItemSlots> &
Required<Pick<RatingItemProps, 'value'>> &
Pick<RatingState, 'compact' | 'precision' | 'size'> & {
Pick<RatingState, 'color' | 'mode' | 'step' | 'size'> & {
iconFillWidth: number;
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const useRatingItem_unstable = (props: RatingItemProps, ref: React.Ref<HT
const displayedRatingValue = context?.hoveredValue ?? ratingValue;

let iconFillWidth;
if ((context && context.compact) || displayedRatingValue >= value) {
if ((context && context.mode === 'read-only-compact') || displayedRatingValue >= value) {
iconFillWidth = 1;
} else if (displayedRatingValue >= value - 0.5) {
iconFillWidth = 0.5;
Expand Down Expand Up @@ -52,7 +52,7 @@ export const useRatingItem_unstable = (props: RatingItemProps, ref: React.Ref<HT
}

let unselectedFilledIcon;
if (context && iconFillWidth < 1 && context.appearance === 'filled') {
if (context && context.mode !== 'interactive' && iconFillWidth < 1) {
unselectedFilledIcon = slot.always(props.unselectedFilledIcon, {
defaultProps: {
children: context.iconFilled,
Expand All @@ -74,7 +74,7 @@ export const useRatingItem_unstable = (props: RatingItemProps, ref: React.Ref<HT
}

let halfValueInput;
if (context && !context.readOnly && context.precision && !context.compact) {
if (context && context.mode === 'interactive' && context.step === 0.5) {
halfValueInput = slot.always(props.halfValueInput, {
defaultProps: {
type: 'radio',
Expand All @@ -91,7 +91,7 @@ export const useRatingItem_unstable = (props: RatingItemProps, ref: React.Ref<HT
}

let fullValueInput;
if (context && !context.readOnly && !context.compact) {
if (context && context.mode === 'interactive') {
fullValueInput = slot.always(props.fullValueInput, {
defaultProps: {
type: 'radio',
Expand All @@ -109,8 +109,9 @@ export const useRatingItem_unstable = (props: RatingItemProps, ref: React.Ref<HT
}

const state: RatingItemState = {
compact: context ? context.compact : false,
precision: context ? context.precision : false,
color: context ? context.color : 'neutral',
mode: context ? context.mode : 'interactive',
step: context ? (context.step === 1 ? 1 : 0.5) : 1,
size: context ? context.size : 'medium',
iconFillWidth,
value,
Expand Down
Loading