Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) O3-4249: Add component to support rendering an icon if it exists #1220

Merged
merged 2 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
294 changes: 192 additions & 102 deletions packages/framework/esm-framework/docs/API.md

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions packages/framework/esm-react-utils/src/RenderIfValueIsTruthy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, { type PropsWithChildren } from 'react';

/**
* A really simple component that renders its children if the prop named `value` has a truthy value
*
* @example
* ```tsx
* <RenderIfValueIsTruthy value={variable}>
* <Component value={variable} />
* </RenderIfValueIsTruthy>
* ````
*
* @param props.value The value to check whether or not its truthy
* @param props.fallback What to render if the value is not truthy. If not specified, nothing will be rendered
* @param props.children The components to render if the `value` is truthy
*/
export const RenderIfValueIsTruthy: React.FC<PropsWithChildren<{ value: unknown; fallback?: React.ReactNode }>> = ({
children,
value,
fallback,
}) => {
return Boolean(value) ? <>{children}</> : fallback ? <>{fallback}</> : null;
ibacher marked this conversation as resolved.
Show resolved Hide resolved
};
28 changes: 28 additions & 0 deletions packages/framework/esm-react-utils/src/UserHasAccess.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,34 @@ export interface UserHasAccessProps {
children?: React.ReactNode;
}

/**
* A React component that renders its children only if the current user exists and has the privilege(s)
* specified by the `privilege` prop. This can be used not to render certain components when the user
* doesn't have the permission to use this.
*
* Note that for top-level extensions (i.e., the component that's the root of the extension), you don't
* need to use this component. Instead, when registering the extension, declare the required privileges
* as part of the extension registration. This is for use deeper in extensions or other components where
* a separate permission check might be needed.
*
* This can also be used to hide components when the current user is not logged in.
*
* @example
* ```ts
* <Form>
* <UserHasAccess privilege='Form Finallizer'>
* <Checkbox id="finalize-form" value={formFinalized} onChange={handleChange} />
* </UserHasAccess>
* </Form>
* ````
*
* @param props.privilege Either a string for a single required privilege or an array of strings for a
* set of required privileges. Note that sets of required privileges must all be matched.
* @param props.fallback What to render if the user does not have access or if the user is not currently
* logged in. If not provided, nothing will be rendered
* @param props.children The children to be rendered only if the user is logged in and has the required
* privileges.
*/
export const UserHasAccess: React.FC<UserHasAccessProps> = ({ privilege, fallback, children }) => {
const [user, setUser] = useState<LoggedInUser | null>(null);

Expand Down
1 change: 1 addition & 0 deletions packages/framework/esm-react-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './UserHasAccess';
export * from './getLifecycle';
export * from './openmrsComponentDecorator';
export * from './OpenmrsContext';
export * from './RenderIfValueIsTruthy';
export * from './useAbortController';
export * from './useAppContext';
export * from './useAssignedExtensions';
Expand Down
1 change: 1 addition & 0 deletions packages/framework/esm-react-utils/src/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './ExtensionSlot';
export * from './UserHasAccess';
export * from './getLifecycle';
export * from './OpenmrsContext';
export * from './RenderIfValueIsTruthy';
export * from './useAbortController';
export * from './useAppContext';
export * from './useAssignedExtensions';
Expand Down
43 changes: 43 additions & 0 deletions packages/framework/esm-styleguide/src/icons/icons.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @category Icons */
import React, { forwardRef, memo, useEffect, useImperativeHandle, useRef } from 'react';
import classNames, { type Argument } from 'classnames';
import { RenderIfValueIsTruthy } from '@openmrs/esm-react-utils';
import style from './icons.module.scss';

export type IconProps = {
Expand Down Expand Up @@ -698,6 +699,48 @@ export const RadiologyIcon = ImageMedicalIcon;
*/
export const ShoppingCartAddItemIcon = ShoppingCartArrowDownIcon;

/**
* This is a utility component that takes an `icon` and render it if the sprite for the icon
ibacher marked this conversation as resolved.
Show resolved Hide resolved
* is available. The goal is to make it easier to conditionally render configuration-specified icons.
*
* @example
* ```tsx
* <MaybeIcon icon='omrs-icon-baby' className={styles.myIconStyles} />
* ```
*/
export const MaybeIcon = memo(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const MaybeIcon = memo(
export const MaybeIcon = memo(

nit: Maybe LoadableIcon reads better here?

forwardRef<SVGSVGElement, { icon: string; fallback?: React.ReactNode } & IconProps>(function MaybeIcon(
{ icon, fallback, ...iconProps },
ref,
) {
const iconRef = useRef(document.getElementById(icon));

useEffect(() => {
const container = document.getElementById('omrs-svgs-container');
const callback: MutationCallback = (mutationList) => {
for (const mutation of mutationList) {
if (mutation.type === 'childList') {
iconRef.current = document.getElementById(icon);
}
}
};

const observer = new MutationObserver(callback);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: I'm not sure about the performance implications of having a per-icon mutation observer vs, say, a global mutation observer that could have callbacks registered with it. This is, however, easier to write.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guess we'll find out!

if (container) {
observer.observe(container, { childList: true });
}

return () => observer.disconnect();
}, [icon]);

return (
<RenderIfValueIsTruthy value={iconRef.current} fallback={fallback}>
<Icon ref={ref} icon={icon} iconProps={iconProps} />
</RenderIfValueIsTruthy>
);
}),
);

export type SvgIconProps = {
icon: string;
iconProps: IconProps;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:global([dir='rtl']) {
.pictogram {
transform: scaleX(-1);
}
}
98 changes: 71 additions & 27 deletions packages/framework/esm-styleguide/src/pictograms/pictograms.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,14 @@
/** @category Pictograms */
import React, { forwardRef, memo } from 'react';
import React, { forwardRef, memo, useEffect, useRef } from 'react';
import classNames, { type Argument } from 'classnames';
import { RenderIfValueIsTruthy } from '@openmrs/esm-react-utils';
import style from './pictograms.module.scss';

export type PictogramProps = {
className?: Argument;
size?: number;
};

export type SvgPictogramProps = {
/** the id of the pictogram */
pictogram: string;
/** properties when using the pictogram */
pictogramProps: PictogramProps;
};

/**
* This is a utility type for custom pictograms. Please maintain alphabetical order when adding new pictograms for readability.
*/
export const Pictogram = memo(
forwardRef<SVGSVGElement, SvgPictogramProps>(function Pictogram({ pictogram, pictogramProps }, ref) {
let { className, size } = Object.assign({}, { size: 92 }, pictogramProps);
if (size <= 26 || size > 144) {
console.error(`Invalid size '${size}' specified for ${pictogram}. Defaulting to 92.`);
size = 92;
}

return (
<svg ref={ref} className={classNames(className)} height={size} width={size}>
<use href={`#${pictogram}`} />
</svg>
);
}),
);

export const AppointmentsPictogram = memo(
forwardRef<SVGSVGElement, PictogramProps>(function AppointmentsPictogram(props, ref) {
return <Pictogram ref={ref} pictogram="omrs-pict-appointments" pictogramProps={props} />;
Expand Down Expand Up @@ -80,3 +56,71 @@ export const ServiceQueuesPictogram = memo(
return <Pictogram ref={ref} pictogram="omrs-pict-service-queues" pictogramProps={props} />;
}),
);

/**
* This is a utility component that takes an `icon` and render it if the sprite for the icon
* is available. The goal is to make it easier to conditionally render configuration-specified icons.
ibacher marked this conversation as resolved.
Show resolved Hide resolved
*
* @example
* ```tsx
* <MaybeIcon icon='omrs-icon-baby' className={styles.myIconStyles} />
ibacher marked this conversation as resolved.
Show resolved Hide resolved
* ```
*/
export const MaybePictogram = memo(
forwardRef<SVGSVGElement, { pictogram: string; fallback?: React.ReactNode } & PictogramProps>(function MaybeIcon(
{ pictogram, fallback, ...pictogramProps },
ref,
) {
const iconRef = useRef(document.getElementById(pictogram));

useEffect(() => {
const container = document.getElementById('omrs-svgs-container');
const callback: MutationCallback = (mutationList) => {
for (const mutation of mutationList) {
if (mutation.type === 'childList') {
iconRef.current = document.getElementById(pictogram);
}
}
};

const observer = new MutationObserver(callback);
if (container) {
observer.observe(container, { childList: true });
}

return () => observer.disconnect();
}, [pictogram]);

return (
<RenderIfValueIsTruthy value={iconRef.current} fallback={fallback}>
<Pictogram ref={ref} pictogram={pictogram} pictogramProps={pictogramProps} />
</RenderIfValueIsTruthy>
);
}),
);

export type SvgPictogramProps = {
/** the id of the pictogram */
pictogram: string;
/** properties when using the pictogram */
pictogramProps: PictogramProps;
};

/**
* This is a utility type for custom pictograms. Please maintain alphabetical order when adding new pictograms for readability.
*/
export const Pictogram = memo(
forwardRef<SVGSVGElement, SvgPictogramProps>(function Pictogram({ pictogram, pictogramProps }, ref) {
let { className, size } = Object.assign({}, { size: 92 }, pictogramProps);
if (size <= 26 || size > 144) {
console.error(`Invalid size '${size}' specified for ${pictogram}. Defaulting to 92.`);
size = 92;
}

return (
<svg ref={ref} className={classNames(style.pictogram, className)} height={size} width={size}>
<use href={`#${pictogram}`} />
</svg>
);
}),
);
Loading