Skip to content

Commit

Permalink
(feat) O3-4249: Add component to support rendering an icon if it exis…
Browse files Browse the repository at this point in the history
…ts (#1220)
  • Loading branch information
ibacher authored Dec 4, 2024
1 parent 7491792 commit c912bbe
Show file tree
Hide file tree
Showing 8 changed files with 367 additions and 129 deletions.
294 changes: 192 additions & 102 deletions packages/framework/esm-framework/docs/API.md

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions packages/framework/esm-react-utils/src/RenderIfValueIsTruthy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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,
}) => {
if (Boolean(value)) {
return <>{children}</>;
}
return fallback ? <>{fallback}</> : null;
};
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 renders it if the sprite for the icon
* 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(
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);
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 `pictogram` and render it if the sprite for the pictogram
* is available. The goal is to make it easier to conditionally render configuration-specified pictograms.
*
* @example
* ```tsx
* <MaybePictogram pictogram='omrs-icon-baby' className={styles.myPictogramStyles} />
* ```
*/
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>
);
}),
);

0 comments on commit c912bbe

Please sign in to comment.