Skip to content

Commit

Permalink
Add useUniqueId hook
Browse files Browse the repository at this point in the history
This hook returns a stable value across multiple rerenders, and can
optionally be overridden with a fixed value

Use this hook in several functional components that currently have
unstable ids that change between rerenders.

Populate the idFactory of this hook in the AppProvider, with eye to
eventually allowing consumers to override the idFactory so it can be
reset between server renders (making this public and configurable is a
follow up step)
  • Loading branch information
BPScott committed Sep 5, 2019
1 parent df56f15 commit 0d9addb
Show file tree
Hide file tree
Showing 17 changed files with 302 additions and 69 deletions.
24 changes: 17 additions & 7 deletions src/components/AppProvider/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import {
StickyManagerContext,
} from '../../utilities/sticky-manager';
import {LinkContext, LinkLikeComponent} from '../../utilities/link';
import {
UniqueIdFactory,
UniqueIdFactoryContext,
globalIdGeneratorFactory,
} from '../../utilities/unique-id';

interface State {
intl: I18n;
Expand All @@ -35,11 +40,14 @@ export interface AppProviderProps extends AppBridgeOptions {
export class AppProvider extends React.Component<AppProviderProps, State> {
private stickyManager: StickyManager;
private scrollLockManager: ScrollLockManager;
private uniqueIdFactory: UniqueIdFactory;

constructor(props: AppProviderProps) {
super(props);
this.stickyManager = new StickyManager();
this.scrollLockManager = new ScrollLockManager();
this.uniqueIdFactory = new UniqueIdFactory(globalIdGeneratorFactory);

const {i18n, apiKey, shopOrigin, forceRedirect, linkComponent} = this.props;

// eslint-disable-next-line react/state-in-constructor
Expand Down Expand Up @@ -91,13 +99,15 @@ export class AppProvider extends React.Component<AppProviderProps, State> {
<I18nContext.Provider value={intl}>
<ScrollLockManagerContext.Provider value={this.scrollLockManager}>
<StickyManagerContext.Provider value={this.stickyManager}>
<AppBridgeContext.Provider value={appBridge}>
<LinkContext.Provider value={link}>
<ThemeProvider theme={theme}>
{React.Children.only(children)}
</ThemeProvider>
</LinkContext.Provider>
</AppBridgeContext.Provider>
<UniqueIdFactoryContext.Provider value={this.uniqueIdFactory}>
<AppBridgeContext.Provider value={appBridge}>
<LinkContext.Provider value={link}>
<ThemeProvider theme={theme}>
{React.Children.only(children)}
</ThemeProvider>
</LinkContext.Provider>
</AppBridgeContext.Provider>
</UniqueIdFactoryContext.Provider>
</StickyManagerContext.Provider>
</ScrollLockManagerContext.Provider>
</I18nContext.Provider>
Expand Down
7 changes: 3 additions & 4 deletions src/components/ChoiceList/ChoiceList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';

import {classNames} from '../../utilities/css';
import {useUniqueId} from '../../utilities/unique-id';
import {Checkbox} from '../Checkbox';
import {RadioButton} from '../RadioButton';
import {InlineError, errorTextID} from '../InlineError';
Expand Down Expand Up @@ -48,8 +48,6 @@ export interface ChoiceListProps {
onChange?(selected: string[], name: string): void;
}

const getUniqueID = createUniqueIDFactory('ChoiceList');

export function ChoiceList({
title,
titleHidden,
Expand All @@ -59,12 +57,13 @@ export function ChoiceList({
onChange = noop,
error,
disabled = false,
name = getUniqueID(),
name: nameProp,
}: ChoiceListProps) {
// Type asserting to any is required for TS3.2 but can be removed when we update to 3.3
// see https://github.com/Microsoft/TypeScript/issues/28768
const ControlComponent: any = allowMultiple ? Checkbox : RadioButton;

const name = useUniqueId('ChoiceList', nameProp);
const finalName = allowMultiple ? `${name}[]` : name;

const className = classNames(
Expand Down
6 changes: 2 additions & 4 deletions src/components/FormLayout/components/Group/Group.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';

import {classNames} from '../../../../utilities/css';
import {wrapWithComponent} from '../../../../utilities/components';
import {useUniqueId} from '../../../../utilities/unique-id';
import styles from '../../FormLayout.scss';
import {Item} from '../Item';

Expand All @@ -13,12 +13,10 @@ export interface GroupProps {
helpText?: React.ReactNode;
}

const getUniqueID = createUniqueIDFactory('FormLayoutGroup');

export function Group({children, condensed, title, helpText}: GroupProps) {
const className = classNames(condensed ? styles.condensed : styles.grouped);

const id = getUniqueID();
const id = useUniqueId('FormLayoutGroup');

let helpTextElement = null;
let helpTextID: undefined | string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import React from 'react';
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';

import {useUniqueId} from '../../../../../../utilities/unique-id';
import {Collapsible} from '../../../../../Collapsible';

import styles from '../../../../Navigation.scss';

const createSecondaryNavigationId = createUniqueIDFactory(
'SecondaryNavigation',
);

interface SecondaryProps {
expanded: boolean;
children?: React.ReactNode;
}

export function Secondary({children, expanded}: SecondaryProps) {
const secondaryNavigationId = createSecondaryNavigationId();
const id = useUniqueId('SecondaryNavigation');
return (
<Collapsible id={secondaryNavigationId} open={expanded}>
<Collapsible id={id} open={expanded}>
<ul className={styles.List}>{children}</ul>
</Collapsible>
);
Expand Down
18 changes: 6 additions & 12 deletions src/components/OptionList/OptionList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, {useState, useRef, useCallback} from 'react';
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
import React, {useState, useCallback} from 'react';

import {arraysAreEqual} from '../../utilities/arrays';
import {IconProps} from '../../types';
import {AvatarProps} from '../Avatar';
import {ThumbnailProps} from '../Thumbnail';
import {useUniqueId} from '../../utilities/unique-id';
import {useDeepEffect} from '../../utilities/use-deep-effect';

import {Option} from './components';
Expand Down Expand Up @@ -34,8 +34,6 @@ export interface SectionDescriptor {

type Descriptor = OptionDescriptor | SectionDescriptor;

const getUniqueId = createUniqueIDFactory('OptionList');

export interface OptionListProps {
/** A unique identifier for the option list */
id?: string;
Expand Down Expand Up @@ -66,16 +64,12 @@ export function OptionList({
role,
optionRole,
onChange,
id: propId,
id: idProp,
}: OptionListProps) {
const [normalizedOptions, setNormalizedOptions] = useState(
createNormalizedOptions(options, sections, title),
);
const id = useRef(propId || getUniqueId());

if (id.current !== propId) {
id.current = propId || id.current;
}
const id = useUniqueId('OptionList', idProp);

useDeepEffect(
() => {
Expand Down Expand Up @@ -122,7 +116,7 @@ export function OptionList({
options.map((option, optionIndex) => {
const isSelected = selected.includes(option.value);
const optionId =
option.id || `${id.current}-${sectionIndex}-${optionIndex}`;
option.id || `${id}-${sectionIndex}-${optionIndex}`;

return (
<Option
Expand All @@ -144,7 +138,7 @@ export function OptionList({
{titleMarkup}
<ul
className={styles.Options}
id={`${id.current}-${sectionIndex}`}
id={`${id}-${sectionIndex}`}
role={role}
aria-multiselectable={allowMultiple}
>
Expand Down
8 changes: 4 additions & 4 deletions src/components/OptionList/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import {TickSmallMinor} from '@shopify/polaris-icons';
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
import {classNames} from '../../../../utilities/css';
import {useUniqueId} from '../../../../utilities/unique-id';
import {Icon} from '../../../Icon';

import styles from './Checkbox.scss';
Expand All @@ -17,10 +17,8 @@ export interface CheckboxProps {
onChange(): void;
}

const getUniqueID = createUniqueIDFactory('Checkbox');

export function Checkbox({
id = getUniqueID(),
id: idProp,
checked = false,
disabled,
active,
Expand All @@ -29,6 +27,8 @@ export function Checkbox({
value,
role,
}: CheckboxProps) {
const id = useUniqueId('Checkbox', idProp);

const className = classNames(styles.Checkbox, active && styles.active);
return (
<div className={className}>
Expand Down
27 changes: 18 additions & 9 deletions src/components/PolarisTestProvider/PolarisTestProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import {
import {AppBridgeContext, AppBridgeOptions} from '../../utilities/app-bridge';
import {I18n, I18nContext, TranslationDictionary} from '../../utilities/i18n';
import {LinkContext, LinkLikeComponent} from '../../utilities/link';
import {
UniqueIdFactory,
UniqueIdFactoryContext,
globalIdGeneratorFactory,
} from '../../utilities/unique-id';

type FrameContextType = NonNullable<React.ContextType<typeof FrameContext>>;

Expand Down Expand Up @@ -53,6 +58,8 @@ export function PolarisTestProvider({

const stickyManager = new StickyManager();

const uniqueIdFactory = new UniqueIdFactory(globalIdGeneratorFactory);

// This typing is odd, but as appBridge is deprecated and going away in v5
// I'm not that worried about it
const appBridgeApp = appBridge as React.ContextType<typeof AppBridgeContext>;
Expand All @@ -66,15 +73,17 @@ export function PolarisTestProvider({
<I18nContext.Provider value={intl}>
<ScrollLockManagerContext.Provider value={scrollLockManager}>
<StickyManagerContext.Provider value={stickyManager}>
<AppBridgeContext.Provider value={appBridgeApp}>
<LinkContext.Provider value={link}>
<ThemeContext.Provider value={mergedTheme}>
<FrameContext.Provider value={mergedFrame}>
{children}
</FrameContext.Provider>
</ThemeContext.Provider>
</LinkContext.Provider>
</AppBridgeContext.Provider>
<UniqueIdFactoryContext.Provider value={uniqueIdFactory}>
<AppBridgeContext.Provider value={appBridgeApp}>
<LinkContext.Provider value={link}>
<ThemeContext.Provider value={mergedTheme}>
<FrameContext.Provider value={mergedFrame}>
{children}
</FrameContext.Provider>
</ThemeContext.Provider>
</LinkContext.Provider>
</AppBridgeContext.Provider>
</UniqueIdFactoryContext.Provider>
</StickyManagerContext.Provider>
</ScrollLockManagerContext.Provider>
</I18nContext.Provider>
Expand Down
11 changes: 6 additions & 5 deletions src/components/RadioButton/RadioButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
import {useUniqueId} from '../../utilities/unique-id';
import {Choice, helpTextID} from '../Choice';
import styles from './RadioButton.scss';

Expand Down Expand Up @@ -32,8 +32,6 @@ export interface BaseProps {

export interface RadioButtonProps extends BaseProps {}

const getUniqueID = createUniqueIDFactory('RadioButton');

export function RadioButton({
ariaDescribedBy: ariaDescribedByProp,
label,
Expand All @@ -44,10 +42,13 @@ export function RadioButton({
onChange,
onFocus,
onBlur,
id = getUniqueID(),
name = id,
id: providedId,
name: providedName,
value,
}: RadioButtonProps) {
const id = useUniqueId('RadioButton', providedId);
const name = providedName || id;

function handleChange({currentTarget}: React.ChangeEvent<HTMLInputElement>) {
onChange && onChange(currentTarget.checked, id);
}
Expand Down
6 changes: 3 additions & 3 deletions src/components/Scrollable/components/ScrollTo/ScrollTo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useContext, useEffect, useRef} from 'react';
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
import {useUniqueId} from '../../../../utilities/unique-id';
import {ScrollableContext} from '../../context';

export function ScrollTo() {
Expand All @@ -14,7 +14,7 @@ export function ScrollTo() {
scrollToPosition(anchorNode.current.offsetTop);
}, [scrollToPosition]);

const getUniqueId = createUniqueIDFactory(`ScrollTo`);
const id = useUniqueId(`ScrollTo`);
// eslint-disable-next-line jsx-a11y/anchor-is-valid
return <a id={getUniqueId()} ref={anchorNode} />;
return <a id={id} ref={anchorNode} />;
}
8 changes: 4 additions & 4 deletions src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React from 'react';
import {ArrowUpDownMinor} from '@shopify/polaris-icons';
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';

import {classNames} from '../../utilities/css';
import {useUniqueId} from '../../utilities/unique-id';
import {Labelled, Action, helpTextID} from '../Labelled';
import {Icon} from '../Icon';
import {Error} from '../../types';
Expand Down Expand Up @@ -72,7 +71,6 @@ export interface BaseProps {
export interface SelectProps extends BaseProps {}

const PLACEHOLDER_VALUE = '';
const getUniqueID = createUniqueIDFactory('Select');

export function Select({
options: optionsProp,
Expand All @@ -83,14 +81,16 @@ export function Select({
disabled,
helpText,
placeholder,
id = getUniqueID(),
id: idProp,
name,
value = PLACEHOLDER_VALUE,
error,
onChange,
onFocus,
onBlur,
}: SelectProps) {
const id = useUniqueId('Select', idProp);

const labelHidden = labelInline ? true : labelHiddenProp;

const className = classNames(
Expand Down
Loading

0 comments on commit 0d9addb

Please sign in to comment.